feat: live agent answers (SSE streaming) + «Чат» tab on phones
CI / ruff · pytest (pull_request) Successful in 2m43s

- POST /dialog/{token}/events/stream: Server-Sent Events with the agent's steps («Смотрю вашу карту ЕМИАС…»,
  «Ищу в клинических рекомендациях…»), the answer as the model writes it (OpenRouter stream=true), then the
  final replies (same as /events, incl. the forbidden-output filter and sources). X-Accel-Buffering: no.
- Web: free-text messages use the stream — a live bubble with steps, typing dots and a cursor; falls back to
  the plain request if the stream breaks.
- Phones: the bottom tab bar gets «Чат» (it had no way back to the chat).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 12:52:19 +03:00
co-authored by Claude Opus 5.5
parent d662a69c7e
commit d1f92407f9
11 changed files with 381 additions and 6 deletions
+3
View File
@@ -21,6 +21,7 @@ from pydantic import BaseModel
from app.config import Settings, get_settings
from app.core.types import ScrubbedText
from app.llm.agent import stream
from app.llm.agent.prompts import SYSTEM_PROMPT
from app.llm.agent.providers import (
GigaChatToolProvider,
@@ -127,6 +128,7 @@ async def answer_chat(
return None
if is_small_talk(question):
return AgentAnswer(text=SMALL_TALK_REPLY, sources=[], tools=[], provider="small_talk")
stream.emit("status", "Думаю над вопросом…")
ctx = ToolContext(token=token)
messages: list[dict[str, Any]] = [
{"role": "system", "content": SYSTEM_PROMPT + await _patient_brief(ctx)},
@@ -159,6 +161,7 @@ async def answer_chat(
}
)
for c in turn.tool_calls[:MAX_TOOL_CALLS_PER_TURN]:
stream.emit("status", stream.TOOL_STATUS.get(c.name, "Проверяю данные…"))
result = await run_tool(ctx, c.name, c.arguments)
messages.append({"role": "tool", "tool_call_id": c.id, "name": c.name, "content": result})
log.warning("chat agent: step limit reached tools=%s", ",".join(ctx.used))