457 lines
16 KiB
Python
457 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.testclient import TestClient
|
|
|
|
from server.utils.auth_middleware import get_db, get_required_user
|
|
|
|
agent_invocation_router_module = importlib.import_module("server.routers.agent_invocation_router")
|
|
|
|
|
|
def _build_app(monkeypatch: pytest.MonkeyPatch, *, authenticated: bool = True) -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(agent_invocation_router_module.agent_invocation_router, prefix="/api")
|
|
|
|
async def fake_db():
|
|
return object()
|
|
|
|
app.dependency_overrides[get_db] = fake_db
|
|
if authenticated:
|
|
|
|
async def fake_user():
|
|
return SimpleNamespace(uid="user-1", role="user", department_id=1)
|
|
|
|
app.dependency_overrides[get_required_user] = fake_user
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
def test_agent_call_run_requires_authentication(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch, authenticated=False)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "user", "content": "Hello"}]},
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_agent_eval_run_returns_service_payload(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_create_agent_eval_run_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"status": "completed",
|
|
"output": "final answer",
|
|
"agent_run_id": "run-1",
|
|
"request_id": "req-1",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"create_agent_eval_run_view",
|
|
fake_create_agent_eval_run_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/eval/runs",
|
|
json={
|
|
"query": "2+2=?",
|
|
"agent_slug": " default-chatbot ",
|
|
"evaluation": {"dataset_name": "dataset-1", "dataset_item_id": "item-1", "ignored": "nope"},
|
|
"meta": {"request_id": "req-1", "attachment_file_ids": ["file-1"]},
|
|
"model_spec": "provider:model",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["output"] == "final answer"
|
|
assert calls["kwargs"]["query"] == "2+2=?"
|
|
assert calls["kwargs"]["agent_slug"] == " default-chatbot "
|
|
assert calls["kwargs"]["evaluation"] == {"dataset_name": "dataset-1", "dataset_item_id": "item-1"}
|
|
assert calls["kwargs"]["meta"] == {"request_id": "req-1", "attachment_file_ids": ["file-1"]}
|
|
assert calls["kwargs"]["model_spec"] == "provider:model"
|
|
assert calls["kwargs"]["current_user"].uid == "user-1"
|
|
|
|
|
|
def test_agent_eval_run_rejects_too_long_request_id(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/eval/runs",
|
|
json={
|
|
"query": "2+2=?",
|
|
"agent_slug": "default-chatbot",
|
|
"meta": {"request_id": "x" * 65},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "request_id 不能超过 64 个字符"
|
|
|
|
|
|
def test_agent_eval_run_returns_504_when_wait_times_out(monkeypatch: pytest.MonkeyPatch):
|
|
async def fake_create_agent_eval_run_view(**_kwargs):
|
|
raise HTTPException(
|
|
status_code=504,
|
|
detail={"message": "运行仍在进行中,等待最终结果超时", "run": {"status": "running"}},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"create_agent_eval_run_view",
|
|
fake_create_agent_eval_run_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/eval/runs",
|
|
json={"query": "2+2=?", "agent_slug": "default-chatbot", "meta": {"request_id": "req-1"}},
|
|
)
|
|
|
|
assert response.status_code == 504
|
|
assert response.json()["detail"]["message"] == "运行仍在进行中,等待最终结果超时"
|
|
assert response.json()["detail"]["run"]["status"] == "running"
|
|
|
|
|
|
def test_legacy_agent_eval_run_path_is_not_registered(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent/eval/runs",
|
|
json={"query": "2+2=?", "agent_slug": "default-chatbot"},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_legacy_agent_call_run_path_is_not_registered(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "user", "content": "Hello"}]},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_agent_call_run_creates_async_run_and_returns_agent_call_payload(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_create_agent_call_run_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"run_id": "run-1",
|
|
"agent_slug": "translator",
|
|
"thread_id": "thread-1",
|
|
"status": "pending",
|
|
"request_id": "req-1",
|
|
"output": "",
|
|
"choices": [{"index": 0, "messages": [{"role": "assistant", "content": ""}], "finish_reason": None}],
|
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"create_agent_call_run_view",
|
|
fake_create_agent_call_run_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": " translator ",
|
|
"messages": [
|
|
{"role": "user", "content": "first"},
|
|
{"role": "assistant", "content": "ignored"},
|
|
{"role": "user", "content": "Hello"},
|
|
],
|
|
"agent_call_meta": {"trace_id": "trace-1"},
|
|
"thread_id": " thread-1 ",
|
|
"request_id": " req-1 ",
|
|
"async_mode": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["run_id"] == "run-1"
|
|
assert response.json()["choices"][0]["finish_reason"] is None
|
|
assert calls["kwargs"]["agent_slug"] == " translator "
|
|
assert calls["kwargs"]["messages"][-1] == {"role": "user", "content": "Hello"}
|
|
assert calls["kwargs"]["agent_call_meta"] == {"trace_id": "trace-1"}
|
|
assert calls["kwargs"]["requested_thread_id"] == " thread-1 "
|
|
assert calls["kwargs"]["request_id"] == " req-1 "
|
|
assert calls["kwargs"]["async_mode"] is True
|
|
assert calls["kwargs"]["stream"] is False
|
|
assert calls["kwargs"]["current_user"].uid == "user-1"
|
|
|
|
|
|
def test_agent_call_run_waits_and_wraps_final_result(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_create_agent_call_run_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"run_id": "run-1",
|
|
"agent_slug": "translator",
|
|
"thread_id": "thread-1",
|
|
"status": "completed",
|
|
"request_id": "req-1",
|
|
"output": "你好",
|
|
"choices": [{"index": 0, "messages": [{"role": "assistant", "content": "你好"}], "finish_reason": "stop"}],
|
|
"usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5},
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"create_agent_call_run_view",
|
|
fake_create_agent_call_run_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": "translator",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"request_id": "req-1",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["run_id"] == "run-1"
|
|
assert response.json()["output"] == "你好"
|
|
assert response.json()["choices"][0]["messages"] == [{"role": "assistant", "content": "你好"}]
|
|
assert response.json()["choices"][0]["finish_reason"] == "stop"
|
|
assert response.json()["usage"] == {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5}
|
|
assert calls["kwargs"]["request_id"] == "req-1"
|
|
|
|
|
|
def test_agent_call_run_returns_504_when_wait_times_out(monkeypatch: pytest.MonkeyPatch):
|
|
async def fake_create_agent_call_run_view(**_kwargs):
|
|
raise HTTPException(
|
|
status_code=504,
|
|
detail={"message": "运行仍在进行中,等待最终结果超时", "run": {"status": "running"}},
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"create_agent_call_run_view",
|
|
fake_create_agent_call_run_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "user", "content": "Hello"}]},
|
|
)
|
|
|
|
assert response.status_code == 504
|
|
assert response.json()["detail"]["message"] == "运行仍在进行中,等待最终结果超时"
|
|
assert response.json()["detail"]["run"]["status"] == "running"
|
|
|
|
|
|
def test_agent_call_run_rejects_context_override(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": "translator",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"agent_call_meta": {"context": {"system_prompt": "只回答中文"}},
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert "agent_call_meta.context 不允许覆盖 Agent context" in response.json()["detail"]
|
|
|
|
|
|
def test_agent_call_run_accepts_openai_text_content_parts(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_create_agent_call_run_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"run_id": "run-1",
|
|
"agent_slug": kwargs["agent_slug"],
|
|
"thread_id": "thread-1",
|
|
"status": "pending",
|
|
"request_id": "req-1",
|
|
"output": "",
|
|
"choices": [{"index": 0, "messages": [{"role": "assistant", "content": ""}], "finish_reason": None}],
|
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
}
|
|
|
|
monkeypatch.setattr(agent_invocation_router_module, "create_agent_call_run_view", fake_create_agent_call_run_view)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": "translator",
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "hello"}]}],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert calls["kwargs"]["messages"] == [{"role": "user", "content": [{"type": "text", "text": "hello"}]}]
|
|
|
|
|
|
def test_agent_call_run_accepts_openai_multimodal_content_parts(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_create_agent_call_run_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"run_id": "run-1",
|
|
"agent_slug": kwargs["agent_slug"],
|
|
"thread_id": "thread-1",
|
|
"status": "pending",
|
|
"request_id": "req-1",
|
|
"output": "",
|
|
"choices": [{"index": 0, "messages": [{"role": "assistant", "content": ""}], "finish_reason": None}],
|
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
}
|
|
|
|
monkeypatch.setattr(agent_invocation_router_module, "create_agent_call_run_view", fake_create_agent_call_run_view)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": "translator",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "describe"},
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": "data:image/png;base64,base64-image", "detail": "low"},
|
|
},
|
|
],
|
|
}
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert calls["kwargs"]["messages"][0]["content"][1]["image_url"] == {
|
|
"url": "data:image/png;base64,base64-image",
|
|
"detail": "low",
|
|
}
|
|
|
|
|
|
def test_agent_call_run_propagates_agent_not_found(monkeypatch: pytest.MonkeyPatch):
|
|
async def fake_create_agent_call_run_view(**_kwargs):
|
|
raise HTTPException(status_code=404, detail="智能体不存在")
|
|
|
|
monkeypatch.setattr(agent_invocation_router_module, "create_agent_call_run_view", fake_create_agent_call_run_view)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "missing", "messages": [{"role": "user", "content": "Hello"}]},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
assert response.json()["detail"] == "智能体不存在"
|
|
|
|
|
|
def test_agent_call_run_rejects_invalid_boundary_payload(monkeypatch: pytest.MonkeyPatch):
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": " ", "messages": [{"role": "user", "content": "Hello"}]},
|
|
)
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "agent_slug 不能为空"
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "user", "content": "Hello"}], "stream": True},
|
|
)
|
|
assert response.status_code == 422
|
|
assert "stream=true" in response.json()["detail"]
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": []},
|
|
)
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "messages 不能为空"
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "assistant", "content": "hello"}]},
|
|
)
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "messages 必须包含 user 消息"
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={"agent_slug": "translator", "messages": [{"role": "user", "content": ""}]},
|
|
)
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "user message content 必须是非空字符串或多模态数组"
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs",
|
|
json={
|
|
"agent_slug": "translator",
|
|
"messages": [{"role": "user", "content": "Hello"}],
|
|
"request_id": "x" * 65,
|
|
},
|
|
)
|
|
assert response.status_code == 422
|
|
assert response.json()["detail"] == "request_id 不能超过 64 个字符"
|
|
|
|
|
|
def test_agent_call_result_returns_service_payload(monkeypatch: pytest.MonkeyPatch):
|
|
calls: dict[str, object] = {}
|
|
|
|
async def fake_get_agent_call_run_result_view(**kwargs):
|
|
calls["kwargs"] = kwargs
|
|
return {
|
|
"run_id": kwargs["run_id"],
|
|
"agent_slug": kwargs["agent_slug"],
|
|
"thread_id": "thread-1",
|
|
"status": "completed",
|
|
"request_id": "req-1",
|
|
"output": "done",
|
|
"choices": [{"index": 0, "messages": [{"role": "assistant", "content": "done"}], "finish_reason": "stop"}],
|
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
agent_invocation_router_module,
|
|
"get_agent_call_run_result_view",
|
|
fake_get_agent_call_run_result_view,
|
|
)
|
|
client = _build_app(monkeypatch)
|
|
|
|
response = client.post(
|
|
"/api/agent-invocation/agent-call/runs/result",
|
|
json={"run_id": "run-1", "agent_slug": "translator"},
|
|
)
|
|
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["output"] == "done"
|
|
assert calls["kwargs"] == {
|
|
"run_id": "run-1",
|
|
"agent_slug": "translator",
|
|
"current_uid": "user-1",
|
|
"db": calls["kwargs"]["db"],
|
|
}
|