Skip to content

feat(llm): proactive insights from home data#723

Closed
cpcloud wants to merge 1091 commits intomainfrom
worktree-velvet-forging-yao
Closed

feat(llm): proactive insights from home data#723
cpcloud wants to merge 1091 commits intomainfrom
worktree-velvet-forging-yao

Conversation

@cpcloud
Copy link
Copy Markdown
Collaborator

@cpcloud cpcloud commented Mar 9, 2026

Summary

  • Add LLM-powered proactive insights as the last dashboard section, opt-in via llm.insights = true config
  • Insights stream incrementally via ChatStream -- items appear as each JSON object completes
  • Three categories: Needs Attention, Gone Quiet, Patterns with sub-header grouping
  • Hard cap of 5 insights total (2 per category) enforced in code + JSON schema maxItems
  • Category sub-headers are decorative (cursor skips them); data rows navigate to entity on enter
  • Non-blocking with spinner; cached per session; auto-invalidated on mutations; r to refresh
  • Staleness indicator on section header; error state as dim "unavailable" message
  • BuildInsightsPrompt() and InsightsJSONSchema() for structured LLM output
  • User-flow tests drive all behavior through keypresses (D, J, j, e, enter, r)

closes #691

cpcloud and others added 30 commits February 22, 2026 06:45
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).
Phased roadmap for the extraction pipeline: Extractor interface
refactor (#465), SQL output replacing ExtractionHints (#474),
vision LLM extractor (#466), and multi-document PDFs (#469).
Includes per-file-type data flow diagrams and VisionLLM trigger
heuristics.

closes #474
…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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/app/dashboard.go
Comment thread internal/app/dashboard.go
Comment thread internal/app/dashboard.go Outdated
cpcloud and others added 19 commits March 10, 2026 20:19
## 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>
Copilot AI review requested due to automatic review settings March 11, 2026 10:16
@cpcloud cpcloud force-pushed the worktree-velvet-forging-yao branch from 51a01a3 to db8a0fd Compare March 11, 2026 10:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/app/dashboard.go
if m.dash.insights.cancel != nil {
m.dash.insights.cancel()
m.dash.insights.cancel = nil
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
}
// 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()

Copilot uses AI. Check for mistakes.
- 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>
@cpcloud cpcloud marked this pull request as draft March 11, 2026 11:08
@cpcloud cpcloud closed this Mar 19, 2026
@cpcloud cpcloud deleted the worktree-velvet-forging-yao branch March 19, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(llm): proactive insights from home data

3 participants