新增兴邦金租评审项目模块, 作为 mini-OpenClaw 之上的完整交付: - 五智能体流水线(data_curator/risk_scout/case_finder/approval_advisor/risk_monitor) - evolution 自进化子系统(轨迹采集+奖励计算+模式归纳prompt增强) - /api/xingbang/* 端点(config/workflow/stream/evolution/trajectories) - 运行时产物(客户产出情报卡 + 进化轨迹数据)已 gitignore, 不含隐私 遵循兴邦三档分级审批制度, 数据源指向兴邦实验数据包
163 lines
6.4 KiB
Python
163 lines
6.4 KiB
Python
"""五智能体定义 — 复用 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],
|
||
})
|
||
result = await agent.ainvoke({"messages": messages})
|
||
final = self._extract_text(result)
|
||
# 保存产出到文件
|
||
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
|
||
}
|