fix(agent): v4.15.1 - protocole tool_calls conforme + plus de repli silencieux sur le mock (echos)
- L'engine envoie le message assistant avec ses tool_calls (id) et chaque resultat d'outil avec son tool_call_id, y compris en cas de refus; la passe suivante n'est plus refusee par l'API - Un fournisseur reel configure qui echoue remonte maintenant une erreur (SSE error) au lieu de repeter la question via le mock hors-ligne (mock reserve a offline/sans cle) - llm_client conserve id + arguments_raw des tool_calls - test de regression test_engine_tool_protocol_messages
This commit is contained in:
+45
-2
@@ -354,11 +354,54 @@ def test_engine_run_creates_document_in_workspace_and_titles_conversation(client
|
||||
assert "Projet" in title
|
||||
|
||||
|
||||
class _Resp:
|
||||
def __init__(self, text="", tool_calls=None):
|
||||
self.text = text
|
||||
self.tool_calls = tool_calls or []
|
||||
self.usage = {}
|
||||
|
||||
|
||||
def test_engine_tool_protocol_messages(client):
|
||||
"""Le message assistant qui annonce un outil porte ses `tool_calls`, et le
|
||||
résultat d'outil est renvoyé avec le `tool_call_id` correspondant — sans quoi
|
||||
le fournisseur refuse la passe suivante (et l'agent retombait sur le mock)."""
|
||||
from app.services.agent_engine import AgentEngine
|
||||
|
||||
class FakeLLM:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
async def complete(self, messages, *, model=None, tools=None, stream=False):
|
||||
self.calls.append([dict(m) for m in messages])
|
||||
if len(self.calls) == 1:
|
||||
return _Resp("Plan de création.",
|
||||
[{"id": "call_abc", "name": "create_collection",
|
||||
"arguments": {"name": "ProtoCol"}}])
|
||||
return _Resp("Collection ProtoCol créée.", [])
|
||||
|
||||
fake = FakeLLM()
|
||||
engine = AgentEngine(_admin_id(), llm=fake)
|
||||
conv = client.post("/api/agent/conversations",
|
||||
json={"title": "protocol"}).json()["id"]
|
||||
events = asyncio.run(_run_engine(engine, conv, "crée la collection ProtoCol"))
|
||||
|
||||
finals = [e for e in events if e["type"] == "final"]
|
||||
assert any("ProtoCol" in e.get("content", "") for e in finals), events
|
||||
|
||||
# la 2e passe reçoit un bloc assistant `tool_calls` + un résultat d'outil lié
|
||||
call2 = fake.calls[1]
|
||||
asm = next((m for m in call2 if m["role"] == "assistant" and m.get("tool_calls")), None)
|
||||
tmsg = next((m for m in call2 if m["role"] == "tool"), None)
|
||||
assert asm is not None and tmsg is not None
|
||||
assert asm["tool_calls"][0]["id"] == "call_abc"
|
||||
assert tmsg["tool_call_id"] == asm["tool_calls"][0]["id"]
|
||||
payload = json.loads(tmsg["content"])
|
||||
assert payload["status"] == "ok"
|
||||
|
||||
|
||||
def _make_conversation(client) -> int:
|
||||
resp = client.post("/api/agent/conversations", json={"title": "test"})
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
# ── Router: agents, conversations, run, skills ──
|
||||
|
||||
def test_router_list_agents(client):
|
||||
|
||||
Reference in New Issue
Block a user