Files
aiagent/backend/scripts/create_zhini_kefu_17.py
renjianbo df4fab1e6e feat: Agent 批量测试、作业助手与上传预览;Windows 启动脚本与文档- 新增 run_agent_test_cases 与示例 JSON、(红头)agent测试用例文档
- 扩展 test_agent_execution(--homework、UTF-8 控制台)
- 后端:uploads 预览、file_read、工作流与对话落盘等
- 前端:AgentChatPreview 与设计器相关调整
- 忽略 redis二进制、agent_workspaces、uploads、tessdata 等本机产物

Made-with: Cursor
2026-04-13 20:17:18 +08:00

312 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
从「知你客服15号」复制为「知你客服17号」
- **工具**:与 15 号相同(平台当前全量内置工具)。
- **主动闭环**:在 LLM 节点写入 **max_tool_iterations**(默认 22强调「先自检再执行再验收」。
- **提示词**:强化主动排障与收敛能力:遇到异常优先本地检查与证据化输出,必要时提出最小补充信息而不是停在“我去看看”。
用法:
cd backend && .\\venv\\Scripts\\python.exe scripts/create_zhini_kefu_17.py
环境变量: PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD,
SOURCE_AGENT_NAME默认 知你客服15号, TARGET_NAME默认 知你客服17号
"""
from __future__ import annotations
import copy
import json
import os
import sys
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple
import requests
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
SOURCE_NAME = os.getenv("SOURCE_AGENT_NAME", "知你客服15号")
TARGET_NAME = os.getenv("TARGET_NAME", "知你客服17号")
TOOLS_V17: List[str] = [
"http_request",
"file_read",
"file_write",
"text_analyze",
"datetime",
"math_calculate",
"system_info",
"json_process",
"database_query",
"adb_log",
]
# 与引擎 workflow_engine 中读取的字段一致(上限 64
DEFAULT_MAX_TOOL_ITERATIONS = 22
PROMPT_V17_MARKER = "【知你客服 17 号 · 主动排障闭环执行】"
PROMPT_V17_EXTRA = f"""
{PROMPT_V17_MARKER}
【角色】你是**主动闭环执行型**客服助手:遇到问题优先主动排查,不停留在“我去看看”。你应在同一轮执行内完成「自检 → 执行 → 验证 → 交付/补救」。
【与 15 号的关系】继承 15 号多步工具能力,进一步强化主动性与结果导向,默认尽可能自助完成而非把步骤推给用户。
【主动执行流程(必须遵守)】
1. **先自检**:任务一开始先用最小代价确认关键前提(如工作区、目标文件是否存在、输入是否完整)。
2. **再执行**:按步骤调用工具推进任务,不要只说“将要检查”却不行动。
3. **必验证**:关键写入/修改后必须立即复核(如 `file_read` 回读、长度/关键词检查)再给结论。
4. **失败补救**:单步失败时至少再尝试 1-2 个合理替代方案(文件名冲突、路径差异、编码问题等),并记录已尝试证据。
5. **无法完成才提问**:仅在确实缺少必要信息时,向用户提“最小补充问题”;否则优先自助闭环。
【工具策略】
- **默认本地闭环**:优先 `system_info`、`file_read`、`file_write`、`text_analyze`、`json_process`。
- `http_request` 仅在用户明确要求联网或本地无法获得信息时使用。
- `database_query` 仅 SELECT禁止写操作。
- 古文/常识续写(如《三字经》段落补全)视为常识任务,优先直接生成并落盘,无需联网。
【末行 JSON单行扩展字段推荐
在原有 `intent`、`reply`、`user_profile` 基础上,可增加:
- `task_complete`: boolean本任务是否已彻底完成
- `progress_report`: string本轮已完成步骤的简要清单
- `continuation_hint`: string若 `task_complete` 为 false提示用户下一句怎么说如「继续」「补充 xxx」
仍须以 **一行合法 JSON** 结尾,勿用 markdown 代码围栏。
【交付格式】
- 最终自然语言中要包含:已执行步骤、验证结果、产物路径(若有)。
- 末行仍以**一行合法 JSON**结束(`intent/reply/user_profile` 可扩展 `task_complete/progress_report/continuation_hint`)。
【纪律】勿刷屏 DSML严禁把 `<DSML...>`、工具调用协议原文输出给用户;`file_write` 同轮避免无故重复覆盖。
"""
def _sanitize_edges(edges: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
seen: set = set()
out: List[Dict[str, Any]] = []
for e in edges or []:
s, t = e.get("source"), e.get("target")
if not s or not t:
continue
if s == t:
continue
key = (s, t)
if key in seen:
continue
seen.add(key)
ne = dict(e)
ne["sourceHandle"] = "right"
ne["targetHandle"] = "left"
if not ne.get("id"):
ne["id"] = f"edge_{s}_{t}"
out.append(ne)
return out
def _find_start_node_ids(nodes: List[Dict[str, Any]]) -> List[str]:
ids: List[str] = []
for n in nodes or []:
nid = n.get("id") or ""
nt = (n.get("type") or (n.get("data") or {}).get("type") or "").lower()
if nt == "start" or nid in ("start", "start-1") or str(nid).startswith("start-"):
ids.append(nid)
return ids
def _compute_ranks(
nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]]
) -> Dict[str, int]:
node_ids = [n["id"] for n in nodes if n.get("id")]
start_ids = _find_start_node_ids(nodes)
incoming: Dict[str, int] = {nid: 0 for nid in node_ids}
for e in edges:
s, t = e.get("source"), e.get("target")
if not s or not t or s == t:
continue
if t in incoming:
incoming[t] += 1
if not start_ids:
start_ids = [nid for nid in node_ids if incoming.get(nid, 0) == 0] or ([node_ids[0]] if node_ids else [])
rank: Dict[str, int] = {s: 0 for s in start_ids}
nmax = max(len(nodes), 8)
for _ in range(nmax + 5):
updated = False
for e in edges:
s, t = e.get("source"), e.get("target")
if not s or not t or s == t:
continue
if s not in rank:
continue
nv = rank[s] + 1
if t not in rank or rank[t] < nv:
rank[t] = nv
updated = True
if not updated:
break
max_r = max(rank.values(), default=0)
for nid in node_ids:
if nid not in rank:
rank[nid] = max_r + 1
max_r += 1
return rank
def _apply_layered_positions(nodes: List[Dict[str, Any]], ranks: Dict[str, int]) -> None:
layers: Dict[int, List[str]] = defaultdict(list)
for nid, r in ranks.items():
layers[r].append(nid)
for r in layers:
layers[r].sort()
x0, y0 = 80.0, 140.0
x_step = 300.0
y_step = 110.0
for r in sorted(layers.keys()):
ids = layers[r]
nlen = len(ids)
y_base = y0 - (nlen - 1) * y_step / 2.0
for j, nid in enumerate(ids):
for node in nodes:
if node.get("id") != nid:
continue
pos = node.setdefault("position", {})
pos["x"] = x0 + r * x_step
pos["y"] = y_base + j * y_step
break
def improve_workflow_layout_and_edges(wf: Dict[str, Any]) -> Tuple[int, int]:
nodes = wf.get("nodes") or []
raw_edges = wf.get("edges") or []
loops = sum(
1
for e in raw_edges
if e.get("source") and e.get("target") and e.get("source") == e.get("target")
)
clean = _sanitize_edges(raw_edges)
removed_dup = len(raw_edges) - len(clean) - loops
wf["edges"] = clean
ranks = _compute_ranks(nodes, clean)
_apply_layered_positions(nodes, ranks)
return loops, max(0, removed_dup)
def _patch_llm_unified(wf: dict, base_prompt: Optional[str] = None) -> None:
for n in wf.get("nodes") or []:
if n.get("id") != "llm-unified":
continue
d = n.setdefault("data", {})
prompt = base_prompt if base_prompt else d.get("prompt") or ""
if PROMPT_V17_MARKER not in prompt:
prompt = (prompt.rstrip() + "\n" + PROMPT_V17_EXTRA).strip()
d["prompt"] = prompt
d["enable_tools"] = True
d["tools"] = list(TOOLS_V17)
d["selected_tools"] = list(TOOLS_V17)
d["max_tool_iterations"] = DEFAULT_MAX_TOOL_ITERATIONS
return
print("警告: 未找到节点 llm-unified", file=sys.stderr)
def _find_agent_id_by_name(h: Dict[str, str], name: str) -> Optional[str]:
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 50}, headers=h, timeout=30)
if r.status_code != 200:
return None
for a in r.json() or []:
if a.get("name") == name:
return a.get("id")
return None
def main() -> int:
r = requests.post(
f"{BASE}/api/v1/auth/login",
data={"username": USER, "password": PWD},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=15,
)
if r.status_code != 200:
print("登录失败:", r.status_code, r.text[:500], file=sys.stderr)
return 1
token = r.json().get("access_token")
if not token:
print("无 access_token", file=sys.stderr)
return 1
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
src_id = _find_agent_id_by_name(h, SOURCE_NAME)
if not src_id:
print(f"未找到源 Agent: {SOURCE_NAME}", file=sys.stderr)
return 1
existing = _find_agent_id_by_name(h, TARGET_NAME)
if existing:
print("已存在", TARGET_NAME, "-> 仅更新工作流", existing)
new_id = existing
g = requests.get(f"{BASE}/api/v1/agents/{new_id}", headers=h, timeout=30)
if g.status_code != 200:
print("读取失败:", g.text, file=sys.stderr)
return 1
agent = g.json()
else:
dup = requests.post(
f"{BASE}/api/v1/agents/{src_id}/duplicate",
headers=h,
json={"name": TARGET_NAME},
timeout=60,
)
if dup.status_code != 201:
print("复制失败:", dup.status_code, dup.text[:800], file=sys.stderr)
return 1
new_id = dup.json()["id"]
agent = dup.json()
print("已创建副本:", new_id, TARGET_NAME)
wf = copy.deepcopy(agent["workflow_config"])
loops, dup_edges = improve_workflow_layout_and_edges(wf)
print(f"连线整理: 去掉自环 {loops} 条, 合并重复边 {dup_edges}")
g2 = requests.get(f"{BASE}/api/v1/agents/{src_id}", headers=h, timeout=30)
base_prompt = None
if g2.status_code == 200:
try:
for n in g2.json().get("workflow_config", {}).get("nodes") or []:
if n.get("id") == "llm-unified":
base_prompt = (n.get("data") or {}).get("prompt")
break
except Exception:
pass
_patch_llm_unified(wf, base_prompt=base_prompt)
desc = (
"知你客服17号在15号基础上强化主动闭环执行"
f"llm-unified 配置 max_tool_iterations={DEFAULT_MAX_TOOL_ITERATIONS}"
"单次执行内优先完成自检→执行→验证→补救,减少“只说检查不行动”;输出单行 JSON可含 task_complete/progress_report。"
)
up = requests.put(
f"{BASE}/api/v1/agents/{new_id}",
headers=h,
json={"description": desc, "workflow_config": wf},
timeout=120,
)
if up.status_code != 200:
print("更新失败:", up.status_code, up.text[:1200], file=sys.stderr)
return 1
print("已写入工具:", ", ".join(TOOLS_V17))
print(f"max_tool_iterations: {DEFAULT_MAX_TOOL_ITERATIONS}")
print("Agent ID:", new_id)
print(json.dumps({"id": new_id, "name": TARGET_NAME}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())