feat(agent): migrate to native CrewAI tool loop and async resume enqueue

This commit is contained in:
zl-q
2026-03-08 16:01:16 +08:00
parent 120df903d2
commit 8a23018b6d
29 changed files with 2234 additions and 1115 deletions
@@ -25,22 +25,39 @@ from models.system_agents import SystemAgents
async def test_run_then_resume_persists_messages_and_session_state(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def _fake_execute(self, *, user_input: str) -> dict[str, object]:
del user_input
return {
"assistant_text": "Mocked answer",
"prompt_tokens": 11,
"completion_tokens": 7,
"total_tokens": 18,
"cost": 0.0025,
"agui_events": [
{"type": "TEXT_MESSAGE_START", "data": {"session_id": "__TBD__"}},
{
"type": "TEXT_MESSAGE_CONTENT",
"data": {"session_id": "__TBD__", "text": "Mocked answer"},
call_count = {"n": 0}
def _fake_execute(
self,
*,
user_input: str,
system_prompt: str | None = None,
tools: list[dict[str, object]] | None = None,
) -> dict[str, object]:
del self, user_input, system_prompt, tools
call_count["n"] += 1
if call_count["n"] == 1:
return {
"assistant_text": "请确认是否跳转。",
"prompt_tokens": 11,
"completion_tokens": 7,
"total_tokens": 18,
"cost": 0.0025,
"pending_front_tool": {
"name": "front.navigate_to_route",
"args": {"target": "/calendar/dayweek", "replace": False},
"target": "frontend",
},
{"type": "TEXT_MESSAGE_END", "data": {"session_id": "__TBD__"}},
],
"agui_events": [],
}
return {
"assistant_text": "已继续执行并完成。",
"prompt_tokens": 3,
"completion_tokens": 2,
"total_tokens": 5,
"cost": 0.001,
"pending_front_tool": None,
"agui_events": [],
}
monkeypatch.setattr(
@@ -85,12 +102,17 @@ async def test_run_then_resume_persists_messages_and_session_state(
await seed_session.commit()
published: list[str] = []
queued_commands: list[dict[str, object]] = []
async def _publish(event: dict[str, object]) -> None:
event_type = event.get("type")
if isinstance(event_type, str):
published.append(event_type)
async def _enqueue(command: dict[str, object]) -> str:
queued_commands.append(command)
return "task-followup-1"
try:
run_input_payload = {
"threadId": str(session_uuid),
@@ -101,7 +123,7 @@ async def test_run_then_resume_persists_messages_and_session_state(
],
"tools": [
{
"name": "navigate_to_route",
"name": "front.navigate_to_route",
"description": "navigate route",
"parameters": {"type": "object"},
}
@@ -115,6 +137,7 @@ async def test_run_then_resume_persists_messages_and_session_state(
"run_input": run_input_payload,
},
publish_event=_publish,
enqueue_command=_enqueue,
run_service=RunService(),
resume_service=ResumeService(),
)
@@ -138,7 +161,7 @@ async def test_run_then_resume_persists_messages_and_session_state(
"toolCallId": pending_tool_call_id,
"content": json.dumps(
{
"toolName": "navigate_to_route",
"toolName": "front.navigate_to_route",
"toolArgs": {
"target": "/calendar/dayweek",
"replace": False,
@@ -158,6 +181,16 @@ async def test_run_then_resume_persists_messages_and_session_state(
},
},
publish_event=_publish,
enqueue_command=_enqueue,
run_service=RunService(),
resume_service=ResumeService(),
)
assert len(queued_commands) == 1
await run_agent_task(
queued_commands[0],
publish_event=_publish,
enqueue_command=_enqueue,
run_service=RunService(),
resume_service=ResumeService(),
)
@@ -168,8 +201,8 @@ async def test_run_then_resume_persists_messages_and_session_state(
assert db_session is not None
assert db_session.status == AgentChatSessionStatus.COMPLETED
assert db_session.message_count == 4
assert db_session.total_tokens == 18
assert db_session.total_cost == Decimal("0.002500")
assert db_session.total_tokens == 23
assert db_session.total_cost == Decimal("0.003500")
assert db_session.state_snapshot == {
"status": "completed",
"pending_tool_call_id": None,
@@ -193,6 +226,7 @@ async def test_run_then_resume_persists_messages_and_session_state(
assert messages[1].input_tokens == 11
assert messages[1].output_tokens == 7
assert messages[1].cost == Decimal("0.002500")
assert messages[3].content == "已继续执行并完成。"
assert "RUN_STARTED" in published
assert "RUN_FINISHED" in published
@@ -134,7 +134,7 @@ def test_patch_me_validation_error_returns_problem_details() -> None:
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unprocessable Content"
assert body["title"] in {"Unprocessable Content", "Unprocessable Entity"}
assert body["status"] == 422
finally:
app.dependency_overrides = {}
@@ -17,9 +17,7 @@ class _FakeAgentService:
def __init__(self) -> None:
self._stream_called = False
async def enqueue_run(
self, *, run_input: RunAgentInput, current_user: CurrentUser
):
async def enqueue_run(self, *, run_input: RunAgentInput, current_user: CurrentUser):
del current_user
return SimpleNamespace(
task_id="task-run-1",
@@ -287,3 +285,64 @@ def test_run_rejects_oversized_user_text_payload() -> None:
assert response.status_code == 422
finally:
app.dependency_overrides = {}
def test_run_rejects_client_supplied_history_messages() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/agent/runs",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-history",
"state": {},
"messages": [
{"id": "a1", "role": "assistant", "content": "old"},
{"id": "u1", "role": "user", "content": "new"},
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert response.status_code == 422
finally:
app.dependency_overrides = {}
def test_resume_accepts_tool_message_without_user_message() -> None:
app.dependency_overrides[get_agent_service] = lambda: _FakeAgentService()
app.dependency_overrides[get_current_user] = lambda: CurrentUser(
id=uuid4(), email="user@example.com"
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/agent/runs/00000000-0000-0000-0000-000000000001/resume",
json={
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-resume-1",
"state": {},
"messages": [
{
"id": "tool-1",
"role": "tool",
"toolCallId": "call-1",
"content": '{"toolName":"navigate_to_route","toolArgs":{"target":"/calendar/dayweek"},"nonce":"n1","result":{"ok":true}}',
}
],
"tools": [],
"context": [],
"forwardedProps": {},
},
)
assert response.status_code == 202
assert response.json()["taskId"] == "task-resume-1"
finally:
app.dependency_overrides = {}