Files
accounted/lib/agent/intents/general-help.ts
T
Jakob Wennberg 2d543ac999 feat(agent): move every model call to Sonnet 5 (#1218)
* feat(agent): move every model call to Sonnet 5

Sonnet 5 is verified enabled on our Bedrock account already: a live probe of
eu.anthropic.claude-sonnet-5 in eu-north-1 answered normally, so no model-access
request was needed. The bare anthropic.claude-sonnet-5 is rejected (on-demand
throughput needs the cross-region inference profile), so the eu. prefix we
already use stays.

This is not a model-string swap. Sonnet 5 REJECTS the fixed thinking budget
outright: thinking {type:'enabled', budget_tokens} returns 400 "not supported
for this model. Use thinking.type.adaptive and output_config.effort". Every
chat intent set a budget, so the assistant would have failed on the first turn
after a bare ID change. Reasoning depth is now an effort level (STANDARD high,
DEEP xhigh), and max_tokens is explicit per tier rather than derived from a
budget that no longer exists.

display:'summarized' is load-bearing, not cosmetic. The default is 'omitted',
which still emits thinking blocks but with empty text. Measured on our own
account at xhigh effort: summarized returned ~1k characters of reasoning, the
default returned none. Without it the collapsible "Tänker ..." block in the
chat would have gone silently empty, which no mocked test would have caught.

Ceilings are raised (16k standard, 24k deep) because Sonnet 5's tokenizer
produces roughly 30% more tokens for the same text and max_tokens now caps
thinking and the visible reply together.

Also resolves the Opus 4.7 landmine recorded in the readiness doc: the composer
comment told ops to flip BEDROCK_OPUS_MODEL_ID to Opus 4.7, which would have
400d every thinking intent against the legacy budget shape. Both model
constants now point at Sonnet 5 and the stale instruction is gone.

Checked but deliberately unchanged: forced tool_choice in atom-selection. The
Sonnet 5 docs require thinking:{type:'disabled'} alongside a forced tool_choice
on Bedrock; probed against our account, the forced call succeeds without it, so
no change was made rather than adding a guard we cannot show is needed.

Other call sites moved too: invoice-inbox extraction, document extraction, the
compliance config, and the CI/CD workflows (pr-agent MODEL and MODEL_WEAK,
swedish-compliance-review, compliance-swarm).

Verified: 11315 tests pass, lint and tsc clean on every touched file, guards
pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(agent): review triage: keep the no-thinking output ceiling, finish the model sweep

max_tokens now caps thinking and the visible reply together, so collapsing the
two tiers into one made every non-thinking intent inherit a 16000 ceiling where
it used to have 4096. Give it its own MAX_TOKENS_NO_THINKING instead, set to the
old 4096 scaled ~30% for Sonnet 5's tokenizer so the effective reply length is
unchanged rather than quietly cut.

scripts/swedish-compliance-review.mjs still fell back to Sonnet 4.6 when
REVIEW_MODEL was unset, so a manual run silently used the old model. The initial
sweep only covered .ts and .yml.

pr-agent's FALLBACK_MODELS listed the primary model as its own fallback, which is
not a fallback; dropped it and rewrote the surrounding comments, which still
described Opus 4.8 and a 200k window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:16:40 +02:00

137 lines
7.3 KiB
TypeScript

import { defineAgentIntent } from './types'
import { SONNET_MODEL, EFFORT_STANDARD } from '@/lib/agent/composer/client'
import { renderAgentGroundRules } from './shared-rules'
// general.help: always-present "Fråga min assistent" from the top nav.
//
// Atom mode is progressive: only the agent_atom_registry metadata lands in
// the system prompt (~200 tokens per atom), and the agent calls
// gnubok_load_skill on demand when a topic actually requires depth. This
// keeps TTFT low for the common "quick question" pattern without forcing
// the entire skill library into every chat turn.
//
// Plan refs: §8 (intent system, V1 #3), §10 (caching strategy: progressive
// disclosure keeps Block 1 small enough that cache reuse pays off across
// users).
interface GeneralHelpArgs {
// Currently routed only with the URL the user is on. We don't capture page
// contents: the chat sheet sits over the page and is intentionally
// page-agnostic so the user can keep working underneath.
route?: string
}
interface GeneralHelpCaptured {
route: string | null
}
export const generalHelp = defineAgentIntent<GeneralHelpArgs, GeneralHelpCaptured>({
id: 'general.help',
buttonLabel: 'Fråga min assistent',
sheetTitle: 'Fråga din assistent',
atoms: {
mode: 'progressive',
horizontal: [],
includeCompanyVertical: false,
includeCompanyModifiers: false,
},
// general.help is the broad chat assistant: used both from the floating
// pill on random pages AND from the /chat surface. Users land here with
// analytical questions ("vad är min största utgiftspost?", "vilka
// leverantörer skulder jag mest?", "hur ser min momsrapport ut?") that
// require actually reading bookkeeping data, not just regulatory atoms.
//
// Tool whitelist is therefore comprehensive on the READ side. Write tools
// (categorize, create_invoice, approve_supplier_invoice, stage_year_end,
// …) deliberately stay out: those belong to the page-specific intents
// where the agent has a single entity in focus and the user expects a
// staged ApprovalCard. From /chat the agent redirects users to the right
// page for write actions instead of trying to do them inline.
//
// Anthropic caches the tools list with the system prompt so a stable
// whitelist costs nothing per turn after first warm-up.
tools: [
// Knowledge + memory
'gnubok_search_tools',
'gnubok_list_skills',
'gnubok_load_skill',
'gnubok_remember_fact',
'gnubok_forget_fact',
// 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_inbox_item',
'gnubok_get_document_content',
'gnubok_get_counterparty_templates',
],
model: SONNET_MODEL,
// Reason before answering: this is the broad chat surface where the agent
// answered regulatory questions from memory and narrated its steps. Thinking
// moves the reasoning into its own channel so the visible reply is a single
// consolidated answer.
thinking: { effort: EFFORT_STANDARD },
capture: async ({ route }) => ({ route: route ?? null }),
promptTemplate: ({ captured, profileSummary }) => {
const lines: string[] = []
if (profileSummary) {
lines.push(`Företagets profil: ${profileSummary}`)
lines.push('')
}
if (captured.route) {
lines.push(`Användaren befinner sig på sidan: ${captured.route}`)
lines.push('')
}
lines.push('Användaren öppnade ditt fönster med "Fråga min assistent". Inget specifikt ärende ännu.')
lines.push('')
lines.push(renderAgentGroundRules())
lines.push('')
lines.push('Härifrån kan du (använd verktygen: citera siffrorna):')
lines.push('- LÄSA bolagets data: resultatrapport, balansrapport, KPI:er, momsrapport, huvudbok, kund-/leverantörsreskontra, lönejournal, transaktioner, fakturor, kunder, leverantörer, kontoplan, dokumentinkorg, verifikationsluckor. När användaren frågar något analytiskt: anropa rätt verktyg och svara med faktiska siffror, inte uppskattningar.')
lines.push('- Svara på regelfrågor: bokföring, moms, lön, bokslut, deklaration. Ladda atominnehåll med gnubok_load_skill vid behov.')
lines.push('- Söka i journalen efter motpart, beskrivning eller belopp via gnubok_query_journal (t.ex. "har jag bokfört detta förut?").')
lines.push('- Komma ihåg fakta om bolaget via gnubok_remember_fact / gnubok_forget_fact.')
lines.push('')
lines.push('Du har INGA skrivverktyg härifrån: du kan läsa och resonera, men inte kategorisera, fakturera, attestera eller stage:a bokslut, och du ska INTE låtsas att du kan.')
lines.push('')
lines.push('KATEGORISERING / BOKFÖRING: så här hanterar du det (vanligaste fallet): Om användaren ber dig kategorisera, bokföra eller "gå igenom" okategoriserade transaktioner, ge då INTE per-transaktions-bokföringsförslag (konto/momsbehandling) i löptext, och fråga ALDRIG "godkänner du dessa?". Två skäl: (1) du ser inte det matchade underlaget (kvitto/faktura) per transaktion härifrån, så förslaget vilar på gissningar; (2) du kan inte stagea någon bokning: det blir en analys användaren inte kan agera på. Hänvisa istället tydligt: "Själva kategoriseringen gör vi i Dokumentinkorgen: lägg kvittot/fakturan där (eller vidarebefordra det till företagets inbox-adress), matcha det mot transaktionen och fråga assistenten därifrån: då ser jag underlaget som hör till transaktionen och lägger ett förslag du godkänner direkt i kortet." Du FÅR ge en kort överblick (hur många som väntar, vilka de äldsta är, vilka som ser kluriga ut) för att hjälpa användaren prioritera, men stanna där, gå inte vidare till konto/moms per rad.')
lines.push('')
lines.push('Övriga skrivåtgärder hänvisas på samma sätt: fakturering → /invoices/new, leverantörsfaktura → /supplier-invoices/[id], moms → momsrapporten, bokslut → /bookkeeping/year-end. Där finns "Fråga …"-knappen med rätt skrivverktyg OCH rätt underlag inkopplat. Försök ALDRIG fabricera/föreslå att du stagear något härifrån.')
lines.push('')
lines.push('Bra rytm för analytiska frågor: (1) anropa rätt läsverktyg, (2) svara med konkreta siffror från resultatet, (3) lägg till en kort förklaring eller nästa-steg-rekommendation om det är meningsfullt. Hellre verkligt svar än "gå till Rapporter och titta själv".')
lines.push('')
lines.push('Vänta in användarens fråga. Hälsa kort och fråga vad du kan hjälpa till med. Var direkt: svaret du skriver nu är det första användaren ser.')
return lines.join('\n')
},
})