feat(calls): optional LLM transcript summaries (LC-396) #410

Merged
longjacksonle merged 1 commit from feat/lc-396-transcript-llm-summary into main 2026-06-20 08:55:55 +02:00

The optional final piece of the LC-393 transcription epic: an operator-selectable AI summary / action-items for saved transcripts. Filed as LC-396.

What it does

When LETS_CHAT_LLM_URL is set, the transcript page gains a "Summarize" action: it sends the transcript text to an OpenAI-compatible /v1/chat/completions endpoint (Ollama, llama.cpp server, vLLM, LocalAI, ...) and stores the returned markdown summary + action items. Cached summaries render on the page; a Regenerate button re-runs it. When no LLM is configured the UI is hidden entirely.

Architecture (mirrors the Phase 3 STT pattern)

  • New llm module: LlmConfig::from_env (LETS_CHAT_LLM_URL / _API_KEY / _MODEL, default model gpt-4o-mini), an LlmClient trait with ReqwestLlmClient (chat-completions POST) + MockLlmClient. AppState gains llm_client: Option<...> + llm_available().
  • Storage: call_transcripts.summary column (migration 0067), cached so it's computed once. Summary is markdown, rendered through the same markdown pipeline as messages, and length-capped on the write path (LC-153).
  • POST /transcripts/{id}/summary: gated like the transcript page, builds the transcript text (bounded prompt), calls the LLM, stores + returns the rendered fragment (htmx swap). 400 when no LLM is configured.
  • Trusted endpoint: reached through the same blessed, NOT-SSRF-filtered http_client::outbound_trusted_post path as STT, so a localhost model works. System prompt asks for a concise ## Summary + ## Action items list.

Testing

  • tests/transcripts.rs -> 12 green: summary_generates_stores_and_gates (mock LLM -> fragment + persisted + shown on the page + non-member 403) and summary_rejected_when_llm_disabled (400).
  • just check, just test, just test-saas all exit 0 (migration 0067 picked up by the common test pools; all 87 plain AppState literals updated, the transcripts harness injects a mock). just build-css regenerated.
  • Manual (needs a live model): the actual completion quality.

Operator-action

Optional: set LETS_CHAT_LLM_URL (+ optional _API_KEY / _MODEL) to an OpenAI-compatible chat-completions endpoint to enable AI transcript summaries; leave unset to hide the feature. Documented in CLAUDE.md.

With this, the LC-393 transcription epic (Phases 1-4) is complete.

Note

Branches off main; #408 (archive) and #409 (export/search) are still open and also touch transcripts/show.html in different sections - if they land first this rebases cleanly (the summary block is a separate part of the page).

The optional final piece of the LC-393 transcription epic: an operator-selectable **AI summary / action-items** for saved transcripts. Filed as LC-396. ## What it does When `LETS_CHAT_LLM_URL` is set, the transcript page gains a **"Summarize"** action: it sends the transcript text to an OpenAI-compatible `/v1/chat/completions` endpoint (Ollama, llama.cpp server, vLLM, LocalAI, ...) and stores the returned markdown summary + action items. Cached summaries render on the page; a **Regenerate** button re-runs it. When no LLM is configured the UI is hidden entirely. ## Architecture (mirrors the Phase 3 STT pattern) - **New `llm` module**: `LlmConfig::from_env` (`LETS_CHAT_LLM_URL` / `_API_KEY` / `_MODEL`, default model `gpt-4o-mini`), an `LlmClient` trait with `ReqwestLlmClient` (chat-completions POST) + `MockLlmClient`. `AppState` gains `llm_client: Option<...>` + `llm_available()`. - **Storage**: `call_transcripts.summary` column (migration `0067`), cached so it's computed once. Summary is markdown, rendered through the **same markdown pipeline as messages**, and length-capped on the write path (LC-153). - **`POST /transcripts/{id}/summary`**: gated like the transcript page, builds the transcript text (bounded prompt), calls the LLM, stores + returns the rendered fragment (htmx swap). 400 when no LLM is configured. - **Trusted endpoint**: reached through the same blessed, NOT-SSRF-filtered `http_client::outbound_trusted_post` path as STT, so a localhost model works. System prompt asks for a concise `## Summary` + `## Action items` list. ## Testing - `tests/transcripts.rs` -> 12 green: `summary_generates_stores_and_gates` (mock LLM -> fragment + persisted + shown on the page + non-member 403) and `summary_rejected_when_llm_disabled` (400). - `just check`, `just test`, `just test-saas` all exit 0 (migration 0067 picked up by the common test pools; all 87 plain `AppState` literals updated, the transcripts harness injects a mock). `just build-css` regenerated. - Manual (needs a live model): the actual completion quality. ## Operator-action Optional: set `LETS_CHAT_LLM_URL` (+ optional `_API_KEY` / `_MODEL`) to an OpenAI-compatible chat-completions endpoint to enable AI transcript summaries; leave unset to hide the feature. Documented in CLAUDE.md. With this, the LC-393 transcription epic (Phases 1-4) is complete. ## Note Branches off `main`; #408 (archive) and #409 (export/search) are still open and also touch `transcripts/show.html` in different sections - if they land first this rebases cleanly (the summary block is a separate part of the page).
feat(calls): optional LLM transcript summaries [operator-action] (LC-396)
All checks were successful
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 3m10s
a410751b2c
The optional final piece of the LC-393 transcription epic: an operator-selectable AI summary / action-items for a saved transcript.

When LETS_CHAT_LLM_URL is set, the transcript page gains a "Summarize" action that sends the transcript text to an OpenAI-compatible /v1/chat/completions endpoint (Ollama, llama.cpp server, vLLM, LocalAI, ...) and stores the returned markdown summary + action items, cached in a new call_transcripts.summary column (migration 0067). Cached summaries render on the page; a Regenerate button re-runs it.

- New `llm` module mirroring the Phase 3 `stt` pattern: LlmConfig::from_env (LETS_CHAT_LLM_URL/_API_KEY/_MODEL, default model gpt-4o-mini), an LlmClient trait with ReqwestLlmClient + MockLlmClient. AppState gains llm_client: Option<...> + llm_available().
- POST /transcripts/{id}/summary: gated like the transcript page, builds the transcript text (bounded), calls the LLM, stores the summary (length-capped for the LC-153 markdown render), returns the rendered fragment (htmx swap). 400 when no LLM is configured.
- The endpoint is operator-trusted and reached through the same blessed, NOT-SSRF-filtered http_client::outbound_trusted_post path as STT, so a localhost model works.
- The summary markdown is rendered through the existing message markdown pipeline; the UI is hidden entirely when no LLM is configured.

All 87 plain test AppState literals get llm_client: None; the transcripts harness injects a mock. Tests cover generate+store+render, the disabled-400, and non-member gating.

Operator-Action: optional - set LETS_CHAT_LLM_URL (+ optional _API_KEY / _MODEL) to an OpenAI-compatible chat-completions endpoint to enable AI transcript summaries; leave unset to hide the feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/lc-396-transcript-llm-summary 2026-06-20 08:55:56 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!410
No description provided.