Files
aiagent/backend/scripts/create_enterprise_scenario_agents.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

570 lines
21 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
"""
批量创建多个「企业场景」示例 Agent画布均为无环 DAG可通过平台校验
场景一览:
1. 企业场景_电商售后 — LLM + http / 文本分析 / 时间
2. 企业场景_研发联调 — LLM + http / JSON / 读文件 / 数学
3. 企业场景_数据分析 — LLM + 只读库查询 / JSON / 文本 / 数学(需数据源与权限)
4. 企业场景_移动运维 — LLM + adb / 读文件 / 文本 / 系统信息
5. 企业场景_合规纪要 — 纯 LLM偏合规与留痕表述
6. 企业场景_销售助理 — LLM + http / text_analyze / datetime
7. 企业场景_多线路由 — Code 解析 query 中的线路标记 → Switch → 三条专精 LLM → Merge
8. 企业场景_审批流样例 — Approval → 通过则 LLM 生成说明 / 拒绝则直接结束(首次执行会待审批)
多线路由标记(写在用户 query 里,任选其一,无标记走默认):
「【客服】」「【研发】」「【运维】」或同义 「[[客服]]」「[[研发]]」「[[运维]]」
用法:
cd backend && .\\venv\\Scripts\\python.exe scripts/create_enterprise_scenario_agents.py
USE_TESTCLIENT=1 # 推荐:不经 TCP
环境变量:
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
ENTERPRISE_SCENARIO_NAMES 可选,逗号分隔,只创建/更新列出的 Agent 名称(完整名)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from typing import Any, Dict, List, Optional, Tuple
# ---------------------------------------------------------------------------
# 图构建辅助
# ---------------------------------------------------------------------------
DEFAULT_LLM_PROVIDER = os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek")
DEFAULT_LLM_MODEL = os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat")
DEFAULT_LLM_TIMEOUT = max(30, int(os.getenv("ENTERPRISE_LLM_TIMEOUT", "180")))
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 or s == t:
continue
key = (s, t, e.get("sourceHandle") or "")
if key in seen:
continue
seen.add(key)
ne = dict(e)
if not ne.get("targetHandle"):
ne["targetHandle"] = "left"
if not ne.get("id"):
sh = ne.get("sourceHandle") or "r"
ne["id"] = f"e_{s}_{t}_{sh}"
out.append(ne)
return out
def _llm(
nid: str,
pos: Tuple[int, int],
label: str,
prompt: str,
*,
temperature: float = 0.3,
tools: Optional[List[str]] = None,
enable_tools: bool = False,
) -> Dict[str, Any]:
tlist = list(tools or [])
en = bool(enable_tools and tlist)
return {
"id": nid,
"type": "llm",
"position": {"x": pos[0], "y": pos[1]},
"data": {
"label": label,
"prompt": prompt,
"provider": DEFAULT_LLM_PROVIDER,
"model": DEFAULT_LLM_MODEL,
"temperature": float(temperature),
"request_timeout": DEFAULT_LLM_TIMEOUT,
"enable_tools": en,
"tools": tlist if en else [],
"selected_tools": tlist if en else [],
},
}
def _linear_start_llm_end(
llm_id: str,
llm_pos: Tuple[int, int],
llm_label: str,
prompt: str,
*,
temperature: float = 0.3,
tools: Optional[List[str]] = None,
enable_tools: bool = False,
) -> Dict[str, Any]:
nodes = [
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 200}, "data": {"label": "开始"}},
_llm(llm_id, llm_pos, llm_label, prompt, temperature=temperature, tools=tools, enable_tools=enable_tools),
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 240, "y": 200}, "data": {"label": "结束"}},
]
edges = _sanitize_edges(
[
{"source": "start-1", "target": llm_id, "sourceHandle": "right", "targetHandle": "left"},
{"source": llm_id, "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
]
)
return {"nodes": nodes, "edges": edges}
# ---------------------------------------------------------------------------
# 各场景 workflow
# ---------------------------------------------------------------------------
WF_ECOM = _linear_start_llm_end(
"llm-1",
(360, 200),
"售后处理",
"""你是电商售后场景助手。处理订单状态咨询、物流、退换货政策说明;不要编造订单号与物流单号。
可调用工具辅助HTTP查公开接口时、文本分析、时间。回答简洁、分条必要时先澄清一项关键信息。""",
temperature=0.35,
tools=["http_request", "text_analyze", "datetime"],
enable_tools=True,
)
WF_DEV = _linear_start_llm_end(
"llm-1",
(360, 200),
"联调助手",
"""你是研发联调助手:帮助解析 API 返回、构造示例请求体、阅读本地说明文件(路径由用户提供)、简单计算。
务必以工具返回为准,勿编造响应体。优先给出可复制的 curl 或 JSON 片段。""",
temperature=0.25,
tools=["http_request", "json_process", "file_read", "math_calculate"],
enable_tools=True,
)
WF_DATA = _linear_start_llm_end(
"llm-1",
(360, 200),
"数据问答",
"""你是企业数据问答助手:在用户提供明确需求时,可用只读 SQLSELECT分析业务表可解析 JSON、做简单统计与文本摘要。
不得执行删改;无数据源或查询失败时如实说明。回答用中文,结论在前。""",
temperature=0.28,
tools=["database_query", "json_process", "text_analyze", "math_calculate"],
enable_tools=True,
)
WF_MOBILE = _linear_start_llm_end(
"llm-1",
(360, 200),
"端侧运维",
"""你是移动端/设备侧运维助手:在用户需要时协助解读 adb 日志、读取用户指定的日志文件、结合系统信息排查。
若环境无 adb 或权限不足,根据工具错误如实说明。不要编造日志内容。""",
temperature=0.3,
tools=["adb_log", "file_read", "text_analyze", "system_info"],
enable_tools=True,
)
WF_COMPLIANCE = _linear_start_llm_end(
"llm-1",
(360, 200),
"合规纪要",
"""你是合规与对内纪要助手。输出应:条理清晰、避免绝对化承诺、敏感处提示「需法务/合规复核」;不编造内部制度条文。
不使用外部工具,基于用户给定材料与问题作答。""",
temperature=0.2,
tools=[],
enable_tools=False,
)
WF_SALES = _linear_start_llm_end(
"llm-1",
(360, 200),
"销售助理",
"""你是 B 端销售助理:协助整理客户痛点、撰写简短跟进话术、摘要公开产品资料(若用户提供 URL 可用 HTTP 拉取)。
保持专业、不夸大效果;数字与条款以工具或用户提供的原文为准。""",
temperature=0.35,
tools=["http_request", "text_analyze", "datetime"],
enable_tools=True,
)
CODE_LANE = """
d = input_data if isinstance(input_data, dict) else {}
q = str(d.get("query", "") or "")
lane = "default"
if "【研发】" in q or "[[研发]]" in q:
lane = "dev"
elif "【运维】" in q or "[[运维]]" in q:
lane = "ops"
elif "【客服】" in q or "[[客服]]" in q:
lane = "cs"
result = dict(d)
result["lane"] = lane
""".strip()
def build_switch_multilane_workflow() -> Dict[str, Any]:
nodes: List[Dict[str, Any]] = [
{"id": "start-1", "type": "start", "position": {"x": 60, "y": 300}, "data": {"label": "开始"}},
{
"id": "code-lane",
"type": "code",
"position": {"x": 260, "y": 300},
"data": {"label": "解析线路", "language": "python", "code": CODE_LANE, "timeout": 15},
},
{
"id": "sw-1",
"type": "switch",
"position": {"x": 500, "y": 300},
"data": {
"label": "业务线路",
"field": "lane",
"cases": {"cs": "br_cs", "dev": "br_dev", "ops": "br_ops"},
"default": "br_default",
},
},
_llm(
"llm-cs",
(760, 120),
"客服线",
"你是企业客服专家。用户已从多线路由进入本分支;忽略线路标记,专注解决问题。简洁、礼貌。",
temperature=0.35,
),
_llm(
"llm-def",
(760, 260),
"默认线",
"你是通用企业助手。用户未指定【客服】/【研发】/【运维】线路;先简要澄清所属场景再回答。",
temperature=0.35,
),
_llm(
"llm-dev",
(760, 400),
"研发线",
"你是研发支持专家。用户已进入研发分支;给出可执行步骤与示例,避免空泛。",
temperature=0.28,
),
_llm(
"llm-ops",
(760, 540),
"运维线",
"你是运维专家。用户已进入运维分支;优先给排查顺序与注意事项,不编造监控数据。",
temperature=0.3,
),
{
"id": "merge-1",
"type": "merge",
"position": {"x": 1060, "y": 330},
"data": {"label": "合并", "mode": "merge_all", "strategy": "object"},
},
{"id": "end-1", "type": "end", "position": {"x": 1300, "y": 330}, "data": {"label": "结束"}},
]
edges = _sanitize_edges(
[
{"source": "start-1", "target": "code-lane", "sourceHandle": "right", "targetHandle": "left"},
{"source": "code-lane", "target": "sw-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "sw-1", "target": "llm-cs", "sourceHandle": "br_cs", "targetHandle": "left"},
{"source": "sw-1", "target": "llm-dev", "sourceHandle": "br_dev", "targetHandle": "left"},
{"source": "sw-1", "target": "llm-ops", "sourceHandle": "br_ops", "targetHandle": "left"},
{"source": "sw-1", "target": "llm-def", "sourceHandle": "br_default", "targetHandle": "left"},
{"source": "llm-cs", "target": "merge-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "llm-dev", "target": "merge-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "llm-ops", "target": "merge-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "llm-def", "target": "merge-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "merge-1", "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
]
)
return {"nodes": nodes, "edges": edges}
def build_hitl_approval_workflow() -> Dict[str, Any]:
nodes: List[Dict[str, Any]] = [
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
{
"id": "appr-1",
"type": "approval",
"position": {"x": 320, "y": 220},
"data": {
"label": "人工审批",
"message": "请确认是否允许本助手根据用户 query 生成对外说明草稿(示例 HITL",
"approved_branch": "approved",
"rejected_branch": "rejected",
},
},
_llm(
"llm-ok",
(580, 120),
"通过后生成",
"审批已通过。请根据用户问题写一段简短、可发送给业务方的说明草稿(中文)。",
temperature=0.35,
),
{
"id": "end-ok",
"type": "end",
"position": {"x": 860, "y": 120},
"data": {"label": "结束(通过)"},
},
{
"id": "end-rej",
"type": "end",
"position": {"x": 580, "y": 320},
"data": {"label": "结束(拒绝)"},
},
]
edges = _sanitize_edges(
[
{"source": "start-1", "target": "appr-1", "sourceHandle": "right", "targetHandle": "left"},
{"source": "appr-1", "target": "llm-ok", "sourceHandle": "approved", "targetHandle": "left"},
{"source": "appr-1", "target": "end-rej", "sourceHandle": "rejected", "targetHandle": "left"},
{"source": "llm-ok", "target": "end-ok", "sourceHandle": "right", "targetHandle": "left"},
]
)
return {"nodes": nodes, "edges": edges}
ScenarioSpec = Tuple[str, str, Dict[str, Any], Dict[str, Any]]
DEFAULT_BUDGET = {"max_steps": 100, "max_llm_invocations": 8, "max_tool_calls": 40}
SWITCH_BUDGET = {"max_steps": 160, "max_llm_invocations": 12, "max_tool_calls": 48}
HITL_BUDGET = {"max_steps": 80, "max_llm_invocations": 6, "max_tool_calls": 20}
def all_scenario_specs() -> List[ScenarioSpec]:
return [
(
"企业场景_电商售后",
"电商售后:订单/物流/退换货咨询,带 HTTP·文本·时间工具。",
WF_ECOM,
DEFAULT_BUDGET,
),
(
"企业场景_研发联调",
"研发联调API/JSON/读文件/计算,适合排障与样例构造。",
WF_DEV,
DEFAULT_BUDGET,
),
(
"企业场景_数据分析",
"数据分析:只读 SQL + JSON/文本/数学(依赖平台数据源)。",
WF_DATA,
DEFAULT_BUDGET,
),
(
"企业场景_移动运维",
"移动运维adb、读文件、文本与系统信息依赖运行环境",
WF_MOBILE,
DEFAULT_BUDGET,
),
(
"企业场景_合规纪要",
"合规纪要:无工具,偏对内记录与风险提示。",
WF_COMPLIANCE,
HITL_BUDGET,
),
(
"企业场景_销售助理",
"销售助理:话术与资料摘要,带 HTTP·文本·时间。",
WF_SALES,
DEFAULT_BUDGET,
),
(
"企业场景_多线路由",
"多线路由query 含【客服】/【研发】/【运维】→ Switch → 专精 LLM → Merge。",
build_switch_multilane_workflow(),
SWITCH_BUDGET,
),
(
"企业场景_审批流样例",
"审批样例:首跑 awaiting_approval通过后 LLM 写说明,拒绝直接结束。",
build_hitl_approval_workflow(),
HITL_BUDGET,
),
]
def _validate_local(wf: Dict[str, Any]) -> None:
from app.services.workflow_validator import validate_workflow
r = validate_workflow(wf.get("nodes") or [], wf.get("edges") or [])
if not r.get("valid"):
errs = r.get("errors") or []
raise ValueError("工作流校验失败: " + "; ".join(errs))
def _filter_specs(
specs: List[ScenarioSpec], only_names: Optional[List[str]]
) -> List[ScenarioSpec]:
if not only_names:
return specs
want = {n.strip() for n in only_names if n.strip()}
return [s for s in specs if s[0] in want]
def _upsert_agent_requests(
base: str,
headers: Dict[str, str],
name: str,
description: str,
wf: Dict[str, Any],
budget: Dict[str, Any],
) -> Tuple[str, bool]:
import requests
def find_id() -> Optional[str]:
rr = requests.get(
f"{base}/api/v1/agents", params={"search": name, "limit": 80}, headers=headers, timeout=45
)
if rr.status_code != 200:
return None
for a in rr.json() or []:
if a.get("name") == name:
return a.get("id")
return None
existing = find_id()
if existing:
ur = requests.put(
f"{base}/api/v1/agents/{existing}",
headers=headers,
json={"description": description, "workflow_config": wf, "budget_config": budget},
timeout=120,
)
if ur.status_code != 200:
raise RuntimeError(f"更新 {name} 失败 {ur.status_code}: {ur.text[:600]}")
return existing, False
cr = requests.post(
f"{base}/api/v1/agents",
headers=headers,
json={"name": name, "description": description, "workflow_config": wf, "budget_config": budget},
timeout=120,
)
if cr.status_code != 201:
raise RuntimeError(f"创建 {name} 失败 {cr.status_code}: {cr.text[:600]}")
return cr.json()["id"], True
def _upsert_agent_testclient(
c: Any,
headers: Dict[str, str],
name: str,
description: str,
wf: Dict[str, Any],
budget: Dict[str, Any],
) -> Tuple[str, bool]:
existing = None
gr = c.get("/api/v1/agents", params={"search": name, "limit": 80}, headers=headers)
if gr.status_code == 200:
for a in gr.json() or []:
if a.get("name") == name:
existing = a.get("id")
break
if existing:
ur = c.put(
f"/api/v1/agents/{existing}",
headers=headers,
json={"description": description, "workflow_config": wf, "budget_config": budget},
)
if ur.status_code != 200:
raise RuntimeError(f"更新 {name} 失败 {ur.status_code}: {ur.text[:600]}")
return existing, False
cr = c.post(
"/api/v1/agents",
headers=headers,
json={"name": name, "description": description, "workflow_config": wf, "budget_config": budget},
)
if cr.status_code != 201:
raise RuntimeError(f"创建 {name} 失败 {cr.status_code}: {cr.text[:600]}")
return cr.json()["id"], True
def main(argv: Optional[List[str]] = None) -> int:
p = argparse.ArgumentParser(description="批量创建企业场景 Agent")
p.add_argument(
"--names",
nargs="*",
help="仅处理这些完整 Agent 名称(可替代环境变量 ENTERPRISE_SCENARIO_NAMES",
)
args = p.parse_args(argv)
env_filter = os.getenv("ENTERPRISE_SCENARIO_NAMES", "").strip()
only_list: Optional[List[str]] = None
if args.names:
only_list = list(args.names)
elif env_filter:
only_list = [x.strip() for x in env_filter.split(",") if x.strip()]
specs = _filter_specs(all_scenario_specs(), only_list)
results: List[Dict[str, Any]] = []
errors: List[str] = []
use_tc = os.getenv("USE_TESTCLIENT", "").strip().lower() in ("1", "true", "yes")
if use_tc:
from fastapi.testclient import TestClient
from app.main import app
c = TestClient(app)
lr = c.post(
"/api/v1/auth/login",
data={
"username": os.getenv("PLATFORM_USERNAME", "admin"),
"password": os.getenv("PLATFORM_PASSWORD", "123456"),
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if lr.status_code != 200:
print("login:", lr.status_code, lr.text[:400], file=sys.stderr)
return 1
token = lr.json().get("access_token")
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
for name, desc, wf, budget in specs:
try:
_validate_local(wf)
aid, created = _upsert_agent_testclient(c, headers, name, desc, wf, budget)
results.append({"name": name, "id": aid, "created": created})
tag = "创建" if created else "更新"
print(f"[{tag}] {name} {aid}")
except Exception as e:
errors.append(f"{name}: {e}")
print(f"[失败] {name}: {e}", file=sys.stderr)
else:
import requests
base = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
lr = requests.post(
f"{base}/api/v1/auth/login",
data={
"username": os.getenv("PLATFORM_USERNAME", "admin"),
"password": os.getenv("PLATFORM_PASSWORD", "123456"),
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=20,
)
if lr.status_code != 200:
print("登录失败:", lr.status_code, lr.text[:400], file=sys.stderr)
return 1
token = lr.json().get("access_token")
if not token:
print("无 access_token", file=sys.stderr)
return 1
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
for name, desc, wf, budget in specs:
try:
_validate_local(wf)
aid, created = _upsert_agent_requests(base, headers, name, desc, wf, budget)
results.append({"name": name, "id": aid, "created": created})
tag = "创建" if created else "更新"
print(f"[{tag}] {name} {aid}")
except Exception as e:
errors.append(f"{name}: {e}")
print(f"[失败] {name}: {e}", file=sys.stderr)
summary = {"ok": len(results), "fail": len(errors), "agents": results, "errors": errors}
print(json.dumps(summary, ensure_ascii=False))
return 0 if not errors else 2
if __name__ == "__main__":
raise SystemExit(main())