"""五智能体定义 — 复用 mini-OpenClaw 的 LLM 与工具,各自带差异化 system prompt。 每个智能体 = 一个独立的 LangChain create_agent: - 共享底层 LLM(DeepSeek) 与读文件工具 - 差异化的 system prompt(角色、规则、输出约定) - 专用工具: read_xingbang_data(读兴邦数据包)、read_output(读上游产出) - 属于同一个全局 AgentManager 编排, 但各 agent 独立 invoke 轨迹采集: 每个 agent 的每次调用(输入/system/prompt/工具序列/输出)都会被 evidence 回调记录, 供 evolution 模块使用。 """ from __future__ import annotations import json from pathlib import Path from typing import Any, Callable from langchain.agents import create_agent from langchain_core.messages import HumanMessage, AIMessage from langchain_core.tools import tool from config import get_rag_mode from graph.prompt_builder import build_system_prompt from xingbang.config import agent_specs, XINGBANG_DATA_DIR, DATA_FILES # ── 专用工具:读取兴邦多源数据包 ──────────────────────── @tool def read_xingbang_data(filename: str) -> str: """读取兴邦金租客户的多源数据文件(JSON/CSV/MD/TXT),返回内容。 可用文件名(需带编号前缀): 01_客户申请信息.json, 02_信用评分卡.json, 03_风险评级报告.md, 04_授信额度测算.csv, 05_征信记录.json, 06_司法执行记录.json, 07_舆情风险评分.json, 08_反欺诈模型输出.json, 09_财务预警模型.csv, 10_财务分析报告.csv, 11_租赁物估值报告.md, 12_行业调研报告.md, 13_历史审批案例库.json, 14_公司制度要点.md, 15_银行流水扫描件.txt """ if not XINGBANG_DATA_DIR.exists(): return f"[错误] 兴邦数据目录不存在: {XINGBANG_DATA_DIR}" path = XINGBANG_DATA_DIR / filename if not path.exists(): return f"[错误] 文件不存在: {filename}" content = path.read_text(encoding="utf-8", errors="ignore") # 限制长度避免超上下文 if len(content) > 4000: content = content[:4000] + "\n...[已截断]" return content @tool def read_output(filename: str, base_dir: str = "") -> str: """读取当前兴邦工作区中上游智能体产出的 Markdown 文件(如 customer_intel.md, due_diligence.md, case_reference.md)。""" file_path = Path(base_dir) / filename if base_dir else Path(filename) if not file_path.exists(): return f"[错误] 上游产出文件不存在: {filename}" content = file_path.read_text(encoding="utf-8", errors="ignore") if len(content) > 8000: content = content[:8000] + "\n...[已截断]" return content # ── 单个评审智能体 ───────────────────────────────────── class XingbangAgent: """一个兴邦评审智能体: 差异化角色 + 复用 LLM + 工具。""" def __init__( self, spec, llm=None, base_dir: Path | None = None, output_dir: Path | None = None, on_evidence: Callable[[str, dict], None] | None = None, ): self.spec = spec self._llm = llm self._base_dir = base_dir self._output_dir = output_dir or (base_dir / "xingbang_output" if base_dir else None) self._on_evidence = on_evidence self._agent = None def _ensure_agent(self): """构建带专属 system prompt 的独立 agent(惰性)。""" if self._agent is not None: return self._agent tools = [read_xingbang_data] tools.append(read_output) prompt = self.spec.system_prompt # 追加读文件工具说明(需要 base_dir 才能读产出) if self._base_dir: prompt += ( f"\n\n[工作目录] 上游产出文件位于 {self._base_dir}。" "读取上游产出时使用 read_output 工具并把 filename 设为产出文件名。" ) self._agent = create_agent(model=self._llm, tools=tools, system_prompt=prompt) return self._agent def _emit(self, kind: str, data: dict): if self._on_evidence: self._on_evidence(kind, data) async def run(self, task: str, context: str = "") -> dict: """执行该智能体任务,返回 {'output': 结果文本, 'file': 保存路径}。""" self._emit("agent_start", {"agent": self.spec.key, "name": self.spec.name}) agent = self._ensure_agent() messages = [] if context: messages.append(HumanMessage(content=f"[上游上下文]\n{context}")) messages.append(HumanMessage(content=task)) self._emit("prompt", { "agent": self.spec.key, "system": self.spec.system_prompt[:500], "task": task[:300], }) import asyncio max_retries = 4 backoff = 5 # 初始等待秒 last_err = None for attempt in range(max_retries): try: result = await agent.ainvoke({"messages": messages}) final = self._extract_text(result) break except Exception as e: # noqa last_err = e msg = str(e) # 仅对 503/限流 重试 if "503" in msg or "overloaded" in msg or "429" in msg: if attempt < max_retries - 1: await asyncio.sleep(backoff * (2 ** attempt)) continue else: raise # 非限流错误立即失败 else: raise RuntimeError(f"网关过载重试后仍失败: {last_err}") # 保存产出到文件 file_path = None if self._output_dir: self._output_dir.mkdir(parents=True, exist_ok=True) file_path = self._output_dir / self.spec.output_file file_path.write_text(final, encoding="utf-8") self._emit("agent_end", { "agent": self.spec.key, "output_file": str(file_path) if file_path else None, "output_preview": final[:300], }) return {"output": final, "file": file_path} @staticmethod def _extract_text(result: dict) -> str: messages = result.get("messages", []) text_parts = [] for m in messages: if hasattr(m, "content") and m.type == "ai" and m.content: text_parts.append(m.content) if text_parts: return "\n\n".join(text_parts).strip() content = result.get("output", "") return content if content else "[智能体未生成有效输出]" def build_agents( llm, base_dir: Path, output_dir: Path | None = None, on_evidence: Callable[[str, dict], None] | None = None, keys: list[str] | None = None, ) -> dict[str, XingbangAgent]: """按流水线顺序构建五智能体实例。""" specs = agent_specs() order = keys or ["data_curator", "risk_scout", "case_finder", "approval_advisor", "risk_monitor"] return { key: XingbangAgent( spec=specs[key], llm=llm, base_dir=base_dir, output_dir=output_dir, on_evidence=on_evidence, ) for key in order }