Summarize: LAN primary with local-Ollama rescue + in-app failure guidance
- Engine: when the primary summarizer is out of retries (or misconfigured), run_summarize now finishes the job on the configured rescue provider (SHONAR_LLM_FALLBACK_*), tags the summary with the provider that wrote it, and stores a human note in job.error; success clears stale notes. - App (auto/lan): passes Ollama as the rescue provider when it is up. - Detail screen: shows the rescue-swap note in plain words, a red 'Summary failed' line with Settings -> Summarizer fix instructions and a Retry summary button on hard failure. - Settings copy explains the fallback. 3 new pytest cases (14/14 pass); live E2E on 2026-09-18: sarcastic summary v6 via LAN on attempt 2.
This commit is contained in:
parent
cb0492fb3e
commit
3061629dc5
6 changed files with 172 additions and 8 deletions
|
|
@ -110,11 +110,13 @@ class FakeLlm:
|
|||
_UNSET = object()
|
||||
|
||||
|
||||
def use_fakes(monkeypatch, stt=_UNSET, llm=_UNSET):
|
||||
def use_fakes(monkeypatch, stt=_UNSET, llm=_UNSET, fallback=None):
|
||||
tprov = FakeTranscriber() if stt is _UNSET else stt
|
||||
lprov = FakeLlm() if llm is _UNSET else llm
|
||||
monkeypatch.setattr(processing, "get_transcription_provider", lambda settings: tprov)
|
||||
monkeypatch.setattr(processing, "get_llm_provider", lambda settings: lprov)
|
||||
monkeypatch.setattr(processing, "get_llm_fallback_provider",
|
||||
lambda settings: fallback)
|
||||
|
||||
|
||||
async def jobs_for(recording_id):
|
||||
|
|
@ -409,3 +411,56 @@ async def test_ai_endpoints_enforce_ownership(client, monkeypatch):
|
|||
for path in ("transcript", "summary", "jobs"):
|
||||
r = await client.get(f"/api/v1/recordings/{rec['id']}/{path}", headers=h)
|
||||
assert r.status_code == 404, path
|
||||
|
||||
|
||||
# --- summarize fallback to the rescue summarizer --------------------------------
|
||||
|
||||
async def test_summarize_fallback_rescues_after_primary_exhausted(client, monkeypatch):
|
||||
primary = FakeLlm(fail=ProviderTransientError("LLM reply was not JSON."))
|
||||
fb = FakeLlm()
|
||||
fb.name = "fake-ollama"
|
||||
use_fakes(monkeypatch, FakeTranscriber(), primary, fallback=fb)
|
||||
token = await user_tokens(client, email="m7-fb1@example.com")
|
||||
rec = await upload_recording(client, token, client_id="m7-fb-1")
|
||||
await processing.run_transcribe({}, rec["id"])
|
||||
# Last try: primary out of budget -> rescue runs and the note lands in job.error.
|
||||
await processing.run_summarize({"job_try": 3}, rec["id"])
|
||||
job = (await jobs_for(rec["id"]))[JobType.summarize]
|
||||
assert job.status == JobStatus.succeeded
|
||||
assert "fake-llm" in (job.error or "") and "fake-ollama" in (job.error or "")
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
s = await client.get(f"/api/v1/recordings/{rec['id']}/summary", headers=h)
|
||||
assert s.status_code == 200
|
||||
assert s.json()["provider"] == "fake-ollama"
|
||||
assert fb.seen == ["hello world from the meeting"]
|
||||
|
||||
|
||||
async def test_summarize_fallback_also_fails_marks_job_failed(client, monkeypatch):
|
||||
err = ProviderTransientError("LLM reply was not JSON.")
|
||||
fb = FakeLlm(fail=ProviderTransientError("ollama down"))
|
||||
fb.name = "fake-ollama"
|
||||
use_fakes(monkeypatch, FakeTranscriber(), FakeLlm(fail=err), fallback=fb)
|
||||
token = await user_tokens(client, email="m7-fb2@example.com")
|
||||
rec = await upload_recording(client, token, client_id="m7-fb-2")
|
||||
await processing.run_transcribe({}, rec["id"])
|
||||
await processing.run_summarize({"job_try": 3}, rec["id"])
|
||||
job = (await jobs_for(rec["id"]))[JobType.summarize]
|
||||
assert job.status == JobStatus.failed
|
||||
assert "not JSON" in job.error # the primary's error is what we surface
|
||||
assert fb.seen == ["hello world from the meeting"] # fallback was tried
|
||||
|
||||
|
||||
async def test_summarize_keeps_retry_budget_before_fallback(client, monkeypatch):
|
||||
# Try 1 of 3 transient: normal arq retry wins, fallback stays untouched.
|
||||
fb = FakeLlm()
|
||||
fb.name = "fake-ollama"
|
||||
use_fakes(monkeypatch, FakeTranscriber(),
|
||||
FakeLlm(fail=ProviderTransientError("boom")), fallback=fb)
|
||||
token = await user_tokens(client, email="m7-fb3@example.com")
|
||||
rec = await upload_recording(client, token, client_id="m7-fb-3")
|
||||
await processing.run_transcribe({}, rec["id"])
|
||||
with pytest.raises(ProviderTransientError):
|
||||
await processing.run_summarize({"job_try": 1}, rec["id"])
|
||||
job = (await jobs_for(rec["id"]))[JobType.summarize]
|
||||
assert job.status == JobStatus.queued
|
||||
assert fb.seen == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue