Summarize streams with real progress: adapters reassemble SSE/NDJSON deltas and report 0-99% as tokens arrive

This commit is contained in:
avi 2026-09-15 19:45:02 -05:00
commit 7fa99e8291
8 changed files with 246 additions and 42 deletions

View file

@ -90,6 +90,49 @@ def _thread_progress_reporter(job_id):
return report
def _async_progress_writer(job_id):
"""Progress callback for providers that stream on our own event loop
(the LLM adapters). Unlike _thread_progress_reporter there is no
worker thread: schedule the tiny write as a task on the running loop.
Same contract as the thread version — best-effort display state — and
identical values are coalesced so a chatty stream does not hammer
SQLite."""
loop = asyncio.get_running_loop()
last = -1
def report(pct: int) -> None:
nonlocal last
if int(pct) == last:
return
last = int(pct)
async def _write() -> None:
from sqlalchemy import update
from shonar.db.session import session_factory
try:
async with session_factory()() as s:
await s.execute(
update(ProcessingJob)
.where(ProcessingJob.id == job_id)
.values(progress=last)
)
await s.commit()
except Exception: # pragma: no cover - display state only
logger.debug("summarize progress write failed for job %s",
job_id, exc_info=True)
try:
loop.create_task(_write())
except RuntimeError: # loop already gone (shutdown race)
logger.debug("summarize progress dropped for job %s", job_id)
return report
MAX_TRIES = 3
# A `running` job younger than this is treated as live work, not a crash
@ -437,8 +480,9 @@ async def run_summarize(ctx: dict, recording_id: str) -> None:
rec.processing_status = ProcessingStatus.processing
await session.commit() # visible before the long LLM call
try:
result = await provider.summarize(text, title=rec.title,
tone=job.tone)
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()