Skip to content

feat(content): LLM-backed "Why this?" explainer for recommendations#586

Merged
arberx merged 1 commit into
mainfrom
feat/recommendation-explain-llm
May 18, 2026
Merged

feat(content): LLM-backed "Why this?" explainer for recommendations#586
arberx merged 1 commit into
mainfrom
feat/recommendation-explain-llm

Conversation

@arberx
Copy link
Copy Markdown
Member

@arberx arberx commented May 18, 2026

Summary

Phase 1 of the LLM-augmented recommendation engine. The heuristic classifier still produces the structured recommendation; this layer adds an on-demand LLM rationale for each content recommendation card that explains why it surfaced and what to do about it — cached per (project, target_ref, prompt_version) so repeat clicks are free.

  • New GET /content/recommendations/:targetRef/analysis (cache-only read) + POST .../analyze (cached or fresh LLM call, with provider / model / forceRefresh overrides).
  • LLM wiring stays out of api-routes — the route accepts an injected ExplainContentRecommendationFn. Canonry wires the pi-ai implementation using the new analyze capability tier from feat(agent): LLM capability tiers — per-provider model selection per task #585, so each provider picks an appropriately cheap-but-capable model (Claude → sonnet-4-6, OpenAI → gpt-5-mini, Gemini → 2.5-flash, Zai → glm-5-turbo).
  • Missing implementation → 503 PROVIDER_ERROR with operator-friendly message; unknown provider override → 400; configured-but-keyless override → 502.

Frontend "Why this?" UI ships in a follow-up PR (see roadmap below).

Architecture notes

api-routes (LLM-agnostic)
  └── ExplainContentRecommendationFn (injected)
          │
canonry (owns LLM wiring)
  └── createRecommendationExplainer({ config })
        ├── pickExplainProvider  → mirrors Aero's detectAgentProvider
        ├── resolveModelForCapability(provider, 'analyze')  ← PR #585
        └── pi-ai complete()  → dollars → millicents

Cache key is (projectId, targetRef, promptVersion). Bumping RECOMMENDATION_EXPLAIN_PROMPT_VERSION invalidates forward without touching the table.

Test plan

  • pnpm -r typecheck — clean
  • pnpm run lint — 0 errors (101 pre-existing warnings, none in new files)
  • npx vitest run259 files, 3114 passed (+29 new tests covering route handlers + helper unit tests with mocked complete(): provider selection, override validation, cache hit/miss, forceRefresh overwrite, dollar→millicent conversion, empty-response guard, missing-explainer 503 path)
  • SDK regen — GET/POST endpoints + DTOs flow through @ainyc/canonry-api-client

Roadmap (follow-ups)

  • PR C: Frontend "Why this?" button on each content recommendation card → expandable panel calling the new endpoint, with provider dropdown for power users
  • Phase 2: LLM page-coverage auto-dismiss (crawl candidate pages, classify if they already address the recommendation)
  • Phase 3: Content brief generation as a separate action card

Builds on #585 (capability tiers).

🤖 Generated with Claude Code

Adds an on-demand LLM rationale for each content recommendation card, cached
per (project, target_ref, prompt_version) so repeat clicks are free. The
heuristic classifier still produces the structured recommendation; this layer
explains the reasoning and suggests concrete next steps in natural language.

Backend:
- New `recommendation_explanations` table + migration v62 (idx on
  project_id + targetRef + promptVersion unique).
- `RecommendationExplanationDto` + `RecommendationExplainRequest` schemas
  in @ainyc/canonry-contracts.
- `GET /projects/:name/content/recommendations/:targetRef/analysis`
  cache-only read (404 when no cached explanation exists).
- `POST /projects/:name/content/recommendations/:targetRef/analyze`
  returns cached row or invokes injected `ExplainContentRecommendationFn`;
  supports `provider` / `model` / `forceRefresh` overrides.

LLM wiring (api-routes stays LLM-agnostic):
- `ExplainContentRecommendationFn` is dependency-injected via
  `ApiRoutesOptions.explainContentRecommendation`. Missing implementation
  → 503 PROVIDER_ERROR with operator-friendly message.
- `packages/canonry` ships the pi-ai implementation
  (`createRecommendationExplainer`) using the new `analyze` capability
  tier from PR #585 — Claude → sonnet-4-6, OpenAI → gpt-5-mini,
  Gemini → 2.5-flash, Zai → glm-5-turbo.
- Provider selection mirrors Aero: caller override → first-configured
  by priority → 502 PROVIDER_ERROR. Unknown override → 400.
- pi-ai dollar cost converted to `costMillicents` (×100,000, rounded
  int) so totals stay drift-free.

Tests: 29 new (route handlers + helper unit tests with mocked
`complete()` — provider selection, override validation, cache hit/miss,
forceRefresh overwrite, cost conversion, empty-response guard).

Closes phase 1 of the LLM-augmented recommendation engine. Frontend
"Why this?" panel ships in a follow-up PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arberx arberx merged commit e0658d2 into main May 18, 2026
11 checks passed
@arberx arberx deleted the feat/recommendation-explain-llm branch May 18, 2026 01:28
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