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 app.core.agent_enums import ( AgentAssetContentType, AgentAssetDomain, AgentAssetStatus, AgentAssetType, AgentName, AgentPermissionLevel, AgentReviewStatus, AgentRunSource, AgentRunStatus, AgentToolType, ) 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, ) from app.services.expense_rule_runtime import ( build_scene_submission_standard_markdown, build_travel_risk_control_standard_markdown, ) from app.services.agent_foundation_constants import ( ATTACHMENT_RULE_ASSET_CODE, ATTACHMENT_RULE_RUNTIME_CONFIG, COMPANY_COMMUNICATION_RULE_SCENARIO_JSON, COMPANY_COMMUNICATION_RULE_VERSION, COMPANY_TRAVEL_RULE_SCENARIO_JSON, COMPANY_TRAVEL_RULE_VERSION, DEMO_EXPENSE_CLAIM_SIGNATURES, DEMO_PAYABLE_SIGNATURES, DEMO_RECEIVABLE_SIGNATURES, LEGACY_RULE_CODES, PLATFORM_DESTINATION_LOCATION_RULE_FILENAME, ) from app.core.logging import get_logger logger = get_logger("app.services.agent_foundation") class AgentFoundationFinancialSeedMixin: 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", ), ] )