Files
freya/apps/freya-backend/src/agent/tools.ts

312 lines
8.9 KiB
TypeScript

import { defineTool } from "@earendil-works/pi-coding-agent"
import { Type } from "typebox"
import type { UserSessionManager } from "../session/index.ts"
import type { QueryDebugTools } from "./debug-tools.ts"
import { createQueryDebugTools } from "./debug-tools.ts"
interface CreateFreyaAgentToolsConfig {
userId: string
sessionManager: UserSessionManager
}
export const FREYA_QUERY_CONTEXT_TOOL = "freya_query_context"
export const FREYA_LIST_SOURCES_TOOL = "freya_list_sources"
export const FREYA_GET_CONTEXT_TOOL = "freya_get_context"
export const FREYA_LIST_CONTEXT_TOOL = "freya_list_context"
export const FREYA_GET_SOURCE_DATA_TOOL = "freya_get_source_data"
export const FREYA_GET_FEED_ITEM_TOOL = "freya_get_feed_item"
export const FREYA_EXECUTE_ACTION_TOOL = "freya_execute_action"
export const FREYA_AGENT_TOOL_NAMES = [
FREYA_LIST_SOURCES_TOOL,
FREYA_GET_CONTEXT_TOOL,
FREYA_GET_FEED_ITEM_TOOL,
FREYA_QUERY_CONTEXT_TOOL,
FREYA_LIST_CONTEXT_TOOL,
FREYA_GET_SOURCE_DATA_TOOL,
FREYA_EXECUTE_ACTION_TOOL,
]
export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
const { userId } = config
const debugTools = createQueryDebugTools(config.sessionManager)
const listSourcesTool = defineTool({
name: FREYA_LIST_SOURCES_TOOL,
label: "List FREYA Sources",
description:
"List enabled FREYA source IDs and summarize available feed items, context entries, actions, and errors.",
parameters: Type.Object({}),
execute: async () => executeDebugTool(debugTools, userId, FREYA_LIST_SOURCES_TOOL, {}),
})
const getContextTool = defineTool({
name: FREYA_GET_CONTEXT_TOOL,
label: "Get FREYA Context",
description:
"Read specific FREYA context entries by key. Use prefix matching to discover entries under a source ID, or exact matching when you know the full key.",
parameters: Type.Object({
key: Type.Array(Type.Unknown(), {
description:
'Context key array, for example ["freya.location"] or ["freya.location", "location"].',
}),
match: Type.Optional(
Type.Union([Type.Literal("exact"), Type.Literal("prefix")], {
description: "Match mode. Defaults to prefix.",
}),
),
}),
execute: async (_toolCallId, params) =>
executeDebugTool(debugTools, userId, FREYA_GET_CONTEXT_TOOL, params),
})
const getFeedItemTool = defineTool({
name: FREYA_GET_FEED_ITEM_TOOL,
label: "Get FREYA Feed Item",
description: "Read one feed item by ID, including related source context, actions, and errors.",
parameters: Type.Object({
feedItemId: Type.String({ description: "Feed item ID to inspect." }),
}),
execute: async (_toolCallId, params) =>
executeDebugTool(debugTools, userId, FREYA_GET_FEED_ITEM_TOOL, params),
})
const queryContextTool = defineTool({
name: FREYA_QUERY_CONTEXT_TOOL,
label: "Query FREYA Context",
description:
"Read the user's current FREYA feed, source graph context, source errors, and available actions.",
parameters: Type.Object({
question: Type.String({
description: "The specific personal-context question to answer.",
}),
feedItemId: Type.Optional(
Type.String({
description: "Optional feed item ID when the user is asking about a specific card.",
}),
),
}),
execute: async (_toolCallId, params) => executeQueryContextTool(config, params),
})
const listContextTool = defineTool({
name: FREYA_LIST_CONTEXT_TOOL,
label: "List FREYA Context",
description:
"List all current FREYA context graph entries for the user. Use this to inspect what personal context is available.",
parameters: Type.Object({}),
execute: async () => executeListContextTool(config),
})
const getSourceDataTool = defineTool({
name: FREYA_GET_SOURCE_DATA_TOOL,
label: "Get FREYA Source Data",
description:
"Get current feed items, context entries, actions, and errors for a specific FREYA source ID.",
parameters: Type.Object({
sourceId: Type.String({
description: "Source ID, for example freya.location, freya.tfl, or freya.weather.",
}),
feedItemId: Type.Optional(
Type.String({
description: "Optional feed item ID to select one item from the source.",
}),
),
}),
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
})
const executeActionTool = defineTool({
name: FREYA_EXECUTE_ACTION_TOOL,
label: "Execute FREYA Action",
description:
"Execute an available FREYA source action immediately without creating a proposal.",
parameters: Type.Object({
sourceId: Type.String({ description: "Source ID that should execute the action." }),
actionId: Type.String({ description: "Source action ID to execute." }),
params: Type.Optional(
Type.Unknown({
description: "Parameters to pass to the source action.",
}),
),
}),
execute: async (_toolCallId, params) => executeActionToolCall(config, params),
})
return [
listSourcesTool,
getContextTool,
getFeedItemTool,
queryContextTool,
listContextTool,
getSourceDataTool,
executeActionTool,
]
}
async function executeDebugTool(
debugTools: QueryDebugTools,
userId: string,
toolName: string,
params: unknown,
) {
const result = await debugTools.execute(userId, toolName, params)
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result),
},
],
details: {},
}
}
async function executeQueryContextTool(
config: CreateFreyaAgentToolsConfig,
params: { question: string; feedItemId?: string },
) {
const userSession = await config.sessionManager.getOrCreate(config.userId)
const feed = await userSession.feed()
const context = userSession.engine.currentContext()
const feedItemId = params.feedItemId
const selectedItem =
typeof feedItemId === "string" ? feed.items.find((item) => item.id === feedItemId) : undefined
const actions = await userSession.listActions()
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
time: context.time.toISOString(),
question: params.question,
feedItemId: feedItemId ?? null,
selectedItem: selectedItem ?? null,
items: feed.items,
context: context.entries(),
availableActions: actions.map((entry) => ({
sourceId: entry.sourceId,
actions: Object.values(entry.actions).map((action) => ({
id: action.id,
description: action.description ?? null,
})),
})),
errors: feed.errors.map((error) => ({
sourceId: error.sourceId,
message: error.error.message,
})),
}),
},
],
details: {},
}
}
async function executeListContextTool(config: CreateFreyaAgentToolsConfig) {
const userSession = await config.sessionManager.getOrCreate(config.userId)
await userSession.feed()
const context = userSession.engine.currentContext()
const entries = context.entries()
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
time: context.time.toISOString(),
count: entries.length,
entries,
}),
},
],
details: {},
}
}
async function executeGetSourceDataTool(
config: CreateFreyaAgentToolsConfig,
params: { sourceId: string; feedItemId?: string },
) {
const userSession = await config.sessionManager.getOrCreate(config.userId)
const feed = await userSession.feed()
const context = userSession.engine.currentContext()
const sourceActions = userSession.hasSource(params.sourceId)
? await userSession.engine.listActions(params.sourceId)
: {}
const items = feed.items.filter((item) => item.sourceId === params.sourceId)
const selectedItem =
params.feedItemId !== undefined
? items.find((item) => item.id === params.feedItemId)
: undefined
const contextEntries = context.entries().filter((entry) => entry.key[0] === params.sourceId)
const errors = feed.errors
.filter((error) => error.sourceId === params.sourceId)
.map((error) => ({
sourceId: error.sourceId,
message: error.error.message,
}))
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
time: context.time.toISOString(),
sourceId: params.sourceId,
hasSource: userSession.hasSource(params.sourceId),
feedItemId: params.feedItemId ?? null,
selectedItem: selectedItem ?? null,
items,
context: contextEntries,
actions: Object.values(sourceActions).map((action) => ({
id: action.id,
description: action.description ?? null,
})),
errors,
}),
},
],
details: {},
}
}
async function executeActionToolCall(
config: CreateFreyaAgentToolsConfig,
params: {
sourceId: string
actionId: string
params?: unknown
},
) {
const userSession = await config.sessionManager.getOrCreate(config.userId)
const result = await userSession.engine.executeAction(
params.sourceId,
params.actionId,
params.params,
)
const actionExecution = {
sourceId: params.sourceId,
actionId: params.actionId,
result: result ?? null,
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
ok: true,
...actionExecution,
}),
},
],
details: { actionExecution },
}
}