feat(bot): chat agent on Gemini 2.5 Flash-Lite — grounded first turn, current-prescription defaults
CI / ruff · pytest (pull_request) Successful in 2m5s
CI / ruff · pytest (pull_request) Successful in 2m5s
Live-tested on OpenRouter: Flash-Lite answers in 3–8 s with correct findings at ~$0.10/$0.40 per 1M tokens (qwen3.7-flash and gpt-oss-120b took 26–32 s). Force a tool call on the first turn, give the model the current prescription, let check_drug_for_patient default to it and return composition, intolerance check and general interactions (alcohol); retry once on an empty final message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ from app.llm.guard import assert_scrubbed
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_ANSWER_CHARS = 1200
|
||||
NUDGE = "Сформулируй ответ пациенту по результатам инструментов выше (3–6 предложений)."
|
||||
_ALLOWED_MSG_KEYS = {"role", "content", "tool_calls", "tool_call_id"}
|
||||
|
||||
|
||||
@@ -63,6 +64,15 @@ def build_provider(settings: Settings | None = None) -> ToolChatProvider | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _patient_brief(ctx: ToolContext) -> str:
|
||||
"""One line of context so the model knows what «мои таблетки» means (drug names only, no identifiers)."""
|
||||
rec = await ctx.record()
|
||||
if rec is None or not rec.current_prescriptions:
|
||||
return "\n\nТекущее назначение психиатра: нет данных."
|
||||
drugs = ", ".join(p.drug for p in rec.current_prescriptions)
|
||||
return f"\n\nТекущее назначение психиатра: {drugs}. «Мои таблетки» = это назначение."
|
||||
|
||||
|
||||
def _wire(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Strip internal keys (tool `name`) before sending."""
|
||||
return [{k: v for k, v in m.items() if k in _ALLOWED_MSG_KEYS} for m in messages]
|
||||
@@ -82,14 +92,20 @@ async def answer_chat(
|
||||
return None
|
||||
ctx = ToolContext(token=token)
|
||||
messages: list[dict[str, Any]] = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "system", "content": SYSTEM_PROMPT + await _patient_brief(ctx)},
|
||||
{"role": "user", "content": str(question)},
|
||||
]
|
||||
nudged = False
|
||||
try:
|
||||
async with asyncio.timeout(s.chat_agent_timeout_s * 2):
|
||||
for _ in range(s.chat_agent_max_steps):
|
||||
turn = await provider.chat(_wire(messages) if provider.name != "mock" else messages, TOOL_SPECS)
|
||||
if not turn.tool_calls:
|
||||
if not (turn.content or "").strip() and not nudged:
|
||||
# Some models (Gemini Flash) occasionally end with an empty message after tool results.
|
||||
nudged = True
|
||||
messages.append({"role": "user", "content": NUDGE})
|
||||
continue
|
||||
return _finish(turn.content or "", ctx, provider.name)
|
||||
messages.append(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user