from __future__ import annotations import hashlib import json from datetime import UTC, date, datetime from decimal import Decimal from pathlib import Path from sqlalchemy import inspect, select, text 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.agent_asset_rule_library import AgentAssetRuleLibraryManager from app.services.agent_asset_spreadsheet import ( AgentAssetSpreadsheetManager, COMPANY_COMMUNICATION_EXPENSE_RULE_CODE, COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME, COMPANY_TRAVEL_EXPENSE_RULE_CODE, COMPANY_TRAVEL_EXPENSE_RULE_FILENAME, FINANCE_RULES_LIBRARY, RISK_RULES_LIBRARY, RuleSpreadsheetMeta, ) PLATFORM_DESTINATION_LOCATION_RULE_CODE = "risk.travel.destination_receipt_location" PLATFORM_DESTINATION_LOCATION_RULE_FILENAME = "risk.travel.destination_receipt_location.json" 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" COMPANY_TRAVEL_RULE_VERSION = "v1.0.0" COMPANY_COMMUNICATION_RULE_VERSION = "v1.0.0" 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._ensure_agent_asset_schema() 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", published_version=None, working_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", published_version="v1.0.0", working_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", published_version="v1.1.0", working_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": "差旅标准模板", }, ) company_travel_rule = AgentAsset( asset_type=AgentAssetType.RULE.value, code=COMPANY_TRAVEL_EXPENSE_RULE_CODE, name="公司差旅费报销规则", description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "travel_policy", "travel_standard"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version=COMPANY_TRAVEL_RULE_VERSION, published_version=COMPANY_TRAVEL_RULE_VERSION, working_version=COMPANY_TRAVEL_RULE_VERSION, config_json={ "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_library": FINANCE_RULES_LIBRARY, "rule_template_label": "差旅报销 Excel 模板", }, ) platform_risk_assets = self._build_platform_risk_seed_assets() company_communication_rule = AgentAsset( asset_type=AgentAssetType.RULE.value, code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE, name="公司通信费报销规则", description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "communication_expense", "expense_standard"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version=COMPANY_COMMUNICATION_RULE_VERSION, published_version=COMPANY_COMMUNICATION_RULE_VERSION, working_version=COMPANY_COMMUNICATION_RULE_VERSION, config_json={ "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_library": FINANCE_RULES_LIBRARY, "rule_template_label": "通信费报销 Excel 模板", }, ) 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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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", published_version="v1.0.0", working_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, *platform_risk_assets, company_travel_rule, company_communication_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() company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed( company_travel_rule, version=COMPANY_TRAVEL_RULE_VERSION, actor_name="系统初始化", ) company_communication_rule_meta = self._ensure_company_communication_rule_spreadsheet_seed( company_communication_rule, version=COMPANY_COMMUNICATION_RULE_VERSION, actor_name="系统初始化", ) 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=asset, version="v1.0.0", content=self._platform_risk_rule_markdown(asset), content_type=AgentAssetContentType.MARKDOWN.value, change_note=f"平台通用风险规则:{asset.name}", created_by="系统初始化", ) for asset in platform_risk_assets ], AgentAssetVersion( asset=company_travel_rule, version=COMPANY_TRAVEL_RULE_VERSION, content=AgentAssetSpreadsheetManager.build_version_markdown( rule_name=company_travel_rule.name, version=COMPANY_TRAVEL_RULE_VERSION, metadata=company_travel_rule_meta, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="初始化差旅费报销 Excel 规则表。", created_by="系统初始化", ), AgentAssetVersion( asset=company_communication_rule, version=COMPANY_COMMUNICATION_RULE_VERSION, content=AgentAssetSpreadsheetManager.build_version_markdown( rule_name=company_communication_rule.name, version=COMPANY_COMMUNICATION_RULE_VERSION, metadata=company_communication_rule_meta, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="初始化通信费报销 Excel 规则表。", 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), ), AgentAssetReview( asset=company_travel_rule, version=COMPANY_TRAVEL_RULE_VERSION, reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="首版 Excel 规则表已确认,可作为财务规则使用。", reviewed_at=datetime.now(UTC), ), AgentAssetReview( asset=company_communication_rule, version=COMPANY_COMMUNICATION_RULE_VERSION, reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="首版 Excel 规则表已确认,可作为财务规则使用。", 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") ) company_travel_rule = self.db.scalar( select(AgentAsset).where(AgentAsset.code == COMPANY_TRAVEL_EXPENSE_RULE_CODE) ) company_communication_rule = self.db.scalar( select(AgentAsset).where(AgentAsset.code == COMPANY_COMMUNICATION_EXPENSE_RULE_CODE) ) 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: if not str(attachment_rule.current_version or "").strip(): attachment_rule.current_version = "v1.0.0" if not str(attachment_rule.working_version or "").strip(): attachment_rule.working_version = attachment_rule.current_version attachment_rule.status = attachment_rule.status or 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: if not str(scene_submission_rule.current_version or "").strip(): scene_submission_rule.current_version = "v1.0.0" if not str(scene_submission_rule.working_version or "").strip(): scene_submission_rule.working_version = scene_submission_rule.current_version if not str(scene_submission_rule.published_version or "").strip(): scene_submission_rule.published_version = scene_submission_rule.current_version scene_submission_rule.status = scene_submission_rule.status or 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: if not str(travel_policy_rule.current_version or "").strip(): travel_policy_rule.current_version = "v1.1.0" if not str(travel_policy_rule.working_version or "").strip(): travel_policy_rule.working_version = travel_policy_rule.current_version if not str(travel_policy_rule.published_version or "").strip(): travel_policy_rule.published_version = travel_policy_rule.current_version travel_policy_rule.status = travel_policy_rule.status or 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), ) self.sync_platform_risk_rules_from_library() if COMPANY_TRAVEL_EXPENSE_RULE_CODE not in existing_codes: company_travel_rule = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code=COMPANY_TRAVEL_EXPENSE_RULE_CODE, name="公司差旅费报销规则", description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "travel_policy", "travel_standard"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version=COMPANY_TRAVEL_RULE_VERSION, config_json={ "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_template_label": "差旅报销 Excel 模板", }, ) if COMPANY_COMMUNICATION_EXPENSE_RULE_CODE not in existing_codes: company_communication_rule = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE, name="公司通信费报销规则", description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。", domain=AgentAssetDomain.EXPENSE.value, scenario_json=["expense", "communication_expense", "expense_standard"], owner="财务制度管理组", reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version=COMPANY_COMMUNICATION_RULE_VERSION, config_json={ "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_template_label": "通信费报销 Excel 模板", }, ) if company_travel_rule is not None: if not str(company_travel_rule.current_version or "").strip(): company_travel_rule.current_version = COMPANY_TRAVEL_RULE_VERSION if not str(company_travel_rule.working_version or "").strip(): company_travel_rule.working_version = company_travel_rule.current_version if not str(company_travel_rule.published_version or "").strip(): company_travel_rule.published_version = company_travel_rule.current_version if not str(company_travel_rule.status or "").strip(): company_travel_rule.status = AgentAssetStatus.ACTIVE.value company_travel_rule.description = "通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。" company_travel_rule.config_json = { **(company_travel_rule.config_json or {}), "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_library": FINANCE_RULES_LIBRARY, "rule_template_label": "差旅报销 Excel 模板", } company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed( company_travel_rule, version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION), actor_name="系统初始化", ) self._ensure_asset_version( company_travel_rule, version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION), content=AgentAssetSpreadsheetManager.build_version_markdown( rule_name=company_travel_rule.name, version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION), metadata=company_travel_rule_meta, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="初始化差旅费报销 Excel 规则表。", created_by="系统初始化", ) if str(company_travel_rule.current_version or "").strip() == COMPANY_TRAVEL_RULE_VERSION: self._ensure_asset_review( company_travel_rule, version=COMPANY_TRAVEL_RULE_VERSION, reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="首版 Excel 规则表已确认,可作为财务规则使用。", reviewed_at=datetime.now(UTC), ) if company_communication_rule is not None: if not str(company_communication_rule.current_version or "").strip(): company_communication_rule.current_version = COMPANY_COMMUNICATION_RULE_VERSION if not str(company_communication_rule.working_version or "").strip(): company_communication_rule.working_version = company_communication_rule.current_version if not str(company_communication_rule.published_version or "").strip(): company_communication_rule.published_version = company_communication_rule.current_version if not str(company_communication_rule.status or "").strip(): company_communication_rule.status = AgentAssetStatus.ACTIVE.value company_communication_rule.description = "通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。" company_communication_rule.config_json = { **(company_communication_rule.config_json or {}), "severity": "medium", "enabled": True, "tag": "财务规则", "detail_mode": "spreadsheet", "rule_library": FINANCE_RULES_LIBRARY, "rule_template_label": "通信费报销 Excel 模板", } company_communication_rule_meta = self._ensure_company_communication_rule_spreadsheet_seed( company_communication_rule, version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION), actor_name="系统初始化", ) self._ensure_asset_version( company_communication_rule, version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION), content=AgentAssetSpreadsheetManager.build_version_markdown( rule_name=company_communication_rule.name, version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION), metadata=company_communication_rule_meta, ), content_type=AgentAssetContentType.MARKDOWN.value, change_note="初始化通信费报销 Excel 规则表。", created_by="系统初始化", ) if str(company_communication_rule.current_version or "").strip() == COMPANY_COMMUNICATION_RULE_VERSION: self._ensure_asset_review( company_communication_rule, version=COMPANY_COMMUNICATION_RULE_VERSION, reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="首版 Excel 规则表已确认,可作为财务规则使用。", 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 _ensure_company_travel_rule_spreadsheet_seed( self, asset: AgentAsset, *, version: str, actor_name: str, ): manager = AgentAssetSpreadsheetManager() manager.ensure_rule_library_dirs() live_document = manager.store_rule_library_spreadsheet( library=FINANCE_RULES_LIBRARY, file_name=COMPANY_TRAVEL_EXPENSE_RULE_FILENAME, content=self._read_or_build_company_travel_rule_file(manager), actor_name=actor_name, source="rule-library", ) existing_document = ( asset.config_json.get("rule_document") if isinstance(asset.config_json, dict) else None ) storage_key = ( str(existing_document.get("storage_key") or "").strip() if isinstance(existing_document, dict) else "" ) if storage_key: try: existing_path = manager.resolve_storage_path(storage_key) except FileNotFoundError: existing_path = None if existing_path is not None and existing_path.exists(): asset.config_json = { **(asset.config_json or {}), "detail_mode": "spreadsheet", "tag": "财务规则", "rule_library": FINANCE_RULES_LIBRARY, "rule_document": { **AgentAssetSpreadsheetManager.build_rule_document_config( live_document, asset_version=version, ), "storage_key": live_document.storage_key, }, } return live_document asset.config_json = { **(asset.config_json or {}), "detail_mode": "spreadsheet", "tag": "财务规则", "rule_library": FINANCE_RULES_LIBRARY, "rule_document": { **AgentAssetSpreadsheetManager.build_rule_document_config( live_document, asset_version=version, ), "storage_key": live_document.storage_key, }, } return live_document def _ensure_company_communication_rule_spreadsheet_seed( self, asset: AgentAsset, *, version: str, actor_name: str, ): return self._ensure_finance_rule_spreadsheet_seed( asset, version=version, actor_name=actor_name, file_name=COMPANY_COMMUNICATION_EXPENSE_RULE_FILENAME, fallback_sheet_name="通信费报销规则", ) @staticmethod def _read_or_build_company_travel_rule_file( manager: AgentAssetSpreadsheetManager, ) -> bytes: live_key = ( Path("rules") / FINANCE_RULES_LIBRARY / COMPANY_TRAVEL_EXPENSE_RULE_FILENAME ).as_posix() live_path = manager.resolve_storage_path(live_key) if live_path.exists(): return live_path.read_bytes() return AgentAssetSpreadsheetManager.build_blank_rule_workbook("差旅费报销规则") def _ensure_finance_rule_spreadsheet_seed( self, asset: AgentAsset, *, version: str, actor_name: str, file_name: str, fallback_sheet_name: str, ): manager = AgentAssetSpreadsheetManager() manager.ensure_rule_library_dirs() live_document = manager.store_rule_library_spreadsheet( library=FINANCE_RULES_LIBRARY, file_name=file_name, content=self._read_or_build_finance_rule_file( manager, file_name=file_name, fallback_sheet_name=fallback_sheet_name, ), actor_name=actor_name, source="rule-library", ) existing_document = ( asset.config_json.get("rule_document") if isinstance(asset.config_json, dict) else None ) storage_key = ( str(existing_document.get("storage_key") or "").strip() if isinstance(existing_document, dict) else "" ) if storage_key: try: existing_path = manager.resolve_storage_path(storage_key) except FileNotFoundError: existing_path = None if existing_path is not None and existing_path.exists(): asset.config_json = { **(asset.config_json or {}), "detail_mode": "spreadsheet", "tag": "财务规则", "rule_library": FINANCE_RULES_LIBRARY, "rule_document": { **AgentAssetSpreadsheetManager.build_rule_document_config( live_document, asset_version=version, ), "storage_key": live_document.storage_key, }, } return live_document asset.config_json = { **(asset.config_json or {}), "detail_mode": "spreadsheet", "tag": "财务规则", "rule_library": FINANCE_RULES_LIBRARY, "rule_document": { **AgentAssetSpreadsheetManager.build_rule_document_config( live_document, asset_version=version, ), "storage_key": live_document.storage_key, }, } return live_document @staticmethod def _read_or_build_finance_rule_file( manager: AgentAssetSpreadsheetManager, *, file_name: str, fallback_sheet_name: str, ) -> bytes: live_key = ( Path("rules") / FINANCE_RULES_LIBRARY / file_name ).as_posix() live_path = manager.resolve_storage_path(live_key) if live_path.exists(): return live_path.read_bytes() return AgentAssetSpreadsheetManager.build_blank_rule_workbook(fallback_sheet_name) 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, published_version=current_version if status == AgentAssetStatus.ACTIVE.value else None, working_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 _ensure_agent_asset_schema(self) -> None: bind = self.db.get_bind() inspector = inspect(bind) if "agent_assets" not in inspector.get_table_names(): return column_names = {column["name"] for column in inspector.get_columns("agent_assets")} migration_statements: list[str] = [] if "published_version" not in column_names: migration_statements.append("ALTER TABLE agent_assets ADD COLUMN published_version VARCHAR(30)") if "working_version" not in column_names: migration_statements.append("ALTER TABLE agent_assets ADD COLUMN working_version VARCHAR(30)") for statement in migration_statements: self.db.execute(text(statement)) self.db.execute( text( "UPDATE agent_assets " "SET working_version = COALESCE(working_version, current_version), " "published_version = CASE " "WHEN published_version IS NOT NULL THEN published_version " "WHEN status = 'active' THEN current_version " "ELSE published_version END" ) ) if migration_statements: self.db.commit() 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()) def _iter_platform_risk_manifests(self) -> list[tuple[str, dict[str, object]]]: manager = AgentAssetRuleLibraryManager() manifests: list[tuple[str, dict[str, object]]] = [] for file_name in sorted(manager.list_rule_library_json_files(library=RISK_RULES_LIBRARY)): payload = manager.read_rule_library_json(library=RISK_RULES_LIBRARY, file_name=file_name) if payload.get("enabled") is False: continue manifests.append((file_name, payload)) return manifests @staticmethod def _resolve_platform_risk_category(manifest: dict[str, object]) -> str: explicit = str(manifest.get("risk_category") or "").strip() if explicit: return explicit rule_code = str(manifest.get("rule_code") or "").strip().lower() applies_to = manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {} domains = {str(item or "").strip().lower() for item in applies_to.get("domains") or []} expense_types = { str(item or "").strip().lower() for item in applies_to.get("expense_types") or [] } if rule_code.startswith("risk.invoice."): return "发票" if "meal" in domains or "entertainment" in expense_types: return "餐饮招待" if "transport" in expense_types or "consecutive_transport" in rule_code: return "交通出行" if "office" in expense_types: return "办公物料" if "travel" in domains or rule_code.startswith("risk.travel."): return "差旅" if rule_code.startswith("risk.expense."): return "费用科目" return "通用" def _platform_risk_scenario_json(self, manifest: dict[str, object]) -> list[str]: category = self._resolve_platform_risk_category(manifest) return [category] if category else ["通用"] def _platform_risk_config_json(self, file_name: str, manifest: dict[str, object]) -> dict[str, object]: outcomes = manifest.get("outcomes") if isinstance(manifest.get("outcomes"), dict) else {} fail_outcome = outcomes.get("fail") if isinstance(outcomes.get("fail"), dict) else {} risk_category = self._resolve_platform_risk_category(manifest) return { "severity": str(fail_outcome.get("severity") or "medium"), "enabled": True, "tag": "风险规则", "detail_mode": "json_risk", "risk_category": risk_category, "rule_library": RISK_RULES_LIBRARY, "rule_document": { "file_name": file_name, "storage_key": f"rules/{RISK_RULES_LIBRARY}/{file_name}", }, "ontology_signal": str(manifest.get("ontology_signal") or "").strip(), "evaluator": str(manifest.get("evaluator") or "").strip(), "source_ref": ( (manifest.get("metadata") or {}).get("source_ref") if isinstance(manifest.get("metadata"), dict) else "" ), } def _build_platform_risk_seed_assets(self) -> list[AgentAsset]: assets: list[AgentAsset] = [] for file_name, manifest in self._iter_platform_risk_manifests(): rule_code = str(manifest.get("rule_code") or "").strip() if not rule_code: continue metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} source_ref = str(metadata.get("source_ref") or "").strip() rule_description = str(manifest.get("description") or "").strip() assets.append( AgentAsset( asset_type=AgentAssetType.RULE.value, code=rule_code, name=str(manifest.get("name") or rule_code), description=rule_description or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}", domain=AgentAssetDomain.EXPENSE.value, scenario_json=self._platform_risk_scenario_json(manifest), owner=str(metadata.get("owner") or "风控与审计部"), reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", published_version="v1.0.0", working_version="v1.0.0", config_json=self._platform_risk_config_json(file_name, manifest), ) ) return assets def sync_platform_risk_rules_from_library(self) -> int: existing_codes = set(self.db.scalars(select(AgentAsset.code)).all()) before_count = len(existing_codes) self._ensure_platform_risk_rules_from_library(existing_codes) self.db.flush() after_codes = set(self.db.scalars(select(AgentAsset.code)).all()) synced = max(len(after_codes) - before_count, 0) manifest_count = len(self._iter_platform_risk_manifests()) logger.info( "Platform risk rules synced from library", extra={"manifest_count": manifest_count, "created_count": synced, "total": len(after_codes)}, ) return manifest_count def _ensure_platform_risk_rules_from_library(self, existing_codes: set[str]) -> None: for file_name, manifest in self._iter_platform_risk_manifests(): rule_code = str(manifest.get("rule_code") or "").strip() if not rule_code: continue metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} source_ref = str(metadata.get("source_ref") or "").strip() rule_description = str(manifest.get("description") or "").strip() config_json = self._platform_risk_config_json(file_name, manifest) scenario_json = self._platform_risk_scenario_json(manifest) asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == rule_code)) if asset is None and rule_code not in existing_codes: asset = self._create_seed_asset( asset_type=AgentAssetType.RULE.value, code=rule_code, name=str(manifest.get("name") or rule_code), description=rule_description or f"平台通用风险规则:{source_ref or manifest.get('name') or rule_code}", domain=AgentAssetDomain.EXPENSE.value, scenario_json=scenario_json, owner=str(metadata.get("owner") or "风控与审计部"), reviewer="顾承宇", status=AgentAssetStatus.ACTIVE.value, current_version="v1.0.0", config_json=config_json, ) if asset is None: continue if not str(asset.current_version or "").strip(): asset.current_version = "v1.0.0" if not str(asset.working_version or "").strip(): asset.working_version = asset.current_version if not str(asset.published_version or "").strip(): asset.published_version = asset.current_version asset.status = asset.status or AgentAssetStatus.ACTIVE.value asset.name = str(manifest.get("name") or asset.name or rule_code) if rule_description: asset.description = rule_description asset.config_json = config_json asset.scenario_json = scenario_json self._ensure_asset_version( asset, version="v1.0.0", content=self._platform_risk_rule_markdown(asset, manifest=manifest, file_name=file_name), content_type=AgentAssetContentType.MARKDOWN.value, change_note=f"平台通用风险规则:{asset.name}", created_by="系统初始化", ) self._ensure_asset_review( asset, version="v1.0.0", reviewer="顾承宇", review_status=AgentReviewStatus.APPROVED.value, review_note="平台内置风险规则,供提交验审与风险问答共用。", reviewed_at=datetime.now(UTC), ) @staticmethod def _platform_risk_rule_markdown( asset: AgentAsset, *, manifest: dict[str, object] | None = None, file_name: str = "", ) -> str: config = asset.config_json if isinstance(asset.config_json, dict) else {} rule_document = config.get("rule_document") if isinstance(config.get("rule_document"), dict) else {} resolved_file_name = file_name or str(rule_document.get("file_name") or "").strip() evaluator = str(config.get("evaluator") or (manifest or {}).get("evaluator") or "").strip() ontology_signal = str(config.get("ontology_signal") or (manifest or {}).get("ontology_signal") or "").strip() source_ref = str(config.get("source_ref") or "").strip() if not source_ref and isinstance(manifest, dict): metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} source_ref = str(metadata.get("source_ref") or "").strip() lines = [ f"# {asset.name}", "", "## 规则类型", "", "- 平台内置通用风险规则(`json_risk`)", ] if evaluator: lines.append(f"- 检查器:`{evaluator}`") if ontology_signal: lines.append(f"- 本体信号:`{ontology_signal}`") if source_ref: lines.extend(["", "## 来源", "", f"- {source_ref}"]) if resolved_file_name: lines.extend( [ "", "## 配置文件", "", f"- `rules/{RISK_RULES_LIBRARY}/{resolved_file_name}`", ] ) return "\n".join(lines) @staticmethod def _platform_destination_location_risk_markdown() -> str: return AgentFoundationService._platform_risk_rule_markdown( AgentAsset(name="申报地点与票据地点一致", config_json={"evaluator": "location_consistency"}), manifest={ "evaluator": "location_consistency", "ontology_signal": "location_mismatch", "metadata": {"source_ref": "常用risk.txt / 一、出差类 / 行程不符"}, }, file_name=PLATFORM_DESTINATION_LOCATION_RULE_FILENAME, ) @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)