ff4425d10e
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>
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import type { AiToolDef } from '@/lib/ai'
|
|
import { agentToolRegistry } from '@/lib/agent/tools/registry'
|
|
import type { AgentActorContext } from '@/lib/agent/tools/types'
|
|
|
|
/**
|
|
* The read-only tools the single-call assistant may call to answer questions
|
|
* about the ledger (audit Option A: "single-call actions over the existing MCP
|
|
* tool functions"). This is what makes the console behave like an MCP client:
|
|
* the model asks for a report, we run the real tool, feed the JSON back.
|
|
*
|
|
* The list is the read slice of general.help's old tool whitelist: the
|
|
* analytical reports and the lookups across the working set. Write/staging
|
|
* tools (categorize, create_invoice, approve_supplier_invoice, stage_year_end,
|
|
* …) and the memory-write tools (remember/forget) are deliberately absent: the
|
|
* console has no ApprovalCard surface, so it reads and guides only; write
|
|
* actions belong to the page-specific intents where a single entity is in
|
|
* focus and the user expects a staged card.
|
|
*
|
|
* The registry is populated by the mcp-server extension at init
|
|
* (ensureInitialized). In a core-only build it is empty, so this returns an
|
|
* empty list and the assistant answers from the prompt + snapshot alone: the
|
|
* graceful, correct degradation.
|
|
*/
|
|
export const LEDGER_READ_TOOLS: readonly string[] = [
|
|
// Reports (the canonical analytical surface)
|
|
'gnubok_get_income_statement',
|
|
'gnubok_get_balance_sheet',
|
|
'gnubok_get_trial_balance',
|
|
'gnubok_get_general_ledger',
|
|
'gnubok_get_kpi_report',
|
|
'gnubok_get_vat_report',
|
|
'gnubok_vat_close_check',
|
|
'gnubok_get_ar_ledger',
|
|
'gnubok_get_supplier_ledger',
|
|
'gnubok_get_reconciliation_status',
|
|
'gnubok_get_salary_journal',
|
|
'gnubok_year_end_readiness',
|
|
// Lookups across the working set
|
|
'gnubok_query_journal',
|
|
'gnubok_list_uncategorized_transactions',
|
|
'gnubok_list_transactions_without_documents',
|
|
'gnubok_list_invoices',
|
|
'gnubok_list_customers',
|
|
'gnubok_list_suppliers',
|
|
'gnubok_list_supplier_invoices',
|
|
'gnubok_list_accounts',
|
|
'gnubok_list_fiscal_periods',
|
|
'gnubok_list_employees',
|
|
'gnubok_list_inbox_items',
|
|
'gnubok_list_unmatched_documents',
|
|
'gnubok_list_voucher_gaps',
|
|
'gnubok_explain_voucher_gap',
|
|
'gnubok_get_counterparty_templates',
|
|
]
|
|
|
|
/**
|
|
* Build the read-only tool defs for one company/user, bound to the same actor
|
|
* context the streaming runtime uses (`agent_chat` + the conversation id, for
|
|
* BFL audit). Each def's execute dispatches the real registered tool.
|
|
*/
|
|
export function buildLedgerTools(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
userId: string,
|
|
conversationId?: string,
|
|
): AiToolDef[] {
|
|
const actor: AgentActorContext = {
|
|
type: 'agent_chat',
|
|
...(conversationId ? { id: conversationId } : {}),
|
|
}
|
|
|
|
return agentToolRegistry
|
|
.getMany([...LEDGER_READ_TOOLS])
|
|
// Belt-and-suspenders on top of the curated whitelist: never expose a tool
|
|
// the registry itself marks writable or destructive, even if it somehow
|
|
// ended up on the list.
|
|
.filter((t) => t.annotations?.readOnlyHint !== false && t.annotations?.destructiveHint !== true)
|
|
.map((t) => ({
|
|
name: t.name,
|
|
description: t.description,
|
|
jsonSchema: t.inputSchema,
|
|
execute: (args: Record<string, unknown>) =>
|
|
t.execute(args, companyId, userId, supabase, actor),
|
|
}))
|
|
}
|