from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import select from app.core.agent_enums import ( AgentAssetContentType, AgentAssetDomain, AgentAssetStatus, AgentAssetType, AgentReviewStatus, ) from app.core.logging import get_logger from app.models.agent_asset import AgentAsset from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager from app.services.agent_asset_spreadsheet import ( RISK_RULES_LIBRARY, ) from app.services.agent_foundation_constants import ( PLATFORM_DESTINATION_LOCATION_RULE_FILENAME, ) logger = get_logger("app.services.agent_foundation") class AgentFoundationRiskRuleMixin: 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 if self._is_user_generated_risk_manifest(payload): continue manifests.append((file_name, payload)) return manifests @staticmethod def _is_user_generated_risk_manifest(manifest: dict[str, object]) -> bool: rule_code = str(manifest.get("rule_code") or "").strip().lower() metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {} stability = str(metadata.get("stability") or "").strip().lower() source_ref = str(metadata.get("source_ref") or "").strip() if stability == "generated_draft": return True if source_ref == "自然语言风险规则": return True return ".generated_" in rule_code @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 AgentFoundationRiskRuleMixin._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, )