mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-23 13:15:29 -04:00
fix ask-user choices across reloads (#4669)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""Regression coverage for durable ``ask_user`` choice cards.
|
||||
|
||||
The live event must arrive after ``tool_output`` so the settled tool trace
|
||||
cannot cover/push away the card. The same payload must be persisted inside
|
||||
``tool_events`` so chat history can reconstruct it after a reload.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import src.agent_loop as agent_loop
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _collect(gen):
|
||||
async def _run():
|
||||
return [chunk async for chunk in gen]
|
||||
|
||||
return asyncio.run(_run())
|
||||
|
||||
|
||||
def _events(chunks):
|
||||
events = []
|
||||
for chunk in chunks:
|
||||
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"):
|
||||
events.append(json.loads(chunk[6:]))
|
||||
return events
|
||||
|
||||
|
||||
def test_ask_user_is_emitted_last_and_persisted(monkeypatch):
|
||||
payload = {
|
||||
"question": "¿Qué proyecto prefieres?",
|
||||
"options": [
|
||||
{"label": "Análisis de reseñas"},
|
||||
{"label": "Clasificación temática"},
|
||||
],
|
||||
"multi": False,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default, raising=False)
|
||||
monkeypatch.setattr(agent_loop, "get_mcp_manager", lambda: None, raising=False)
|
||||
monkeypatch.setattr(agent_loop, "estimate_tokens", lambda *args, **kwargs: 10, raising=False)
|
||||
|
||||
async def fake_stream(_candidates, messages, **kwargs):
|
||||
call = {"name": "ask_user", "arguments": json.dumps(payload, ensure_ascii=False)}
|
||||
yield f'data: {json.dumps({"type": "tool_calls", "calls": [call]})}\n\n'
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
async def fake_execute(block, *args, **kwargs):
|
||||
parsed = json.loads(block.content)
|
||||
return (
|
||||
"ask_user",
|
||||
{
|
||||
"ask_user": parsed,
|
||||
"output": "Awaiting their selection.",
|
||||
"exit_code": 0,
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream, raising=False)
|
||||
monkeypatch.setattr(agent_loop, "execute_tool_block", fake_execute, raising=False)
|
||||
|
||||
chunks = _collect(
|
||||
agent_loop.stream_agent_loop(
|
||||
"https://api.openai.com/v1",
|
||||
"gpt-4o",
|
||||
[{"role": "user", "content": "Ayúdame a elegir un proyecto."}],
|
||||
relevant_tools={"ask_user"},
|
||||
_is_teacher_run=True,
|
||||
)
|
||||
)
|
||||
events = _events(chunks)
|
||||
|
||||
tool_output_index = next(i for i, event in enumerate(events) if event.get("type") == "tool_output")
|
||||
ask_user_index = next(i for i, event in enumerate(events) if event.get("type") == "ask_user")
|
||||
assert tool_output_index < ask_user_index
|
||||
|
||||
tool_output = events[tool_output_index]
|
||||
assert tool_output["ask_user"] == payload
|
||||
assert "¿Qué proyecto prefieres?" in tool_output["command"]
|
||||
assert "\\u00" not in tool_output["command"]
|
||||
|
||||
metrics = next(event["data"] for event in events if event.get("type") == "metrics")
|
||||
assert metrics["tool_events"][0]["ask_user"] == payload
|
||||
|
||||
|
||||
def test_frontend_uses_one_renderer_for_live_and_restored_cards():
|
||||
chat = (ROOT / "static" / "js" / "chat.js").read_text(encoding="utf-8")
|
||||
renderer = (ROOT / "static" / "js" / "chatRenderer.js").read_text(encoding="utf-8")
|
||||
|
||||
assert "chatRenderer.renderAskUserCard(json.data || {})" in chat
|
||||
assert "export function renderAskUserCard" in renderer
|
||||
assert "renderAskUserCard(pendingAskUser" in renderer
|
||||
assert "if (role === 'user') removeAskUserCards(box)" in renderer
|
||||
@@ -85,6 +85,19 @@ def test_serializer_round_trips_structured_args():
|
||||
assert json.loads(block.content) == args
|
||||
|
||||
|
||||
def test_serializer_keeps_unicode_readable_for_tool_trace():
|
||||
from src.tool_schemas import function_call_to_tool_block
|
||||
|
||||
args = {
|
||||
"question": "¿Qué proyecto prefieres?",
|
||||
"options": [{"label": "Reseñas"}, {"label": "Clasificación"}],
|
||||
}
|
||||
block = function_call_to_tool_block("ask_user", json.dumps(args, ensure_ascii=False))
|
||||
assert "¿Qué proyecto prefieres?" in block.content
|
||||
assert "Reseñas" in block.content
|
||||
assert "\\u00" not in block.content
|
||||
|
||||
|
||||
def test_registered_everywhere():
|
||||
# TOOL_TAGS gate (serializer rejects unknown tools)
|
||||
assert "ask_user" in TOOL_TAGS
|
||||
|
||||
Reference in New Issue
Block a user