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

@@ -11,6 +11,7 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.api.deps import CurrentUserContext
from app.core.agent_enums import (
AgentAssetDomain,
AgentAssetStatus,
@@ -21,6 +22,7 @@ from app.db.base import Base
from app.models.agent_asset import AgentAsset
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.golden_case import GoldenCase
from app.schemas.agent_asset import (
AgentAssetReviewCreate,
AgentAssetRiskRuleGenerateRequest,
@@ -29,18 +31,22 @@ from app.schemas.agent_asset import (
AgentAssetRiskRuleScenarioTestRequest,
AgentAssetRiskRuleSimulationRequest,
)
from app.services.agent_asset_release_guard import (
AgentAssetReleaseGuardService,
ReleaseEvaluationInput,
)
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.agent_assets import AgentAssetService
from app.services.agent_foundation_risk_rules import AgentFoundationRiskRuleMixin
from app.services.expense_claim_platform_risk import ExpenseClaimPlatformRiskMixin
from app.services.risk_rule_manifest_classifier import is_budget_risk_manifest
from app.services.risk_rule_flow_diagram import (
RiskRuleFlowDiagramRenderer,
RiskRuleFlowDiagramSpec,
)
from app.services.risk_rule_generation import RiskRuleGenerationService
from app.services.risk_rule_generation_jobs import RiskRuleGenerationJobService
from app.services.risk_rule_manifest_classifier import is_budget_risk_manifest
from app.services.risk_rule_manifest_normalizer import normalize_risk_rule_manifest
from app.services.risk_rule_scoring import calculate_risk_rule_score, risk_level_from_score
from app.services.risk_rule_template_executor import RiskRuleTemplateExecutor
@@ -1135,9 +1141,7 @@ def test_legacy_city_route_keyword_manifest_is_normalized_before_display_and_exe
"outcomes": {"fail": {"severity": "high"}},
"metadata": {
"condition_summary": "检查住宿城市、申报地点、行程城市是否出现规则描述中的风险关键词",
"flow": {
"decision": "检查住宿城市、申报地点、行程城市是否出现规则描述中的风险关键词"
},
"flow": {"decision": "检查住宿城市、申报地点、行程城市是否出现规则描述中的风险关键词"},
},
"flow_diagram_svg": (
'<svg data-risk-flow-style="review-node-only">'
@@ -1293,7 +1297,16 @@ def test_simulation_uses_current_rule_manifest_for_ticket_city_mismatch(tmp_path
),
actor="pytest",
)
service = AgentAssetService(db)
service = AgentAssetService(
db,
current_user=CurrentUserContext(
username="pytest",
name="pytest",
role_codes=["manager"],
is_admin=True,
tenant_id="default",
),
)
service.rule_library_manager = manager
asset = db.get(AgentAsset, asset_id)
assert asset is not None
@@ -1357,7 +1370,16 @@ def test_risk_rule_requires_test_report_before_review_and_publish(tmp_path) -> N
),
actor="pytest",
)
service = AgentAssetService(db)
service = AgentAssetService(
db,
current_user=CurrentUserContext(
username="pytest",
name="pytest",
role_codes=["manager"],
is_admin=True,
tenant_id="default",
),
)
service.rule_library_manager = manager
asset = db.get(AgentAsset, asset_id)
@@ -1430,7 +1452,10 @@ def test_risk_rule_requires_test_report_before_review_and_publish(tmp_path) -> N
scenario = service.run_risk_rule_scenario_test(
asset_id,
AgentAssetRiskRuleScenarioTestRequest(intent="用最近30天的住宿报销单试运行"),
AgentAssetRiskRuleScenarioTestRequest(
target_tenant_id="default",
intent="用最近30天的住宿报销单试运行",
),
actor="pytest",
)
assert scenario.passed is True
@@ -1443,6 +1468,21 @@ def test_risk_rule_requires_test_report_before_review_and_publish(tmp_path) -> N
)
assert report.passed is True
for case in sample.input_json["cases"]:
db.add(
GoldenCase(
case_key=f"{asset.code}:{case['case_id']}",
rule_code=asset.code,
name=case["name"],
values_json=case["values"],
expected_hit=case["expected_hit"],
expected_severity=case["expected_severity"],
status="active",
source="pytest",
)
)
db.commit()
review = service.create_review(
asset_id,
AgentAssetReviewCreate(
@@ -1455,8 +1495,65 @@ def test_risk_rule_requires_test_report_before_review_and_publish(tmp_path) -> N
)
assert review.review_status == AgentReviewStatus.PENDING.value
published = service.publish_risk_rule(asset_id, actor="manager")
assert published.status == AgentAssetStatus.REVIEW.value
assert published.published_version is None
assert published.config_json["release_guard"]["stage"] == "shadow"
approved = service.create_review(
asset_id,
AgentAssetReviewCreate(
version=asset.working_version or "v0.1.0",
reviewer="manager",
review_status=AgentReviewStatus.APPROVED,
review_note="批准进入灰度发布",
),
actor="manager",
)
assert approved.review_status == AgentReviewStatus.APPROVED.value
release_guard = AgentAssetReleaseGuardService(
db,
rule_library_manager=manager,
)
release_guard.record_evaluation(
asset_id,
ReleaseEvaluationInput(
total=20,
failure_count=0,
precision=1.0,
details={
"metric_source": "release_runtime_telemetry",
"negative_ground_truth_status": "available",
"recall_lower_bound": 1.0,
},
),
actor="monitor",
)
release_guard.promote(asset_id, actor="manager")
release_guard.record_evaluation(
asset_id,
ReleaseEvaluationInput(
total=100,
failure_count=0,
precision=1.0,
details={
"metric_source": "release_runtime_telemetry",
"negative_ground_truth_status": "available",
"recall_lower_bound": 1.0,
},
),
actor="monitor",
)
release_guard.promote(asset_id, actor="manager")
published = db.get(AgentAsset, asset_id)
assert published is not None
assert published.status == AgentAssetStatus.ACTIVE.value
assert published.published_version == asset.working_version
golden_run = service.repository.get_latest_test_run(
asset_id,
version=asset.working_version,
test_type="golden",
)
assert golden_run is not None
assert golden_run.passed is True
disabled = service.set_risk_rule_enabled(
asset_id,