Skip to content

Phase 6: LLM layer (local Ollama, guarded, three-rung fallback) - #17

Merged
withrvr merged 1 commit into
claude/erp-collection-reporting-workflow-iycqtofrom
phase/06-llm-layer
Aug 31, 2026
Merged

withrvr merged 1 commit into
claude/erp-collection-reporting-workflow-iycqtofrom
phase/06-llm-layer

Conversation

@withrvr

@withrvr withrvr commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Phase 6: LLM layer

What this adds

Three-rung narrative generation: local Ollama (phi4-mini via LiteLLM), an optional cloud model (never required), then the Phase 5 deterministic Jinja template. Every rung's output is numeric-guarded -- an invented number is rejected outright, never surfaced. Run.summary_source records which rung actually produced a given run's narrative.

Assessment requirement served

MASTER_PLAN.md Phase 6 done-when: "the pipeline completes with the network disconnected, and llm_calls_total shows local calls succeeding." Verified against a real, unmocked local Ollama instance (not just unit-tested) -- Ollama never touches the internet, so this holds literally.

Docs updated

  • README.md - [x] ARCHITECTURE.md - [x] API.md
  • RULES.md (no rule changes) - [x] DEVELOPMENT.md - [x] CHANGELOG.md

Verification

uv run pytest app/collections/tests -v   # 154 passed (142 -> 154), ~90s incl. real Ollama calls
uv run mypy app/collections/ai app/collections/control app/collections/config.py app/collections/observability app/collections/service.py app/collections/models.py app/collections/api   # clean (besides the pre-existing openpyxl stub gap)

Also verified through the actual Docker Compose stack: docker compose exec backend reaches the real API end to end, correctly degrading to summary_source: "fallback" since the container can't reach Windows-bound Ollama without an extra OLLAMA_HOST=0.0.0.0 step (documented in ARCHITECTURE.md).

Known gaps carried to next phase

  • Phase 7's exception_explainer and Phase 10's schema_mapper are the first real consumers of ai/context.py's anydoc conversion -- not called by anything yet.
  • No /metrics Prometheus scrape endpoint yet -- llm_calls_total is an in-process counter; Phase 12 formalizes it.

…rung fallback

Every run's narrative can now come from a real local LLM (phi4-mini via
Ollama) -- but never has to, and never gets the final word: a numeric
guard rejects any output that invents a number, falling through to the
deterministic template built in Phase 5. Verified end to end against a
real, unmocked Ollama instance, not just unit-tested in isolation.

- ai/provider.py: call_ollama/call_cloud, the LiteLLM seam. Cloud is
  opt-in only (COLLECTIONS_CLOUD_LLM_MODEL), never required.
- ai/guard.py: numbers_are_contained -- every number in an LLM output
  must appear in its input, or the output is rejected outright.
- ai/roles/summary_narrator.py: narrate(), the three-rung chain
  (Ollama -> cloud -> ai/fallback.render_summary), numeric-guarded at
  every rung. Never raises -- the deterministic third rung always
  succeeds.
- ai/context.py: anydoc workbook-to-Markdown for LLM context only --
  not consumed by any Phase 6 role (summary_narrator narrates the
  already-computed metrics dict, never raw sheets); ready for Phase
  7's exception_explainer and Phase 10's schema_mapper.
- observability/metrics.py: llm_calls_total, a plain in-process
  counter for now -- Phase 6's own done-when, verified directly
  against real local Ollama calls.
- models.py: Run.summary_source records which rung actually produced
  a run's narrative, surfaced via GET /collections/summary.
- compose.override.yml: dev backend wired toward Ollama at
  host.docker.internal:11434, with the binding caveat documented
  (Ollama must listen on 0.0.0.0, not loopback-only, for a container
  to reach it -- verified the fallback chain degrades correctly
  either way).

New dependencies: litellm, firecrawl-anydoc.

Refs: docs/ARCHITECTURE.md's LLM layer section, docs/API.md
@withrvr
withrvr merged commit b531205 into claude/erp-collection-reporting-workflow-iycqto Aug 31, 2026
3 of 7 checks passed
@withrvr
withrvr deleted the phase/06-llm-layer branch August 31, 2026 19:30
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.

1 participant