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
This commit is contained in:
@@ -35,6 +35,10 @@ 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()
|
||||
@@ -76,7 +80,10 @@ def _llm(
|
||||
"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 [],
|
||||
@@ -195,17 +202,17 @@ result["lane"] = lane
|
||||
|
||||
def build_switch_multilane_workflow() -> Dict[str, Any]:
|
||||
nodes: List[Dict[str, Any]] = [
|
||||
{"id": "start-1", "type": "start", "position": {"x": 40, "y": 280}, "data": {"label": "开始"}},
|
||||
{"id": "start-1", "type": "start", "position": {"x": 60, "y": 300}, "data": {"label": "开始"}},
|
||||
{
|
||||
"id": "code-lane",
|
||||
"type": "code",
|
||||
"position": {"x": 240, "y": 280},
|
||||
"position": {"x": 260, "y": 300},
|
||||
"data": {"label": "解析线路", "language": "python", "code": CODE_LANE, "timeout": 15},
|
||||
},
|
||||
{
|
||||
"id": "sw-1",
|
||||
"type": "switch",
|
||||
"position": {"x": 460, "y": 280},
|
||||
"position": {"x": 500, "y": 300},
|
||||
"data": {
|
||||
"label": "业务线路",
|
||||
"field": "lane",
|
||||
@@ -215,39 +222,39 @@ def build_switch_multilane_workflow() -> Dict[str, Any]:
|
||||
},
|
||||
_llm(
|
||||
"llm-cs",
|
||||
(700, 80),
|
||||
(760, 120),
|
||||
"客服线",
|
||||
"你是企业客服专家。用户已从多线路由进入本分支;忽略线路标记,专注解决问题。简洁、礼貌。",
|
||||
temperature=0.35,
|
||||
),
|
||||
_llm(
|
||||
"llm-def",
|
||||
(760, 260),
|
||||
"默认线",
|
||||
"你是通用企业助手。用户未指定【客服】/【研发】/【运维】线路;先简要澄清所属场景再回答。",
|
||||
temperature=0.35,
|
||||
),
|
||||
_llm(
|
||||
"llm-dev",
|
||||
(700, 280),
|
||||
(760, 400),
|
||||
"研发线",
|
||||
"你是研发支持专家。用户已进入研发分支;给出可执行步骤与示例,避免空泛。",
|
||||
temperature=0.28,
|
||||
),
|
||||
_llm(
|
||||
"llm-ops",
|
||||
(700, 480),
|
||||
(760, 540),
|
||||
"运维线",
|
||||
"你是运维专家。用户已进入运维分支;优先给排查顺序与注意事项,不编造监控数据。",
|
||||
temperature=0.3,
|
||||
),
|
||||
_llm(
|
||||
"llm-def",
|
||||
(700, 640),
|
||||
"默认线",
|
||||
"你是通用企业助手。用户未指定【客服】/【研发】/【运维】线路;先简要澄清所属场景再回答。",
|
||||
temperature=0.35,
|
||||
),
|
||||
{
|
||||
"id": "merge-1",
|
||||
"type": "merge",
|
||||
"position": {"x": 980, "y": 280},
|
||||
"position": {"x": 1060, "y": 330},
|
||||
"data": {"label": "合并", "mode": "merge_all", "strategy": "object"},
|
||||
},
|
||||
{"id": "end-1", "type": "end", "position": {"x": 1220, "y": 280}, "data": {"label": "结束"}},
|
||||
{"id": "end-1", "type": "end", "position": {"x": 1300, "y": 330}, "data": {"label": "结束"}},
|
||||
]
|
||||
edges = _sanitize_edges(
|
||||
[
|
||||
|
||||
217
backend/scripts/create_homework_manager_agent.py
Normal file
217
backend/scripts/create_homework_manager_agent.py
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建或更新「学生作业管理助手」Agent:单链 Start → LLM → End。
|
||||
侧重:记录作业项、截止日、优先级;跟进完成情况;温和督促与周回顾(不代写可提交的作业正文)。
|
||||
|
||||
用法:
|
||||
cd backend && .\\venv\\Scripts\\python.exe scripts/create_homework_manager_agent.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
|
||||
AGENT_NAME(默认 学生作业管理助手)
|
||||
HOMEWORK_LLM_PROVIDER / HOMEWORK_LLM_MODEL / HOMEWORK_LLM_TIMEOUT(可选)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
|
||||
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")
|
||||
AGENT_NAME = os.getenv("AGENT_NAME", "学生作业管理助手")
|
||||
|
||||
PROVIDER = os.getenv(
|
||||
"HOMEWORK_LLM_PROVIDER", os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek")
|
||||
)
|
||||
MODEL = os.getenv(
|
||||
"HOMEWORK_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat")
|
||||
)
|
||||
REQ_TIMEOUT = max(
|
||||
30,
|
||||
int(
|
||||
os.getenv(
|
||||
"HOMEWORK_LLM_TIMEOUT", os.getenv("ENTERPRISE_LLM_TIMEOUT", "180")
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
BUDGET_CONFIG = {
|
||||
"max_steps": 80,
|
||||
"max_llm_invocations": 6,
|
||||
"max_tool_calls": 20,
|
||||
}
|
||||
|
||||
HOMEWORK_TOOLS = ["file_read", "text_analyze", "datetime", "json_process"]
|
||||
|
||||
HOMEWORK_PROMPT = """你是「学生作业管理助手」,帮助学生**记作业**与**监督完成**,语气友好、具体、可执行。
|
||||
|
||||
【核心能力】
|
||||
1. **记作业**:从用户自然语言中提取「科目 / 作业内容 / 截止日期与时间 / 老师要求要点 / 预估耗时」,整理成清单。
|
||||
- 若用户用回形针**上传**了文件或照片,消息里会出现「相对工作区根路径」列表:**必须先调用 file_read**,用返回的 `content`(正文/OCR 文本)整理进作业清单,勿编造未读到的内容。
|
||||
- 支持常见格式:纯文本/Markdown、**PDF**、**Word(.docx)**、**Excel(.xlsx)**、**照片**(作业拍照等,依赖 OCR;若工具返回需安装 Tesseract 等提示,请如实转告用户并仍可基于用户口述继续记作业)。
|
||||
2. **监督完成**:根据清单追问进度(未开始/进行中/已完成);对临近截止的任务给**温和提醒**(不制造焦虑);可建议拆成小步骤与每日 15–30 分钟微习惯。
|
||||
3. **周回顾**:用户要求时,用 json_process 或清晰表格输出本周完成率、延期项与下周优先三件事。
|
||||
|
||||
【原则】
|
||||
- **不代写**可提交的作业正文、实验报告、论文等;可提供提纲、自检表、引用规范提示。
|
||||
- 日期时间以用户所在语境为准;需要当前时间可借助工具 datetime。
|
||||
- 不确定的信息(如具体截止时刻)先列出假设并请用户确认。
|
||||
- 输出优先中文;列表用编号,便于复制到备忘录。
|
||||
|
||||
【交互习惯】
|
||||
- 用户只说「记一下数学作业」时,主动追问截止日与具体要求(一次问 1–2 个点,避免审问感)。
|
||||
- 用户汇报「做完了」时,确认是否需拍照/上传检查清单,并建议归档到下一条任务前的小结一句话。
|
||||
"""
|
||||
|
||||
|
||||
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 build_workflow() -> Dict[str, Any]:
|
||||
llm_pos: Tuple[int, int] = (380, 220)
|
||||
nodes: List[Dict[str, Any]] = [
|
||||
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
|
||||
{
|
||||
"id": "llm-homework",
|
||||
"type": "llm",
|
||||
"position": {"x": llm_pos[0], "y": llm_pos[1]},
|
||||
"data": {
|
||||
"label": "作业管理",
|
||||
"prompt": HOMEWORK_PROMPT,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": 0.3,
|
||||
"request_timeout": REQ_TIMEOUT,
|
||||
"enable_tools": True,
|
||||
"tools": list(HOMEWORK_TOOLS),
|
||||
"selected_tools": list(HOMEWORK_TOOLS),
|
||||
"max_tool_iterations": 10,
|
||||
},
|
||||
},
|
||||
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
|
||||
]
|
||||
edges = _sanitize_edges(
|
||||
[
|
||||
{"source": "start-1", "target": "llm-homework", "sourceHandle": "right", "targetHandle": "left"},
|
||||
{"source": "llm-homework", "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
|
||||
]
|
||||
)
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
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 _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
|
||||
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:
|
||||
wf = build_workflow()
|
||||
try:
|
||||
_validate_local(wf)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
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"}
|
||||
|
||||
desc = (
|
||||
"学生作业管理助手:记作业(科目、内容、截止日)、跟进度、温和督促与周回顾;"
|
||||
"支持上传文件/照片后用 file_read 提取正文(文本、PDF、docx、xlsx、图片 OCR)与 json_process 整理;"
|
||||
f"默认模型 {PROVIDER}/{MODEL},单次执行内工具迭代上限 10。"
|
||||
)
|
||||
|
||||
existing = _find_agent_id(h, AGENT_NAME)
|
||||
if existing:
|
||||
ur = requests.put(
|
||||
f"{BASE}/api/v1/agents/{existing}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_CONFIG,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if ur.status_code != 200:
|
||||
print("更新失败:", ur.status_code, ur.text[:800], file=sys.stderr)
|
||||
return 1
|
||||
print("已更新", AGENT_NAME, existing)
|
||||
print(json.dumps({"id": existing, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
cr = requests.post(
|
||||
f"{BASE}/api/v1/agents",
|
||||
headers=h,
|
||||
json={
|
||||
"name": AGENT_NAME,
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_CONFIG,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if cr.status_code != 201:
|
||||
print("创建失败:", cr.status_code, cr.text[:800], file=sys.stderr)
|
||||
return 1
|
||||
aid = cr.json()["id"]
|
||||
print("已创建", AGENT_NAME, aid)
|
||||
print(json.dumps({"id": aid, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
201
backend/scripts/create_intelligent_tutor_agent.py
Normal file
201
backend/scripts/create_intelligent_tutor_agent.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建或更新「智能助教」Agent:单链 Start → LLM → End,面向课程答疑、作业辅导与学习规划。
|
||||
|
||||
用法:
|
||||
cd backend && .\\venv\\Scripts\\python.exe scripts/create_intelligent_tutor_agent.py
|
||||
|
||||
环境变量:
|
||||
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
|
||||
AGENT_NAME(默认 智能助教)
|
||||
TUTOR_LLM_PROVIDER / TUTOR_LLM_MODEL / TUTOR_LLM_TIMEOUT(可选,覆盖默认 DeepSeek 与超时秒数)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, BACKEND_DIR)
|
||||
|
||||
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")
|
||||
AGENT_NAME = os.getenv("AGENT_NAME", "智能助教")
|
||||
|
||||
PROVIDER = os.getenv("TUTOR_LLM_PROVIDER", os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek"))
|
||||
MODEL = os.getenv("TUTOR_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat"))
|
||||
REQ_TIMEOUT = max(30, int(os.getenv("TUTOR_LLM_TIMEOUT", os.getenv("ENTERPRISE_LLM_TIMEOUT", "180"))))
|
||||
|
||||
BUDGET_CONFIG = {
|
||||
"max_steps": 80,
|
||||
"max_llm_invocations": 6,
|
||||
"max_tool_calls": 24,
|
||||
}
|
||||
|
||||
TUTOR_TOOLS = ["file_read", "text_analyze", "datetime", "json_process"]
|
||||
|
||||
TUTOR_PROMPT = """你是「智能助教」,面向高校/职业课程场景辅助学习与教学准备。
|
||||
|
||||
【能力】
|
||||
- 概念讲解:用清晰结构(定义→要点→小例子)说明知识点。
|
||||
- 习题辅导:给出**解题思路与关键步骤**,引导学生自己完成计算与证明;不要直接给出可照抄的整卷答案或替考内容。
|
||||
- 学习规划:根据用户目标与可用时间,建议复习顺序与自检清单。
|
||||
- 材料辅助:若用户提到本地课件/笔记路径,可用工具读取后基于原文摘要与答疑。
|
||||
|
||||
【边界】
|
||||
- 不编造教材页码、不虚构课程政策;不确定时明确说明并建议向任课教师核实。
|
||||
- 涉及实验安全、医疗、法律等高风险领域时提示寻求专业人士。
|
||||
- 输出简洁,优先中文;需要公式时用 LaTeX 或纯文本均可读形式。
|
||||
|
||||
【输出】
|
||||
- 先给结论或步骤概览,再展开细节;复杂问题分条编号。
|
||||
"""
|
||||
|
||||
|
||||
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 build_workflow() -> Dict[str, Any]:
|
||||
llm_pos: Tuple[int, int] = (380, 220)
|
||||
nodes: List[Dict[str, Any]] = [
|
||||
{"id": "start-1", "type": "start", "position": {"x": 80, "y": 220}, "data": {"label": "开始"}},
|
||||
{
|
||||
"id": "llm-tutor",
|
||||
"type": "llm",
|
||||
"position": {"x": llm_pos[0], "y": llm_pos[1]},
|
||||
"data": {
|
||||
"label": "智能助教",
|
||||
"prompt": TUTOR_PROMPT,
|
||||
"provider": PROVIDER,
|
||||
"model": MODEL,
|
||||
"temperature": 0.35,
|
||||
"request_timeout": REQ_TIMEOUT,
|
||||
"enable_tools": True,
|
||||
"tools": list(TUTOR_TOOLS),
|
||||
"selected_tools": list(TUTOR_TOOLS),
|
||||
"max_tool_iterations": 12,
|
||||
},
|
||||
},
|
||||
{"id": "end-1", "type": "end", "position": {"x": llm_pos[0] + 260, "y": 220}, "data": {"label": "结束"}},
|
||||
]
|
||||
edges = _sanitize_edges(
|
||||
[
|
||||
{"source": "start-1", "target": "llm-tutor", "sourceHandle": "right", "targetHandle": "left"},
|
||||
{"source": "llm-tutor", "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
|
||||
]
|
||||
)
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
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 _find_agent_id(h: Dict[str, str], name: str) -> Optional[str]:
|
||||
r = requests.get(f"{BASE}/api/v1/agents", params={"search": name, "limit": 80}, headers=h, timeout=45)
|
||||
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:
|
||||
wf = build_workflow()
|
||||
try:
|
||||
_validate_local(wf)
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
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"}
|
||||
|
||||
desc = (
|
||||
"智能助教:课程答疑、习题思路辅导与学习规划;支持读取本地材料(file_read)与文本分析;"
|
||||
f"默认模型 {PROVIDER}/{MODEL},单次执行内工具迭代上限 12。"
|
||||
)
|
||||
|
||||
existing = _find_agent_id(h, AGENT_NAME)
|
||||
if existing:
|
||||
ur = requests.put(
|
||||
f"{BASE}/api/v1/agents/{existing}",
|
||||
headers=h,
|
||||
json={
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_CONFIG,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if ur.status_code != 200:
|
||||
print("更新失败:", ur.status_code, ur.text[:800], file=sys.stderr)
|
||||
return 1
|
||||
print("已更新", AGENT_NAME, existing)
|
||||
print(json.dumps({"id": existing, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
cr = requests.post(
|
||||
f"{BASE}/api/v1/agents",
|
||||
headers=h,
|
||||
json={
|
||||
"name": AGENT_NAME,
|
||||
"description": desc,
|
||||
"workflow_config": wf,
|
||||
"budget_config": BUDGET_CONFIG,
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
if cr.status_code != 201:
|
||||
print("创建失败:", cr.status_code, cr.text[:800], file=sys.stderr)
|
||||
return 1
|
||||
aid = cr.json()["id"]
|
||||
print("已创建", AGENT_NAME, aid)
|
||||
print(json.dumps({"id": aid, "name": AGENT_NAME}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -45,7 +45,8 @@ TOOLS_V15: List[str] = [
|
||||
]
|
||||
|
||||
# 与引擎 workflow_engine 中读取的字段一致(上限 64)
|
||||
DEFAULT_MAX_TOOL_ITERATIONS = 28
|
||||
# 15 号强调可持续执行,但避免过高迭代导致无效工具打转
|
||||
DEFAULT_MAX_TOOL_ITERATIONS = 14
|
||||
|
||||
PROMPT_V15_MARKER = "【知你客服 15 号 · 可持续任务执行】"
|
||||
|
||||
@@ -58,10 +59,11 @@ PROMPT_V15_EXTRA = f"""
|
||||
【与 14 号的关系】继承 14 号全部内置工具与纪律;**工具列表未删减**,平台侧已为 15 号提高**单次执行内工具迭代次数**(见节点 `max_tool_iterations`)。
|
||||
|
||||
【执行策略】
|
||||
1. **多步工具链**:先 `system_info` 确认工作区再 `file_write`;需要外部信息再 `http_request`;需要数据再 `database_query`(仅 SELECT)。每一步根据上一步真实返回再决策。
|
||||
1. **默认本地闭环**:先 `system_info` 确认工作区,再 `file_read/file_write/text_analyze` 完成本地任务;仅当用户**明确要求联网检索**(如“上网查”“联网获取”)时才可调用 `http_request`。
|
||||
2. **持续反馈**:在最终自然语言中说明**已做步骤**与**当前结果**;勿编造工具返回。
|
||||
3. **何时停**:目标达成 → 在末行 JSON 中标明完成;缺用户输入/权限/环境 → 清楚说明缺什么。
|
||||
4. **单次装不下时**:在 `reply` 中说明进度,并建议用户**下一轮发送「继续」**;可把未完成要点写入 `user_profile` 或依赖会话记忆中的 `conversation_history` 衔接(勿用空 JSON 覆盖画像)。
|
||||
5. **古文/常识续写类任务**(如《三字经》补全段落):视为通用知识,不得为此调用 `http_request`;应直接给出内容并按需落盘。
|
||||
|
||||
【末行 JSON(单行)扩展字段(推荐)】
|
||||
在原有 `intent`、`reply`、`user_profile` 基础上,可增加:
|
||||
@@ -71,7 +73,7 @@ PROMPT_V15_EXTRA = f"""
|
||||
|
||||
仍须以 **一行合法 JSON** 结尾,勿用 markdown 代码围栏。
|
||||
|
||||
【纪律】继承 14 号:勿刷屏 DSML;`database_query` 仅 SELECT;`file_write` 同轮勿无故重复写入同一文件除非必要。
|
||||
【纪律】继承 14 号:勿刷屏 DSML;严禁把 `<|DSML|...>`、工具调用协议原文输出给用户;`database_query` 仅 SELECT;`file_write` 同轮勿无故重复写入同一文件除非必要。
|
||||
"""
|
||||
|
||||
|
||||
|
||||
311
backend/scripts/create_zhini_kefu_17.py
Normal file
311
backend/scripts/create_zhini_kefu_17.py
Normal file
@@ -0,0 +1,311 @@
|
||||
#!/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())
|
||||
@@ -40,7 +40,7 @@ def init_builtin_tools():
|
||||
# 保存到数据库
|
||||
tools_to_create = [
|
||||
("http_request", HTTP_REQUEST_SCHEMA, "发送HTTP请求,支持GET、POST、PUT、DELETE方法"),
|
||||
("file_read", FILE_READ_SCHEMA, "读取文件内容,只能读取项目目录下的文件")
|
||||
("file_read", FILE_READ_SCHEMA, "读取工作区内文件:文本、PDF、docx、xlsx、图片 OCR 等")
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
|
||||
156
backend/scripts/test_homework_agent_hello.py
Normal file
156
backend/scripts/test_homework_agent_hello.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
测试“学生作业管理助手”最小对话链路:
|
||||
1) 登录
|
||||
2) 按名称查找 Agent
|
||||
3) 创建执行并发送“你好”
|
||||
4) 轮询直到完成,打印助手回复
|
||||
|
||||
示例:
|
||||
python scripts/test_homework_agent_hello.py --password 123456
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="测试学生作业管理助手发送“你好”")
|
||||
parser.add_argument("--base-url", default="http://127.0.0.1:8037", help="后端地址")
|
||||
parser.add_argument("--username", default="admin", help="登录用户名")
|
||||
parser.add_argument("--password", default="123456", help="登录密码")
|
||||
parser.add_argument("--agent-name", default="学生作业管理助手", help="目标 Agent 名称")
|
||||
parser.add_argument("--message", default="你好", help="发送内容")
|
||||
parser.add_argument("--timeout-seconds", type=int, default=90, help="轮询超时秒数")
|
||||
parser.add_argument("--poll-interval", type=float, default=1.5, help="轮询间隔秒")
|
||||
parser.add_argument("--request-timeout", type=int, default=30, help="单次HTTP请求超时秒数")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def login(base_url: str, username: str, password: str, timeout: int = 20) -> str:
|
||||
url = f"{base_url}/api/v1/auth/login"
|
||||
resp = requests.post(
|
||||
url,
|
||||
data={"username": username, "password": password},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
timeout=timeout,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
raise RuntimeError(f"登录成功但未返回 access_token: {data}")
|
||||
return token
|
||||
|
||||
|
||||
def pick_agent_id(base_url: str, headers: Dict[str, str], agent_name: str, timeout: int = 20) -> str:
|
||||
url = f"{base_url}/api/v1/agents"
|
||||
resp = requests.get(url, headers=headers, params={"search": agent_name, "limit": 100}, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
agents: List[Dict[str, Any]] = resp.json() or []
|
||||
|
||||
exact = [a for a in agents if (a.get("name") or "").strip() == agent_name]
|
||||
target = exact[0] if exact else (agents[0] if agents else None)
|
||||
if not target:
|
||||
raise RuntimeError(f"未找到 Agent: {agent_name}")
|
||||
return str(target["id"])
|
||||
|
||||
|
||||
def create_execution(
|
||||
base_url: str, headers: Dict[str, str], agent_id: str, message: str, timeout: int = 30
|
||||
) -> Dict[str, Any]:
|
||||
url = f"{base_url}/api/v1/executions"
|
||||
payload = {
|
||||
"agent_id": agent_id,
|
||||
"input_data": {
|
||||
"USER_INPUT": message,
|
||||
"query": message,
|
||||
"user_id": f"preview_{agent_id}",
|
||||
"attachments": [],
|
||||
},
|
||||
}
|
||||
resp = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def extract_reply(exec_data: Dict[str, Any]) -> str:
|
||||
output_data = exec_data.get("output_data")
|
||||
if not output_data:
|
||||
return ""
|
||||
if isinstance(output_data, str):
|
||||
return output_data
|
||||
if isinstance(output_data, dict):
|
||||
for key in ("result", "output", "response", "text"):
|
||||
value = output_data.get(key)
|
||||
if value is not None:
|
||||
return value if isinstance(value, str) else str(value)
|
||||
return str(output_data)
|
||||
return str(output_data)
|
||||
|
||||
|
||||
def get_execution(base_url: str, headers: Dict[str, str], execution_id: str, timeout: int = 20) -> Dict[str, Any]:
|
||||
url = f"{base_url}/api/v1/executions/{execution_id}"
|
||||
resp = requests.get(url, headers=headers, timeout=timeout)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
base_url = args.base_url.rstrip("/")
|
||||
|
||||
try:
|
||||
token = login(base_url, args.username, args.password, timeout=args.request_timeout)
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
print(f"[OK] 登录成功: {args.username}")
|
||||
|
||||
agent_id = pick_agent_id(base_url, headers, args.agent_name, timeout=args.request_timeout)
|
||||
print(f"[OK] 找到 Agent: {args.agent_name} ({agent_id})")
|
||||
|
||||
created = create_execution(
|
||||
base_url, headers, agent_id, args.message, timeout=args.request_timeout
|
||||
)
|
||||
execution_id = str(created["id"])
|
||||
print(f"[OK] 已发送消息: {args.message}")
|
||||
print(f"[INFO] execution_id: {execution_id}")
|
||||
|
||||
deadline = time.time() + max(1, args.timeout_seconds)
|
||||
while time.time() < deadline:
|
||||
detail = get_execution(base_url, headers, execution_id, timeout=args.request_timeout)
|
||||
status = detail.get("status")
|
||||
if status in ("completed", "failed", "cancelled", "awaiting_approval"):
|
||||
print(f"[INFO] 最终状态: {status}")
|
||||
if status == "completed":
|
||||
reply = extract_reply(detail)
|
||||
print("[AGENT_REPLY]")
|
||||
print(reply or "(空回复)")
|
||||
return 0
|
||||
print(detail.get("error_message") or "(无错误信息)")
|
||||
return 2
|
||||
print(f"[POLL] status={status}")
|
||||
time.sleep(max(0.2, args.poll_interval))
|
||||
|
||||
print("[TIMEOUT] 轮询超时,执行尚未完成")
|
||||
return 3
|
||||
except requests.HTTPError as e:
|
||||
body = ""
|
||||
if e.response is not None:
|
||||
try:
|
||||
body = e.response.text[:500]
|
||||
except Exception:
|
||||
body = "<无法读取响应内容>"
|
||||
print(f"[HTTP_ERROR] {e} {body}")
|
||||
return 4
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[ERROR] {e}")
|
||||
return 5
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user