Commit 6c43e95
authored
fix(chat): persist user message and partial reply when stream is aborted (eneo-ai#349) (eneo-ai#412)
* fix(chat): persist user message and partial reply when stream is aborted (eneo-ai#349)
Two-phase persistence so an aborted SSE stream no longer loses the user's
question or the partially-streamed assistant reply.
Phase 1: SessionService.create_question_placeholder inserts a Questions row
with the user's question and an empty answer before the LLM stream begins.
The router's existing setup transaction commits this row before any byte
streams out, so it is durable regardless of what happens during streaming.
Phase 2: complete_question_with_answer (normal completion) or the new
module-level persist_partial_question_answer (abort path) update the row.
The abort helper opens a fresh sessionmanager.session() instead of riding
the request-scoped AsyncSession, which FastAPI tears down concurrently
with the SSE close. The streaming generator's finally fires the partial
save via asyncio.create_task — no await across GeneratorExit, no reliance
on a dying session.
Pattern applied to both assistant_service._handle_response and the
group_chat_service selector-echo path. The main group-chat happy path
delegates to assistant_service.ask and is covered automatically.
Frontend: ChatService.askQuestion no longer early-returns on abort,
letting reloadHistory run so the sidebar reflects the persisted state
without a manual refresh.
Removed the now-orphan SessionService.add_question_to_session — zero
callers in src, tests, frontend, or docs after the swap. 2523 unit tests
remain green; 6 new unit tests + 4 new integration tests against real
Postgres cover the new paths.
Closes eneo-ai#349.
* style(sessions): apply ruff format to session_service.py
* fix(chat): harden abort persistence per review (eneo-ai#412)
Addresses four issues raised in PR review:
1. count_tokens safety. tiktoken can raise on unknown model names, which
previously killed the asyncio.create_task call in the streaming
generator's finally and silently collapsed the entire partial save.
New safe_count_tokens helper catches and falls back to 0. Used both in
create_question_placeholder (seeds num_tokens_question) and in the two
_handle_response finally blocks.
2. Skip redundant UPDATE when no content streamed. If the LLM errors
before yielding any chunks (or the user aborts that early), the
placeholder already captures the question and an answer="" UPDATE is
a no-op. Add `if not completed and response_string` guard. Document
that placeholder rows can appear with answer="" after pre-stream LLM
errors — that is intentional, the row reflects what the user asked.
3. Strong-ref background tasks. asyncio.create_task internally holds
only a weak reference; without a strong ref a GC pass can cancel the
task mid-flight (silent data loss on exactly the path this PR exists
to protect). New schedule_background_save helper keeps a module-level
set and removes via add_done_callback. Both assistant_service and
group_chat_service now route through it.
4. Backfill num_tokens_question on placeholder insert. create_question_placeholder
now seeds num_tokens_question via safe_count_tokens(question, model),
so usage analytics no longer undercount aborted requests. The normal-
completion path overwrites with provider-reported counts as before.
Adds 5 new unit tests:
- placeholder seeds num_tokens_question from count_tokens
- placeholder falls back to 0 when count_tokens raises
- placeholder records 0 when no completion model
- streaming generator skips partial save when response_string is empty
- streaming generator schedules partial save even when count_tokens raises
- schedule_background_save keeps strong ref until task completion1 parent b36b2b5 commit 6c43e95
7 files changed
Lines changed: 1424 additions & 252 deletions
File tree
- backend
- src/intric
- assistants
- group_chat/application
- questions
- sessions
- tests
- integration/services
- unit
- frontend/apps/web/src/lib/features/chat
Large diffs are not rendered by default.
Lines changed: 72 additions & 35 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
337 | 337 | | |
338 | 338 | | |
339 | 339 | | |
| 340 | + | |
340 | 341 | | |
341 | 342 | | |
342 | 343 | | |
343 | 344 | | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
344 | 349 | | |
345 | 350 | | |
346 | 351 | | |
347 | | - | |
348 | 352 | | |
349 | | - | |
350 | | - | |
351 | | - | |
352 | | - | |
353 | | - | |
354 | | - | |
355 | | - | |
356 | | - | |
357 | | - | |
358 | | - | |
359 | | - | |
360 | | - | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
361 | 384 | | |
362 | | - | |
363 | | - | |
364 | | - | |
365 | | - | |
366 | | - | |
367 | | - | |
368 | | - | |
369 | | - | |
370 | | - | |
371 | | - | |
372 | | - | |
373 | | - | |
374 | | - | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | | - | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
379 | 408 | | |
380 | 409 | | |
381 | 410 | | |
382 | 411 | | |
383 | 412 | | |
384 | 413 | | |
385 | | - | |
386 | | - | |
| 414 | + | |
| 415 | + | |
387 | 416 | | |
388 | 417 | | |
389 | 418 | | |
390 | | - | |
391 | 419 | | |
392 | 420 | | |
393 | | - | |
394 | 421 | | |
395 | | - | |
396 | 422 | | |
397 | 423 | | |
398 | 424 | | |
| |||
466 | 492 | | |
467 | 493 | | |
468 | 494 | | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
469 | 504 | | |
470 | 505 | | |
471 | 506 | | |
472 | 507 | | |
473 | 508 | | |
474 | 509 | | |
| 510 | + | |
475 | 511 | | |
476 | 512 | | |
477 | 513 | | |
| |||
484 | 520 | | |
485 | 521 | | |
486 | 522 | | |
| 523 | + | |
487 | 524 | | |
488 | 525 | | |
489 | 526 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
129 | 131 | | |
130 | 132 | | |
131 | 133 | | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
132 | 205 | | |
133 | 206 | | |
134 | 207 | | |
| |||
0 commit comments