fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return
This commit is contained in:
+30
-3
@@ -18,6 +18,10 @@ Implements just enough of the aipi surface:
|
||||
- user message containing ``think out loud`` -> the answer is preceded by
|
||||
~800 chars of deterministic ``reasoning_content`` chunks (the
|
||||
thinking-display story, phase 17).
|
||||
- user message containing ``think out loud then hesitate`` -> the
|
||||
``think out loud`` stream, then a 4s pause before the first content
|
||||
frame (the sources-midstream story, phase 20 — a deterministic
|
||||
"leave during pure thinking" navigation window).
|
||||
- system prompt containing ``<tuning>`` (phase 15, steering notes) ->
|
||||
the composed answer ends with `` (tuning: <first note line>)`` —
|
||||
makes prompt injection observable in the UI deterministically.
|
||||
@@ -89,6 +93,16 @@ LONG_ANSWER_END = "LONG-ANSWER-END"
|
||||
#: contain the substring, so every other suite is unaffected.
|
||||
THINKING_TRIGGER = "think out loud"
|
||||
|
||||
#: Phase 20 (sources-midstream bug): a user message containing this
|
||||
#: substring (case-insensitive) gets the phase-17 thinking stream followed
|
||||
#: by a multi-second pause before the FIRST content frame — the
|
||||
#: navigation window for the "leave during pure thinking" scenario
|
||||
#: (owner-confirmed A1.2: nothing brain-side may be persisted then).
|
||||
#: Strictly longer than ``THINKING_TRIGGER``, so the phase-17 suite's
|
||||
#: questions are unaffected.
|
||||
SLOW_PRETOKEN_TRIGGER = "think out loud then hesitate"
|
||||
PRE_CONTENT_PAUSE_S = 4.0
|
||||
|
||||
|
||||
def long_answer() -> str:
|
||||
"""~900-word deterministic walkthrough (phase 11): numbered steps plus
|
||||
@@ -222,7 +236,9 @@ def embeddings(body: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _sse_stream(answer: str, delay: float, thinking: str = "") -> Any:
|
||||
def _sse_stream(
|
||||
answer: str, delay: float, thinking: str = "", pre_content_delay: float = 0.0
|
||||
) -> Any:
|
||||
"""SSE frames for one chat completion (phase 17: + reasoning).
|
||||
|
||||
When ``thinking`` is non-empty its 12-char slices go out FIRST as
|
||||
@@ -230,6 +246,11 @@ def _sse_stream(answer: str, delay: float, thinking: str = "") -> Any:
|
||||
as the content frames, the aipi wire convention (reasoning before
|
||||
content). Without ``thinking`` the output is byte-identical to the
|
||||
content-only stream, so the other story suites are unaffected.
|
||||
|
||||
``pre_content_delay`` (phase 20) inserts a silence gap between the end
|
||||
of the thinking stream and the first content frame — the client stays
|
||||
in its pre-token "thinking" state the whole time (0.02s cadence and
|
||||
frame shapes are unchanged, so 0.0 is byte-identical to before).
|
||||
"""
|
||||
model = "turbo"
|
||||
chunk_id = f"chatcmpl-{uuid.uuid4()}"
|
||||
@@ -247,6 +268,8 @@ def _sse_stream(answer: str, delay: float, thinking: str = "") -> Any:
|
||||
}
|
||||
yield f"data: {json_dumps(payload)}\n\n"
|
||||
time.sleep(0.02)
|
||||
if pre_content_delay:
|
||||
time.sleep(pre_content_delay)
|
||||
for piece in re.findall(r".{1,12}", answer, re.S):
|
||||
payload = {
|
||||
"id": chunk_id,
|
||||
@@ -295,7 +318,11 @@ def _apply_max_tokens(answer: str, max_tokens: Any) -> str:
|
||||
def chat_completions(body: dict[str, Any]) -> Any:
|
||||
answer = _apply_max_tokens(compose_answer(body), body.get("max_tokens"))
|
||||
delay = 3.0 if "pretend to think slowly" in _user(body) else 0.0
|
||||
thinking = compose_thinking(body) if THINKING_TRIGGER in _user(body).lower() else ""
|
||||
user_lower = _user(body).lower()
|
||||
thinking = compose_thinking(body) if THINKING_TRIGGER in user_lower else ""
|
||||
pre_content = (
|
||||
PRE_CONTENT_PAUSE_S if SLOW_PRETOKEN_TRIGGER in user_lower else 0.0
|
||||
)
|
||||
|
||||
if not body.get("stream"):
|
||||
message: dict[str, Any] = {"role": "assistant", "content": answer}
|
||||
@@ -315,7 +342,7 @@ def chat_completions(body: dict[str, Any]) -> Any:
|
||||
}
|
||||
|
||||
return StreamingResponse(
|
||||
_sse_stream(answer, delay, thinking=thinking),
|
||||
_sse_stream(answer, delay, thinking=thinking, pre_content_delay=pre_content),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user