feat(agent): let the single-call assistant read the ledger via read-only MCP tools (#1767)
The /chat assistant (audit Option A / rip) shipped in #1759 reading only the company name + entity type, so it answered "jag har ingen bokföringsdata" to every figures question ("vad är min största utgiftspost?"). It now behaves like an MCP client: it answers over a bounded, READ-only tool loop across the same MCP read tools the old streaming assistant had, plus an always-on company snapshot as the backstop. Provider-agnostic by construction, so it still runs on a local model: - lib/ai generateText gains optional `tools` + `maxSteps`. The OpenAI-compatible service forwards them to the Vercel AI SDK (stopWhen: stepCountIs), which runs the loop; the Anthropic-family service hand-rolls a small loop against messages.create. Kept on the raw Anthropic SDK: no new deps, and the no-tools path is byte-identical, so hosted extraction/composer/etc. are unchanged. - lib/agent/ask/ledger-tools.ts: the read slice of general.help's whitelist (income statement, VAT, ledgers, query_journal, reskontror, lists…) from agentToolRegistry, dispatched with the agent_chat actor run-turn uses. Write/ staging + memory-write tools are excluded; readOnlyHint/destructiveHint are re-checked. Empty in a core-only build → snapshot-only, graceful. - lib/agent/ask/snapshot.ts: a compact company_settings + deadlines block so a model that can't/won't call tools still answers status questions. Never carries figures (those come from the live tools). - ask-service attaches tools + snapshot when a userId is present and uses a tool-aware system prompt; the route calls ensureInitialized() so the registry is populated and threads userId/conversationId through. Works on Bedrock and on any local model with function-calling (Qwen). Tests: the anthropic hand-rolled loop (tool call → result → answer, is_error handling, step-budget forced answer), openai tool forwarding, the read-only adapter filter, the snapshot format, and the ask-service wiring. 457 agent+ai tests green, lint/guards clean. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Opus 4.8
parent
febb4cc0c2
commit
ff4425d10e
@@ -48,11 +48,39 @@ export interface AiUsage {
|
||||
cacheReadInputTokens: number | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A tool the model may call during a bounded generateText loop.
|
||||
*
|
||||
* Provider-agnostic by construction: the OpenAI-compatible service maps it to
|
||||
* a Vercel AI SDK `tool()` and lets the SDK drive the loop; the Anthropic-
|
||||
* family service maps it to a Messages `tool` block and runs the loop by hand.
|
||||
* `execute` runs the tool and returns any JSON-serialisable value, which is
|
||||
* fed back to the model as the tool result. Only ever pass READ-only tools:
|
||||
* this path has no approval-card surface for staged writes.
|
||||
*/
|
||||
export interface AiToolDef {
|
||||
name: string
|
||||
description: string
|
||||
/** JSON Schema (draft-07 subset) for the tool's arguments. */
|
||||
jsonSchema: Record<string, unknown>
|
||||
execute: (args: Record<string, unknown>) => Promise<unknown>
|
||||
}
|
||||
|
||||
export interface GenerateTextRequest {
|
||||
tier: AiTier
|
||||
system?: string
|
||||
prompt: string
|
||||
maxTokens: number
|
||||
/**
|
||||
* Read-only tools the model may call to gather data before answering. When
|
||||
* present AND the backend supports tool use (capabilities.toolUse), the
|
||||
* service runs a bounded tool loop of up to `maxSteps` model turns; a
|
||||
* backend without tool support ignores them and answers from the prompt
|
||||
* alone (so a snapshot in the prompt is the fallback grounding).
|
||||
*/
|
||||
tools?: AiToolDef[]
|
||||
/** Max model turns in the tool loop (default 4). Ignored when `tools` is absent. */
|
||||
maxSteps?: number
|
||||
}
|
||||
|
||||
export interface GenerateTextResult {
|
||||
|
||||
Reference in New Issue
Block a user