Files
X-Financial/server/src/app/services/agent_foundation.py

1424 lines
59 KiB
Python
Raw Normal View History

2026-05-11 03:51:24 +00:00
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,
)
2026-05-11 03:51:24 +00:00
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": "附件或单据不完整,需补件后再提交。",
},
}
2026-05-11 03:51:24 +00:00
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()
2026-05-11 03:51:24 +00:00
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()
2026-05-11 03:51:24 +00:00
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(
2026-05-11 03:51:24 +00:00
asset_type=AgentAssetType.RULE.value,
code=ATTACHMENT_RULE_ASSET_CODE,
name="报销附件与单据完整性规则",
description="统一定义报销提交时的附件数量、票据类型和补件处理口径,作为上线前待审核规则。",
2026-05-11 03:51:24 +00:00
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,
},
2026-05-11 03:51:24 +00:00
)
scene_submission_rule = AgentAsset(
2026-05-11 03:51:24 +00:00
asset_type=AgentAssetType.RULE.value,
code="rule.expense.scene_submission_standard",
name="报销场景提交与附件标准",
description="统一定义各报销场景的必填字段、附件类型要求和金额阈值。",
2026-05-11 03:51:24 +00:00
domain=AgentAssetDomain.EXPENSE.value,
scenario_json=["expense", "risk_check", "scene_policy", "attachment_policy"],
2026-05-11 03:51:24 +00:00
owner="费用运营组",
reviewer="顾承宇",
status=AgentAssetStatus.ACTIVE.value,
2026-05-11 03:51:24 +00:00
current_version="v1.0.0",
config_json={
"severity": "high",
"enabled": True,
"runtime_kind": "scene_matrix",
"rule_template_label": "系统内置场景矩阵规则",
},
2026-05-11 03:51:24 +00:00
)
travel_policy_rule = AgentAsset(
2026-05-11 03:51:24 +00:00
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": "差旅标准模板",
},
2026-05-11 03:51:24 +00:00
)
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},
)
llm_wiki_task = AgentAsset(
asset_type=AgentAssetType.TASK.value,
code="task.hermes.llm_wiki_rule_formation",
name="Hermes 制度知识与规则草稿形成",
description="按知识库变化增量重建报销制度 LLM Wiki并形成知识候选与规则草稿。",
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 3 * * *", "agent": AgentName.HERMES.value},
)
2026-05-11 03:51:24 +00:00
self.db.add_all(
[
attachment_rule,
scene_submission_rule,
travel_policy_rule,
2026-05-11 03:51:24 +00:00
skill_expense_asset,
skill_ar_asset,
invoice_mcp_asset,
ledger_mcp_asset,
task_asset,
ar_summary_task,
rule_digest_task,
llm_wiki_task,
2026-05-11 03:51:24 +00:00
]
)
self.db.flush()
self.db.add_all(
[
AgentAssetVersion(
asset=attachment_rule,
2026-05-11 03:51:24 +00:00
version="v0.9.0",
content=self._attachment_submission_requirement_markdown(
version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。",
include_review_note=True,
2026-05-11 03:51:24 +00:00
),
content_type=AgentAssetContentType.MARKDOWN.value,
change_note="首版草稿。",
created_by="高嘉禾",
),
AgentAssetVersion(
asset=attachment_rule,
2026-05-11 03:51:24 +00:00
version="v1.0.0",
content=self._attachment_submission_requirement_markdown(
version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。",
include_review_note=True,
2026-05-11 03:51:24 +00:00
),
content_type=AgentAssetContentType.MARKDOWN.value,
change_note="补充票据替代与差旅等效凭证口径,待审核。",
2026-05-11 03:51:24 +00:00
created_by="高嘉禾",
),
AgentAssetVersion(
asset=scene_submission_rule,
version="v1.0.0",
content=self._scene_submission_standard_markdown(),
2026-05-11 03:51:24 +00:00
content_type=AgentAssetContentType.MARKDOWN.value,
change_note="首版报销场景提交标准,覆盖附件类型、必填字段和金额阈值。",
created_by="系统初始化",
2026-05-11 03:51:24 +00:00
),
AgentAssetVersion(
asset=travel_policy_rule,
version="v1.0.0",
content=self._travel_risk_control_standard_markdown(version="v1.0.0"),
2026-05-11 03:51:24 +00:00
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="系统初始化",
2026-05-11 03:51:24 +00:00
),
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=llm_wiki_task,
version="v1.0.0",
content=self._json_content(
{
"task_type": "llm_wiki_rule_formation",
"schedule": "0 3 * * *",
"target_agent": AgentName.HERMES.value,
"folder": "报销制度",
"changed_only": True,
}
),
content_type=AgentAssetContentType.JSON.value,
change_note="初始化制度知识与规则草稿形成任务。",
created_by="系统初始化",
),
2026-05-11 03:51:24 +00:00
]
)
self.db.add_all(
[
AgentAssetReview(
asset=attachment_rule,
2026-05-11 03:51:24 +00:00
version="v1.0.0",
reviewer="高嘉禾",
review_status=AgentReviewStatus.PENDING.value,
review_note="等待制度管理员确认收据替代与补件时限口径。",
2026-05-11 03:51:24 +00:00
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="制度口径已确认,并已补充可执行配置供审核引擎读取。",
2026-05-11 03:51:24 +00:00
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)
2026-05-11 03:51:24 +00:00
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,
2026-05-11 03:51:24 +00:00
before_json=None,
after_json={"version": "v1.0.0"},
request_id="seed-audit-001",
),
AuditLog(
actor="高嘉禾",
2026-05-11 03:51:24 +00:00
action="review_rule",
resource_type="rule",
resource_id=ATTACHMENT_RULE_ASSET_CODE,
2026-05-11 03:51:24 +00:00
before_json={"review_status": "pending"},
after_json={"review_status": "pending"},
2026-05-11 03:51:24 +00:00
request_id="seed-audit-002",
),
AuditLog(
actor="系统初始化",
action="activate_rule",
resource_type="rule",
resource_id="rule.expense.scene_submission_standard",
before_json={"status": "draft"},
2026-05-11 03:51:24 +00:00
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)
2026-05-11 03:51:24 +00:00
)
scene_submission_rule = self.db.scalar(
select(AgentAsset).where(AgentAsset.code == "rule.expense.scene_submission_standard")
2026-05-11 03:51:24 +00:00
)
travel_policy_rule = self.db.scalar(
select(AgentAsset).where(AgentAsset.code == "rule.expense.travel_risk_control_standard")
2026-05-11 03:51:24 +00:00
)
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,
}
2026-05-11 03:51:24 +00:00
self._ensure_asset_version(
attachment_rule,
version="v0.9.0",
content=self._attachment_submission_requirement_markdown(
version_note="首版附件完整性规则草稿,覆盖基础票据与补件口径。",
include_review_note=True,
2026-05-11 03:51:24 +00:00
),
content_type=AgentAssetContentType.MARKDOWN.value,
change_note="首版草稿。",
created_by="高嘉禾",
2026-05-11 03:51:24 +00:00
)
self._ensure_asset_version(
attachment_rule,
2026-05-11 03:51:24 +00:00
version="v1.0.0",
content=self._attachment_submission_requirement_markdown(
version_note="补充票据缺失、收据替代和差旅等效凭证口径,待审核。",
include_review_note=True,
2026-05-11 03:51:24 +00:00
),
content_type=AgentAssetContentType.MARKDOWN.value,
change_note="补充票据替代与差旅等效凭证口径,待审核。",
2026-05-11 03:51:24 +00:00
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": "系统内置场景矩阵规则",
},
)
2026-05-11 03:51:24 +00:00
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": "系统内置场景矩阵规则",
}
2026-05-11 03:51:24 +00:00
self._ensure_asset_version(
scene_submission_rule,
version="v1.0.0",
content=self._scene_submission_standard_markdown(),
2026-05-11 03:51:24 +00:00
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),
2026-05-11 03:51:24 +00:00
)
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.llm_wiki_rule_formation" not in existing_codes:
asset = self._create_seed_asset(
asset_type=AgentAssetType.TASK.value,
code="task.hermes.llm_wiki_rule_formation",
name="Hermes 制度知识与规则草稿形成",
description="按知识库变化增量重建报销制度 LLM Wiki并形成知识候选与规则草稿。",
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 3 * * *", "agent": AgentName.HERMES.value},
)
self._ensure_asset_version(
asset,
version="v1.0.0",
content=self._json_content(
{
"task_type": "llm_wiki_rule_formation",
"schedule": "0 3 * * *",
"target_agent": AgentName.HERMES.value,
"folder": "报销制度",
"changed_only": True,
}
),
content_type=AgentAssetContentType.JSON.value,
change_note="初始化制度知识与规则草稿形成任务。",
created_by="系统初始化",
)
2026-05-11 03:51:24 +00:00
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())
2026-05-11 03:51:24 +00:00
@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)