feat(agent): migrate to native CrewAI tool loop and async resume enqueue
This commit is contained in:
@@ -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 = {}
|
||||
|
||||
Reference in New Issue
Block a user