2026-04-09 21:58:53 +08:00
|
|
|
|
#!/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
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# 图构建辅助
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-04-13 20:17:18 +08:00
|
|
|
|
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")))
|
|
|
|
|
|
|
2026-04-09 21:58:53 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-13 20:17:18 +08:00
|
|
|
|
"provider": DEFAULT_LLM_PROVIDER,
|
|
|
|
|
|
"model": DEFAULT_LLM_MODEL,
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"temperature": float(temperature),
|
2026-04-13 20:17:18 +08:00
|
|
|
|
"request_timeout": DEFAULT_LLM_TIMEOUT,
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"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),
|
|
|
|
|
|
"数据问答",
|
|
|
|
|
|
"""你是企业数据问答助手:在用户提供明确需求时,可用只读 SQL(SELECT)分析业务表;可解析 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]] = [
|
2026-04-13 20:17:18 +08:00
|
|
|
|
{"id": "start-1", "type": "start", "position": {"x": 60, "y": 300}, "data": {"label": "开始"}},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
{
|
|
|
|
|
|
"id": "code-lane",
|
|
|
|
|
|
"type": "code",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
"position": {"x": 260, "y": 300},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"data": {"label": "解析线路", "language": "python", "code": CODE_LANE, "timeout": 15},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "sw-1",
|
|
|
|
|
|
"type": "switch",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
"position": {"x": 500, "y": 300},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"data": {
|
|
|
|
|
|
"label": "业务线路",
|
|
|
|
|
|
"field": "lane",
|
|
|
|
|
|
"cases": {"cs": "br_cs", "dev": "br_dev", "ops": "br_ops"},
|
|
|
|
|
|
"default": "br_default",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
_llm(
|
|
|
|
|
|
"llm-cs",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
(760, 120),
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"客服线",
|
|
|
|
|
|
"你是企业客服专家。用户已从多线路由进入本分支;忽略线路标记,专注解决问题。简洁、礼貌。",
|
|
|
|
|
|
temperature=0.35,
|
|
|
|
|
|
),
|
2026-04-13 20:17:18 +08:00
|
|
|
|
_llm(
|
|
|
|
|
|
"llm-def",
|
|
|
|
|
|
(760, 260),
|
|
|
|
|
|
"默认线",
|
|
|
|
|
|
"你是通用企业助手。用户未指定【客服】/【研发】/【运维】线路;先简要澄清所属场景再回答。",
|
|
|
|
|
|
temperature=0.35,
|
|
|
|
|
|
),
|
2026-04-09 21:58:53 +08:00
|
|
|
|
_llm(
|
|
|
|
|
|
"llm-dev",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
(760, 400),
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"研发线",
|
|
|
|
|
|
"你是研发支持专家。用户已进入研发分支;给出可执行步骤与示例,避免空泛。",
|
|
|
|
|
|
temperature=0.28,
|
|
|
|
|
|
),
|
|
|
|
|
|
_llm(
|
|
|
|
|
|
"llm-ops",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
(760, 540),
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"运维线",
|
|
|
|
|
|
"你是运维专家。用户已进入运维分支;优先给排查顺序与注意事项,不编造监控数据。",
|
|
|
|
|
|
temperature=0.3,
|
|
|
|
|
|
),
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "merge-1",
|
|
|
|
|
|
"type": "merge",
|
2026-04-13 20:17:18 +08:00
|
|
|
|
"position": {"x": 1060, "y": 330},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
"data": {"label": "合并", "mode": "merge_all", "strategy": "object"},
|
|
|
|
|
|
},
|
2026-04-13 20:17:18 +08:00
|
|
|
|
{"id": "end-1", "type": "end", "position": {"x": 1300, "y": 330}, "data": {"label": "结束"}},
|
2026-04-09 21:58:53 +08:00
|
|
|
|
]
|
|
|
|
|
|
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())
|