mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 15:41:59 +00:00
03c18c4d78
一、文案(前端约 1600 处,后端传到界面的约 880 处) - 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」; 总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别 - 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法 - 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」); 报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供; 发布弹窗里结论句的选项不再是 off / require_citation - 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份 - 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例 二、说错的提示(都有测试证明) - 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀 - 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页 - 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填 - 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因 - 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头 - 导出的证据 CSV 里两列同名,第三列改为「片段编号」 三、前后端对齐 - 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对 - 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上 四、检查 - 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证 - 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、 界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
182 lines
6.2 KiB
Python
182 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.tools.net import UnsafeUrlError, safe_request
|
|
from app.tools.registry import ToolContext, register
|
|
|
|
# --------------------------------------------------------------------------
|
|
# HTTP 请求
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
class HttpRequestArgs(BaseModel):
|
|
url: str = Field(description="完整的 http/https URL")
|
|
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"] = "GET"
|
|
headers: dict[str, str] | None = Field(default=None, description="额外请求头")
|
|
params: dict[str, str] | None = Field(default=None, description="查询参数")
|
|
json_body: dict[str, Any] | None = Field(default=None, description="JSON 请求体")
|
|
|
|
|
|
@register(
|
|
name="http_request",
|
|
category="网络",
|
|
description="发起 HTTP 请求并返回状态码与响应体。内网地址会被拒绝。",
|
|
args_schema=HttpRequestArgs,
|
|
dangerous=True,
|
|
)
|
|
async def http_request(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = HttpRequestArgs(**kwargs)
|
|
try:
|
|
result = await safe_request(
|
|
args.method,
|
|
args.url,
|
|
headers=args.headers,
|
|
params=args.params,
|
|
json_body=args.json_body,
|
|
)
|
|
except UnsafeUrlError as e:
|
|
return {"error": str(e)}
|
|
body = result["body"]
|
|
if isinstance(body, str) and len(body) > 20_000:
|
|
result["body"] = body[:20_000] + "\n…(已截断)"
|
|
return result
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 抓网页
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
class WebFetchArgs(BaseModel):
|
|
url: str = Field(description="要抓取的网页地址")
|
|
max_chars: int = Field(default=8000, description="返回正文的最大字符数")
|
|
|
|
|
|
def _html_to_text(html: str) -> tuple[str, str]:
|
|
"""抽正文。去掉脚本样式导航,保留标题和段落结构。"""
|
|
from bs4 import BeautifulSoup
|
|
|
|
soup = BeautifulSoup(html, "html.parser")
|
|
for tag in soup(["script", "style", "noscript", "iframe", "svg", "nav", "footer", "header"]):
|
|
tag.decompose()
|
|
title = (soup.title.string or "").strip() if soup.title else ""
|
|
main = soup.find("article") or soup.find("main") or soup.body or soup
|
|
lines: list[str] = []
|
|
for el in main.find_all(["h1", "h2", "h3", "h4", "li", "p", "pre", "blockquote"]):
|
|
text = " ".join(el.get_text(" ", strip=True).split())
|
|
if not text:
|
|
continue
|
|
if el.name.startswith("h"):
|
|
lines.append(f"\n{'#' * int(el.name[1])} {text}")
|
|
elif el.name == "li":
|
|
lines.append(f"- {text}")
|
|
else:
|
|
lines.append(text)
|
|
body = "\n".join(lines)
|
|
if not body.strip():
|
|
body = " ".join(main.get_text(" ", strip=True).split())
|
|
return title, body
|
|
|
|
|
|
@register(
|
|
name="web_fetch",
|
|
category="网络",
|
|
description="抓取网页并转换成干净的正文文本,适合让模型阅读。",
|
|
args_schema=WebFetchArgs,
|
|
)
|
|
async def web_fetch(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = WebFetchArgs(**kwargs)
|
|
try:
|
|
result = await safe_request("GET", args.url, headers={"User-Agent": "AgentLab/0.1"})
|
|
except UnsafeUrlError as e:
|
|
return {"error": str(e)}
|
|
if result["status"] >= 400:
|
|
return {"error": f"HTTP {result['status']}", "url": result["url"]}
|
|
title, text = _html_to_text(str(result["body"]))
|
|
truncated = len(text) > args.max_chars
|
|
return {
|
|
"url": result["url"],
|
|
"title": title,
|
|
"content": text[: args.max_chars] + ("\n…(已截断)" if truncated else ""),
|
|
"truncated": truncated,
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# 搜索
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
class WebSearchArgs(BaseModel):
|
|
query: str = Field(description="搜索关键词")
|
|
max_results: int = Field(default=5, ge=1, le=20)
|
|
|
|
|
|
async def _tavily(query: str, n: int, key: str) -> list[dict[str, str]]:
|
|
result = await safe_request(
|
|
"POST",
|
|
"https://api.tavily.com/search",
|
|
json_body={"api_key": key, "query": query, "max_results": n},
|
|
)
|
|
data = json.loads(str(result["body"]))
|
|
return [
|
|
{"title": r.get("title", ""), "url": r.get("url", ""), "snippet": r.get("content", "")}
|
|
for r in data.get("results", [])
|
|
]
|
|
|
|
|
|
async def _duckduckgo(query: str, n: int) -> list[dict[str, str]]:
|
|
"""没有搜索 API key 时的兜底:解析 DuckDuckGo 的 lite 页面。"""
|
|
from bs4 import BeautifulSoup
|
|
|
|
result = await safe_request(
|
|
"POST",
|
|
"https://lite.duckduckgo.com/lite/",
|
|
data=f"q={query}",
|
|
headers={
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"User-Agent": "Mozilla/5.0 (compatible; AgentLab/0.1)",
|
|
},
|
|
)
|
|
soup = BeautifulSoup(str(result["body"]), "html.parser")
|
|
out: list[dict[str, str]] = []
|
|
for row in soup.select("a.result-link"):
|
|
out.append(
|
|
{
|
|
"title": row.get_text(" ", strip=True),
|
|
"url": row.get("href", ""),
|
|
"snippet": "",
|
|
}
|
|
)
|
|
if len(out) >= n:
|
|
break
|
|
return out
|
|
|
|
|
|
@register(
|
|
name="web_search",
|
|
category="网络",
|
|
description="搜索互联网。已配置 TAVILY_API_KEY 时使用 Tavily,否则使用 DuckDuckGo。",
|
|
args_schema=WebSearchArgs,
|
|
)
|
|
async def web_search(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = WebSearchArgs(**kwargs)
|
|
key = os.environ.get("TAVILY_API_KEY")
|
|
try:
|
|
if key:
|
|
results = await _tavily(args.query, args.max_results, key)
|
|
engine = "tavily"
|
|
else:
|
|
results = await _duckduckgo(args.query, args.max_results)
|
|
engine = "duckduckgo"
|
|
except Exception as e: # noqa: BLE001 - 搜索失败不该让整个 run 挂掉
|
|
from app.core.errors import explain
|
|
|
|
return {"error": f"搜索失败:{explain(e)[0]}", "results": []}
|
|
return {"engine": engine, "query": args.query, "results": results}
|