feat(llm): proactive insights from home data#723
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
closes #457 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four dashboard queries could return non-deterministic row order when the primary sort column ties (e.g. two items with the same updated_at): - ListMaintenanceWithSchedule - ListActiveProjects - ListOpenIncidents - ListExpiringWarranties Append `id desc` to each, matching the convention used by every other ORDER BY in the data layer.
Enable all non-experimental, non-opinionated gocritic checks and explicitly list revive in the linter enable set. Disable checks that are noise for a TUI app (hugeParam, rangeValCopy) and stylistic preferences (ifElseChain, singleCaseSwitch). Suppress revive's exported rule. Fix one appendCombine finding in chat.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add revive with enable-default-rules to preserve its default ruleset. Disable exported and package-comments rules. Fix remaining findings: unused parameters (collapse.go, model.go, migrator.go), builtin redefinition of max (model.go), unused spec param in renderPillCell (table.go), and nolint empty-block drains (ddlmod.go, client_test.go). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the text extraction foundation for the document extraction pipeline (#200). Introduces ExtractedText and OCRData columns on Document, a pure-Go PDF text extractor using ledongthuc/pdf, and a design plan document. - Add ExtractedText (string) and OCRData ([]byte) to Document model - Implement ExtractText for PDF, text/*, and markdown MIME types - Add IsScanned heuristic (empty/whitespace text = scanned) - Include test fixture generator and sample.pdf Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the OCR layer for the document extraction pipeline (#200). Scanned PDFs and images are recognized via tesseract + pdftoppm when available, with graceful degradation and a one-time hint when tools are missing. - Add OCR function with PDF rasterization (pdftoppm) and image OCR paths - Parse tesseract TSV output preserving word/line/paragraph structure - Add tool detection with sync.Once caching (HasTesseract, HasPDFToPPM) - Add tesseract + poppler-utils to devShell in flake.nix - Add one-time tesseract hint setting in data/settings.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the LLM extraction layer for the document extraction pipeline (#200). When an extraction model is configured, documents are analyzed to extract vendor, amounts, dates, entity links, and maintenance schedules. - Add ExtractionHints and EntityContext types with validation maps - Build extraction prompt with entity context for LLM matching - Parse flexible LLM JSON responses (money as string/float, multiple date formats, code-fenced responses) - Add [extraction] config section with model, max_ocr_pages, enabled - Wire extraction config through app Model and main.go Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the Pipeline orchestrator that sequences text extraction, OCR, and LLM extraction into a single Run call (#200). Wire it into the document upload flow with entity context from the database. - Add Pipeline.Run orchestrating all three extraction layers - Add Store.EntityNames for LLM entity matching context - Rewrite parseDocumentFormData to use Pipeline with documentParseResult - Add buildExtractionPipeline and showTesseractHint to app Model - Pre-fill document title from LLM suggestion, notes from summary - Surface non-fatal extraction errors via status bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rogress Add an interactive overlay that shows real-time progress when documents are processed through the extraction pipeline (text -> OCR -> LLM). Each step displays a spinner, elapsed time, and detail (page count, model name, character count). Users accept results with `a` or cancel with `esc`. Key changes: - Extraction overlay with step navigation (j/k), expand/collapse (enter) - Channel-based OCR streaming with per-page and rasterization progress - LLM token streaming in overlay with accumulated JSON display - Accept/cancel flow: results held until user presses `a` - Proper context cancellation: esc cancels all in-flight work - OCR failure gracefully continues to LLM step - currency_unit field in extraction schema for cents/dollars disambiguation - Configurable pdftotext timeout (extraction.text_timeout / MICASA_TEXT_TIMEOUT) - ExtractionPromptInput struct replaces 7 positional params - Cached extraction LLM client on model - Docs: configuration, keybindings, and documents guide updated closes #200 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VHS tape that demonstrates the document extraction overlay: importing a scanned PDF, OCR progress, LLM extraction, and accepting results. Requires Ollama running with qwen3:0.6b. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combine sample.pdf (digital text) and scanned-invoice.pdf (image pages) into a 109KB mixed-inspection.pdf fixture. The pipeline test verifies that pdftotext extracts digital pages while OCR handles scanned ones -- the common case for real-world inspection reports and permits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without these tools, all OCR and PDF text extraction tests are skipped in CI. Install poppler-utils (pdftotext, pdftoppm) and tesseract-ocr on all three platforms so the pipeline tests exercise real code paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace checked-in binary test fixtures (sample.pdf, invoice.png, scanned-invoice.pdf, mixed-inspection.pdf) with 4 bash scripts that generate them on demand. Fixtures are now gitignored and generated via shell hook (local dev) or CI step (with shell: bash for Windows). - gen-sample-pdf.bash: base64-embedded minimal PDF (no deps) - gen-invoice-png.bash: magick-generated realistic invoice image - gen-scanned-pdf.bash: magick image-to-PDF conversion - gen-mixed-pdf.bash: pdfunite digital + scanned pages - 5 nix apps: 4 individual + gen-testdata combined - CI: imagemagick added to all platforms, shell: bash fixture step closes #200 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use Shift+F to jump directly to Docs tab - Add new document instead of editing existing one - Increase terminal height for overlay centering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tape Clamp base view to terminal height before overlay compositing so the extraction overlay centers correctly when opened from an in-place form save (where the form content overflows the terminal). Fix expanded log content: add left border pipe (│) to visually separate log output from step headers, add blank line spacing between expanded steps, and use directional triangles (▾ down when expanded, ▸ right when collapsed). Fix the demo tape: enter Edit mode before Shift+A, use a temp directory with just the test PDF so the file picker navigation is deterministic. closes #200 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ubuntu 22.04/24.04 ship ImageMagick v6 which only provides `convert`, not the v7 `magick` command. Symlink convert to magick on Linux CI so gen scripts work uniformly across all platforms. Add gen-sample-text-png.bash for the OCR image integration test fixture. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Choco's poppler package extracts binaries into a nested directory that isn't on PATH. Split the install into two steps: choco install in PowerShell, then find pdfunite.exe and add its directory to GITHUB_PATH in bash. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The choco poppler package only ships source code, not compiled binaries. Drop it from Windows CI and gracefully skip gen-mixed-pdf when pdfunite is unavailable. The mixed-PDF test already skips when the fixture is missing.
macOS ImageMagick needs ghostscript for text rendering (magick -annotate). Without it, fixture images are blank and tesseract returns empty text. Also add skipOrFatalCI helper: tests skip locally when tools are missing, but fail hard in CI on Linux/macOS where all tools should be installed.
Tests for the two main gaps in coverage: - Pipeline with LLM: mock httptest server returns canned extraction JSON, verifying the full text -> LLM -> parsed hints path. Also tests LLM server down, garbage response, and no-text skip. - OCRWithProgress: empty data, context cancellation, and integration tests for image and PDF paths (need tesseract/pdftoppm in CI).
…n walker The procedural applyEnvOverrides was a 56-line if-chain that grew with every new config knob. Env var names were disconnected from the fields they mapped to, making it easy to add a field but forget the override. Replace it with `env` struct tags on every overridable field and a reflection walker that reads them at load time. This makes the env var name the single source of truth, co-located with the field definition. Additionally: - applyEnvOverrides is now fallible: invalid env var values produce actionable errors (e.g. MICASA_MAX_OCR_PAGES="garbage": expected integer) instead of being silently ignored - OLLAMA_HOST /v1 normalization moved to post-processing in LoadFromPath - EnvVars() derives env-to-config-key mapping from struct tags - Get()/Keys() resolve dot-delimited TOML keys via reflection - plans/config-library-evaluation.md documents the evaluation of koanf, viper, and caarlos0/env Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `micasa config --get <key>` for reading resolved config values using dot-delimited TOML keys (e.g. llm.model, documents.max_file_size). Useful for scripting and debugging without parsing the TOML file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
## Summary - Add rule to Nix section of AGENTS.md requiring Python to be run via `nix run 'nixpkgs#python3' -- $@` when Nix is available, never bare `python`/`python3` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary - Add `/rev` skill: rebase onto main, address unresolved PR review feedback (GraphQL thread fetching with `databaseId` for REST replies, auto-resolve when confident), and delegate CI fixes to `/fix-ci` - Add `/fix-ci` skill: standalone skill to diagnose failing CI from `link` URLs, fetch logs, fix, verify locally, and push - Register both skill triggers in AGENTS.md --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Surface LLM-powered insights as the last dashboard section when llm.insights = true in config. Insights are generated on-demand when the dashboard opens, rendered non-blocking with a spinner, cached per session, and auto-invalidated on mutations. Each insight row navigates to the relevant tab/entity. Press `r` to manually refresh. - Add `Insights *bool` config field with TOML/env support - Add `insightsState` to track loading, items, staleness, errors - Add `fetchInsights()` async tea.Cmd using ChatComplete + JSON schema - Add `BuildInsightsPrompt()` and `InsightsJSONSchema()` in llm package - Add dashboard section rendering with spinner, error, staleness - Add navigable rows that jump to target tab/entity - Wire stale invalidation into `reloadAfterMutation()` - Add comprehensive tests across config, dashboard, and prompt packages closes #691 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Render insights errors with the standard dashSectionHeader pill and a DashSubtitle (gray dim italic) message instead of bold vermillion, so the error reads as a non-critical degradation aligned with the other dashboard sections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace field-setting tests with proper user-interaction tests that drive behavior through keypresses: D to open dashboard, J/k to navigate sections, e to expand, j to enter rows, enter to jump, r to refresh. Deliver LLM results via insightsResultMsg through Update() instead of setting model fields directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `context_length` config option for Ollama's num_ctx parameter, allowing users to control the context window size. Implemented via an HTTP transport wrapper that intercepts Ollama API requests since any-llm-go hardcodes num_ctx=32000. - Add `WithNoThinking()` chat option to disable reasoning for structured JSON output, preventing thinking tokens from consuming the response budget. - Include entity IDs in DataDump() so the LLM can reference correct database IDs for insight navigation. - Add empty response guard with actionable error message. - Bump any-llm-go v0.8.0 -> v0.9.0 (anthropic-sdk-go, ollama, genai). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Check req.Body.Close() return value (errcheck) - Wrap RoundTrip error in numCtxTransport (wrapcheck) - Add nolint:gosec for context cancel stored in struct field - Replace require with error handling in HTTP test handlers (testifylint) - Use assert.InDelta for float comparisons (testifylint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
golines splits the call across lines, placing the nolint on the closing paren where gosec ignores it. Standalone comment on the preceding line ensures gosec sees it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Gate dashboard refresh key on insightsWanted() (checks both insightsEnabled and llmClient) instead of insightsEnabled alone - Validate context_length >= 0 during config load with clear error - Update DataDump() doc comment to reflect that entity IDs are now included Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…header
- Update plan doc to describe the actual JSON object format
({"insights": [...]}) instead of a bare JSON array
- Only show insights error state when there are no cached items,
preventing a duplicate "Insights" header after a failed refresh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Insights with a valid tab but entity_id=0 (group-level observations) were incorrectly marked InfoOnly, showing "house data, not in any tab" on Enter. Now they navigate to the tab without selecting a specific row. InfoOnly is reserved for insights with truly unrecognized tab names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Put spinner on same line as section pill instead of separate line - Add 30s periodic tick to keep staleness timestamp fresh while dashboard is open (stops when closed) - Fix "now ago" → "just now" for <1min staleness - Require entity_id >= 1: update prompt rules, JSON schema minimum, example, and add defense-in-depth filter in fetchInsights() - Forbid numeric IDs in insight text (names only) - Update tests for new entity_id requirements and spinner rendering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add generation ID to correlate insights requests with results, discarding stale responses from canceled goroutines - Cancel in-flight insights on dashboard close, nil cancel func after completion to prevent context leaks - Refactor dashboard rendering: replace three separate loading/error/ staleness blocks with unified section loop + tracked header line index - Add nav entry for loading/error insights state so cursor stays aligned - Fix numCtxTransport to create options map when absent, not just when the library already sends one - Remove brittle assertion on exact library default num_ctx value - Wire context_length through extraction pipeline (extractionConfig, extractState, SetExtraction, extractionLLMClient) - Fix doc comments and plan doc to match implementation (JSON object, entity_id minimum 1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The preamble rule wasn't sufficient for smaller models. Duplicate the constraint in the output format guidelines where the LLM reads field definitions, and clarify that entity_id is the correct place for IDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Insights pill uses muted (rose) color to visually distinguish LLM-generated content from deterministic dashboard sections - Remove "refreshing insights" status bar message -- the spinner on the pill is sufficient feedback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove loading guard from dashInsightsRows so cached insight items remain visible while a refresh is in-flight (spinner still shows on the header line). - Strip ANSI escapes and take only the first line of error messages before rendering in the dashboard overlay to prevent layout breakage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- fetchInsights now uses context.WithTimeout with the configured LLM inference timeout instead of a bare cancel-only context. - refreshInsights explicitly cancels in-flight requests and clears the loading flag before calling maybeStartInsights, so pressing r during a fetch actually restarts it. - maybeStartInsights schedules insightsStaleTick when reusing cached results so the "x ago" indicator keeps updating on dashboard reopen. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch from ChatComplete to ChatStream so insight items appear in the dashboard as each complete JSON object finishes streaming. Add structured categories (attention, stale, pattern) to produce genuinely useful insights grouped by urgency. Streaming: - Add insightsStreamStartedMsg and insightsChunkMsg message types - Add parsePartialInsights for bracket-balanced JSON extraction - Rebuild dashboard nav on each chunk for immediate navigability - Use standard spinner.Dot Categories: - Add insightCategory type with three values: attention (needs action), stale (forgotten/abandoned items), pattern (non-obvious trends) - Rewrite prompt to emphasize non-obvious observations and natural language, discouraging regurgitation of obvious dashboard data - Group insights by category with sub-header rows in the dashboard - Update JSON schema to require category field closes #748 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51a01a3 to
db8a0fd
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if m.dash.insights.cancel != nil { | ||
| m.dash.insights.cancel() | ||
| m.dash.insights.cancel = nil | ||
| } |
There was a problem hiding this comment.
cancelInsights() only cancels the context and clears cancel/streamCh, but it does not reset m.dash.insights.loading. If the dashboard is closed while insights are loading (toggleDashboard() calls cancelInsights()), loading can remain true indefinitely, keeping the spinner Tick handler active and preventing maybeStartInsights() from ever starting a new fetch on the next dashboard open. Consider setting loading=false (and likely resetting streamBuf / optionally bumping generation to ignore late chunks) when canceling an in-flight request.
| } | |
| } | |
| // Ensure we don't remain in a "loading" state after cancellation and | |
| // discard any partially buffered stream data from the aborted request. | |
| m.dash.insights.loading = false | |
| m.dash.insights.streamBuf.Reset() |
- Hard cap: maxInsights=5, maxInsightsPerCategory=2 enforced in code - JSON schema maxItems=5 as defense-in-depth - Tighter prompt: "AT MOST 5 insights", "Quality over quantity" - Category sub-headers (Needs Attention, Gone Quiet, Patterns) marked Skip=true so cursor jumps past them during j/k navigation - dashDown/dashUp/dashBottom skip decorative entries via dashNavSkippable - New Skip field on dashNavEntry distinct from InfoOnly (preserves insurance renewal and other info-only rows as cursor targets) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
llm.insights = trueconfigChatStream-- items appear as each JSON object completesmaxItemsrto refreshBuildInsightsPrompt()andInsightsJSONSchema()for structured LLM outputD,J,j,e,enter,r)closes #691