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_ALGORITHM_REPLAY_TASK_CODE, DIGITAL_EMPLOYEE_BUDGET_PRECONTROL_TASK_CODE, DIGITAL_EMPLOYEE_DEPARTMENT_BASELINE_TASK_CODE, DIGITAL_EMPLOYEE_FALSE_POSITIVE_SAMPLE_TASK_CODE, DIGITAL_EMPLOYEE_FEEDBACK_SAMPLE_TASK_CODE, DIGITAL_EMPLOYEE_FINANCE_DASHBOARD_SNAPSHOT_TASK_CODE, DIGITAL_EMPLOYEE_FINANCE_REPORT_TASK_CODE, DIGITAL_EMPLOYEE_MULTI_EVIDENCE_TASK_CODE, DIGITAL_EMPLOYEE_POLICY_ALIGNMENT_TASK_CODE, DIGITAL_EMPLOYEE_POLICY_CLAUSE_EXTRACT_TASK_CODE, DIGITAL_EMPLOYEE_POLICY_GAP_TASK_CODE, DIGITAL_EMPLOYEE_PROFILE_SCAN_TASK_CODE, DIGITAL_EMPLOYEE_REMINDER_SCAN_TASK_CODE, DIGITAL_EMPLOYEE_RISK_GRAPH_SCAN_TASK_CODE, DIGITAL_EMPLOYEE_RULE_DISCOVERY_TASK_CODE, DIGITAL_EMPLOYEE_RULE_TEMPLATE_ORGANIZE_TASK_CODE, DIGITAL_EMPLOYEE_SKILL_CATEGORIES, DIGITAL_EMPLOYEE_SPATIOTEMPORAL_TASK_CODE, DIGITAL_EMPLOYEE_SUPPLIER_PROFILE_TASK_CODE, DIGITAL_EMPLOYEE_SUPPLIER_RELATION_TASK_CODE, ) DIGITAL_EMPLOYEE_ANALYSIS_ROLE_BOUNDARY = ( "规则由人定义,风险由人确认,主流程由外层智能体执行," "数字员工只读取事实、规则命中和反馈结果,生成后台分析、报告和待复核材料。" ) class AgentFoundationDigitalEmployeeTaskMixin: def _runtime_digital_employee_task_specs(self) -> tuple[dict[str, object], ...]: return ( self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_FINANCE_DASHBOARD_SNAPSHOT_TASK_CODE, name="财务经营快照沉淀", description="按固定周期统计报销金额、费用结构、预算占用、高额单据和个人费用排行,刷新财务看板缓存。", scenario_json=["schedule", "finance_dashboard", "expense", "budget"], owner="财务运营组", cron="0 2 * * *", skill_category="整理", skill_name="finance-dashboard-snapshot-analyst", output_format="finance_dashboard_snapshot", input_sources=[ "expense_claims", "expense_items", "budget_snapshots", "employee_profiles", ], execution_strategy="scheduled_dashboard_cache", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_REMINDER_SCAN_TASK_CODE, name="定时提醒与待办扫描", description="按计划扫描待审批单据、预算编制周期、差旅申请到期和逾期报销,生成可触达的提醒事项。", scenario_json=["schedule", "reminder", "approval", "budget", "travel"], owner="财务运营组", cron="0 2 * * *", skill_category="升级", skill_name="digital-employee-reminder-scanner", output_format="digital_employee_reminder_report", input_sources=[ "expense_claims", "approval_tasks", "budgets", "travel_applications", ], execution_strategy="scheduled_reminder_scan", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_FINANCE_REPORT_TASK_CODE, name="财务报告编排与邮件投递", description=( "按周、季、年整合费用、预算、风险、画像和提醒结果," "生成图文 PDF 报告并按邮箱设置投递给财务管理人员。" ), scenario_json=["schedule", "finance_report", "pdf", "email", "management"], owner="财务运营组", cron="30 8 * * 1", skill_category="整理", skill_name="finance-report-orchestrator", output_format="finance_report_pdf_delivery", input_sources=[ "finance_dashboard_snapshots", "budget_snapshots", "risk_observations", "employee_profiles", "digital_employee_reminders", "system_mail_settings", ], execution_strategy="scheduled_pdf_email_report", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_POLICY_CLAUSE_EXTRACT_TASK_CODE, name="制度条款结构化抽取", description="按计划从财务制度和报销政策中抽取适用范围、限制条件、金额标准、审批要求和证据字段。", scenario_json=["schedule", "knowledge", "policy_clause", "ontology"], owner="财务制度管理组", cron="15 3 * * *", skill_category="整理", skill_name="finance-policy-clause-extractor", output_format="policy_clause_structuring_report", input_sources=["finance_policies", "knowledge_documents", "ontology_parse_logs"], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_POLICY_ALIGNMENT_TASK_CODE, name="报销政策口径对齐", description="对齐不同制度、规则中心和知识库中的报销口径,发现同义、冲突、缺失和过期条款。", scenario_json=["schedule", "knowledge", "expense_policy", "rule_center"], owner="财务制度管理组", cron="30 3 * * *", skill_category="整理", skill_name="expense-policy-alignment", output_format="policy_alignment_report", input_sources=["finance_policies", "risk_rules", "knowledge_items"], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_RULE_TEMPLATE_ORGANIZE_TASK_CODE, name="规则命中样本整理", description="把外层智能体流程已经产生的规则命中、制度引用和历史样本整理为字段映射与复核材料,不新增、不改写、不发布规则。", scenario_json=["schedule", "rule_hit", "risk_rule", "policy_ref"], owner="风控与审计部", cron="45 3 * * 1", skill_category="整理", skill_name="rule-execution-case-organizer", output_format="rule_hit_sample_pack", input_sources=["approved_risk_rules", "policy_refs", "rule_hits"], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_DEPARTMENT_BASELINE_TASK_CODE, name="部门费用基线沉淀", description="按部门、费用类型和时间窗口沉淀费用基线,为预算柔性控制和同类对比提供长期参照。", scenario_json=["schedule", "department", "baseline", "expense"], owner="风控与审计部", cron="45 8 * * 1", skill_category="积累", skill_name="department-expense-baseline-accumulator", output_format="department_expense_baseline_snapshot", input_sources=["expense_claims", "expense_items", "profile_baselines"], execution_strategy="reuse_employee_profile_baseline", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_SUPPLIER_PROFILE_TASK_CODE, name="供应商风险画像沉淀", description="沉淀供应商、商户、酒店和收款方的费用频次、金额分布、异常关系和历史风险反馈。", scenario_json=["schedule", "supplier", "baseline", "risk_graph"], owner="风控与审计部", cron="0 8 * * 2", skill_category="积累", skill_name="supplier-risk-profile-accumulator", output_format="supplier_risk_profile_snapshot", input_sources=["expense_claims", "invoice_entities", "risk_observations"], execution_strategy="reuse_profile_baseline", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_FALSE_POSITIVE_SAMPLE_TASK_CODE, name="历史误报样本沉淀", description="归集被人工标记为误报、忽略或撤销的风险观察,形成算法回放和人工复核校准样本。", scenario_json=["schedule", "false_positive", "feedback", "replay"], owner="风控与审计部", cron="20 10 * * 1", skill_category="积累", skill_name="false-positive-sample-accumulator", output_format="false_positive_sample_pool", input_sources=["risk_observations", "risk_observation_feedback"], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_FEEDBACK_SAMPLE_TASK_CODE, name="风险观察反馈样本沉淀", description="归集确认、补件、升级、改写和人工复核反馈,形成风险观察反馈样本池。", scenario_json=["schedule", "feedback", "risk_observation", "sample_pool"], owner="风控与审计部", cron="40 10 * * 1", skill_category="积累", skill_name="risk-feedback-sample-accumulator", output_format="risk_feedback_sample_pool", input_sources=["risk_observations", "risk_observation_feedback", "agent_runs"], execution_strategy="definition_ready", ), { "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, "allowed_outputs": [ "facts", "rule_hits", "risk_clues", "evidence_refs", "human_review_required", ], "role_boundary": DIGITAL_EMPLOYEE_ANALYSIS_ROLE_BOUNDARY, "writes_rules": False, }, }, { "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, "allowed_outputs": [ "facts", "profile_snapshots", "baseline_metrics", "evidence_refs", "human_review_required", ], "role_boundary": DIGITAL_EMPLOYEE_ANALYSIS_ROLE_BOUNDARY, "writes_rules": False, }, }, self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_MULTI_EVIDENCE_TASK_CODE, name="单据多凭证一致性评估", description="比对报销单、费用明细、发票、流水、合同和事前申请之间的金额、数量、主体和时间字段。", scenario_json=["schedule", "expense", "multi_evidence", "risk_observation"], owner="风控与审计部", cron="15 9 * * *", skill_category="评估", skill_name="multi-evidence-consistency-evaluator", output_format="multi_evidence_consistency_report", input_sources=["expense_claims", "expense_items", "invoices", "attachments"], execution_strategy="reuse_financial_risk_graph_scan", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_SPATIOTEMPORAL_TASK_CODE, name="差旅时空一致性评估", description="评估差旅发生时间、提交时间、票据地点、消费地点、行程轨迹和开票地点是否一致。", scenario_json=["schedule", "travel", "spatiotemporal", "risk_observation"], owner="风控与审计部", cron="30 9 * * *", skill_category="评估", skill_name="travel-spatiotemporal-consistency-evaluator", output_format="spatiotemporal_consistency_report", input_sources=[ "expense_claims", "expense_items", "invoice_locations", "travel_routes", ], execution_strategy="reuse_financial_risk_graph_scan", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_BUDGET_PRECONTROL_TASK_CODE, name="预算占用与超标预警", description="评估预算占用、费用标准、历史基线和柔性控制边界,输出提交前或审批前预警建议。", scenario_json=["schedule", "budget", "expense", "precontrol"], owner="预算管理组", cron="45 9 * * *", skill_category="评估", skill_name="budget-overrun-precontrol-evaluator", output_format="budget_precontrol_warning_report", input_sources=[ "expense_claims", "budget_snapshots", "policy_refs", "profile_baselines", ], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_SUPPLIER_RELATION_TASK_CODE, name="供应商异常关系评估", description="识别员工、部门、供应商、票据和报销单之间的异常聚集、重复关系和跨部门集中风险。", scenario_json=["schedule", "supplier", "risk_graph", "relationship"], owner="风控与审计部", cron="0 9 * * 2", skill_category="评估", skill_name="supplier-abnormal-relation-evaluator", output_format="supplier_abnormal_relation_report", input_sources=[ "risk_graph", "expense_claims", "invoice_entities", "entity_registry", ], execution_strategy="reuse_financial_risk_graph_scan", ), { "code": DIGITAL_EMPLOYEE_RULE_DISCOVERY_TASK_CODE, "name": "风险线索归集", "description": ( "按计划复盘申请、报销、规则命中和人工反馈," "归集带事实依据的潜在线索,提交人工复核,不生成规则。" ), "scenario_json": ["schedule", "application", "reimbursement", "risk_clue"], "owner": "风控与审计部", "reviewer": "顾承宇", "cron": "0 10 * * 1", "skill_category": "升级", "markdown": self._risk_clue_collector_skill_markdown, "change_note": "初始化风险线索归集能力。", "config": { "task_type": "risk_clue_collect", "skill_name": "risk-clue-collector", "input_sources": [ "expense_applications", "expense_claims", "rule_hits", "risk_observation_feedback", ], "output_format": "risk_clue_review_packet", "allowed_outputs": [ "facts", "rule_hits", "risk_clues", "evidence_refs", "human_review_required", ], "role_boundary": DIGITAL_EMPLOYEE_ANALYSIS_ROLE_BOUNDARY, "writes_rules": False, "human_review_required": True, }, }, self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_ALGORITHM_REPLAY_TASK_CODE, name="风险算法回放评测", description="复跑历史风险观察、反馈标签、本体版本和规则版本,评估算法升级前后的误报率和确认率。", scenario_json=["schedule", "algorithm_replay", "evaluation", "feedback"], owner="风控与审计部", cron="30 10 * * 1", skill_category="升级", skill_name="risk-algorithm-replay-evaluator", output_format="algorithm_replay_evaluation_report", input_sources=[ "algorithm_replay_sets", "risk_observations", "risk_observation_feedback", ], execution_strategy="definition_ready", ), self._digital_employee_task_spec( code=DIGITAL_EMPLOYEE_POLICY_GAP_TASK_CODE, name="制度引用缺口提示", description="整理申请、报销、规则命中和人工反馈中缺少制度引用的事实位置,提示人工补齐制度依据,不输出规则变更建议。", scenario_json=["schedule", "policy_reference", "evidence_gap", "human_review"], owner="财务制度管理组", cron="0 11 * * 1", skill_category="升级", skill_name="policy-reference-gap-hinter", output_format="policy_reference_gap_hint_report", input_sources=[ "policy_refs", "rule_hits", "expense_claims", "risk_feedback_samples", ], execution_strategy="definition_ready", ), ) def _digital_employee_task_spec( self, *, code: str, name: str, description: str, scenario_json: list[str], owner: str, cron: str, skill_category: str, skill_name: str, output_format: str, input_sources: list[str], execution_strategy: str, ) -> dict[str, object]: return { "code": code, "name": name, "description": description, "scenario_json": scenario_json, "owner": owner, "reviewer": "顾承宇", "cron": cron, "skill_category": skill_category, "markdown": lambda: self._generic_digital_employee_skill_markdown( skill_name=skill_name, title=name, description=description, ), "change_note": f"初始化{name}能力。", "config": { "skill_name": skill_name, "input_sources": input_sources, "output_format": output_format, "writes_work_record": True, "execution_strategy": execution_strategy, "allowed_outputs": [ "facts", "rule_hits", "risk_clues", "evidence_refs", "human_review_required", ], "role_boundary": DIGITAL_EMPLOYEE_ANALYSIS_ROLE_BOUNDARY, "writes_rules": False, }, } def _generic_digital_employee_skill_markdown( self, *, skill_name: str, title: str, description: str, ) -> str: return self._read_domain_skill_markdown( skill_name, [ "---", f"name: {skill_name}", f"description: {description}", "---", "", f"# {title}", "", "## 功能说明", "", description, ], ) 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), "skill_category": str(spec["skill_category"]), "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)