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
|
|
@ -39,7 +39,9 @@ from shonar.db.models import (
|
|||
)
|
||||
from shonar.services.ai import (
|
||||
AIError,
|
||||
ProviderConfigError,
|
||||
ProviderTransientError,
|
||||
get_llm_fallback_provider,
|
||||
get_llm_provider,
|
||||
get_transcription_provider,
|
||||
)
|
||||
|
|
@ -479,19 +481,48 @@ async def run_summarize(ctx: dict, recording_id: str) -> None:
|
|||
job.stage = "summarizing"
|
||||
rec.processing_status = ProcessingStatus.processing
|
||||
await session.commit() # visible before the long LLM call
|
||||
fallback_note: str | None = None
|
||||
result_provider = provider.name # overwritten only on the rescue path
|
||||
try:
|
||||
result = await provider.summarize(
|
||||
text, title=rec.title, tone=job.tone,
|
||||
on_progress=_async_progress_writer(job.id))
|
||||
except AIError as e:
|
||||
await _fail(session, rec, job, str(e), ctx, e)
|
||||
await session.commit()
|
||||
return
|
||||
await store_summary(session, rec, result.to_dict(), provider.name,
|
||||
# Transient failures keep their normal retry budget first (the
|
||||
# LAN GPU often recovers on try 2); the rescue summarizer runs
|
||||
# when the primary is OUT of tries or misconfigured, and only
|
||||
# if one is configured. The summary then comes from the
|
||||
# fallback and job.error carries a note for the UI.
|
||||
retryable = (isinstance(e, ProviderTransientError)
|
||||
and _job_try(ctx) < MAX_TRIES)
|
||||
# (result, provider_name, note) when the rescue succeeds.
|
||||
rescue: tuple[object, str, str] | None = None
|
||||
if not retryable:
|
||||
fallback = get_llm_fallback_provider(settings)
|
||||
if fallback is not None:
|
||||
try:
|
||||
fb_result = await fallback.summarize(
|
||||
text, title=rec.title, tone=job.tone,
|
||||
on_progress=_async_progress_writer(job.id))
|
||||
except AIError as fe:
|
||||
logger.warning(
|
||||
"summarize fallback (%s) also failed: %s",
|
||||
fallback.name, fe)
|
||||
else:
|
||||
rescue = (fb_result, fallback.name, (
|
||||
f'primary "{provider.name}" failed ({e}). '
|
||||
f'This summary was written by "{fallback.name}".'))
|
||||
if rescue is None:
|
||||
await _fail(session, rec, job, str(e), ctx, e)
|
||||
await session.commit()
|
||||
return
|
||||
result, result_provider, fallback_note = rescue
|
||||
await store_summary(session, rec, result.to_dict(), result_provider,
|
||||
result.model, tone=job.tone)
|
||||
job.status = JobStatus.succeeded
|
||||
job.stage = None
|
||||
job.progress = 100
|
||||
job.error = fallback_note # None on the clean path: clears stale notes
|
||||
job.finished_at = utcnow()
|
||||
rec.processing_status = ProcessingStatus.completed
|
||||
rec.processing_error = None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue