feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -0,0 +1,301 @@
from __future__ import annotations
import json
from collections.abc import Generator
from datetime import UTC, datetime, timedelta
import pytest
from auth_helpers import install_legacy_header_auth_override
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.api.deps import get_db
from app.api.v1.endpoints.agent_runs import router as agent_runs_router
from app.db.base import Base
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.services.agent_runs import AgentRunService
from app.services.finance_dashboard_scope import (
FINANCE_DASHBOARD_TASK_TYPE,
resolve_finance_dashboard_data_scope,
)
def _session_factory() -> sessionmaker[Session]:
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=engine)
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
def _run(
*,
run_id: str,
started_at: datetime,
route_tenant_id: str | None,
ontology_tenant_id: str | None,
status: str = "succeeded",
error_message: str | None = None,
) -> AgentRun:
route_json: dict[str, object] = {"stage": "tenant-security-test"}
ontology_json: dict[str, object] = {
"scenario": "expense",
"intent": "query",
}
if route_tenant_id is not None:
route_json["tenant_id"] = route_tenant_id
if ontology_tenant_id is not None:
ontology_json["tenant_id"] = ontology_tenant_id
return AgentRun(
run_id=run_id,
agent="orchestrator",
source="user_message",
user_id=f"user-{run_id}",
ontology_json=ontology_json,
route_json=route_json,
permission_level="read",
status=status,
result_summary=f"summary-{run_id}",
error_message=error_message,
started_at=started_at,
finished_at=started_at + timedelta(seconds=1),
)
def _seed_runs(db: Session) -> None:
now = datetime.now(UTC)
tenant_a = _run(
run_id="run-normal-tenant-a",
started_at=now - timedelta(minutes=4),
route_tenant_id="tenant-a",
ontology_tenant_id="tenant-a",
)
tenant_b = _run(
run_id="run-normal-tenant-b",
started_at=now - timedelta(minutes=3),
route_tenant_id="tenant-b",
ontology_tenant_id="tenant-b",
status="failed",
error_message="tenant-b-run-secret",
)
legacy_unscoped = _run(
run_id="run-normal-unscoped",
started_at=now - timedelta(minutes=2),
route_tenant_id=None,
ontology_tenant_id=None,
status="failed",
error_message="legacy-unscoped-secret",
)
partial_scope = _run(
run_id="run-normal-partial-scope",
started_at=now - timedelta(seconds=90),
route_tenant_id="tenant-a",
ontology_tenant_id=None,
status="failed",
error_message="partial-scope-secret",
)
conflicting = _run(
run_id="run-normal-conflicting",
started_at=now - timedelta(minutes=1),
route_tenant_id="tenant-a",
ontology_tenant_id="tenant-b",
status="failed",
error_message="conflicting-scope-secret",
)
finance_scope = resolve_finance_dashboard_data_scope("tenant-a")
finance_snapshot = _run(
run_id="run-finance-tenant-a",
started_at=now,
route_tenant_id="tenant-a",
ontology_tenant_id="tenant-a",
status="failed",
error_message="finance-snapshot-secret",
)
finance_snapshot.route_json.update(
{
"task_type": FINANCE_DASHBOARD_TASK_TYPE,
"data_scope": finance_scope,
}
)
finance_snapshot.ontology_json["data_scope"] = finance_scope
db.add_all(
[
tenant_a,
tenant_b,
legacy_unscoped,
partial_scope,
conflicting,
finance_snapshot,
]
)
db.flush()
db.add_all(
[
AgentToolCall(
run_id=tenant_a.run_id,
tool_type="database",
tool_name="tenant-a.tool",
request_json={"private": "tenant-a-request"},
response_json={"private": "tenant-a-response"},
status="succeeded",
duration_ms=3,
),
AgentToolCall(
run_id=tenant_b.run_id,
tool_type="database",
tool_name="tenant-b.tool",
request_json={"private": "tenant-b-request-secret"},
response_json={"private": "tenant-b-response-secret"},
status="failed",
duration_ms=5,
error_message="tenant-b-tool-secret",
),
SemanticParseLog(
run_id=tenant_a.run_id,
user_id="tenant-a-user",
raw_query="tenant-a-raw-query",
scenario="expense",
intent="query",
confidence=0.9,
),
SemanticParseLog(
run_id=tenant_b.run_id,
user_id="tenant-b-user",
raw_query="tenant-b-raw-query-secret",
scenario="expense",
intent="query",
confidence=0.9,
),
]
)
db.commit()
def _client(
session_factory: sessionmaker[Session],
) -> TestClient:
app = FastAPI()
app.include_router(agent_runs_router)
install_legacy_header_auth_override(app)
def override_db() -> Generator[Session, None, None]:
with session_factory() as db:
yield db
app.dependency_overrides[get_db] = override_db
return TestClient(app)
def _headers(tenant_id: str, *, is_admin: bool = False) -> dict[str, str]:
return {
"X-Auth-Username": f"user-{tenant_id}",
"X-Auth-Role-Codes": "user",
"X-Auth-Is-Admin": "true" if is_admin else "false",
"X-Auth-Tenant-Id": tenant_id,
}
def test_agent_run_endpoints_fail_closed_for_normal_cross_tenant_runs() -> None:
session_factory = _session_factory()
with session_factory() as db:
_seed_runs(db)
client = _client(session_factory)
tenant_a_list = client.get(
"/agent-runs",
params={"limit": 1},
headers=_headers("tenant-a"),
)
tenant_a_summary = client.get(
"/agent-runs/summary",
params={"limit": 1},
headers=_headers("tenant-a"),
)
tenant_b_list = client.get(
"/agent-runs",
headers=_headers("tenant-b"),
)
assert tenant_a_list.status_code == 200
assert [item["run_id"] for item in tenant_a_list.json()] == ["run-normal-tenant-a"]
assert tenant_a_summary.status_code == 200
assert tenant_a_summary.json()["total_runs"] == 1
assert tenant_a_summary.json()["failed_runs"] == 0
assert tenant_a_summary.json()["recent_errors"] == []
assert tenant_b_list.status_code == 200
assert [item["run_id"] for item in tenant_b_list.json()] == ["run-normal-tenant-b"]
tenant_a_payload = json.dumps(
[tenant_a_list.json(), tenant_a_summary.json()],
ensure_ascii=False,
)
assert "tenant-b" not in tenant_a_payload
assert "legacy-unscoped-secret" not in tenant_a_payload
assert "partial-scope-secret" not in tenant_a_payload
assert "conflicting-scope-secret" not in tenant_a_payload
assert "finance-snapshot-secret" not in tenant_a_payload
own_detail = client.get(
"/agent-runs/run-normal-tenant-a",
headers=_headers("tenant-a"),
)
assert own_detail.status_code == 200
assert own_detail.json()["tool_calls"][0]["request_json"] == {"private": "tenant-a-request"}
assert own_detail.json()["semantic_parse"]["raw_query"] == "tenant-a-raw-query"
hidden_run_ids = [
"run-normal-tenant-b",
"run-normal-unscoped",
"run-normal-partial-scope",
"run-normal-conflicting",
]
for run_id in hidden_run_ids:
response = client.get(
f"/agent-runs/{run_id}",
headers=_headers("tenant-a", is_admin=True),
)
assert response.status_code == 404
response_body = json.dumps(response.json(), ensure_ascii=False)
assert "tenant-b-request-secret" not in response_body
assert "tenant-b-response-secret" not in response_body
assert "tenant-b-raw-query-secret" not in response_body
def test_agent_run_creation_stamps_both_payloads_and_rejects_conflicts() -> None:
session_factory = _session_factory()
with session_factory() as db:
service = AgentRunService(db)
created = service.create_run(
agent="orchestrator",
source="user_message",
tenant_id="tenant-a",
ontology_json={"scenario": "expense"},
route_json={"stage": "created"},
status="running",
)
assert created.ontology_json["tenant_id"] == "tenant-a"
assert created.route_json["tenant_id"] == "tenant-a"
updated = service.update_run(
created.run_id,
ontology_json={"scenario": "expense", "intent": "query"},
route_json={"stage": "finished"},
status="succeeded",
)
assert updated.ontology_json["tenant_id"] == "tenant-a"
assert updated.route_json["tenant_id"] == "tenant-a"
with pytest.raises(ValueError, match="tenant_id 与业务上下文冲突"):
service.create_run(
agent="orchestrator",
source="user_message",
tenant_id="tenant-a",
route_json={"tenant_id": "tenant-b"},
status="running",
)