GitHub Actions commited on
Commit
33950ca
Β·
1 Parent(s): 0ab7e48

Deploy 607a293

Browse files
Files changed (1) hide show
  1. app/pipeline/nodes/generate.py +85 -70
app/pipeline/nodes/generate.py CHANGED
@@ -264,9 +264,12 @@ def make_generate_node(llm_client: LLMClient, gemini_client=None) -> Callable[[P
264
  complexity="simple",
265
  )
266
  full_answer = ""
267
- async for token in stream:
268
- full_answer += token
269
- writer({"type": "token", "text": token})
 
 
 
270
  return {"answer": full_answer, "sources": source_refs, "path": "enumeration"}
271
 
272
  # ── Not-found path ────────────────────────────────────────────────────────────
@@ -337,73 +340,85 @@ def make_generate_node(llm_client: LLMClient, gemini_client=None) -> Callable[[P
337
  token_chunk_count = 0 # number of token chunks received
338
  think_first_emitted = False # CoT first-sentence status already sent
339
 
340
- async for token in stream:
341
- raw_answer += token
342
- token_chunk_count += 1
343
-
344
- if think_done or no_cot:
345
- # Phase 2: emit all but the last _TAG_BOUNDARY_WINDOW chars so that
346
- # a split closing tag ("</thi" in chunk N, "nk>" in chunk N+1) is
347
- # always buffered until complete before routing to the token event.
348
- phase2_buf += token
349
- if len(phase2_buf) > _TAG_BOUNDARY_WINDOW:
350
- emit_part = phase2_buf[:-_TAG_BOUNDARY_WINDOW]
351
- phase2_buf = phase2_buf[-_TAG_BOUNDARY_WINDOW:]
352
- writer({"type": "token", "text": emit_part})
353
- continue
354
-
355
- buf += token
356
-
357
- if not in_think:
358
- if "<think>" in buf:
359
- in_think = True
360
- pre = buf[: buf.index("<think>")]
361
- if pre.strip():
362
- # Text before the think tag is part of the answer.
363
- writer({"type": "token", "text": pre})
364
- buf = buf[buf.index("<think>") + 7:] # 7 = len("<think>")
365
- elif token_chunk_count >= _THINK_LOOKAHEAD:
366
- # No CoT block in first 50 chunks β€” emit buffered and go direct.
367
- no_cot = True
368
- writer({"type": "token", "text": buf})
369
- buf = ""
370
- else:
371
- # Phase 1: inside the <think> block; buffer until </think>.
372
- if "</think>" in buf:
373
- idx = buf.index("</think>")
374
- think_txt = buf[:idx].strip()
375
- after_think = buf[idx + 9:] # 9 = len("</think>")
376
-
377
- if think_txt and not think_first_emitted:
378
- # Surface the first sentence as a legible status label.
379
- for j, ch in enumerate(think_txt):
380
- if ch in ".?!\n":
381
- first_sent = think_txt[: j + 1].strip()[:120]
382
- writer({"type": "status", "label": first_sent})
383
- think_first_emitted = True
384
- break
385
-
386
- if think_txt:
387
- writer({"type": "thinking", "text": think_txt})
388
-
389
- think_done = True
390
- buf = ""
391
- if after_think.strip():
392
- writer({"type": "token", "text": after_think})
393
-
394
- # Flush Phase 2 tail buffer β€” strip any orphaned closing think tags that
395
- # arrived in the final window before emitting to the client.
396
- if phase2_buf:
397
- clean_tail = _THINK_CLOSE_RE.sub("", phase2_buf)
398
- if clean_tail:
399
- writer({"type": "token", "text": clean_tail})
400
-
401
- # Flush Phase 1 / lookahead buffer if stream ended mid-detection.
402
- # Apply canonical strip so an incomplete opening tag doesn't surface.
403
- if buf:
404
- clean_buf = _strip_think_tags(buf)
405
- if clean_buf:
406
- writer({"type": "token", "text": clean_buf})
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  # ── Strip CoT scratchpad β€” single canonical pass ──────────────────────
409
  # _strip_think_tags is the ONLY think-tag removal in the codebase.
 
264
  complexity="simple",
265
  )
266
  full_answer = ""
267
+ try:
268
+ async for token in stream:
269
+ full_answer += token
270
+ writer({"type": "token", "text": token})
271
+ except (Exception, asyncio.CancelledError) as exc:
272
+ logger.warning("Enumeration stream interrupted: %s / Returning partial answer.", exc)
273
  return {"answer": full_answer, "sources": source_refs, "path": "enumeration"}
274
 
275
  # ── Not-found path ────────────────────────────────────────────────────────────
 
340
  token_chunk_count = 0 # number of token chunks received
341
  think_first_emitted = False # CoT first-sentence status already sent
342
 
343
+ try:
344
+ async for token in stream:
345
+ raw_answer += token
346
+ token_chunk_count += 1
347
+
348
+ if think_done or no_cot:
349
+ # Phase 2: emit all but the last _TAG_BOUNDARY_WINDOW chars so that
350
+ # a split closing tag ("</thi" in chunk N, "nk>" in chunk N+1) is
351
+ # always buffered until complete before routing to the token event.
352
+ phase2_buf += token
353
+ if len(phase2_buf) > _TAG_BOUNDARY_WINDOW:
354
+ emit_part = phase2_buf[:-_TAG_BOUNDARY_WINDOW]
355
+ phase2_buf = phase2_buf[-_TAG_BOUNDARY_WINDOW:]
356
+ writer({"type": "token", "text": emit_part})
357
+ continue
358
+
359
+ buf += token
360
+
361
+ if not in_think:
362
+ if "<think>" in buf:
363
+ in_think = True
364
+ pre = buf[: buf.index("<think>")]
365
+ if pre.strip():
366
+ # Text before the think tag is part of the answer.
367
+ writer({"type": "token", "text": pre})
368
+ buf = buf[buf.index("<think>") + 7:] # 7 = len("<think>")
369
+ elif token_chunk_count >= _THINK_LOOKAHEAD:
370
+ # No CoT block in first 50 chunks β€” emit buffered and go direct.
371
+ no_cot = True
372
+ writer({"type": "token", "text": buf})
373
+ buf = ""
374
+ else:
375
+ # Phase 1: inside the <think> block; buffer until </think>.
376
+ if "</think>" in buf:
377
+ idx = buf.index("</think>")
378
+ think_txt = buf[:idx].strip()
379
+ after_think = buf[idx + 9:] # 9 = len("</think>")
380
+
381
+ if think_txt and not think_first_emitted:
382
+ # Surface the first sentence as a legible status label.
383
+ for j, ch in enumerate(think_txt):
384
+ if ch in ".?!\n":
385
+ first_sent = think_txt[: j + 1].strip()[:120]
386
+ writer({"type": "status", "label": first_sent})
387
+ think_first_emitted = True
388
+ break
389
+
390
+ if think_txt:
391
+ writer({"type": "thinking", "text": think_txt})
392
+
393
+ think_done = True
394
+ buf = ""
395
+ if after_think.strip():
396
+ writer({"type": "token", "text": after_think})
397
+
398
+ # Flush Phase 2 tail buffer β€” strip any orphaned closing think tags that
399
+ # arrived in the final window before emitting to the client.
400
+ if phase2_buf:
401
+ clean_tail = _THINK_CLOSE_RE.sub("", phase2_buf)
402
+ if clean_tail:
403
+ writer({"type": "token", "text": clean_tail})
404
+
405
+ # Flush Phase 1 / lookahead buffer if stream ended mid-detection.
406
+ # Apply canonical strip so an incomplete opening tag doesn't surface.
407
+ if buf:
408
+ clean_buf = _strip_think_tags(buf)
409
+ if clean_buf:
410
+ writer({"type": "token", "text": clean_buf})
411
+ except (Exception, asyncio.CancelledError) as exc:
412
+ logger.warning("Generation stream interrupted: %s / Returning partial answer.", exc)
413
+ # Flush whatever we have if the stream was cut
414
+ if buf:
415
+ clean_buf = _strip_think_tags(buf)
416
+ if clean_buf:
417
+ writer({"type": "token", "text": clean_buf})
418
+ if phase2_buf:
419
+ clean_tail = _THINK_CLOSE_RE.sub("", phase2_buf)
420
+ if clean_tail:
421
+ writer({"type": "token", "text": clean_tail})
422
 
423
  # ── Strip CoT scratchpad β€” single canonical pass ──────────────────────
424
  # _strip_think_tags is the ONLY think-tag removal in the codebase.