mirror of
https://github.com/kennethnym/freya
synced 2026-07-04 15:11:15 +01:00
refactor: split query agent toolbox (#139)
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import { defineTool } from "@earendil-works/pi-coding-agent"
|
||||
import { type } from "arktype"
|
||||
import { Type } from "typebox"
|
||||
|
||||
import type { UserSessionManager } from "../session/index.ts"
|
||||
import type { QueryDebugTools } from "./debug-tools.ts"
|
||||
|
||||
import { createQueryDebugTools } from "./debug-tools.ts"
|
||||
import type { QueryAgentToolbox } from "./query-agent-toolbox.ts"
|
||||
|
||||
interface CreateFreyaAgentToolsConfig {
|
||||
userId: string
|
||||
sessionManager: UserSessionManager
|
||||
toolbox: QueryAgentToolbox
|
||||
}
|
||||
|
||||
export const FREYA_QUERY_CONTEXT_TOOL = "freya_query_context"
|
||||
@@ -19,6 +16,41 @@ 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"
|
||||
|
||||
const ContextKeyObjectPart = type("Record<string, string | number | boolean>").narrow(
|
||||
(value) => !Array.isArray(value),
|
||||
)
|
||||
const ContextKeyPart = type("string | number").or(ContextKeyObjectPart)
|
||||
|
||||
const GetContextToolParams = type({
|
||||
"+": "reject",
|
||||
key: ContextKeyPart.array().atLeastLength(1),
|
||||
"match?": "'exact' | 'prefix'",
|
||||
})
|
||||
|
||||
const GetFeedItemToolParams = type({
|
||||
"+": "reject",
|
||||
feedItemId: type.string.atLeastLength(1),
|
||||
})
|
||||
|
||||
const QueryContextToolParams = type({
|
||||
"+": "reject",
|
||||
question: type.string.atLeastLength(1),
|
||||
"feedItemId?": "string",
|
||||
})
|
||||
|
||||
const GetSourceDataToolParams = type({
|
||||
"+": "reject",
|
||||
sourceId: type.string.atLeastLength(1),
|
||||
"feedItemId?": "string",
|
||||
})
|
||||
|
||||
const ExecuteActionToolParams = type({
|
||||
"+": "reject",
|
||||
sourceId: type.string.atLeastLength(1),
|
||||
actionId: type.string.atLeastLength(1),
|
||||
"params?": "unknown",
|
||||
})
|
||||
|
||||
export const FREYA_AGENT_TOOL_NAMES = [
|
||||
FREYA_LIST_SOURCES_TOOL,
|
||||
FREYA_GET_CONTEXT_TOOL,
|
||||
@@ -30,16 +62,13 @@ export const FREYA_AGENT_TOOL_NAMES = [
|
||||
]
|
||||
|
||||
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, {}),
|
||||
parameters: Type.Object({}, { additionalProperties: false }),
|
||||
execute: async () => executeListSourcesTool(config.toolbox),
|
||||
})
|
||||
|
||||
const getContextTool = defineTool({
|
||||
@@ -47,30 +76,34 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
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.",
|
||||
parameters: Type.Object(
|
||||
{
|
||||
key: Type.Array(Type.Unknown(), {
|
||||
description:
|
||||
'Context key array, for example ["freya.location"] or ["freya.location", "location"].',
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async (_toolCallId, params) =>
|
||||
executeDebugTool(debugTools, userId, FREYA_GET_CONTEXT_TOOL, params),
|
||||
match: Type.Optional(
|
||||
Type.Union([Type.Literal("exact"), Type.Literal("prefix")], {
|
||||
description: "Match mode. Defaults to prefix.",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, params) => executeGetContextTool(config.toolbox, 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),
|
||||
parameters: Type.Object(
|
||||
{
|
||||
feedItemId: Type.String({ description: "Feed item ID to inspect." }),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, params) => executeGetFeedItemTool(config.toolbox, params),
|
||||
})
|
||||
|
||||
const queryContextTool = defineTool({
|
||||
@@ -78,17 +111,20 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
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.",
|
||||
parameters: Type.Object(
|
||||
{
|
||||
question: Type.String({
|
||||
description: "The specific personal-context question to answer.",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async (_toolCallId, params) => executeQueryContextTool(config, params),
|
||||
feedItemId: Type.Optional(
|
||||
Type.String({
|
||||
description: "Optional feed item ID when the user is asking about a specific card.",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, params) => executeQueryContextTool(config.toolbox, params),
|
||||
})
|
||||
|
||||
const listContextTool = defineTool({
|
||||
@@ -96,8 +132,8 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
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),
|
||||
parameters: Type.Object({}, { additionalProperties: false }),
|
||||
execute: async () => executeListContextTool(config.toolbox),
|
||||
})
|
||||
|
||||
const getSourceDataTool = defineTool({
|
||||
@@ -105,17 +141,20 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
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.",
|
||||
parameters: Type.Object(
|
||||
{
|
||||
sourceId: Type.String({
|
||||
description: "Source ID, for example freya.location, freya.tfl, or freya.weather.",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
|
||||
feedItemId: Type.Optional(
|
||||
Type.String({
|
||||
description: "Optional feed item ID to select one item from the source.",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, params) => executeGetSourceDataTool(config.toolbox, params),
|
||||
})
|
||||
|
||||
const executeActionTool = defineTool({
|
||||
@@ -123,16 +162,19 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
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),
|
||||
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.",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
execute: async (_toolCallId, params) => executeActionToolCall(config.toolbox, params),
|
||||
})
|
||||
|
||||
return [
|
||||
@@ -146,166 +188,57 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
]
|
||||
}
|
||||
|
||||
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 executeListSourcesTool(toolbox: QueryAgentToolbox) {
|
||||
return toolbox.listSources()
|
||||
}
|
||||
|
||||
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 executeGetContextTool(toolbox: QueryAgentToolbox, rawParams: unknown) {
|
||||
const params = GetContextToolParams(rawParams)
|
||||
if (params instanceof type.errors) {
|
||||
throw new Error(params.summary)
|
||||
}
|
||||
|
||||
const match = params.match ?? "prefix"
|
||||
|
||||
return toolbox.getContext(params.key, match)
|
||||
}
|
||||
|
||||
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 executeGetFeedItemTool(toolbox: QueryAgentToolbox, rawParams: unknown) {
|
||||
const params = GetFeedItemToolParams(rawParams)
|
||||
if (params instanceof type.errors) {
|
||||
throw new Error(params.summary)
|
||||
}
|
||||
|
||||
return toolbox.getFeedItem(params.feedItemId)
|
||||
}
|
||||
|
||||
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 executeQueryContextTool(toolbox: QueryAgentToolbox, rawParams: unknown) {
|
||||
const params = QueryContextToolParams(rawParams)
|
||||
if (params instanceof type.errors) {
|
||||
throw new Error(params.summary)
|
||||
}
|
||||
|
||||
return toolbox.queryContext(params.question, params.feedItemId)
|
||||
}
|
||||
|
||||
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 },
|
||||
}
|
||||
async function executeListContextTool(toolbox: QueryAgentToolbox) {
|
||||
return toolbox.listContext()
|
||||
}
|
||||
|
||||
async function executeGetSourceDataTool(toolbox: QueryAgentToolbox, rawParams: unknown) {
|
||||
const params = GetSourceDataToolParams(rawParams)
|
||||
if (params instanceof type.errors) {
|
||||
throw new Error(params.summary)
|
||||
}
|
||||
|
||||
return toolbox.getSourceData(params.sourceId, params.feedItemId)
|
||||
}
|
||||
|
||||
async function executeActionToolCall(toolbox: QueryAgentToolbox, rawParams: unknown) {
|
||||
const params = ExecuteActionToolParams(rawParams)
|
||||
if (params instanceof type.errors) {
|
||||
throw new Error(params.summary)
|
||||
}
|
||||
|
||||
return toolbox.executeAction(params.sourceId, params.actionId, params.params)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user