feat: 迁移奶龙风控业务模块与演示文档
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""风控 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("config/jwt/jwt-private.pem").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())
|
||||
Reference in New Issue
Block a user