diff --git a/agent/languageDetect.ts b/agent/languageDetect.ts index 2cd4de3..0442635 100644 --- a/agent/languageDetect.ts +++ b/agent/languageDetect.ts @@ -26,14 +26,6 @@ const USER_LANGUAGE_OUTPUT_SCHEMA = { }, } as const; -export function formatLanguagePrompt(language: UserLanguage | null) { - if (!language) { - return "Respond in the user's language."; - } - - return `Respond in ${language.language} (${language.code}).`; -} - function parseUserLanguage(content: string | undefined): UserLanguage | null { if (!content) { return null; diff --git a/agent/simpleAgent.ts b/agent/simpleAgent.ts index 2a5e008..60933c3 100644 --- a/agent/simpleAgent.ts +++ b/agent/simpleAgent.ts @@ -4,7 +4,6 @@ import { logger, type AdminUser, type CompletionAdapter, - type HttpExtra, type IAdminForth, } from "adminforth"; import { BaseCallbackHandler } from "@langchain/core/callbacks/base"; @@ -26,8 +25,8 @@ export const contextSchema = z.object({ userTimeZone: z.string(), sessionId: z.string(), turnId: z.string(), + abortSignal: z.custom().optional(), currentPage: z.custom().optional(), - httpExtra: z.custom>().optional(), emitToolCallEvent: z.custom(), }); @@ -234,8 +233,8 @@ export async function callAgent(params: { sessionId: string; turnId: string; currentPage?: CurrentPageContext; - httpExtra?: Partial; userTimeZone: string; + abortSignal?: AbortSignal; emitToolCallEvent: ToolCallEventSink; sequenceDebugSink: SequenceDebugModelCallSink; }) { @@ -253,8 +252,8 @@ export async function callAgent(params: { sessionId, turnId, currentPage, - httpExtra, userTimeZone, + abortSignal, emitToolCallEvent, sequenceDebugSink, } = params; @@ -289,6 +288,7 @@ export async function callAgent(params: { streamMode: "messages", recursionLimit: 100, callbacks: [createAgentLlmMetricsLogger()], + signal: abortSignal, configurable: { thread_id: sessionId, }, @@ -297,8 +297,8 @@ export async function callAgent(params: { userTimeZone, sessionId, turnId, + abortSignal, currentPage, - httpExtra, emitToolCallEvent, }, }); diff --git a/agent/systemPrompt.ts b/agent/systemPrompt.ts index 76c0e51..35052df 100644 --- a/agent/systemPrompt.ts +++ b/agent/systemPrompt.ts @@ -1,10 +1,10 @@ -import type { AdminForthResource, IAdminForth } from "adminforth"; +import type { AdminForthResource, AdminUser, IAdminForth } from "adminforth"; +import type { UserLanguage } from "./languageDetect.js"; import { listBundledSkillManifests, listProjectSkillManifests, type AgentSkillManifest, } from "./skills/registry.js"; -import { ALWAYS_AVAILABLE_API_TOOL_NAMES } from "./tools/index.js"; export const DEFAULT_AGENT_SYSTEM_PROMPT = [ "You are helpful AI Assistant for Admin Panel.", @@ -46,6 +46,39 @@ export function appendCustomSystemPrompt( return `${systemPrompt}\n\n${normalizedCustomSystemPrompt}`; } +function formatLanguagePrompt(language: UserLanguage | null) { + if (!language) { + return "Respond in the user's language."; + } + + return `Respond in ${language.language} (${language.code}).`; +} + +function formatAdminUserPrompt(adminUser: AdminUser, usernameField: string) { + const adminUserContext = { + id: adminUser.pk, + email: adminUser.dbUser[usernameField], + }; + return [ + "Current admin user context:", + JSON.stringify(adminUserContext, null, 2), + "Use this admin user email when the user asks to send information to themselves, the current admin, or the logged-in user.", + ].join("\n"); +} + +export function buildAgentTurnSystemPrompt(input: { + agentSystemPrompt: string; + adminUser: AdminUser; + usernameField: string; + userLanguage: UserLanguage | null; +}) { + return [ + input.agentSystemPrompt, + formatAdminUserPrompt(input.adminUser, input.usernameField), + formatLanguagePrompt(input.userLanguage), + ].join("\n\n"); +} + function formatResources(resources: AdminForthResource[]) { return resources .map((resource) => `- resourceId: ${resource.resourceId}\n label: ${resource.label}`) @@ -66,7 +99,6 @@ export async function buildAgentSystemPrompt( listProjectSkillManifests(adminforth.config.customization.customComponentsDir), listBundledSkillManifests(), ]); - const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", "); const adminBasePath = adminforth.config.baseUrlSlashed; const hiddenResourceIdSet = new Set(hiddenResourceIds); const visibleResources = adminforth.config.resources.filter( @@ -76,7 +108,6 @@ export async function buildAgentSystemPrompt( DEFAULT_AGENT_SYSTEM_PROMPT, `ADMIN_BASE_PATH: ${adminBasePath}`, `List of resources:\n${formatResources(visibleResources)}`, - `You have always-available base tools: ${alwaysAvailableTools}.`, primarySkills.length > 0 ? `You have primary skills set:\n${formatSkills(primarySkills, "skill_name")}` : "", diff --git a/agent/toolCallEvents.ts b/agent/toolCallEvents.ts index a5c1ecb..63eda7e 100644 --- a/agent/toolCallEvents.ts +++ b/agent/toolCallEvents.ts @@ -1,6 +1,5 @@ import { randomUUID } from "crypto"; import YAML from "yaml"; -import { serializeUnknownError } from "../apiBasedTools.js"; export type ToolCallEvent = | { @@ -61,6 +60,36 @@ function sanitizeToolCallOutputForDebug(output: unknown) { ); } +function serializeErrorForDebug(error: unknown): unknown { + if (!(error instanceof Error)) { + return error; + } + + const errorWithCause = error as Error & { cause?: unknown }; + const serialized: Record = { + name: error.name, + message: error.message, + }; + + if (error.stack) { + serialized.stack = error.stack; + } + + if (errorWithCause.cause !== undefined) { + serialized.cause = serializeErrorForDebug(errorWithCause.cause); + } + + for (const key of Object.getOwnPropertyNames(error)) { + if (key in serialized) { + continue; + } + + serialized[key] = (error as unknown as Record)[key]; + } + + return serialized; +} + export function createToolCallTracker(params: { emit: ToolCallEventSink; toolCallId?: string; @@ -99,7 +128,7 @@ export function createToolCallTracker(params: { phase: "end", durationMs: Date.now() - startedAt, output: null, - error: YAML.stringify(serializeUnknownError(error)).trimEnd(), + error: YAML.stringify(serializeErrorForDebug(error)).trimEnd(), }); }, }; diff --git a/agent/tools/apiTool.ts b/agent/tools/apiTool.ts index 71e1ee2..d6f598a 100644 --- a/agent/tools/apiTool.ts +++ b/agent/tools/apiTool.ts @@ -51,7 +51,7 @@ export function createApiTool(toolName: string, apiBasedTool: ApiBasedTool) { const normalizedInput = (input ?? {}) as Record; return apiBasedTool.call({ adminUser: runtime.context.adminUser, - httpExtra: runtime.context.httpExtra, + abortSignal: runtime.context.abortSignal, inputs: normalizedInput, userTimeZone: runtime.context.userTimeZone, }); diff --git a/agentResponseEvents.ts b/agentResponseEvents.ts new file mode 100644 index 0000000..fc401fd --- /dev/null +++ b/agentResponseEvents.ts @@ -0,0 +1,197 @@ +import { randomUUID } from "crypto"; + +import type { ToolCallEvent } from "./agent/toolCallEvents.js"; + +type AgentEventStreamResponse = { + writeHead: (statusCode: number, headers: Record) => void; + write: (chunk: string) => unknown; + end: () => unknown; + writableEnded: boolean; + destroyed: boolean; +}; + +type AgentEventStreamOptions = { + vercelAiUiMessageStream?: boolean; + closeActiveBlockOnToolStart?: boolean; +}; + +export function createAgentEventStream( + res: AgentEventStreamResponse, + options: AgentEventStreamOptions = {}, +) { + let isStreamClosed = false; + let activeBlock: { type: "text" | "reasoning"; id: string } | null = null; + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + ...(options.vercelAiUiMessageStream + ? { "x-vercel-ai-ui-message-stream": "v1" } + : {}), + }); + + const stream = { + send(obj: unknown) { + if (isStreamClosed || res.writableEnded || res.destroyed) { + return; + } + + res.write(`data: ${JSON.stringify(obj)}\n\n`); + }, + + endActiveBlock() { + if (!activeBlock) { + return; + } + + stream.send({ + type: `${activeBlock.type}-end`, + id: activeBlock.id, + }); + + activeBlock = null; + }, + + startBlock(type: "text" | "reasoning") { + if (activeBlock?.type === type) { + return activeBlock.id; + } + + stream.endActiveBlock(); + + const id = randomUUID(); + activeBlock = { type, id }; + + stream.send({ + type: `${type}-start`, + id, + }); + + return id; + }, + + start(messageId: string) { + stream.send({ + type: "start", + messageId, + }); + }, + + textDelta(delta: string) { + const textId = stream.startBlock("text"); + stream.send({ + type: "text-delta", + id: textId, + delta, + }); + }, + + reasoningDelta(delta: string) { + const reasoningId = stream.startBlock("reasoning"); + stream.send({ + type: "reasoning-delta", + id: reasoningId, + delta, + }); + }, + + toolCall(event: ToolCallEvent) { + if (options.closeActiveBlockOnToolStart && event.phase === "start") { + stream.endActiveBlock(); + } + + stream.send({ + type: "data-tool-call", + data: event, + }); + }, + + transcript(text: string, language?: string) { + stream.send({ + type: "transcript", + data: { + text, + language, + }, + }); + }, + + response(text: string, sessionId: string, turnId: string) { + stream.send({ + type: "response", + data: { + text, + sessionId, + turnId, + }, + }); + }, + + speechResponse( + transcript: { text: string; language?: string }, + response: { text: string }, + sessionId: string, + turnId: string, + ) { + stream.send({ + type: "speech-response", + data: { + transcript, + response, + sessionId, + turnId, + }, + }); + }, + + audioStart(mimeType: string, format: string) { + stream.send({ + type: "audio-start", + data: { + mimeType, + format, + }, + }); + }, + + audioDelta(value: Uint8Array) { + stream.send({ + type: "audio-delta", + data: { + base64: Buffer.from(value).toString("base64"), + }, + }); + }, + + audioDone() { + stream.send({ + type: "audio-done", + }); + }, + + error(error: string) { + stream.send({ + type: "error", + error, + }); + }, + + end() { + if (isStreamClosed || res.writableEnded || res.destroyed) { + return; + } + + stream.endActiveBlock(); + stream.send({ + type: "finish", + }); + + res.write("data: [DONE]\n\n"); + isStreamClosed = true; + res.end(); + }, + }; + + return stream; +} diff --git a/apiBasedTools.ts b/apiBasedTools.ts index aa7fa4e..3d6a0e5 100644 --- a/apiBasedTools.ts +++ b/apiBasedTools.ts @@ -2,35 +2,26 @@ import { AdminForthDataTypes, logger, type AdminUser, - type HttpExtra, type IAdminForth, + type IAdminForthHttpResponse, type IRegisteredApiSchema, } from 'adminforth'; import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone.js'; import utc from 'dayjs/plugin/utc.js'; -import { inspect } from 'util'; import YAML from 'yaml'; dayjs.extend(utc); dayjs.extend(timezone); -type CookieItem = { - key: string; - value: string; -}; - -type ToolOverrideCallParams = Pick; type ToolOverrideContext = { adminforth: IAdminForth; output?: unknown; adminUser?: AdminUser; - httpExtra?: Partial; inputs?: Record; resourceLabel?: string; userTimeZone?: string; - invokeTool: (toolName: string, params?: ToolOverrideCallParams) => Promise; }; type ToolOverride = { @@ -46,12 +37,16 @@ type GetResourceDataToolResponse = { }; type DateTimeColumnType = AdminForthDataTypes.DATETIME | AdminForthDataTypes.TIME; -type InternalApiOriginProvider = { - getInternalApiOrigin?: () => string | undefined; +type RegisteredApiToolSchema = IRegisteredApiSchema & { + handler: (input: unknown) => void | Promise; }; const DEFAULT_USER_TIME_ZONE = 'UTC'; +function hasRegisteredApiToolHandler(schema: IRegisteredApiSchema): schema is RegisteredApiToolSchema { + return typeof (schema as { handler?: unknown }).handler === 'function'; +} + function getInputString(inputs: Record | undefined, key: string) { const value = inputs?.[key]; @@ -171,15 +166,14 @@ const TOOL_OVERRIDES: Record = { export type ApiBasedToolCallParams = { adminUser?: AdminUser; adminuser?: AdminUser; + abortSignal?: AbortSignal; inputs?: Record; - httpExtra?: Partial; userTimeZone?: string; }; export type ApiBasedTool = { description?: string; input_schema?: unknown; - input_schma?: unknown; output_schema?: unknown; call: (params?: ApiBasedToolCallParams) => Promise; }; @@ -228,44 +222,6 @@ function sanitizeForYaml( return JSON.parse(serialized); } -export function serializeUnknownError(error: unknown): Record { - if (error instanceof Error) { - const errorWithCause = error as Error & { cause?: unknown }; - const errorRecord = error as unknown as Record; - const serialized: Record = { - name: error.name, - message: error.message, - stack: error.stack, - }; - - if (errorWithCause.cause !== undefined) { - serialized.cause = serializeUnknownError(errorWithCause.cause); - } - - for (const key of Object.getOwnPropertyNames(error)) { - if (key in serialized) { - continue; - } - - serialized[key] = errorRecord[key]; - } - - return serialized; - } - - if (typeof error === 'object' && error !== null) { - return { - type: error.constructor?.name ?? 'Object', - inspected: inspect(error, { depth: 6, breakLength: 120 }), - }; - } - - return { - type: typeof error, - value: error, - }; -} - function wipePath(target: unknown, pathParts: string[]): void { if (!target || typeof target !== 'object' || pathParts.length === 0) { return; @@ -367,23 +323,12 @@ function formatDateTimeColumns( async function applyToolOverride(params: { adminforth: IAdminForth; adminUser?: AdminUser; - httpExtra?: Partial; inputs?: Record; - invokeTool: (toolName: string, params?: ToolOverrideCallParams) => Promise; output: unknown; toolName: string; userTimeZone?: string; }): Promise { - const { - adminforth, - adminUser, - httpExtra, - inputs, - invokeTool, - output, - toolName, - userTimeZone, - } = params; + const { adminforth, adminUser, inputs, output, toolName, userTimeZone } = params; const sanitizedOutput = sanitizeForYaml(output); const override = TOOL_OVERRIDES[toolName]; @@ -395,41 +340,15 @@ async function applyToolOverride(params: { wipePath(sanitizedOutput, path.split('.')); } - if (!override.post_process_response) { - return sanitizedOutput; - } - - const postProcessedOutput = await override.post_process_response({ - adminforth, - output: sanitizedOutput, - adminUser, - httpExtra, - inputs, - userTimeZone, - invokeTool: async (nestedToolName, nestedParams = {}) => { - const nestedInputs = nestedParams.inputs ?? inputs; - const nestedHttpExtra = nestedParams.httpExtra ?? httpExtra; - const nestedUserTimeZone = nestedParams.userTimeZone ?? userTimeZone; - const nestedOutput = await invokeTool(nestedToolName, { - inputs: nestedInputs, - httpExtra: nestedHttpExtra, - userTimeZone: nestedUserTimeZone, - }); - - return applyToolOverride({ + return override.post_process_response + ? sanitizeForYaml(await override.post_process_response({ adminforth, adminUser, - httpExtra: nestedHttpExtra, - inputs: nestedInputs, - invokeTool, - output: nestedOutput, - toolName: nestedToolName, - userTimeZone: nestedUserTimeZone, - }); - }, - }); - - return sanitizeForYaml(postProcessedOutput); + output: sanitizedOutput, + inputs, + userTimeZone, + })) + : sanitizedOutput; } function endpointPathToToolName(path: string) { @@ -465,40 +384,19 @@ function formatLogNameList(names: string[]) { export async function formatApiBasedToolCall(params: { adminforth: IAdminForth; adminUser?: AdminUser; - httpExtra?: Partial; inputs?: Record; toolName: string; userTimeZone?: string; }) { - const formatTool = TOOL_OVERRIDES[params.toolName]?.format_tool; - - return await formatTool?.({ + return await TOOL_OVERRIDES[params.toolName]?.format_tool?.({ adminforth: params.adminforth, adminUser: params.adminUser, - httpExtra: params.httpExtra, inputs: params.inputs, resourceLabel: resourceLabel(params.adminforth, params.inputs), userTimeZone: params.userTimeZone, - invokeTool: async () => { - throw new Error('Tool info formatting cannot invoke tools'); - }, }); } -function normalizeCookies( - cookies?: Partial['cookies'] | Record, -): CookieItem[] { - if (!cookies) { - return []; - } - - if (Array.isArray(cookies)) { - return cookies; - } - - return Object.entries(cookies).map(([key, value]) => ({ key, value })); -} - function normalizeDateTimeInputsToUtc( body: Record, adminforth: IAdminForth, @@ -532,10 +430,8 @@ function normalizeDateTimeInputsToUtc( return dayjs.tz(value, userTimeZone).utc().toISOString(); } - if (columnType === AdminForthDataTypes.TIME) { - const userDate = dayjs().tz(userTimeZone).format('YYYY-MM-DD'); - return dayjs.tz(`${userDate}T${value}`, userTimeZone).utc().format('HH:mm:ss'); - } + const userDate = dayjs().tz(userTimeZone).format('YYYY-MM-DD'); + return dayjs.tz(`${userDate}T${value}`, userTimeZone).utc().format('HH:mm:ss'); }; const normalizeValue = (value: unknown, key?: string): unknown => { @@ -577,157 +473,115 @@ function normalizeDateTimeInputsToUtc( return normalizeValue(body) as Record; } -const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']); -const HEADERS_NOT_FORWARDED_TO_API_TOOL = new Set([ - 'connection', - 'content-length', - 'host', - 'keep-alive', - 'proxy-authenticate', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', -]); - -function isAbsoluteHttpUrl(value: string) { - try { - const url = new URL(value); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch { - return false; - } -} - -function resolveOpenApiRequestUrl(params: { - adminforth: IAdminForth; - path: string; - toolName: string; -}) { - const internalApiOrigin = (params.adminforth.express as InternalApiOriginProvider) - .getInternalApiOrigin?.(); - - if (internalApiOrigin) { - const path = isAbsoluteHttpUrl(params.path) - ? `${new URL(params.path).pathname}${new URL(params.path).search}` - : params.path; +const METHODS_WITHOUT_REQUEST_BODY = new Set(['GET', 'HEAD']); - return new URL(path, internalApiOrigin).toString(); - } +function createDirectToolResponse(): IAdminForthHttpResponse & { + headers: Array<[string, string]>; + status: number; + message?: string; +} { + const headers: Array<[string, string]> = []; - throw new Error( - `Tool "${params.toolName}" cannot call OpenAPI path "${params.path}" because internal API origin is unavailable.`, - ); + return { + headers, + status: 200, + setHeader(name, value) { + headers.push([name, value]); + }, + setStatus(code, message) { + this.status = code; + this.message = message; + }, + blobStream() { + throw new Error('blobStream is not available for API-based agent tools'); + }, + }; } -function createToolRequestHeaders( - httpExtra: Partial | undefined, - userTimeZone?: string, +function validationErrorResponse( + error: 'REQUEST_VALIDATION_FAILED' | 'RESPONSE_VALIDATION_FAILED', + details: unknown, ) { - const headers: Record = {}; - - for (const [name, value] of Object.entries(httpExtra?.headers ?? {})) { - const headerName = name.toLowerCase(); - - if (typeof value === 'string' && !HEADERS_NOT_FORWARDED_TO_API_TOOL.has(headerName)) { - headers[headerName] = value; - } - } - - headers.accept = 'application/json'; - headers['content-type'] = 'application/json'; - - if (userTimeZone) { - headers['x-timezone'] = userTimeZone; - } - - const cookieHeader = normalizeCookies(httpExtra?.cookies) - .map(({ key, value }) => `${key}=${value}`) - .join('; '); - - if (cookieHeader && !headers.cookie) { - headers.cookie = cookieHeader; - } - - return headers; -} - -function appendInputsToQueryString(url: string, inputs: Record) { - const nextUrl = new URL(url); - - for (const [key, value] of Object.entries(inputs)) { - if (value === undefined) { - continue; - } - - if (Array.isArray(value)) { - for (const item of value) { - nextUrl.searchParams.append( - key, - typeof item === 'object' && item !== null ? JSON.stringify(item) : String(item), - ); - } - continue; - } - - nextUrl.searchParams.set( - key, - typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value), - ); - } - - return nextUrl.toString(); -} - -async function parseOpenApiToolResponse(response: Response) { - const responseText = await response.text(); - const payload = responseText && response.headers.get('content-type')?.includes('application/json') - ? JSON.parse(responseText) - : responseText; - - if (response.ok) { - return responseText ? payload : { status: response.status }; - } - return { - error: 'HTTP_ERROR', - status: response.status, - statusText: response.statusText, - response: payload, + error, + details, }; } async function callOpenApiSchema(params: { adminforth: IAdminForth; - httpExtra?: Partial; + adminUser?: AdminUser; + abortSignal?: AbortSignal; inputs?: Record; - schema: IRegisteredApiSchema; + schema: RegisteredApiToolSchema; toolName: string; userTimeZone?: string; }) { - const { adminforth, httpExtra, inputs, schema, toolName, userTimeZone } = params; + const { adminforth, adminUser, abortSignal, inputs, schema, toolName, userTimeZone } = params; const method = schema.method.toUpperCase(); - const body = normalizeDateTimeInputsToUtc( - (inputs ?? httpExtra?.body ?? {}) as Record, + const normalizedInputs = normalizeDateTimeInputsToUtc( + (inputs ?? {}) as Record, adminforth, userTimeZone, ); - const requestUrl = resolveOpenApiRequestUrl({ - adminforth, - path: schema.path, - toolName, - }); const hasRequestBody = !METHODS_WITHOUT_REQUEST_BODY.has(method); - logger.info(`Calling OpenAPI tool "${toolName}" with method ${method} at URL ${requestUrl}`); - const response = await fetch(hasRequestBody ? requestUrl : appendInputsToQueryString(requestUrl, body), { - method, - headers: createToolRequestHeaders(httpExtra, userTimeZone), - body: hasRequestBody ? JSON.stringify(body) : undefined, + const body = hasRequestBody ? normalizedInputs : {}; + const query = hasRequestBody ? {} : normalizedInputs; + const requestValidation = adminforth.openApi.validateRequestSchema(schema, body); + + if (!requestValidation.valid) { + return validationErrorResponse('REQUEST_VALIDATION_FAILED', requestValidation.errors); + } + + const response = createDirectToolResponse(); + logger.info(`Calling OpenAPI tool "${toolName}" with direct handler`); + const tr = ( + msg: string, + category: string, + trParams: unknown, + pluralizationNumber?: number, + ) => adminforth.tr(msg, category, undefined, trParams, pluralizationNumber); + const output = await schema.handler({ + body, + query, + headers: {}, + cookies: [], + adminUser, + response, + requestUrl: schema.path, + abortSignal: abortSignal ?? new AbortController().signal, + _raw_express_req: undefined as never, + _raw_express_res: undefined as never, + tr, }); - logger.info(`Received response with status ${response.status} from OpenAPI tool "${toolName}"`); - return parseOpenApiToolResponse(response); + if (response.message) { + return response.status >= 400 + ? { + error: 'HANDLER_ERROR', + status: response.status, + response: response.message, + } + : response.message; + } + + if (output === null) { + return { status: response.status }; + } + + const responseValidation = adminforth.openApi.validateResponseSchema(schema, output); + + if (!responseValidation.valid) { + return validationErrorResponse('RESPONSE_VALIDATION_FAILED', responseValidation.errors); + } + + return response.status >= 400 + ? { + error: 'HANDLER_ERROR', + status: response.status, + response: output, + } + : output; } export function prepareApiBasedTools( @@ -735,15 +589,15 @@ export function prepareApiBasedTools( hiddenResourceIds: Iterable = [], ): Record { const apiBasedTools: Record = {}; - const openApiSchemas = adminforth.openApi.registeredSchemas.filter( - (schema) => schema.request_schema || schema.response_schema, - ); - const openApiSchemasByToolName = new Map(); + const openApiSchemas = adminforth.openApi.registeredSchemas; + const openApiSchemasByToolName = new Map(); const hiddenResourceIdSet = new Set(hiddenResourceIds); for (const schema of openApiSchemas) { const toolName = openApiSchemaPathToToolName(schema.path, adminforth); - openApiSchemasByToolName.set(toolName, schema); + if (hasRegisteredApiToolHandler(schema)) { + openApiSchemasByToolName.set(toolName, schema); + } } logger.info( @@ -759,9 +613,8 @@ export function prepareApiBasedTools( apiBasedTools[toolName] = { description: schema.description, input_schema: schema.request_schema, - input_schma: schema.request_schema, output_schema: schema.response_schema, - call: async ({ adminUser, adminuser, inputs, httpExtra, userTimeZone } = {}) => { + call: async ({ adminUser, adminuser, abortSignal, inputs, userTimeZone } = {}) => { if (isHiddenResourceCall(hiddenResourceIdSet, inputs)) { return YAML.stringify({ error: 'RESOURCE_NOT_AVAILABLE', @@ -769,38 +622,20 @@ export function prepareApiBasedTools( }); } - const invokeTool = async ( - nextToolName: string, - nextParams: ToolOverrideCallParams = {}, - ) => { - const nextSchema = openApiSchemasByToolName.get(nextToolName); - - if (!nextSchema) { - throw new Error(`Tool ${nextToolName} is not registered in OpenAPI`); - } - - return callOpenApiSchema({ - adminforth, - schema: nextSchema, - toolName: nextToolName, - inputs: nextParams.inputs, - httpExtra: nextParams.httpExtra, - userTimeZone: nextParams.userTimeZone, - }); - }; - - const output = await invokeTool(toolName, { + const output = await callOpenApiSchema({ + adminforth, + adminUser: adminUser ?? adminuser, + abortSignal, + schema, + toolName, inputs, - httpExtra, userTimeZone, }); const processedOutput = await applyToolOverride({ adminforth, adminUser: adminUser ?? adminuser, - httpExtra, inputs, - invokeTool, output, toolName, userTimeZone, @@ -822,7 +657,6 @@ export function serializeApiBasedTool(tool: ApiBasedTool | undefined) { return { description: tool.description, input_schema: tool.input_schema, - input_schma: tool.input_schma, output_schema: tool.output_schema, call: '[Function]', }; diff --git a/custom/ChatSurface.vue b/custom/ChatSurface.vue index 99b02d7..05ff977 100644 --- a/custom/ChatSurface.vue +++ b/custom/ChatSurface.vue @@ -118,8 +118,12 @@ transition: `transform ${agentTransitions.TRANSITION_DURATION}ms ease-in-out` }" > -
+