Files
group_fqcd_jr/tools/risk_agent_e2e.py

113 lines
3.8 KiB
Python

"""风控 Agent 的真实 Agent Run、工具调用和 SSE 验收。"""
from __future__ import annotations
import asyncio
import datetime as dt
import json
import uuid
from pathlib import Path
import httpx
import jwt
from sqlalchemy import text
from app.core.config import get_settings
from app.infrastructure.db import SessionFactory
from app.main import create_app
from app.worker.runtime import WorkerRuntime
PRIVATE_KEY = Path(get_settings().jwt_private_key_path).read_text(encoding="utf-8")
RISK_USER = "9002"
AGENT_TYPE = "risk"
INTENT = "risk_overview"
TOOL = "get_risk_overview"
MESSAGE = "请查看当前风险概览"
def token(subject: str) -> str:
now = dt.datetime.now(dt.UTC)
return jwt.encode(
{
"sub": subject,
"iss": get_settings().jwt_issuer,
"aud": get_settings().jwt_audience,
"exp": now + dt.timedelta(minutes=30),
"nbf": now - dt.timedelta(seconds=5),
"jti": str(uuid.uuid4()),
},
PRIVATE_KEY,
algorithm="RS256",
)
async def audit_rows(trace_id: str) -> list[dict[str, object]]:
async with SessionFactory() as session:
rows = (
await session.execute(
text(
"SELECT action_type, detail FROM interaction_audit "
"WHERE JSON_UNQUOTE(JSON_EXTRACT(detail,'$.trace_id'))=:trace "
"ORDER BY id"
),
{"trace": trace_id},
)
).mappings().all()
result = []
for row in rows:
detail = row["detail"]
if isinstance(detail, str):
detail = json.loads(detail)
result.append({"action_type": row["action_type"], "detail": detail})
return result
async def main() -> None:
app = create_app()
auth = {"Authorization": f"Bearer {token(RISK_USER)}"}
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test",
timeout=120,
) as client:
accepted = await client.post(
"/api/v1/agent-runs",
headers=auth,
json={
"agent_type": AGENT_TYPE,
"message": MESSAGE,
"session_id": f"risk-e2e-{uuid.uuid4()}",
"idempotency_key": uuid.uuid4().hex,
},
)
if accepted.status_code != 202:
raise SystemExit(f"受理失败:{accepted.status_code} {accepted.text}")
data = accepted.json()["data"]
run_id = str(data["run_id"])
trace_id = str(data["trace_id"])
await WorkerRuntime().execute(run_id)
detail = (await client.get(f"/api/v1/agent-runs/{run_id}", headers=auth)).json()["data"]
events = await client.get(
f"/api/v1/agent-runs/{run_id}/events",
headers={**auth, "Accept": "text/event-stream"},
)
audits = await audit_rows(trace_id)
tool_calls = (detail.get("result") or {}).get("tool_calls") or {}
calls = tool_calls.get("calls") or []
print(f"run_id={run_id}")
print(f"status={detail.get('status')} error_code={detail.get('error_code')}")
print(f"tool_calls={json.dumps(calls, ensure_ascii=False)}")
print(f"audit_actions={[row['action_type'] for row in audits]}")
print(f"sse_content_type={events.headers.get('content-type')}")
print(f"sse_has_done={'event: done' in events.text}")
if detail.get("status") != "succeeded":
raise SystemExit(f"风控 Agent 运行失败:{detail}")
if not any(call.get("tool_name") == TOOL for call in calls):
raise SystemExit("未记录 get_risk_overview 工具调用")
if not any(row["action_type"] == "agent.tool_executed" for row in audits):
raise SystemExit("未记录工具审计")
if __name__ == "__main__":
asyncio.run(main())