// `searchDocs` — the retrieval tool the support agent calls. // // Tool shape (the `*.tool.tsx` convention): a named `defineTool(...)` // DESCRIPTOR with the Schema-typed `input` / `output` and NO body, plus // a default-exported `(input, ctx) => …` handler. The framework wires // the default export onto the descriptor at discovery time, and the // agent's `tools: { searchDocs }` imports the SAME module instance. // // This file is SERVER-ONLY — it's referenced from the agent EXECUTOR // (`support.agent.server.tsx`), never from a browser-loaded descriptor, // so importing the `database` handle here is safe. // // The handler does vector retrieval: `database.docs.nearestNeighbours( // query, k)` returns the k nearest docs (each row gets a synthetic // `distance`). Because `docs` carries `vectorEmbedding()`, the STRING // overload is valid — the runtime embeds `query` before searching. Any // failure (no AI key, embed error) is caught → empty array, so a tool // hiccup surfaces to the model as "no results" instead of aborting the // whole agent turn. import { defineTool } from '@voltro/ai' import type { AppContext } from '@voltro/runtime' import { Schema } from 'effect' import { database } from '../database/schema' const MAX_RESULTS = 5 const SNIPPET_LEN = 280 export const searchDocs = defineTool({ name: 'searchDocs', description: 'Search the support knowledge base for documents relevant to a question. ' + 'Returns up to 5 results, each with a title and a short snippet of the body.', input: Schema.Struct({ query: Schema.String }), output: Schema.Array( Schema.Struct({ title: Schema.String, snippet: Schema.String }), ), }) export default async ( { query }: { query: string }, ctx: AppContext, ): Promise> => { try { const rows = await ctx.store.query( database.docs.nearestNeighbours(query, MAX_RESULTS).descriptor, ) return rows.map((row) => { const body = String((row as { body?: unknown }).body ?? '') return { title: String((row as { title?: unknown }).title ?? ''), snippet: body.length > SNIPPET_LEN ? `${body.slice(0, SNIPPET_LEN)}…` : body, } }) } catch { // No AI key / embed failure / cold table → degrade to "no results" // rather than aborting the agent's tool loop. return [] } }