feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user