Files
X-Financial/server/src/app/services/agent_foundation_digital_employee_tasks.py
caoxiaozhu 7989f3a159 feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
2026-05-30 15:46:51 +08:00

199 lines
7.9 KiB
Python

from __future__ import annotations
from sqlalchemy import select
from app.core.agent_enums import (
AgentAssetContentType,
AgentAssetDomain,
AgentAssetStatus,
AgentAssetType,
AgentName,
)
from app.models.agent_asset import AgentAsset
from app.services.agent_foundation_constants import (
DIGITAL_EMPLOYEE_PROFILE_SCAN_TASK_CODE,
DIGITAL_EMPLOYEE_RISK_GRAPH_SCAN_TASK_CODE,
DIGITAL_EMPLOYEE_RULE_DISCOVERY_TASK_CODE,
DIGITAL_EMPLOYEE_SKILL_CATEGORIES,
)
class AgentFoundationDigitalEmployeeTaskMixin:
def _runtime_digital_employee_task_specs(self) -> tuple[dict[str, object], ...]:
return (
{
"code": DIGITAL_EMPLOYEE_RISK_GRAPH_SCAN_TASK_CODE,
"name": "财务风险图谱巡检",
"description": "按计划扫描报销单、票据、审批链、员工画像和规则命中结果,生成风险观察与可复核证据链。",
"scenario_json": ["schedule", "expense", "risk_graph", "risk_observation"],
"owner": "风控与审计部",
"reviewer": "顾承宇",
"cron": "0 9 * * *",
"skill_category": "评估",
"markdown": self._financial_risk_graph_scan_skill_markdown,
"change_note": "初始化财务风险图谱巡检能力。",
"config": {
"skill_name": "financial-risk-graph-scanner",
"scan_scope": [
"expense_claims",
"invoices",
"approval_chain",
"employee_profiles",
"risk_rules",
],
"output_format": "risk_observation_report",
"writes_risk_observations": True,
},
},
{
"code": DIGITAL_EMPLOYEE_PROFILE_SCAN_TASK_CODE,
"name": "员工行为画像巡检",
"description": "按计划更新员工费用行为、材料完整性、审批效率和智能协作画像,为风险图谱提供画像基线。",
"scenario_json": ["schedule", "employee_profile", "baseline", "risk_graph"],
"owner": "风控与审计部",
"reviewer": "顾承宇",
"cron": "30 8 * * 1",
"skill_category": "评估",
"markdown": self._employee_behavior_profile_scan_skill_markdown,
"change_note": "初始化员工行为画像巡检能力。",
"config": {
"skill_name": "employee-behavior-profile-scanner",
"profile_dimensions": [
"expense_intensity",
"material_completeness",
"approval_efficiency",
"ai_collaboration",
],
"output_format": "employee_behavior_profile_snapshot",
"writes_profile_snapshots": True,
},
},
{
"code": DIGITAL_EMPLOYEE_RULE_DISCOVERY_TASK_CODE,
"name": "风险规则候选发现",
"description": "按计划复盘风险观察和人工反馈,生成带证据、来源和置信度的候选规则,不直接上线。",
"scenario_json": ["schedule", "risk_observation", "feedback", "rule_candidate"],
"owner": "风控与审计部",
"reviewer": "顾承宇",
"cron": "0 10 * * 1",
"skill_category": "升级",
"markdown": self._risk_rule_discovery_skill_markdown,
"change_note": "初始化风险规则候选发现能力。",
"config": {
"skill_name": "risk-rule-discovery",
"input_sources": [
"risk_observations",
"risk_observation_feedback",
"algorithm_replay_sets",
],
"output_format": "candidate_risk_rules",
"auto_publish": False,
},
},
)
def _upsert_runtime_digital_employee_tasks(self, existing_codes: set[str]) -> None:
for spec in self._runtime_digital_employee_task_specs():
self._upsert_runtime_digital_employee_task(existing_codes, spec)
def _upsert_runtime_digital_employee_task(
self,
existing_codes: set[str],
spec: dict[str, object],
) -> None:
code = str(spec["code"])
config = self._build_runtime_digital_employee_config(spec)
if code not in existing_codes:
asset = self._create_seed_asset(
asset_type=AgentAssetType.TASK.value,
code=code,
name=str(spec["name"]),
description=str(spec["description"]),
domain=AgentAssetDomain.SYSTEM.value,
scenario_json=list(spec["scenario_json"]),
owner=str(spec["owner"]),
reviewer=str(spec["reviewer"]),
status=AgentAssetStatus.ACTIVE.value,
current_version="v1.0.0",
config_json=config,
)
else:
asset = self.db.scalar(select(AgentAsset).where(AgentAsset.code == code))
if asset is None:
return
self._refresh_runtime_digital_employee_asset(asset, spec)
markdown_builder = spec["markdown"]
if not callable(markdown_builder):
return
self._ensure_asset_version(
asset,
version="v1.0.0",
content=markdown_builder(),
content_type=AgentAssetContentType.MARKDOWN.value,
change_note=str(spec["change_note"]),
created_by="系统初始化",
)
def _build_runtime_digital_employee_config(
self,
spec: dict[str, object],
*,
existing_config: dict[str, object] | None = None,
) -> dict[str, object]:
code = str(spec["code"])
cron = str(spec["cron"])
base = {
**self._digital_employee_task_config(code, cron),
"schedule": cron,
"cron_expression": cron,
**dict(spec["config"]),
}
if not existing_config:
return base
existing_cron = (
existing_config.get("cron")
or existing_config.get("schedule")
or existing_config.get("cron_expression")
)
schedule_config = (
{"cron": existing_cron, "schedule": existing_cron, "cron_expression": existing_cron}
if existing_cron
else {}
)
return {
**existing_config,
"agent": AgentName.HERMES.value,
"task_type": code.replace("task.hermes.", "").replace(".", "_"),
"skill_category": str(spec["skill_category"]),
"skill_category_options": list(DIGITAL_EMPLOYEE_SKILL_CATEGORIES),
**dict(spec["config"]),
**schedule_config,
}
def _refresh_runtime_digital_employee_asset(
self,
asset: AgentAsset,
spec: dict[str, object],
) -> None:
asset.name = str(spec["name"])
asset.description = str(spec["description"])
asset.owner = str(spec["owner"])
asset.reviewer = str(spec["reviewer"])
asset.domain = AgentAssetDomain.SYSTEM.value
asset.scenario_json = list(spec["scenario_json"])
if not str(asset.status or "").strip():
asset.status = AgentAssetStatus.ACTIVE.value
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
asset.config_json = self._build_runtime_digital_employee_config(
spec,
existing_config=dict(asset.config_json or {}),
)
self.db.add(asset)