from __future__ import annotations import json from datetime import UTC, date, datetime from decimal import Decimal from sqlalchemy import select from sqlalchemy.orm import Session from app.core.agent_enums import ( AgentAssetContentType, AgentAssetDomain, AgentAssetStatus, AgentAssetType, AgentName, AgentPermissionLevel, AgentReviewStatus, AgentRunSource, AgentRunStatus, AgentToolType, ) from app.core.config import get_settings from app.core.logging import get_logger from app.db.base import Base from app.db.session import get_session_factory from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog from app.models.audit_log import AuditLog from app.models.financial_record import ( AccountsPayableRecord, AccountsReceivableRecord, ExpenseClaim, ExpenseClaimItem, ) from app.services.expense_rule_runtime import ( build_scene_submission_standard_markdown, build_travel_risk_control_standard_markdown, ) logger = get_logger("app.services.agent_foundation") DEMO_EXPENSE_CLAIM_SIGNATURES = { ( "EXP-202605-001", "张三", "华南客户拜访差旅报销", "3280.00", "submitted", ), ( "EXP-202605-002", "李四", "客户路演餐费", "860.00", "approved", ), ( "EXP-202605-003", "王五", "市场活动会务差旅", "3280.00", "review", ), } DEMO_RECEIVABLE_SIGNATURES = { ("AR-202605-001", "客户A", "50000.00", "partial"), ("AR-202605-002", "客户B", "78000.00", "overdue"), } DEMO_PAYABLE_SIGNATURES = { ("AP-202605-001", "供应商A", "33000.00", "scheduled"), ("AP-202605-002", "供应商B", "96000.00", "overdue"), } LEGACY_RULE_CODES = ( "rule.expense.duplicate_expense_check", "rule.expense.travel_receipt_requirements", "rule.ap.payment_dual_review", ) ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements" ATTACHMENT_RULE_RUNTIME_CONFIG = { "kind": "policy_rule_draft", "version": 1, "template_key": "attachment_requirement_v1", "rule_name": "报销附件与单据完整性规则", "scenario": "attachment_policy", "source_document_name": "报销制度 / 单据与附件要求", "review_required": True, "target": { "expense_types": [ "travel", "hotel", "transport", "meal", "office", "meeting", "training", "communication", "welfare", "other", ], "scene_codes": ["expense", "attachment_policy", "invoice_anomaly"], }, "attachment_requirements": { "min_attachment_count": 1, "items": [ { "document_type": "vat_invoice", "required": True, "min_count": 1, "description": "金额类报销原则上必须提供合法票据。", }, { "document_type": "receipt", "required": False, "min_count": 1, "description": "特殊场景无发票时需补充收据与情况说明。", }, { "document_type": "flight_itinerary", "required": False, "min_count": 1, "description": "差旅交通报销需提供行程单或等效凭证。", }, { "document_type": "hotel_invoice", "required": False, "min_count": 1, "description": "住宿报销需提供酒店票据或等效住宿凭证。", }, ], "manual_fill_required": False, }, "missing_attachment_action": "block", "output": { "risk_code": "invoice_anomaly", "action": "block", "message": "附件或单据不完整,需补件后再提交。", }, } def prepare_agent_foundation() -> None: settings = get_settings() if not settings.setup_completed: logger.info("Agent foundation bootstrap skipped because setup is incomplete") return session_factory = get_session_factory() with session_factory() as db: AgentFoundationService(db).ensure_foundation_ready() class AgentFoundationService: def __init__(self, db: Session) -> None: self.db = db def ensure_foundation_ready(self) -> None: try: Base.metadata.create_all(bind=self.db.get_bind()) self._seed_agent_assets() self._sync_demo_financial_records() self._seed_runs_and_logs() self.db.commit() except Exception: self.db.rollback() logger.exception("Failed to prepare agent foundation") raise def _sync_demo_financial_records(self) -> None: if get_settings().seed_demo_financial_records: self._seed_financial_records() return self._purge_demo_financial_records() def _seed_agent_assets(self) -> None: existing_codes = set(self.db.scalars(select(AgentAsset.code)).all()) if existing_codes: self._top_up_agent_assets(existing_codes) return attachment_rule = AgentAsset( asset_type=AgentAssetType.RULE.value, code=ATTACHMENT_RULE_ASSET_CODE, name="报销附件与单据完整性规则", description="统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "attachment_policy", "invoice_anomaly"], owner="财务制度管理组", reviewer="高嘉禾", status=AgentAssetStatus.REVIEW.value, current_version="v1.0.0", config_json={ "severity": "high", "enabled": False, "runtime_kind": "policy_rule_draft", "rule_template_key": "attachment_requirement_v1", "rule_template_label": "附件要求模板", "runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG, }, ) scene_submission_rule = AgentAsset( asset_type=AgentAssetType.RULE.value, code="rule.expense.scene_submission_standard", name="报销场景提交与附件标准", description="统一定义各报销场景的必填字段、附件类型要求和金额阈值。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "scene_policy", "attachment_policy"], owner="费用运营组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={ "severity": "high", "enabled": True, "runtime_kind": "scene_matrix", "rule_template_label": "系统内置场景矩阵规则", }, ) travel_policy_rule = AgentAsset( asset_type=AgentAssetType.RULE.value, code="rule.expense.travel_risk_control_standard", name="差旅报销风险管控制度", description="统一定义差旅报销的行程闭环、酒店地点一致性、职级差标和风险处置口径。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "travel_policy", "travel_standard"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.1.0", config_json={ "severity": "high", "enabled": True, "block_on_high_risk": True, "warning_on_medium_risk": True, "source_doc": "document/development/risks/travel-risk-control-standard.md", "runtime_kind": "travel_policy", "rule_template_key": "travel_standard_v1", "rule_template_label": "差旅标准模板", }, ) skill_expense_asset = AgentAsset( asset_type=AgentAssetType.SKILL.value, code="skill.expense.summary_lookup", name="报销汇总查询技能", description="根据时间、员工和部门汇总报销金额与单据数量。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "query", "summary"], owner="平台研发组", reviewer="陈硕", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"input_schema": ["time_range", "employee", "department"]}, ) skill_ar_asset = AgentAsset( asset_type=AgentAssetType.SKILL.value, code="skill.ar.aging_summary", name="应收账龄汇总技能", description="按客户、账龄和逾期状态汇总应收风险分布。", domain=AgentAssetDomain.AR.value, scenario_json=["accounts_receivable", "query", "aging_summary"], owner="平台研发组", reviewer="陈硕", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"input_schema": ["customer", "aging_bucket", "status"]}, ) invoice_mcp_asset = AgentAsset( asset_type=AgentAssetType.MCP.value, code="mcp.invoice.verify_mock", name="发票验真 Mock 服务", description="模拟发票验真、发票状态查询和异常降级说明。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["expense", "invoice_validation"], owner="平台研发组", reviewer="周悦宁", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"endpoint": "mock://invoice/verify", "timeout_ms": 1200}, ) ledger_mcp_asset = AgentAsset( asset_type=AgentAssetType.MCP.value, code="mcp.ledger.snapshot_mock", name="总账快照 Mock 服务", description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["expense", "accounts_receivable", "accounts_payable"], owner="平台研发组", reviewer="周悦宁", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500}, ) task_asset = AgentAsset( asset_type=AgentAssetType.TASK.value, code="task.hermes.daily_risk_scan", name="Hermes 每日风险巡检", description="每天早上巡检重复报销、金额超标、逾期应收和异常付款。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "risk_check"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 9 * * *", "agent": AgentName.HERMES.value}, ) ar_summary_task = AgentAsset( asset_type=AgentAssetType.TASK.value, code="task.hermes.weekly_ar_summary", name="Hermes 每周应收账龄汇总", description="每周汇总逾期应收、账龄分布和客户风险变化。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "accounts_receivable", "summary"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value}, ) rule_digest_task = AgentAsset( asset_type=AgentAssetType.TASK.value, code="task.hermes.rule_review_digest", name="Hermes 规则待审摘要", description="每天汇总待审规则、待补样例和被拒规则修订建议。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "rule_center", "review_digest"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value}, ) knowledge_index_task = AgentAsset( asset_type=AgentAssetType.TASK.value, code="task.hermes.knowledge_index_sync", name="Hermes ??????", description="?????????? LightRAG ???????", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "knowledge", "rule_center"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value}, ) self.db.add_all( [ attachment_rule, scene_submission_rule, travel_policy_rule, skill_expense_asset, skill_ar_asset, invoice_mcp_asset, ledger_mcp_asset, task_asset, ar_summary_task, rule_digest_task, knowledge_index_task, ] ) self.db.flush() self.db.add_all( [ AgentAssetVersion( asset=attachment_rule, version="v0.9.0", content=self._attachment_submission_requirement_markdown( version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。", include_review_note=True, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版草稿。", created_by="高嘉禾", ), AgentAssetVersion( asset=attachment_rule, version="v1.0.0", content=self._attachment_submission_requirement_markdown( version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。", include_review_note=True, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="补充票据替代与差旅等效凭证口径,待审核。", created_by="高嘉禾", ), AgentAssetVersion( asset=scene_submission_rule, version="v1.0.0", content=self._scene_submission_standard_markdown(), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版报销场景提交标准,覆盖附件类型、必填字段和金额阈值。", created_by="系统初始化", ), AgentAssetVersion( asset=travel_policy_rule, version="v1.0.0", content=self._travel_risk_control_standard_markdown(version="v1.0.0"), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版差旅制度执行规则,覆盖行程闭环与基础差标校验。", created_by="系统初始化", ), AgentAssetVersion( asset=travel_policy_rule, version="v1.1.0", content=self._travel_risk_control_standard_markdown(version="v1.1.0"), content_type=AgentAssetContentType.MARKDOWN.value, change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。", created_by="系统初始化", ), AgentAssetVersion( asset=skill_expense_asset, version="v1.0.0", content=self._json_content( { "inputs": ["time_range", "employee", "department"], "outputs": ["total_amount", "claim_count"], "dependencies": ["database.expense_claims"], } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化技能快照。", created_by="系统初始化", ), AgentAssetVersion( asset=skill_ar_asset, version="v1.0.0", content=self._json_content( { "inputs": ["customer", "aging_bucket", "status"], "outputs": ["receivable_total", "overdue_total", "customer_count"], "dependencies": ["database.accounts_receivable"], } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化应收账龄技能快照。", created_by="系统初始化", ), AgentAssetVersion( asset=invoice_mcp_asset, version="v1.0.0", content=self._json_content( { "service_type": "mock", "auth_mode": "none", "degrade_strategy": "return_stub_with_warning", } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化 MCP 快照。", created_by="系统初始化", ), AgentAssetVersion( asset=ledger_mcp_asset, version="v1.0.0", content=self._json_content( { "service_type": "mock", "auth_mode": "service_account", "degrade_strategy": "return_cached_snapshot_with_warning", } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化总账快照 MCP。", created_by="系统初始化", ), AgentAssetVersion( asset=task_asset, version="v1.0.0", content=self._json_content( { "task_type": "daily_risk_scan", "schedule": "0 9 * * *", "target_agent": AgentName.HERMES.value, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化任务快照。", created_by="系统初始化", ), AgentAssetVersion( asset=ar_summary_task, version="v1.0.0", content=self._json_content( { "task_type": "weekly_ar_summary", "schedule": "0 10 * * 1", "target_agent": AgentName.HERMES.value, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化应收账龄汇总任务。", created_by="系统初始化", ), AgentAssetVersion( asset=rule_digest_task, version="v1.0.0", content=self._json_content( { "task_type": "rule_review_digest", "schedule": "0 18 * * *", "target_agent": AgentName.HERMES.value, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化规则待审摘要任务。", created_by="系统初始化", ), AgentAssetVersion( asset=knowledge_index_task, version="v1.0.0", content=self._json_content( { "task_type": "knowledge_index_sync", "schedule": "0 0 * * *", "target_agent": AgentName.HERMES.value, "folder": "报销制度", "changed_only": True, "index_engine": "lightrag", } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化制度知识与规则草稿形成任务。", created_by="系统初始化", ), ] ) self.db.add_all( [ AgentAssetReview( asset=attachment_rule, version="v1.0.0", reviewer="高嘉禾", review_status=AgentReviewStatus.PENDING.value, review_note="等待制度管理员确认收据替代与补件时限口径。", reviewed_at=None, ), AgentAssetReview( asset=scene_submission_rule, version="v1.0.0", reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="可作为报销场景统一审核标准正式执行。", reviewed_at=datetime.now(UTC), ), AgentAssetReview( asset=travel_policy_rule, version="v1.1.0", reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。", reviewed_at=datetime.now(UTC), ), ] ) def _seed_financial_records(self) -> None: if self.db.scalar(select(ExpenseClaim.id).limit(1)) is not None: return claim_1 = ExpenseClaim( claim_no="EXP-202605-001", employee_name="张三", department_name="财务共享中心", project_code="PRJ-EXP-01", expense_type="travel", reason="华南客户拜访差旅报销", location="深圳", amount=Decimal("3280.00"), currency="CNY", invoice_count=3, occurred_at=datetime(2026, 5, 6, 9, 0, tzinfo=UTC), submitted_at=datetime(2026, 5, 7, 10, 20, tzinfo=UTC), status="submitted", approval_stage="finance_review", risk_flags_json=["amount_over_limit"], ) claim_1.items = [ ExpenseClaimItem( item_date=date(2026, 5, 5), item_type="hotel", item_reason="客户拜访住宿", item_location="深圳", item_amount=Decimal("1880.00"), invoice_id="INV-HOTEL-001", ), ExpenseClaimItem( item_date=date(2026, 5, 6), item_type="transport", item_reason="往返交通", item_location="深圳", item_amount=Decimal("1400.00"), invoice_id="INV-TRANS-009", ), ] claim_2 = ExpenseClaim( claim_no="EXP-202605-002", employee_name="李四", department_name="华东销售部", project_code="PRJ-SALES-02", expense_type="meal", reason="客户路演餐费", location="上海", amount=Decimal("860.00"), currency="CNY", invoice_count=1, occurred_at=datetime(2026, 5, 8, 12, 0, tzinfo=UTC), submitted_at=datetime(2026, 5, 8, 18, 30, tzinfo=UTC), status="approved", approval_stage="completed", risk_flags_json=[], ) claim_3 = ExpenseClaim( claim_no="EXP-202605-003", employee_name="王五", department_name="市场品牌部", project_code="PRJ-MKT-08", expense_type="travel", reason="市场活动会务差旅", location="北京", amount=Decimal("3280.00"), currency="CNY", invoice_count=2, occurred_at=datetime(2026, 5, 6, 11, 30, tzinfo=UTC), submitted_at=datetime(2026, 5, 8, 9, 10, tzinfo=UTC), status="review", approval_stage="risk_check", risk_flags_json=["duplicate_expense"], ) ar_records = [ AccountsReceivableRecord( receivable_no="AR-202605-001", customer_id="CUS-A", customer_name="客户A", contract_no="CTR-AR-1001", invoice_no="INV-AR-9001", amount_receivable=Decimal("120000.00"), amount_received=Decimal("70000.00"), amount_outstanding=Decimal("50000.00"), currency="CNY", posting_date=date(2026, 4, 1), due_date=date(2026, 4, 30), aging_days=11, status="partial", risk_flags_json=[], ), AccountsReceivableRecord( receivable_no="AR-202605-002", customer_id="CUS-B", customer_name="客户B", contract_no="CTR-AR-1002", invoice_no="INV-AR-9002", amount_receivable=Decimal("88000.00"), amount_received=Decimal("10000.00"), amount_outstanding=Decimal("78000.00"), currency="CNY", posting_date=date(2026, 3, 15), due_date=date(2026, 4, 15), aging_days=26, status="overdue", risk_flags_json=["ar_overdue"], ), ] ap_records = [ AccountsPayableRecord( payable_no="AP-202605-001", vendor_id="VEN-A", vendor_name="供应商A", invoice_no="INV-AP-5001", amount_payable=Decimal("43000.00"), amount_paid=Decimal("10000.00"), amount_outstanding=Decimal("33000.00"), currency="CNY", posting_date=date(2026, 4, 20), due_date=date(2026, 5, 12), aging_days=0, status="scheduled", risk_flags_json=[], ), AccountsPayableRecord( payable_no="AP-202605-002", vendor_id="VEN-B", vendor_name="供应商B", invoice_no="INV-AP-5002", amount_payable=Decimal("96000.00"), amount_paid=Decimal("0.00"), amount_outstanding=Decimal("96000.00"), currency="CNY", posting_date=date(2026, 4, 10), due_date=date(2026, 5, 5), aging_days=6, status="overdue", risk_flags_json=["ap_overdue"], ), ] self.db.add_all([claim_1, claim_2, claim_3, *ar_records, *ap_records]) def _purge_demo_financial_records(self) -> None: demo_claims = list(self.db.scalars(select(ExpenseClaim)).all()) for claim in demo_claims: signature = ( str(claim.claim_no or "").strip(), str(claim.employee_name or "").strip(), str(claim.reason or "").strip(), f"{Decimal(claim.amount or 0):.2f}", str(claim.status or "").strip(), ) if signature in DEMO_EXPENSE_CLAIM_SIGNATURES: self.db.delete(claim) demo_receivables = list(self.db.scalars(select(AccountsReceivableRecord)).all()) for record in demo_receivables: signature = ( str(record.receivable_no or "").strip(), str(record.customer_name or "").strip(), f"{Decimal(record.amount_outstanding or 0):.2f}", str(record.status or "").strip(), ) if signature in DEMO_RECEIVABLE_SIGNATURES: self.db.delete(record) demo_payables = list(self.db.scalars(select(AccountsPayableRecord)).all()) for record in demo_payables: signature = ( str(record.payable_no or "").strip(), str(record.vendor_name or "").strip(), f"{Decimal(record.amount_outstanding or 0):.2f}", str(record.status or "").strip(), ) if signature in DEMO_PAYABLE_SIGNATURES: self.db.delete(record) def _seed_runs_and_logs(self) -> None: if self.db.scalar(select(AgentRun.id).limit(1)) is not None: return task_asset = self.db.scalar( select(AgentAsset).where(AgentAsset.code == "task.hermes.daily_risk_scan") ) user_run = AgentRun( run_id="run_user_20260511_001", agent=AgentName.USER_AGENT.value, source=AgentRunSource.USER_MESSAGE.value, user_id="emp_001", task_id=None, ontology_json={"scenario": "expense", "intent": "query"}, route_json={"selected_agent": AgentName.USER_AGENT.value, "route_reason": "user query"}, permission_level=AgentPermissionLevel.READ.value, status=AgentRunStatus.SUCCEEDED.value, result_summary="已返回本周报销金额和风险摘要。", started_at=datetime(2026, 5, 11, 8, 35, tzinfo=UTC), finished_at=datetime(2026, 5, 11, 8, 35, 2, tzinfo=UTC), ) hermes_run = AgentRun( run_id="run_hermes_20260511_001", agent=AgentName.HERMES.value, source=AgentRunSource.SCHEDULE.value, user_id=None, task_id=task_asset.id if task_asset else None, ontology_json={"scenario": "expense", "intent": "risk_check"}, route_json={ "selected_agent": AgentName.HERMES.value, "route_reason": "scheduled risk scan", }, permission_level=AgentPermissionLevel.READ.value, status=AgentRunStatus.SUCCEEDED.value, result_summary="Hermes 已生成今日风险巡检摘要。", started_at=datetime(2026, 5, 11, 9, 0, tzinfo=UTC), finished_at=datetime(2026, 5, 11, 9, 0, 4, tzinfo=UTC), ) blocked_run = AgentRun( run_id="run_user_20260511_002", agent=AgentName.ORCHESTRATOR.value, source=AgentRunSource.USER_MESSAGE.value, user_id="emp_002", task_id=None, ontology_json={"scenario": "accounts_payable", "intent": "operate"}, route_json={ "selected_agent": AgentName.USER_AGENT.value, "route_reason": "payment request", }, permission_level=AgentPermissionLevel.APPROVAL_REQUIRED.value, status=AgentRunStatus.BLOCKED.value, result_summary="动作需要人工确认。", error_message="直接付款属于高风险动作,已阻断自动执行。", started_at=datetime(2026, 5, 11, 10, 5, tzinfo=UTC), finished_at=datetime(2026, 5, 11, 10, 5, 1, tzinfo=UTC), ) self.db.add_all([user_run, hermes_run, blocked_run]) self.db.flush() self.db.add_all( [ AgentToolCall( run_id=user_run.run_id, tool_type=AgentToolType.DATABASE.value, tool_name="expense_claims.lookup", request_json={"time_range": "this_week", "employee": "all"}, response_json={"claim_count": 3, "total_amount": "7420.00"}, status="succeeded", duration_ms=48, ), AgentToolCall( run_id=hermes_run.run_id, tool_type=AgentToolType.MCP.value, tool_name="invoice.verify_mock", request_json={"claim_no": "EXP-202605-003"}, response_json={ "warning": "external service degraded", "fallback": "used mock response", }, status="failed", duration_ms=132, error_message="mock upstream timeout", ), AgentToolCall( run_id=blocked_run.run_id, tool_type=AgentToolType.RULE_ENGINE.value, tool_name="permission.guard", request_json={"action": "direct_payment"}, response_json={"requires_confirmation": True}, status="succeeded", duration_ms=5, ), SemanticParseLog( run_id=user_run.run_id, user_id="emp_001", raw_query="查一下本周报销超标风险", scenario="expense", intent="risk_check", entities_json=[], time_range_json={"start_date": "2026-05-11", "end_date": "2026-05-17"}, metrics_json=["amount"], constraints_json=[], risk_flags_json=["amount_over_limit"], permission_json={"level": AgentPermissionLevel.READ.value}, confidence=0.93, ), SemanticParseLog( run_id=blocked_run.run_id, user_id="emp_002", raw_query="帮我直接付款给供应商B", scenario="accounts_payable", intent="operate", entities_json=[{"type": "vendor", "value": "供应商B"}], time_range_json={}, metrics_json=["amount"], constraints_json=[], risk_flags_json=["ap_overdue"], permission_json={"level": AgentPermissionLevel.APPROVAL_REQUIRED.value}, confidence=0.96, ), ] ) if self.db.scalar(select(AuditLog.id).limit(1)) is None: self.db.add_all( [ AuditLog( actor="系统初始化", action="save_rule_markdown", resource_type="rule", resource_id=ATTACHMENT_RULE_ASSET_CODE, before_json=None, after_json={"version": "v1.0.0"}, request_id="seed-audit-001", ), AuditLog( actor="高嘉禾", action="review_rule", resource_type="rule", resource_id=ATTACHMENT_RULE_ASSET_CODE, before_json={"review_status": "pending"}, after_json={"review_status": "pending"}, request_id="seed-audit-002", ), AuditLog( actor="系统初始化", action="activate_rule", resource_type="rule", resource_id="rule.expense.scene_submission_standard", before_json={"status": "draft"}, after_json={"status": "active"}, request_id="seed-audit-003", ), AuditLog( actor="Hermes", action="update_task_status", resource_type="task", resource_id="task.hermes.daily_risk_scan", before_json={"status": "idle"}, after_json={"status": "succeeded"}, request_id="seed-audit-004", ), ] ) def _top_up_agent_assets(self, existing_codes: set[str]) -> None: self._remove_legacy_rule_assets() existing_codes = set(self.db.scalars(select(AgentAsset.code)).all()) attachment_rule = self.db.scalar( select(AgentAsset).where(AgentAsset.code == ATTACHMENT_RULE_ASSET_CODE) ) scene_submission_rule = self.db.scalar( select(AgentAsset).where(AgentAsset.code == "rule.expense.scene_submission_standard") ) travel_policy_rule = self.db.scalar( select(AgentAsset).where(AgentAsset.code == "rule.expense.travel_risk_control_standard") ) if ATTACHMENT_RULE_ASSET_CODE not in existing_codes: attachment_rule = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code=ATTACHMENT_RULE_ASSET_CODE, name="报销附件与单据完整性规则", description="统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "attachment_policy", "invoice_anomaly"], owner="财务制度管理组", reviewer="高嘉禾", status=AgentAssetStatus.REVIEW.value, current_version="v1.0.0", config_json={ "severity": "high", "enabled": False, "runtime_kind": "policy_rule_draft", "rule_template_key": "attachment_requirement_v1", "rule_template_label": "附件要求模板", "runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG, }, ) if attachment_rule is not None: attachment_rule.current_version = "v1.0.0" attachment_rule.status = AgentAssetStatus.REVIEW.value attachment_rule.description = "统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。" attachment_rule.config_json = { "severity": "high", "enabled": False, "runtime_kind": "policy_rule_draft", "rule_template_key": "attachment_requirement_v1", "rule_template_label": "附件要求模板", "runtime_rule": ATTACHMENT_RULE_RUNTIME_CONFIG, } self._ensure_asset_version( attachment_rule, version="v0.9.0", content=self._attachment_submission_requirement_markdown( version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。", include_review_note=True, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版草稿。", created_by="高嘉禾", ) self._ensure_asset_version( attachment_rule, version="v1.0.0", content=self._attachment_submission_requirement_markdown( version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。", include_review_note=True, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="补充票据替代与差旅等效凭证口径,待审核。", created_by="高嘉禾", ) self._ensure_asset_review( attachment_rule, version="v1.0.0", reviewer="高嘉禾", review_status=AgentReviewStatus.PENDING.value, review_note="等待制度管理员确认收据替代与补件时限口径。", reviewed_at=None, ) if "rule.expense.scene_submission_standard" not in existing_codes: scene_submission_rule = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code="rule.expense.scene_submission_standard", name="报销场景提交与附件标准", description="统一定义各报销场景的必填字段、附件类型要求和金额阈值。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "scene_policy", "attachment_policy"], owner="费用运营组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={ "severity": "high", "enabled": True, "runtime_kind": "scene_matrix", "rule_template_label": "系统内置场景矩阵规则", }, ) if scene_submission_rule is not None: scene_submission_rule.current_version = "v1.0.0" scene_submission_rule.status = AgentAssetStatus.ACTIVE.value scene_submission_rule.description = "统一定义各报销场景的必填字段、附件类型要求和金额阈值。" scene_submission_rule.config_json = { "severity": "high", "enabled": True, "runtime_kind": "scene_matrix", "rule_template_label": "系统内置场景矩阵规则", } self._ensure_asset_version( scene_submission_rule, version="v1.0.0", content=self._scene_submission_standard_markdown(), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版报销场景提交标准,覆盖附件类型、必填字段和金额阈值。", created_by="系统初始化", ) self._ensure_asset_review( scene_submission_rule, version="v1.0.0", reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="可作为报销场景统一审核标准正式执行。", reviewed_at=datetime.now(UTC), ) if "rule.expense.travel_risk_control_standard" not in existing_codes: travel_policy_rule = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code="rule.expense.travel_risk_control_standard", name="差旅报销风险管控制度", description="统一定义差旅报销的行程闭环、酒店地点一致性、职级差标和风险处置口径。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "risk_check", "travel_policy", "travel_standard"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.1.0", config_json={ "severity": "high", "enabled": True, "block_on_high_risk": True, "warning_on_medium_risk": True, "source_doc": "document/development/risks/travel-risk-control-standard.md", "runtime_kind": "travel_policy", "rule_template_key": "travel_standard_v1", "rule_template_label": "差旅标准模板", }, ) if travel_policy_rule is not None: travel_policy_rule.current_version = "v1.1.0" travel_policy_rule.status = AgentAssetStatus.ACTIVE.value travel_policy_rule.config_json = { "severity": "high", "enabled": True, "block_on_high_risk": True, "warning_on_medium_risk": True, "source_doc": "document/development/risks/travel-risk-control-standard.md", "runtime_kind": "travel_policy", "rule_template_key": "travel_standard_v1", "rule_template_label": "差旅标准模板", } self._ensure_asset_version( travel_policy_rule, version="v1.0.0", content=self._travel_risk_control_standard_markdown(version="v1.0.0"), content_type=AgentAssetContentType.MARKDOWN.value, change_note="首版差旅制度执行规则,覆盖行程闭环与基础差标校验。", created_by="系统初始化", ) self._ensure_asset_version( travel_policy_rule, version="v1.1.0", content=self._travel_risk_control_standard_markdown(version="v1.1.0"), content_type=AgentAssetContentType.MARKDOWN.value, change_note="补充可执行规则块,供审核引擎直接消费差旅制度标准。", created_by="系统初始化", ) self._ensure_asset_review( travel_policy_rule, version="v1.1.0", reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="制度口径已确认,并已补充可执行配置供审核引擎读取。", reviewed_at=datetime.now(UTC), ) if "skill.ar.aging_summary" not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.SKILL.value, code="skill.ar.aging_summary", name="应收账龄汇总技能", description="按客户、账龄和逾期状态汇总应收风险分布。", domain=AgentAssetDomain.AR.value, scenario_json=["accounts_receivable", "query", "aging_summary"], owner="平台研发组", reviewer="陈硕", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"input_schema": ["customer", "aging_bucket", "status"]}, ) self._ensure_asset_version( asset, version="v1.0.0", content=self._json_content( { "inputs": ["customer", "aging_bucket", "status"], "outputs": ["receivable_total", "overdue_total", "customer_count"], "dependencies": ["database.accounts_receivable"], } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化应收账龄技能快照。", created_by="系统初始化", ) if "mcp.ledger.snapshot_mock" not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.MCP.value, code="mcp.ledger.snapshot_mock", name="总账快照 Mock 服务", description="模拟返回应收、应付和费用汇总快照,供 Agent 查询和巡检。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["expense", "accounts_receivable", "accounts_payable"], owner="平台研发组", reviewer="周悦宁", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"endpoint": "mock://ledger/snapshot", "timeout_ms": 1500}, ) self._ensure_asset_version( asset, version="v1.0.0", content=self._json_content( { "service_type": "mock", "auth_mode": "service_account", "degrade_strategy": "return_cached_snapshot_with_warning", } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化总账快照 MCP。", created_by="系统初始化", ) if "task.hermes.weekly_ar_summary" not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.TASK.value, code="task.hermes.weekly_ar_summary", name="Hermes 每周应收账龄汇总", description="每周汇总逾期应收、账龄分布和客户风险变化。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "accounts_receivable", "summary"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 10 * * 1", "agent": AgentName.HERMES.value}, ) self._ensure_asset_version( asset, version="v1.0.0", content=self._json_content( { "task_type": "weekly_ar_summary", "schedule": "0 10 * * 1", "target_agent": AgentName.HERMES.value, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化应收账龄汇总任务。", created_by="系统初始化", ) if "task.hermes.rule_review_digest" not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.TASK.value, code="task.hermes.rule_review_digest", name="Hermes 规则待审摘要", description="每天汇总待审规则、待补样例和被拒规则修订建议。", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "rule_center", "review_digest"], owner="风控与审计部", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 18 * * *", "agent": AgentName.HERMES.value}, ) self._ensure_asset_version( asset, version="v1.0.0", content=self._json_content( { "task_type": "rule_review_digest", "schedule": "0 18 * * *", "target_agent": AgentName.HERMES.value, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化规则待审摘要任务。", created_by="系统初始化", ) if "task.hermes.knowledge_index_sync" not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.TASK.value, code="task.hermes.knowledge_index_sync", name="Hermes ??????", description="?????????? LightRAG ???????", domain=AgentAssetDomain.SYSTEM.value, scenario_json=["schedule", "knowledge", "rule_center"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json={"cron": "0 0 * * *", "agent": AgentName.HERMES.value}, ) self._ensure_asset_version( asset, version="v1.0.0", content=self._json_content( { "task_type": "knowledge_index_sync", "schedule": "0 0 * * *", "target_agent": AgentName.HERMES.value, "folder": "报销制度", "changed_only": True, } ), content_type=AgentAssetContentType.JSON.value, change_note="初始化制度知识与规则草稿形成任务。", created_by="系统初始化", ) def _create_seed_asset( self, *, asset_type: str, code: str, name: str, description: str, domain: str, scenario_json: list[str], owner: str, reviewer: str, status: str, current_version: str, config_json: dict[str, object], ) -> AgentAsset: asset = AgentAsset( asset_type=asset_type, code=code, name=name, description=description, domain=domain, scenario_json=scenario_json, owner=owner, reviewer=reviewer, status=status, current_version=current_version, config_json=config_json, ) self.db.add(asset) self.db.flush() return asset def _ensure_asset_version( self, asset: AgentAsset, *, version: str, content: str, content_type: str, change_note: str, created_by: str, ) -> None: existing = self.db.scalar( select(AgentAssetVersion).where( AgentAssetVersion.asset_id == asset.id, AgentAssetVersion.version == version, ) ) if existing is not None: return self.db.add( AgentAssetVersion( asset_id=asset.id, version=version, content=content, content_type=content_type, change_note=change_note, created_by=created_by, ) ) def _ensure_asset_review( self, asset: AgentAsset, *, version: str, reviewer: str, review_status: str, review_note: str, reviewed_at: datetime | None, ) -> None: existing = self.db.scalar( select(AgentAssetReview).where( AgentAssetReview.asset_id == asset.id, AgentAssetReview.version == version, AgentAssetReview.review_status == review_status, ) ) if existing is not None: return self.db.add( AgentAssetReview( asset_id=asset.id, version=version, reviewer=reviewer, review_status=review_status, review_note=review_note, reviewed_at=reviewed_at, ) ) def _remove_legacy_rule_assets(self) -> None: assets = list( self.db.scalars( select(AgentAsset).where(AgentAsset.code.in_(LEGACY_RULE_CODES)) ).all() ) for asset in assets: self.db.delete(asset) obsolete_logs = list( self.db.scalars( select(AuditLog).where(AuditLog.resource_id.in_(LEGACY_RULE_CODES)) ).all() ) for log in obsolete_logs: self.db.delete(log) def _attachment_submission_requirement_markdown( self, *, version_note: str, include_review_note: bool, ) -> str: sections = [ "# 报销附件与单据完整性规则", "", "## 模板信息", "", "- 模板键:`attachment_requirement_v1`", "- 来源文档:报销制度 / 单据与附件要求", "- 审核状态:待审核", "", "## 目标", "", "统一约束报销提交时的票据、附件与替代凭证要求,避免缺件、错件和无依据流转。", "", "## 适用范围", "", "适用于员工报销提交场景,重点覆盖差旅、住宿、交通、餐费、办公和其他费用的附件校验。", "", "## 输入字段", "", "- expense_type", "- attachments", "- invoice_count", "- reason", "", "## 判断规则", "", "- 报销提交前至少需要 1 份有效附件。", "- 金额类报销原则上应提供合法票据;特殊场景无发票时,必须补充收据与情况说明。", "- 差旅交通报销需提供行程单或等效凭证;住宿报销需提供酒店票据或等效住宿凭证。", "- 缺少必要附件时直接拦截,并提示补件后重新提交。", "", "## 输出", "", "- 风险编码:`invoice_anomaly`", "- 默认动作:`block`", "- 处理说明:附件或单据不完整时退回补充。", "", "## 来源依据", "", "- 报销制度对票据、附件、替代凭证和补件要求的统一约束。", "", "## 审核约束", "", "- 当前规则属于真实业务规则,但仍处于待审核状态。", "- 上线前需由制度管理员确认收据替代、补件时限和特殊场景豁免口径。", f"- 当前版本说明:{version_note}", "", "## 管理员备注", "", "需要结合公司正式报销制度,补充各场景附件替代口径与例外审批要求。", ] if include_review_note: sections.extend(["", "```expense-rule", json.dumps(ATTACHMENT_RULE_RUNTIME_CONFIG, ensure_ascii=False, indent=2), "```"]) return "\n".join(sections) def _scene_submission_standard_markdown(self) -> str: return self._markdown_content(build_scene_submission_standard_markdown()) def _travel_risk_control_standard_markdown(self, *, version: str = "v1.1.0") -> str: return self._markdown_content(build_travel_risk_control_standard_markdown()) @staticmethod def _markdown_content(content: str) -> str: return content @staticmethod def _json_content(content: dict[str, object]) -> str: return json.dumps(content, ensure_ascii=False, sort_keys=True, indent=2)