Open Finapp
Premium

AI Chat

Local LLM assistant for transactions, wallets, categories, and analytics.

Overview

Finapp ships a local AI assistant powered by Ollama and @tanstack/ai. The chat lives in a slide-over panel (AiChatPanel.vue) and exposes ~24 tools that wrap Pinia store mutations and queries. No data leaves the user's machine.

Entry points

  • FAB button (AiFab.vue) - floating action button on all authenticated pages
  • Keyboard shortcut Cmd/Ctrl+I (see shortcuts.ts)
  • Slide-over panel - right side, sm:max-w-md

Architecture

User input → AiChatPanel
  ├── useAiChat.send(text)
  │   ├── pruneContext(messages) - sliding window + tool result compression
  │   ├── chat() via @tanstack/ai with Ollama adapter
  │   ├── streams TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END / TEXT_MESSAGE_CONTENT
  │   ├── autoRetry - detects failed mutations + incomplete chains
  │   ├── client-side fallback - directly executes mutations if model stalls
  │   ├── toast notifications - visual feedback for mutations
  │   └── lastAction tracker - enables undo
  ├── AiToolCallsList.vue - renders rich cards per tool
  └── AiMessageContent.vue - markdown assistant text

Tool catalog

All tools live in app/components/ai/tools/ and are registered via createAllTools().

Transactions

  • create_trn - new expense or income
  • create_adjustment - set wallet balance to target value (delta is computed)
  • create_transfer - transfer between wallets (supports currency exchange)
  • update_trn - patch fields on existing transaction by id
  • delete_trn - delete by id
  • duplicate_trn - clone a transaction with current date
  • bulk_delete_trns - delete many by filter (safety cap via maxDelete)
  • list_trns - filter + aggregate
  • search_trns - full-text search in descriptions
  • undo_last_action - reverse last AI mutation

Wallets

  • create_wallet, update_wallet, delete_wallet, list_wallets
  • archive_wallet / unarchive_wallet
  • set_wallet_exclude_in_total
  • reorder_wallets

Categories

  • create_category, update_category, delete_category, list_categories

Analytics

  • get_summary - totals, income/expense, top N categories
  • get_wallet_stats - single-wallet income/expense/net
  • compare_periods - current vs previous period, delta and %

Settings

  • set_base_currency, set_locale, get_settings
  • set_theme - primary/neutral color, border radius, black-as-primary

System prompt

Built in useSystemPrompt.ts. Key rules:

  • Tool-first: every factual question about wallets/categories/transactions MUST call a tool, not answer from memory
  • Multi-turn: if required fields are missing (e.g. user said "купил кофе" without amount), ask ONE specific follow-up; merge on next turn
  • Chain pattern: delete_trn / update_trn / duplicate_trn require list_trns first to get a real id
  • FORBIDDEN: inventing ids, asking user for ids, reusing past tool results
  • Mutations: rules 12a-12i cover transfer, adjustment, settings, wallet management, ordering, undo
  • DATE RULE: omit date unless user explicitly said one - tools default to now

Context pruning

pruneContext.ts implements a sliding window + tool result compression:

  • maxContextTurns (default 6) - only the last N user+assistant pairs go to the model
  • compressToolsAfterTurns (default 1) - older assistant messages keep tool_calls compressed to [tool_name: summary] strings

This prevents model "I already did X" hallucinations that appear in long sessions.

Auto-retry + client fallback

useAiChat.ts implements two layers of robustness:

  1. autoRetry - if tool call failed (validation error or ok: false), send a retry hint with the real id from list_trns
  2. executeFallback - if model still stalls after retry on duplicate_trn / delete_trn / undo_last_action, the client directly executes the mutation and injects the result as a synthetic tool_call. This bypasses model reliability issues.

Idle clear

idleMinutesBeforeClear (default 30) - if the chat was idle for N minutes since last message, history is silently cleared on next send(). Prevents context pollution across sessions.

Model recommendations

ModelSpeedToolsNotes
gemma4:latestfastexcellentDefault. Best speed/accuracy balance
gemma4:26bmediumgoodUse if RAM-rich and want higher accuracy
qwen3:8bfastexcellentAlternative with similar characteristics

See modelMeta.ts. Other Ollama models can be used but are not validated.

Customization

All tunable settings live in aiStore.settings:

  • model, baseUrl, think, autoRetry, debug
  • maxContextTurns, compressToolsAfterTurns, idleMinutesBeforeClear

Accessible via the settings popover in the chat header.

Known limitations

  • Multi-turn create_trn is model-dependent. On gemma4:latest end-to-end follow-up collection (e.g. user says "купил кофе", assistant asks for amount/wallet, assistant calls create_trn after answers) succeeds in roughly 70% of attempts. The model sometimes mis-maps the answer to the wrong parameter (e.g. puts "наличными" into description instead of wallet) or omits type. For reliable multi-turn, prefer qwen3:8b or provide all fields in one message.
  • Chain tools (duplicate_trn, delete_trn, update_trn) can stall on gemma4:latest in long sessions - the model sometimes replies in text asking for an id instead of chaining list_trns → mutation. This is mitigated by client-side fallback (executeFallback in useAiChat.ts) which directly executes the mutation and injects the result as a synthetic tool_call.
  • "I already did that" hallucinations can still appear if maxContextTurns is set too high. The default compressToolsAfterTurns: 1 compresses old tool results to prevent this, but raising the window beyond 8-10 turns brings the risk back.
  • Local network dependency. If Ollama is not running at baseUrl, the chat is unusable. There is no remote fallback by design - all inference stays local.
  • Validation errors from @tanstack/ai are reported as state: 'done' (not error) with { error: "..." } in the result. autoRetry handles this via the isTcFailed helper that inspects the result JSON.
  • No bulk reorder of categories - useCategoriesStore does not expose a saveCategoriesOrder helper, so the AI cannot reorder categories.

Adding a new tool

  1. Create app/components/ai/tools/<name>.ts exporting a create*Tools() function
  2. Use toolDefinition({ description, inputSchema, name }) from @tanstack/ai
  3. Return the full result shape you want to render (include ok: true on success)
  4. Register in tools/index.ts
  5. Add a rich card in AiToolCallsList.richKind() if needed
  6. Update useSystemPrompt.ts with a rule describing when to use the tool
  7. Add a bench case in /tmp/ai-bench.mjs for regression testing