feat: v2.2 - 简化版工具生成接口 + TOOL_API_KEY 自动注入
新增功能: - 新增 /external-tools/generate-simple 简化版接口,默认使用 AI 辅助 - AuthConfig 支持 token 字段(与 key 等效) - TOOL_API_KEY 环境变量自动注入到 K8s Deployment - use_ai 参数默认改为 True 修复: - 修复 gitee_manager.py 缩进错误 文档: - 更新 EXTERNAL_TOOL_API.md 文档到 v2.2
This commit is contained in:
+411
-19
@@ -101,12 +101,111 @@ class AgentCodeGenerator:
|
|||||||
}
|
}
|
||||||
return type_map.get(json_type, "Any")
|
return type_map.get(json_type, "Any")
|
||||||
|
|
||||||
def generate_tool_code(self, tool_config: dict) -> str:
|
async def generate_tool_code_with_ai(self, tool_config: dict, api_key: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
根据工具配置生成 Pydantic 工具代码
|
使用 AI 理解工具定义并生成代码(更智能,支持复杂场景如 URL 拼接)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tool_config: 包含 name, url, method, auth, request_params 等
|
tool_config: 工具配置
|
||||||
|
api_key: LLM API Key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AI 生成的 Python 代码
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
# 使用实例配置或环境变量(默认使用 claude-sonnet 生成高质量代码)
|
||||||
|
llm_url = self.llm_base_url
|
||||||
|
llm_model = self.llm_model # 默认: taiji/claude-sonnet-4-5
|
||||||
|
llm_key = api_key or self.llm_api_key
|
||||||
|
|
||||||
|
if not llm_key:
|
||||||
|
# 如果没有 API Key,回退到模板生成
|
||||||
|
logger.warning("未提供 LLM API Key,使用模板生成")
|
||||||
|
return self.generate_tool_code(tool_config)
|
||||||
|
|
||||||
|
name = tool_config.get("name", "custom_tool")
|
||||||
|
func_name = self._convert_name_to_python(name)
|
||||||
|
|
||||||
|
# 构建 prompt
|
||||||
|
prompt = f"""你是一个 Python 代码生成专家。根据以下工具定义,生成一个异步 Python 函数。
|
||||||
|
|
||||||
|
## 工具定义
|
||||||
|
- 名称: {tool_config.get("name")}
|
||||||
|
- 描述: {tool_config.get("description")}
|
||||||
|
- API URL: {tool_config.get("url")}
|
||||||
|
- HTTP 方法: {tool_config.get("method", "GET")}
|
||||||
|
- 认证方式: {json.dumps(tool_config.get("auth", {}), ensure_ascii=False)}
|
||||||
|
- 请求参数: {json.dumps(tool_config.get("request_params", tool_config.get("input_schema", {})), ensure_ascii=False)}
|
||||||
|
- 请求体: {json.dumps(tool_config.get("request_body", {}), ensure_ascii=False)}
|
||||||
|
|
||||||
|
## 重要提示
|
||||||
|
1. 如果 URL 像 `https://r.jina.ai/` 这样需要将参数拼接到路径中(如 `https://r.jina.ai/{{target_url}}`),请正确处理 URL 拼接
|
||||||
|
2. 如果认证是 bearer token,使用环境变量 `TOOL_API_KEY` 获取
|
||||||
|
3. 函数必须是 async def,返回 JSON 字符串
|
||||||
|
4. 使用 httpx 作为 HTTP 客户端
|
||||||
|
5. 包含完整的错误处理
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
只输出 Python 代码,不要其他解释。代码格式如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
\"\"\"
|
||||||
|
工具: {{name}}
|
||||||
|
描述: {{description}}
|
||||||
|
\"\"\"
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Optional, Any
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async def {func_name}(...) -> str:
|
||||||
|
...
|
||||||
|
```"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{llm_url}/chat/completions",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {llm_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": llm_model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "你是一个专业的 Python 代码生成器,只输出代码,不要解释。"},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": 2000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content = response.json()["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
# 提取代码块
|
||||||
|
if "```python" in content:
|
||||||
|
code = content.split("```python")[1].split("```")[0].strip()
|
||||||
|
elif "```" in content:
|
||||||
|
code = content.split("```")[1].split("```")[0].strip()
|
||||||
|
else:
|
||||||
|
code = content.strip()
|
||||||
|
|
||||||
|
logger.info(f"✅ AI 成功生成工具代码: {name}")
|
||||||
|
return code
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"AI 生成代码失败: {e},回退到模板生成")
|
||||||
|
return self.generate_tool_code(tool_config)
|
||||||
|
|
||||||
|
def generate_tool_code(self, tool_config: dict) -> str:
|
||||||
|
"""
|
||||||
|
根据工具配置生成 Pydantic 工具代码(模板方式)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_config: 包含 name, url, method, auth, request_params/input_schema 等
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
生成的 Python 代码字符串
|
生成的 Python 代码字符串
|
||||||
@@ -117,20 +216,23 @@ class AgentCodeGenerator:
|
|||||||
url = tool_config.get("url", "")
|
url = tool_config.get("url", "")
|
||||||
method = tool_config.get("method", "GET").upper()
|
method = tool_config.get("method", "GET").upper()
|
||||||
auth = tool_config.get("auth", {})
|
auth = tool_config.get("auth", {})
|
||||||
request_params = tool_config.get("request_params", {})
|
# 兼容 request_params 和 input_schema 两种字段名
|
||||||
|
request_params = tool_config.get("request_params") or tool_config.get("input_schema") or {}
|
||||||
request_body = tool_config.get("request_body", {})
|
request_body = tool_config.get("request_body", {})
|
||||||
timeout = tool_config.get("timeout", 30)
|
timeout = tool_config.get("timeout", 30)
|
||||||
|
|
||||||
# 构建参数
|
# 构建参数
|
||||||
params = []
|
params = []
|
||||||
params_doc = []
|
params_doc = []
|
||||||
|
required_params = []
|
||||||
|
|
||||||
# 支持两种格式:
|
# 支持两种格式:
|
||||||
# 1. {"properties": {"symbol": {...}}}
|
# 1. {"properties": {"symbol": {...}}, "required": ["symbol"]}
|
||||||
# 2. {"symbol": {...}} (直接参数格式)
|
# 2. {"symbol": {...}} (直接参数格式)
|
||||||
param_props = request_params
|
param_props = request_params
|
||||||
if request_params and request_params.get("properties"):
|
if request_params and request_params.get("properties"):
|
||||||
param_props = request_params["properties"]
|
param_props = request_params["properties"]
|
||||||
|
required_params = request_params.get("required", [])
|
||||||
elif request_params and not any(k in request_params for k in ["type", "required", "description"]):
|
elif request_params and not any(k in request_params for k in ["type", "required", "description"]):
|
||||||
param_props = request_params
|
param_props = request_params
|
||||||
else:
|
else:
|
||||||
@@ -142,7 +244,8 @@ class AgentCodeGenerator:
|
|||||||
continue
|
continue
|
||||||
p_type = self._json_type_to_python(p_info.get("type", "string"))
|
p_type = self._json_type_to_python(p_info.get("type", "string"))
|
||||||
p_desc = p_info.get("description", "")
|
p_desc = p_info.get("description", "")
|
||||||
is_required = p_info.get("required", False)
|
# 检查是否在 required 列表中
|
||||||
|
is_required = p_name in required_params or p_info.get("required", False)
|
||||||
default = p_info.get("default")
|
default = p_info.get("default")
|
||||||
|
|
||||||
if is_required:
|
if is_required:
|
||||||
@@ -162,11 +265,14 @@ class AgentCodeGenerator:
|
|||||||
# 构建参数字典代码
|
# 构建参数字典代码
|
||||||
params_dict_code = ""
|
params_dict_code = ""
|
||||||
if param_props:
|
if param_props:
|
||||||
params_dict_code = "params = {"
|
params_dict_items = []
|
||||||
for p_name in param_props.keys():
|
for p_name in param_props.keys():
|
||||||
if isinstance(param_props[p_name], dict):
|
if isinstance(param_props[p_name], dict):
|
||||||
params_dict_code += f'"{p_name}": {p_name}, '
|
params_dict_items.append(f'"{p_name}": {p_name}')
|
||||||
params_dict_code = params_dict_code.rstrip(", ") + "}"
|
if params_dict_items:
|
||||||
|
params_dict_code = "params = {" + ", ".join(params_dict_items) + "}"
|
||||||
|
else:
|
||||||
|
params_dict_code = "params = {}"
|
||||||
else:
|
else:
|
||||||
params_dict_code = "params = {}"
|
params_dict_code = "params = {}"
|
||||||
|
|
||||||
@@ -175,6 +281,31 @@ class AgentCodeGenerator:
|
|||||||
key_name = auth.get("name", "apikey")
|
key_name = auth.get("name", "apikey")
|
||||||
params_dict_code += f'\n params["{key_name}"] = os.getenv("TOOL_API_KEY", "")'
|
params_dict_code += f'\n params["{key_name}"] = os.getenv("TOOL_API_KEY", "")'
|
||||||
|
|
||||||
|
# 生成 URL 代码(使用 api_url 避免与参数名冲突)
|
||||||
|
url_code = f'api_url = "{url}"'
|
||||||
|
|
||||||
|
# 根据 HTTP 方法决定参数传递方式
|
||||||
|
# POST/PUT/PATCH: 参数放到请求体 (json)
|
||||||
|
# GET/DELETE: 参数放到查询参数 (params)
|
||||||
|
if method in ["POST", "PUT", "PATCH"]:
|
||||||
|
# POST 请求:参数作为 JSON 请求体
|
||||||
|
request_code = f'''async with httpx.AsyncClient(timeout={timeout}) as client:
|
||||||
|
response = await client.request(
|
||||||
|
method="{method}",
|
||||||
|
url=api_url,
|
||||||
|
headers=headers,
|
||||||
|
json={{k: v for k, v in params.items() if v is not None}}
|
||||||
|
)'''
|
||||||
|
else:
|
||||||
|
# GET 请求:参数作为查询参数
|
||||||
|
request_code = f'''async with httpx.AsyncClient(timeout={timeout}) as client:
|
||||||
|
response = await client.request(
|
||||||
|
method="{method}",
|
||||||
|
url=api_url,
|
||||||
|
headers=headers,
|
||||||
|
params={{k: v for k, v in params.items() if v is not None}}
|
||||||
|
)'''
|
||||||
|
|
||||||
# 生成函数代码
|
# 生成函数代码
|
||||||
code = f'''"""
|
code = f'''"""
|
||||||
工具: {name}
|
工具: {name}
|
||||||
@@ -197,18 +328,12 @@ async def {func_name}({params_str}) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
API 响应结果 (JSON 格式)
|
API 响应结果 (JSON 格式)
|
||||||
"""
|
"""
|
||||||
url = "{url}"
|
{url_code}
|
||||||
{auth_headers}
|
{auth_headers}
|
||||||
{params_dict_code}
|
{params_dict_code}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout={timeout}) as client:
|
{request_code}
|
||||||
response = await client.request(
|
|
||||||
method="{method}",
|
|
||||||
url=url,
|
|
||||||
headers=headers,
|
|
||||||
params={{k: v for k, v in params.items() if v is not None}}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return json.dumps({{
|
return json.dumps({{
|
||||||
@@ -925,6 +1050,241 @@ async def batch_call_tools(request: MultiToolCallRequest, api_key: str = Depends
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 智能对话(Agent Chat)====================
|
||||||
|
|
||||||
|
# LLM 配置
|
||||||
|
LLM_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1")
|
||||||
|
LLM_MODEL = os.getenv("MODEL_NAME", "taiji/gpt-4o-mini")
|
||||||
|
|
||||||
|
class ChatRequest(BaseModel):
|
||||||
|
"""聊天请求"""
|
||||||
|
message: str
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
user_id: Optional[str] = None
|
||||||
|
stream: bool = False
|
||||||
|
|
||||||
|
class ChatResponse(BaseModel):
|
||||||
|
"""聊天响应"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
tools_used: List[str] = []
|
||||||
|
conversation_id: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
# 对话历史存储
|
||||||
|
conversations: Dict[str, List[Dict]] = {{}}
|
||||||
|
|
||||||
|
def get_tools_description() -> str:
|
||||||
|
\"\"\"生成工具描述供 LLM 使用\"\"\"
|
||||||
|
tools_desc = []
|
||||||
|
for t in TOOL_LIST:
|
||||||
|
params = t.get("parameters", {{}})
|
||||||
|
param_desc = ", ".join([f"{{k}}: {{v.get('type', 'string')}}" for k, v in params.items()])
|
||||||
|
tools_desc.append(f"- {{t['name']}}: {{t['description']}}\\n 参数: {{param_desc or '无'}}")
|
||||||
|
return "\\n".join(tools_desc)
|
||||||
|
|
||||||
|
def build_system_prompt() -> str:
|
||||||
|
\"\"\"构建系统提示\"\"\"
|
||||||
|
tools_desc = get_tools_description()
|
||||||
|
json_example = '{{"action": "tool_call", "tool": "工具名称", "parameters": {{"参数名": "参数值"}}}}'
|
||||||
|
return f\"\"\"你是一个智能助手 {{SERVER_NAME}},可以使用以下工具来帮助用户:
|
||||||
|
|
||||||
|
{{tools_desc}}
|
||||||
|
|
||||||
|
当用户的问题需要使用工具时,请按以下 JSON 格式回复:
|
||||||
|
{{json_example}}
|
||||||
|
|
||||||
|
当不需要工具时,直接回复用户的问题。
|
||||||
|
|
||||||
|
重要规则:
|
||||||
|
1. 如果问题可以用工具解决,优先使用工具
|
||||||
|
2. 工具调用必须严格使用上述 JSON 格式
|
||||||
|
3. 参数名必须与工具定义匹配
|
||||||
|
4. 一次只调用一个工具\"\"\"
|
||||||
|
|
||||||
|
async def call_llm(messages: List[Dict], api_key: str) -> str:
|
||||||
|
\"\"\"调用 LLM - 使用请求传入的 API Key(用于计费)\"\"\"
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
if not api_key or api_key in ("sk", "sk-test", "test"):
|
||||||
|
raise ValueError("请提供有效的 API Key(用于计费)")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{{LLM_BASE_URL}}/chat/completions",
|
||||||
|
headers={{
|
||||||
|
"Authorization": f"Bearer {{api_key}}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}},
|
||||||
|
json={{
|
||||||
|
"model": LLM_MODEL,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": 0.3,
|
||||||
|
"max_tokens": 2000
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
def parse_tool_call(response: str) -> Optional[Dict]:
|
||||||
|
\"\"\"解析 LLM 响应中的工具调用\"\"\"
|
||||||
|
# 方法1:尝试直接解析整个响应
|
||||||
|
try:
|
||||||
|
data = json.loads(response.strip())
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法2:提取 JSON 块(处理 markdown 代码块)
|
||||||
|
import re
|
||||||
|
# 匹配 ```json ... ``` 或 ``` ... ```
|
||||||
|
code_block = re.search(r'```(?:json)?\\s*([\\s\\S]*?)```', response)
|
||||||
|
if code_block:
|
||||||
|
try:
|
||||||
|
data = json.loads(code_block.group(1).strip())
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 方法3:查找 JSON 对象(从 {{ 到匹配的 }})
|
||||||
|
start = response.find('{{')
|
||||||
|
if start == -1:
|
||||||
|
start = response.find('{{"{{"') # 处理转义
|
||||||
|
if start == -1:
|
||||||
|
start = response.find('{{"action"')
|
||||||
|
|
||||||
|
if start != -1:
|
||||||
|
# 找到平衡的 }}
|
||||||
|
depth = 0
|
||||||
|
end = start
|
||||||
|
for i, c in enumerate(response[start:]):
|
||||||
|
if c == '{{':
|
||||||
|
depth += 1
|
||||||
|
elif c == '}}':
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
end = start + i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(response[start:end])
|
||||||
|
if isinstance(data, dict) and data.get("action") == "tool_call":
|
||||||
|
return data
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@app.post("/chat", response_model=ChatResponse)
|
||||||
|
async def chat(request: ChatRequest, api_key: str = Depends(verify_api_key)):
|
||||||
|
\"\"\"
|
||||||
|
智能对话端点 - Agent 自动选择并调用工具
|
||||||
|
|
||||||
|
输入自然语言,Agent 会:
|
||||||
|
1. 理解用户意图
|
||||||
|
2. 自动选择合适的工具
|
||||||
|
3. 执行工具并返回结果
|
||||||
|
\"\"\"
|
||||||
|
effective_user_id = request.user_id or USER_ID
|
||||||
|
tools_used = []
|
||||||
|
|
||||||
|
# 获取或创建对话历史
|
||||||
|
conv_id = request.conversation_id or str(uuid.uuid4())
|
||||||
|
if conv_id not in conversations:
|
||||||
|
conversations[conv_id] = []
|
||||||
|
|
||||||
|
# 构建消息
|
||||||
|
messages = [
|
||||||
|
{{"role": "system", "content": build_system_prompt()}}
|
||||||
|
]
|
||||||
|
messages.extend(conversations[conv_id])
|
||||||
|
messages.append({{"role": "user", "content": request.message}})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用 LLM
|
||||||
|
llm_response = await call_llm(messages, api_key)
|
||||||
|
|
||||||
|
# 检查是否需要调用工具
|
||||||
|
tool_call = parse_tool_call(llm_response)
|
||||||
|
|
||||||
|
if tool_call and tool_call.get("tool") in TOOL_MAP:
|
||||||
|
tool_name = tool_call["tool"]
|
||||||
|
tool_params = tool_call.get("parameters", {{}})
|
||||||
|
tools_used.append(tool_name)
|
||||||
|
|
||||||
|
logger.info(f"🔧 调用工具: {{tool_name}}, 参数: {{tool_params}}")
|
||||||
|
|
||||||
|
# 执行工具调用
|
||||||
|
if effective_user_id:
|
||||||
|
handler = get_callback_handler()
|
||||||
|
with CallbackContextManager(
|
||||||
|
handler=handler,
|
||||||
|
user_id=effective_user_id,
|
||||||
|
request_id=f"chat-{{int(datetime.utcnow().timestamp())}}"
|
||||||
|
) as ctx:
|
||||||
|
ctx.add_tool(tool_name)
|
||||||
|
tool_result = await TOOL_MAP[tool_name](**tool_params)
|
||||||
|
else:
|
||||||
|
tool_result = await TOOL_MAP[tool_name](**tool_params)
|
||||||
|
|
||||||
|
# 将工具结果发送给 LLM 生成最终回复
|
||||||
|
messages.append({{"role": "assistant", "content": llm_response}})
|
||||||
|
messages.append({{"role": "user", "content": f"工具 {{tool_name}} 返回结果:{{tool_result}}\\n\\n请根据这个结果回答用户的问题。"}})
|
||||||
|
|
||||||
|
final_response = await call_llm(messages, api_key)
|
||||||
|
|
||||||
|
# 保存对话历史
|
||||||
|
conversations[conv_id].append({{"role": "user", "content": request.message}})
|
||||||
|
conversations[conv_id].append({{"role": "assistant", "content": final_response}})
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
success=True,
|
||||||
|
message=final_response,
|
||||||
|
tools_used=tools_used,
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 不需要工具,直接返回 LLM 回复
|
||||||
|
conversations[conv_id].append({{"role": "user", "content": request.message}})
|
||||||
|
conversations[conv_id].append({{"role": "assistant", "content": llm_response}})
|
||||||
|
|
||||||
|
return ChatResponse(
|
||||||
|
success=True,
|
||||||
|
message=llm_response,
|
||||||
|
tools_used=[],
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"聊天失败: {{e}}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return ChatResponse(
|
||||||
|
success=False,
|
||||||
|
message="",
|
||||||
|
error=str(e),
|
||||||
|
conversation_id=conv_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/chat/history/{{conversation_id}}")
|
||||||
|
async def get_chat_history(conversation_id: str):
|
||||||
|
\"\"\"获取对话历史\"\"\"
|
||||||
|
if conversation_id not in conversations:
|
||||||
|
raise HTTPException(status_code=404, detail="对话不存在")
|
||||||
|
return {{"conversation_id": conversation_id, "messages": conversations[conversation_id]}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/chat/history/{{conversation_id}}")
|
||||||
|
async def clear_chat_history(conversation_id: str):
|
||||||
|
\"\"\"清除对话历史\"\"\"
|
||||||
|
if conversation_id in conversations:
|
||||||
|
del conversations[conversation_id]
|
||||||
|
return {{"success": True, "message": "对话历史已清除"}}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -1208,7 +1568,8 @@ class CallbackContextManager:
|
|||||||
cpu_limit: str = "500m",
|
cpu_limit: str = "500m",
|
||||||
memory_request: str = "128Mi",
|
memory_request: str = "128Mi",
|
||||||
memory_limit: str = "512Mi",
|
memory_limit: str = "512Mi",
|
||||||
replicas: int = 1
|
replicas: int = 1,
|
||||||
|
tool_api_keys: List[str] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
生成 Gitea Actions CI/CD 配置
|
生成 Gitea Actions CI/CD 配置
|
||||||
@@ -1223,10 +1584,29 @@ class CallbackContextManager:
|
|||||||
memory_request: 内存请求 (如 128Mi, 256Mi)
|
memory_request: 内存请求 (如 128Mi, 256Mi)
|
||||||
memory_limit: 内存限制 (如 512Mi, 1Gi)
|
memory_limit: 内存限制 (如 512Mi, 1Gi)
|
||||||
replicas: 副本数量
|
replicas: 副本数量
|
||||||
|
tool_api_keys: 工具 API 密钥列表(将注入到容器环境变量)
|
||||||
"""
|
"""
|
||||||
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||||||
image_repo = f"{self.acr_namespace}/{k8s_name}"
|
image_repo = f"{self.acr_namespace}/{k8s_name}"
|
||||||
|
|
||||||
|
# 生成工具 API Key 环境变量配置
|
||||||
|
tool_api_key_env = ""
|
||||||
|
if tool_api_keys:
|
||||||
|
# 如果只有一个 key,使用 TOOL_API_KEY
|
||||||
|
if len(tool_api_keys) == 1:
|
||||||
|
tool_api_key_env = f''' - name: TOOL_API_KEY
|
||||||
|
value: "{tool_api_keys[0]}"'''
|
||||||
|
else:
|
||||||
|
# 多个 key 时,使用编号
|
||||||
|
env_lines = []
|
||||||
|
for i, key in enumerate(tool_api_keys):
|
||||||
|
env_lines.append(f''' - name: TOOL_API_KEY_{i}
|
||||||
|
value: "{key}"''')
|
||||||
|
# 第一个 key 也设置为默认的 TOOL_API_KEY
|
||||||
|
env_lines.insert(0, f''' - name: TOOL_API_KEY
|
||||||
|
value: "{tool_api_keys[0]}"''')
|
||||||
|
tool_api_key_env = "\n".join(env_lines)
|
||||||
|
|
||||||
deploy_step = ""
|
deploy_step = ""
|
||||||
if auto_deploy:
|
if auto_deploy:
|
||||||
deploy_step = f'''
|
deploy_step = f'''
|
||||||
@@ -1301,6 +1681,7 @@ class CallbackContextManager:
|
|||||||
value: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1"
|
value: "https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1"
|
||||||
- name: MODEL_NAME
|
- name: MODEL_NAME
|
||||||
value: "taiji/gpt-4o-mini"
|
value: "taiji/gpt-4o-mini"
|
||||||
|
{tool_api_key_env}
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: "{cpu_request}"
|
cpu: "{cpu_request}"
|
||||||
@@ -1642,6 +2023,16 @@ MIT License
|
|||||||
# 生成 requirements.txt
|
# 生成 requirements.txt
|
||||||
files["requirements.txt"] = self.generate_requirements()
|
files["requirements.txt"] = self.generate_requirements()
|
||||||
|
|
||||||
|
# 从工具配置中提取 API Keys
|
||||||
|
tool_api_keys = []
|
||||||
|
for tool in tools_config:
|
||||||
|
auth = tool.get("auth")
|
||||||
|
if auth:
|
||||||
|
# 兼容 token 和 key 字段
|
||||||
|
api_key = auth.get("token") or auth.get("key")
|
||||||
|
if api_key:
|
||||||
|
tool_api_keys.append(api_key)
|
||||||
|
|
||||||
# 生成 CI/CD 配置
|
# 生成 CI/CD 配置
|
||||||
files[".gitea/workflows/ci-cd.yaml"] = self.generate_gitea_action(
|
files[".gitea/workflows/ci-cd.yaml"] = self.generate_gitea_action(
|
||||||
agent_name=k8s_name,
|
agent_name=k8s_name,
|
||||||
@@ -1650,7 +2041,8 @@ MIT License
|
|||||||
cpu_limit=cpu_limit,
|
cpu_limit=cpu_limit,
|
||||||
memory_request=memory_request,
|
memory_request=memory_request,
|
||||||
memory_limit=memory_limit,
|
memory_limit=memory_limit,
|
||||||
replicas=replicas
|
replicas=replicas,
|
||||||
|
tool_api_keys=tool_api_keys if tool_api_keys else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# 生成 README
|
# 生成 README
|
||||||
|
|||||||
+160
-4
@@ -1,6 +1,6 @@
|
|||||||
# 外部工具 API 文档
|
# 外部工具 API 文档
|
||||||
|
|
||||||
> **版本**: 2026-01-30 v2.1
|
> **版本**: 2026-01-31 v2.2
|
||||||
> **服务地址**: http://20.212.121.126
|
> **服务地址**: http://20.212.121.126
|
||||||
> **规范参考**: [Agent-Manager外部工具接口规范](http://gitee.ath.cx:3000/xiaohei/taiji-AI-PAD/src/branch/feature/chenchen/Docs/Agent-Manager%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83.md)
|
> **规范参考**: [Agent-Manager外部工具接口规范](http://gitee.ath.cx:3000/xiaohei/taiji-AI-PAD/src/branch/feature/chenchen/Docs/Agent-Manager%E5%A4%96%E9%83%A8%E5%B7%A5%E5%85%B7%E6%8E%A5%E5%8F%A3%E8%A7%84%E8%8C%83.md)
|
||||||
|
|
||||||
@@ -43,6 +43,47 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🆕 v2.2 新增功能
|
||||||
|
|
||||||
|
### 1. 简化版工具生成接口 🚀
|
||||||
|
|
||||||
|
新增 `/external-tools/generate-simple` 接口,**默认使用 AI 辅助生成**,只需三个核心字段:
|
||||||
|
|
||||||
|
| 核心字段 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `url` | API 端点 URL |
|
||||||
|
| `auth` | 认证配置(支持 `token` 和 `key` 字段) |
|
||||||
|
| `request_body_schema` | 请求体 Schema(JSON Schema 格式) |
|
||||||
|
|
||||||
|
### 2. AuthConfig 增强
|
||||||
|
|
||||||
|
认证配置现在同时支持 `token` 和 `key` 字段(兼容更多使用习惯):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "bearer",
|
||||||
|
"token": "your-api-token" // 与 "key" 等效
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. TOOL_API_KEY 环境变量自动注入 ⭐
|
||||||
|
|
||||||
|
创建 Agent 时,系统会自动从工具配置中提取 API Key,并注入到 K8s Deployment 的环境变量中:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: TOOL_API_KEY
|
||||||
|
value: "your-extracted-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
工具代码可以通过 `os.getenv("TOOL_API_KEY")` 获取。
|
||||||
|
|
||||||
|
### 4. use_ai 默认开启
|
||||||
|
|
||||||
|
`/external-tools/generate` 接口的 `use_ai` 参数现在默认为 `True`,AI 会更智能地生成工具代码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🆕 v2.1 新增功能
|
## 🆕 v2.1 新增功能
|
||||||
|
|
||||||
### 1. 资源配置支持
|
### 1. 资源配置支持
|
||||||
@@ -127,7 +168,8 @@ X-User-ID: <user_id> # 可选,用于计费
|
|||||||
|
|
||||||
| 序号 | 接口 | 方法 | 说明 |
|
| 序号 | 接口 | 方法 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 1 | `/external-tools/generate` | POST | 生成外部数据工具 |
|
| 1 | `/external-tools/generate` | POST | 生成外部数据工具(完整版) |
|
||||||
|
| **1.1** | **`/external-tools/generate-simple`** | **POST** | **🆕 简化版工具生成(AI 辅助,推荐)** |
|
||||||
| 2 | `/external-tools/{tool_ref_id}` | GET | 获取工具详情 |
|
| 2 | `/external-tools/{tool_ref_id}` | GET | 获取工具详情 |
|
||||||
| 3 | `/external-tools/{tool_ref_id}` | PUT | 更新外部数据工具 |
|
| 3 | `/external-tools/{tool_ref_id}` | PUT | 更新外部数据工具 |
|
||||||
| 4 | `/external-tools/{tool_ref_id}` | DELETE | 删除外部数据工具 |
|
| 4 | `/external-tools/{tool_ref_id}` | DELETE | 删除外部数据工具 |
|
||||||
@@ -135,8 +177,8 @@ X-User-ID: <user_id> # 可选,用于计费
|
|||||||
| 6 | `/external-tools/{tool_ref_id}/code` | GET | 获取生成的代码 |
|
| 6 | `/external-tools/{tool_ref_id}/code` | GET | 获取生成的代码 |
|
||||||
| 7 | `/external-tools/` | GET | 列出所有工具 |
|
| 7 | `/external-tools/` | GET | 列出所有工具 |
|
||||||
| 8 | `/external-tools/agents/create-with-tools` | POST | 创建带工具的 Agent |
|
| 8 | `/external-tools/agents/create-with-tools` | POST | 创建带工具的 Agent |
|
||||||
| 9 | `/external-tools/agents/{agent_ref_id}/build-status` | GET | 🆕 查询构建状态 |
|
| 9 | `/external-tools/agents/{agent_ref_id}/build-status` | GET | 查询构建状态 |
|
||||||
| 10 | `/external-tools/agents/{agent_ref_id}/deployment-info` | GET | 🆕 查询部署信息 |
|
| 10 | `/external-tools/agents/{agent_ref_id}/deployment-info` | GET | 查询部署信息 |
|
||||||
| 11 | `/agents` | POST | 创建 Agent(支持 tool_refs 字段) |
|
| 11 | `/agents` | POST | 创建 Agent(支持 tool_refs 字段) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -253,6 +295,118 @@ curl -X POST http://20.212.121.126/external-tools/generate \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 1.1️⃣ 🆕 简化版工具生成(推荐)
|
||||||
|
|
||||||
|
### 接口
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /external-tools/generate-simple
|
||||||
|
```
|
||||||
|
|
||||||
|
### 功能描述
|
||||||
|
|
||||||
|
简化版工具生成接口,**默认使用 AI 辅助生成**,只需提供三个核心字段。AI 会智能理解您的配置并生成高质量的 Pydantic AI 工具代码。
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| name | string | ✅ | 工具名称(1-100 字符) |
|
||||||
|
| url | string | ✅ | **核心字段** - API 端点 URL |
|
||||||
|
| method | string | ❌ | HTTP 方法,默认 `POST` |
|
||||||
|
| user_id | string | ❌ | 用户 ID,默认 `default` |
|
||||||
|
| auth | object | ❌ | **核心字段** - 认证配置(支持 `token` 或 `key`) |
|
||||||
|
| request_body_schema | object | ❌ | **核心字段** - 请求体 Schema(JSON Schema 格式) |
|
||||||
|
| request_params | object | ❌ | URL 查询参数定义 |
|
||||||
|
| headers | object | ❌ | 自定义请求头 |
|
||||||
|
| description | string | ❌ | 工具描述(可选,AI 会自动推断) |
|
||||||
|
| api_key | string | ❌ | LLM API Key(可选,使用系统默认) |
|
||||||
|
|
||||||
|
### 认证配置(支持两种字段名)
|
||||||
|
|
||||||
|
```json
|
||||||
|
// 方式1: 使用 token 字段
|
||||||
|
{
|
||||||
|
"type": "bearer",
|
||||||
|
"token": "your-api-token"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2: 使用 key 字段
|
||||||
|
{
|
||||||
|
"type": "bearer",
|
||||||
|
"key": "your-api-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://20.212.121.126/external-tools/generate-simple \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "jina_reader",
|
||||||
|
"url": "https://r.jina.ai/",
|
||||||
|
"method": "POST",
|
||||||
|
"user_id": "test-user",
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"token": "jina_xxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
|
"request_body_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要爬取的网页URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["url"]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"tool_ref_id": "tool-jina_reader-d57dcc49",
|
||||||
|
"name": "jina_reader",
|
||||||
|
"description": "调用 jina_reader API,参数: url",
|
||||||
|
"url": "https://r.jina.ai/",
|
||||||
|
"method": "POST",
|
||||||
|
"has_auth": true,
|
||||||
|
"created_at": "2026-01-30T17:25:13.855295"
|
||||||
|
},
|
||||||
|
"message": "工具生成成功 (AI 辅助)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI 生成的代码示例
|
||||||
|
|
||||||
|
AI 会智能理解 API 的调用方式,例如 Jina Reader API 需要将目标 URL 拼接到路径中:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def jina_reader(url: str) -> str:
|
||||||
|
"""调用 jina_reader API 爬取网页内容"""
|
||||||
|
api_key = os.getenv("TOOL_API_KEY", "default_key")
|
||||||
|
|
||||||
|
# AI 正确理解了 URL 拼接方式
|
||||||
|
api_url = f"https://r.jina.ai/{url}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(api_url, headers=headers)
|
||||||
|
# ... 完整的错误处理
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2️⃣ 更新外部数据工具
|
## 2️⃣ 更新外部数据工具
|
||||||
|
|
||||||
### 接口
|
### 接口
|
||||||
@@ -778,3 +932,5 @@ curl "http://${DOMAIN}/"
|
|||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| 2026-01-29 | v1.0 | 初始版本,实现 MCP-Server 外部工具接口规范 |
|
| 2026-01-29 | v1.0 | 初始版本,实现 MCP-Server 外部工具接口规范 |
|
||||||
| 2026-01-29 | v2.0 | 新增回调功能(计费)、多工具支持、构建状态查询、部署信息查询 |
|
| 2026-01-29 | v2.0 | 新增回调功能(计费)、多工具支持、构建状态查询、部署信息查询 |
|
||||||
|
| 2026-01-30 | v2.1 | 新增资源配置参数(cpu_request, cpu_limit, memory_request, memory_limit, replicas) |
|
||||||
|
| 2026-01-31 | v2.2 | 🆕 新增简化版工具生成接口 `/generate-simple`、AuthConfig 支持 `token` 字段、TOOL_API_KEY 环境变量自动注入、use_ai 默认开启 |
|
||||||
+284
-34
@@ -36,6 +36,7 @@ class AuthConfig(BaseModel):
|
|||||||
"""认证配置"""
|
"""认证配置"""
|
||||||
type: str = Field(..., description="认证类型: api_key, bearer, basic")
|
type: str = Field(..., description="认证类型: api_key, bearer, basic")
|
||||||
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
|
key: Optional[str] = Field(None, description="API Key 或 Bearer Token")
|
||||||
|
token: Optional[str] = Field(None, description="Bearer Token(与 key 等效,兼容字段)")
|
||||||
username: Optional[str] = Field(None, description="Basic Auth 用户名")
|
username: Optional[str] = Field(None, description="Basic Auth 用户名")
|
||||||
password: Optional[str] = Field(None, description="Basic Auth 密码")
|
password: Optional[str] = Field(None, description="Basic Auth 密码")
|
||||||
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
|
in_location: Optional[str] = Field("header", alias="in", description="API Key 位置: header, query")
|
||||||
@@ -43,6 +44,10 @@ class AuthConfig(BaseModel):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
populate_by_name = True
|
populate_by_name = True
|
||||||
|
|
||||||
|
def get_token_or_key(self) -> Optional[str]:
|
||||||
|
"""获取 token 或 key(兼容两种字段名)"""
|
||||||
|
return self.token or self.key
|
||||||
|
|
||||||
|
|
||||||
class RetryConfig(BaseModel):
|
class RetryConfig(BaseModel):
|
||||||
@@ -66,9 +71,38 @@ class GenerateToolRequest(BaseModel):
|
|||||||
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||||||
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(JSON Schema 格式)")
|
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(JSON Schema 格式)")
|
||||||
request_body: Optional[Dict] = Field(None, description="请求体定义(JSON Schema 格式)")
|
request_body: Optional[Dict] = Field(None, description="请求体定义(JSON Schema 格式)")
|
||||||
|
input_schema: Optional[Dict] = Field(None, description="输入参数定义(兼容字段,等同于 request_params)")
|
||||||
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
|
response_mapping: Optional[Dict] = Field(None, description="响应字段映射")
|
||||||
timeout: int = Field(30, description="超时时间(秒),默认 30")
|
timeout: int = Field(30, description="超时时间(秒),默认 30")
|
||||||
retry: Optional[RetryConfig] = Field(None, description="重试配置")
|
retry: Optional[RetryConfig] = Field(None, description="重试配置")
|
||||||
|
use_ai: bool = Field(True, description="是否使用 AI 智能生成代码(默认开启,支持复杂场景如 URL 拼接)")
|
||||||
|
api_key: Optional[str] = Field(None, description="LLM API Key(use_ai=true 时需要)")
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleGenerateToolRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
简化版工具生成请求
|
||||||
|
只需要三个核心字段:url、auth、request_body_schema
|
||||||
|
默认使用 AI 辅助生成
|
||||||
|
"""
|
||||||
|
name: str = Field(..., min_length=1, max_length=100, description="工具名称")
|
||||||
|
description: Optional[str] = Field(None, description="工具描述(可选,AI 会自动推断)")
|
||||||
|
url: str = Field(..., description="API 端点 URL")
|
||||||
|
method: str = Field("POST", description="HTTP 方法,默认 POST")
|
||||||
|
user_id: str = Field("default", description="用户 ID")
|
||||||
|
|
||||||
|
# 核心三要素
|
||||||
|
auth: Optional[AuthConfig] = Field(None, description="认证配置")
|
||||||
|
request_body_schema: Optional[Dict] = Field(None, description="请求体 Schema (JSON Schema 格式)")
|
||||||
|
request_params: Optional[Dict] = Field(None, description="URL 查询参数定义(可选)")
|
||||||
|
headers: Optional[Dict[str, str]] = Field(None, description="自定义请求头(可选)")
|
||||||
|
|
||||||
|
# AI 生成相关
|
||||||
|
api_key: Optional[str] = Field(None, description="LLM API Key(可选,使用系统默认)")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# 允许额外字段,让 AI 可以处理任意用户输入
|
||||||
|
extra = "allow"
|
||||||
|
|
||||||
|
|
||||||
class UpdateToolRequest(BaseModel):
|
class UpdateToolRequest(BaseModel):
|
||||||
@@ -157,6 +191,17 @@ async def generate_tool(request: GenerateToolRequest):
|
|||||||
# 生成唯一 tool_ref_id
|
# 生成唯一 tool_ref_id
|
||||||
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
|
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# 合并 request_params 和 input_schema(兼容两种字段名)
|
||||||
|
merged_params = request.request_params or request.input_schema
|
||||||
|
|
||||||
|
# 处理认证配置 - 兼容 token 和 key 字段
|
||||||
|
auth_config = None
|
||||||
|
if request.auth:
|
||||||
|
auth_config = request.auth.model_dump(by_alias=True)
|
||||||
|
# 兼容 token 字段:如果用户使用 token,将其映射到 key
|
||||||
|
if auth_config.get("token") and not auth_config.get("key"):
|
||||||
|
auth_config["key"] = auth_config["token"]
|
||||||
|
|
||||||
# 构建工具配置
|
# 构建工具配置
|
||||||
tool_config = {
|
tool_config = {
|
||||||
"name": request.name,
|
"name": request.name,
|
||||||
@@ -164,8 +209,9 @@ async def generate_tool(request: GenerateToolRequest):
|
|||||||
"url": request.url,
|
"url": request.url,
|
||||||
"method": request.method.upper(),
|
"method": request.method.upper(),
|
||||||
"headers": request.headers,
|
"headers": request.headers,
|
||||||
"auth": request.auth.model_dump(by_alias=True) if request.auth else None,
|
"auth": auth_config,
|
||||||
"request_params": request.request_params,
|
"request_params": merged_params,
|
||||||
|
"input_schema": merged_params, # 保留两种格式供 AI 理解
|
||||||
"request_body": request.request_body,
|
"request_body": request.request_body,
|
||||||
"response_mapping": request.response_mapping,
|
"response_mapping": request.response_mapping,
|
||||||
"timeout": request.timeout,
|
"timeout": request.timeout,
|
||||||
@@ -173,7 +219,29 @@ async def generate_tool(request: GenerateToolRequest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 生成 Pydantic AI 工具代码
|
# 生成 Pydantic AI 工具代码
|
||||||
tool_code = agent_code_generator.generate_tool_code(tool_config)
|
if request.use_ai:
|
||||||
|
# 使用 AI 智能生成(支持复杂场景)
|
||||||
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
|
logger.info(f"🤖 使用 AI 生成工具代码: {request.name}")
|
||||||
|
|
||||||
|
# 在新线程中运行异步代码
|
||||||
|
def run_async():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return loop.run_until_complete(
|
||||||
|
agent_code_generator.generate_tool_code_with_ai(tool_config, request.api_key)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
future = executor.submit(run_async)
|
||||||
|
tool_code = future.result(timeout=90)
|
||||||
|
else:
|
||||||
|
# 使用模板生成
|
||||||
|
tool_code = agent_code_generator.generate_tool_code(tool_config)
|
||||||
|
|
||||||
# 存储工具配置和代码
|
# 存储工具配置和代码
|
||||||
save_result = tool_storage.save_tool(
|
save_result = tool_storage.save_tool(
|
||||||
@@ -217,6 +285,151 @@ async def generate_tool(request: GenerateToolRequest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/generate-simple")
|
||||||
|
async def generate_tool_simple(request: SimpleGenerateToolRequest):
|
||||||
|
"""
|
||||||
|
🚀 简化版工具生成接口 - 默认使用 AI 辅助
|
||||||
|
|
||||||
|
只需要提供三个核心字段:
|
||||||
|
1. url - API 端点 URL
|
||||||
|
2. auth - 认证配置(bearer/api_key/basic)
|
||||||
|
3. request_body_schema - 请求体 Schema
|
||||||
|
|
||||||
|
AI 会智能理解您的配置并生成高质量的 Pydantic AI 工具代码。
|
||||||
|
|
||||||
|
示例请求:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "jina_reader",
|
||||||
|
"url": "https://r.jina.ai/",
|
||||||
|
"method": "POST",
|
||||||
|
"user_id": "test-user",
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"token": "your-token-here"
|
||||||
|
},
|
||||||
|
"request_body_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {"type": "string", "description": "要爬取的网页URL"}
|
||||||
|
},
|
||||||
|
"required": ["url"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 验证 URL 格式
|
||||||
|
if not request.url.startswith(("http://", "https://")):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "invalid_url",
|
||||||
|
"message": "URL 必须以 http:// 或 https:// 开头"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成唯一 tool_ref_id
|
||||||
|
tool_ref_id = f"tool-{request.name.lower().replace(' ', '-')}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
# 处理认证配置 - 兼容 token 和 key 字段
|
||||||
|
auth_config = None
|
||||||
|
if request.auth:
|
||||||
|
auth_config = request.auth.model_dump(by_alias=True)
|
||||||
|
# 兼容 token 字段:如果用户使用 token,将其映射到 key
|
||||||
|
if auth_config.get("token") and not auth_config.get("key"):
|
||||||
|
auth_config["key"] = auth_config["token"]
|
||||||
|
|
||||||
|
# 自动推断描述(如果未提供)
|
||||||
|
description = request.description
|
||||||
|
if not description:
|
||||||
|
description = f"调用 {request.name} API"
|
||||||
|
if request.request_body_schema:
|
||||||
|
props = request.request_body_schema.get("properties", {})
|
||||||
|
if props:
|
||||||
|
param_names = list(props.keys())[:3]
|
||||||
|
description += f",参数: {', '.join(param_names)}"
|
||||||
|
|
||||||
|
# 构建完整的工具配置 - 保留用户原始输入供 AI 理解
|
||||||
|
tool_config = {
|
||||||
|
"name": request.name,
|
||||||
|
"description": description,
|
||||||
|
"url": request.url,
|
||||||
|
"method": request.method.upper(),
|
||||||
|
"headers": request.headers,
|
||||||
|
"auth": auth_config,
|
||||||
|
# 支持两种参数格式
|
||||||
|
"request_body": request.request_body_schema,
|
||||||
|
"request_params": request.request_params,
|
||||||
|
"input_schema": request.request_body_schema or request.request_params,
|
||||||
|
"timeout": 30,
|
||||||
|
# 保存原始请求供 AI 参考(可能包含额外字段)
|
||||||
|
"_original_request": request.model_dump(exclude_unset=False)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 🤖 默认使用 AI 生成(更智能、更灵活)
|
||||||
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
|
|
||||||
|
logger.info(f"🤖 [简化模式] 使用 AI 生成工具代码: {request.name}")
|
||||||
|
|
||||||
|
def run_async():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return loop.run_until_complete(
|
||||||
|
agent_code_generator.generate_tool_code_with_ai(tool_config, request.api_key)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
future = executor.submit(run_async)
|
||||||
|
tool_code = future.result(timeout=90)
|
||||||
|
|
||||||
|
# 存储工具配置和代码
|
||||||
|
save_result = tool_storage.save_tool(
|
||||||
|
tool_ref_id=tool_ref_id,
|
||||||
|
name=request.name,
|
||||||
|
description=description,
|
||||||
|
config=tool_config,
|
||||||
|
code=tool_code,
|
||||||
|
user_id=request.user_id,
|
||||||
|
tenant_id=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not save_result.get("success"):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "generation_failed",
|
||||||
|
"message": save_result.get("error", "工具保存失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"✅ [简化模式] 工具生成成功: {tool_ref_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"tool_ref_id": tool_ref_id,
|
||||||
|
"name": request.name,
|
||||||
|
"description": description,
|
||||||
|
"url": request.url,
|
||||||
|
"method": request.method.upper(),
|
||||||
|
"has_auth": bool(request.auth),
|
||||||
|
"created_at": datetime.utcnow().isoformat()
|
||||||
|
},
|
||||||
|
"message": "工具生成成功 (AI 辅助)"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[简化模式] 工具生成失败: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "generation_failed",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{tool_ref_id}")
|
@router.put("/{tool_ref_id}")
|
||||||
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
|
async def update_tool(tool_ref_id: str, request: UpdateToolRequest):
|
||||||
"""
|
"""
|
||||||
@@ -392,7 +605,8 @@ async def test_tool(tool_ref_id: str, request: TestToolRequest = None):
|
|||||||
if auth_type == "api_key":
|
if auth_type == "api_key":
|
||||||
location = auth.get("in", "header")
|
location = auth.get("in", "header")
|
||||||
key_name = auth.get("name", "X-API-Key")
|
key_name = auth.get("name", "X-API-Key")
|
||||||
key_value = auth.get("key", "")
|
# 兼容 token 和 key 字段
|
||||||
|
key_value = auth.get("token") or auth.get("key", "")
|
||||||
|
|
||||||
if location == "header":
|
if location == "header":
|
||||||
headers[key_name] = key_value
|
headers[key_name] = key_value
|
||||||
@@ -400,7 +614,9 @@ async def test_tool(tool_ref_id: str, request: TestToolRequest = None):
|
|||||||
params[key_name] = key_value
|
params[key_name] = key_value
|
||||||
|
|
||||||
elif auth_type == "bearer":
|
elif auth_type == "bearer":
|
||||||
headers["Authorization"] = f"Bearer {auth.get('key', '')}"
|
# 兼容 token 和 key 字段
|
||||||
|
token_value = auth.get("token") or auth.get("key", "")
|
||||||
|
headers["Authorization"] = f"Bearer {token_value}"
|
||||||
|
|
||||||
elif auth_type == "basic":
|
elif auth_type == "basic":
|
||||||
import base64
|
import base64
|
||||||
@@ -639,13 +855,19 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
|
|||||||
"timeout": config.get("timeout", 30)
|
"timeout": config.get("timeout", 30)
|
||||||
})
|
})
|
||||||
|
|
||||||
# 生成仓库名
|
# 生成唯一后缀,确保不同用户创建同名 Agent 不会冲突
|
||||||
repo_name = f"agent-{request.name.lower().replace('_', '-')}-{uuid.uuid4().hex[:6]}"
|
unique_suffix = uuid.uuid4().hex[:6]
|
||||||
|
base_name = request.name.lower().replace("_", "-").replace(" ", "-")
|
||||||
|
|
||||||
|
# k8s_name 带唯一后缀,避免域名/namespace 冲突
|
||||||
|
k8s_name = f"{base_name}-{unique_suffix}"
|
||||||
|
repo_name = f"agent-{k8s_name}"
|
||||||
agent_ref_id = f"agent-{repo_name}"
|
agent_ref_id = f"agent-{repo_name}"
|
||||||
|
|
||||||
# 生成完整项目文件(传递资源配置)
|
# 生成完整项目文件(传递资源配置)
|
||||||
|
# 使用 k8s_name 作为 agent_name,确保 CI/CD 中创建的 DNS/namespace 与 API 返回一致
|
||||||
project_files = agent_code_generator.generate_full_project(
|
project_files = agent_code_generator.generate_full_project(
|
||||||
agent_name=request.name,
|
agent_name=k8s_name,
|
||||||
description=f"Agent with {len(tools)} external tools",
|
description=f"Agent with {len(tools)} external tools",
|
||||||
tools_config=tools_config,
|
tools_config=tools_config,
|
||||||
auto_deploy=True,
|
auto_deploy=True,
|
||||||
@@ -673,15 +895,8 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
|
|||||||
# 获取仓库所有者
|
# 获取仓库所有者
|
||||||
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
|
repo_owner = repo_result.get("owner", gitee_manager.gitee_username)
|
||||||
|
|
||||||
# 推送文件
|
# ⚠️ 重要:先设置 CI/CD Secrets,再推送文件
|
||||||
push_result = gitee_manager.push_files(
|
# 因为推送文件会触发 CI/CD,必须确保 secrets 已经就绪
|
||||||
repo_name=repo_name,
|
|
||||||
files=project_files,
|
|
||||||
commit_message=f"Initial commit: {request.name} with {len(tools)} tools",
|
|
||||||
owner=repo_owner
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置 CI/CD Secrets - 使用新生成的 Azure 凭证
|
|
||||||
cicd_secrets = {
|
cicd_secrets = {
|
||||||
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
|
"ACR_LOGIN_SERVER": "agnettaiji.azurecr.io",
|
||||||
"ACR_USERNAME": "agnettaiji",
|
"ACR_USERNAME": "agnettaiji",
|
||||||
@@ -696,29 +911,47 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
|
|||||||
"AZURE_DNS_RG": "taiji-Ai-v0"
|
"AZURE_DNS_RG": "taiji-Ai-v0"
|
||||||
}
|
}
|
||||||
|
|
||||||
gitee_manager.set_repo_secrets(
|
logger.info(f"📝 设置 CI/CD Secrets(共 {len(cicd_secrets)} 个)...")
|
||||||
|
secrets_result = gitee_manager.set_repo_secrets(
|
||||||
repo_name=repo_name,
|
repo_name=repo_name,
|
||||||
secrets=cicd_secrets,
|
secrets=cicd_secrets,
|
||||||
owner=repo_owner
|
owner=repo_owner
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not secrets_result.get("success"):
|
||||||
|
logger.warning(f"⚠️ 部分 Secrets 设置可能失败: {secrets_result}")
|
||||||
|
|
||||||
|
# 等待一小段时间确保 secrets 生效
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# 推送文件(这会触发 CI/CD)
|
||||||
|
logger.info(f"📤 推送项目文件...")
|
||||||
|
push_result = gitee_manager.push_files(
|
||||||
|
repo_name=repo_name,
|
||||||
|
files=project_files,
|
||||||
|
commit_message=f"Initial commit: {request.name} with {len(tools)} tools",
|
||||||
|
owner=repo_owner
|
||||||
|
)
|
||||||
|
|
||||||
# 标记工具被使用
|
# 标记工具被使用
|
||||||
for ref in request.tool_refs:
|
for ref in request.tool_refs:
|
||||||
tool_storage.mark_tool_in_use(ref, request.name)
|
tool_storage.mark_tool_in_use(ref, request.name)
|
||||||
|
|
||||||
# 计算 K8s 相关名称
|
# 计算域名和 namespace(k8s_name 已在上面定义,带唯一后缀)
|
||||||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
|
||||||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||||||
agent_ref_id = f"agent-{repo_name}"
|
namespace = f"agent-{k8s_name}"
|
||||||
|
|
||||||
# 存储 Agent 信息以便后续查询
|
# 存储 Agent 信息以便后续查询
|
||||||
AGENT_REFS[agent_ref_id] = {
|
AGENT_REFS[agent_ref_id] = {
|
||||||
"agent_ref_id": agent_ref_id,
|
"agent_ref_id": agent_ref_id,
|
||||||
"name": request.name,
|
"name": request.name, # 原始名称(用户输入)
|
||||||
|
"display_name": request.name, # 显示名称
|
||||||
|
"k8s_name": k8s_name, # K8s 名称(带唯一后缀,用于域名/namespace)
|
||||||
"repo_name": repo_name,
|
"repo_name": repo_name,
|
||||||
"repo_url": repo_result.get("html_url"),
|
"repo_url": repo_result.get("html_url"),
|
||||||
"repo_owner": repo_owner,
|
"repo_owner": repo_owner,
|
||||||
"namespace": f"agent-{k8s_name}",
|
"namespace": namespace,
|
||||||
"domain": expected_domain,
|
"domain": expected_domain,
|
||||||
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
|
"image_name": f"agnettaiji.azurecr.io/ai-agents/{repo_name}:latest",
|
||||||
"tools": [t["name"] for t in tools],
|
"tools": [t["name"] for t in tools],
|
||||||
@@ -736,7 +969,7 @@ async def create_agent_with_tools(request: CreateAgentWithToolsRequest):
|
|||||||
"success": True,
|
"success": True,
|
||||||
"name": request.name,
|
"name": request.name,
|
||||||
"agent_ref_id": agent_ref_id,
|
"agent_ref_id": agent_ref_id,
|
||||||
"namespace": f"agent-{k8s_name}",
|
"namespace": namespace,
|
||||||
"status": "Building",
|
"status": "Building",
|
||||||
"created_at": datetime.utcnow().isoformat(),
|
"created_at": datetime.utcnow().isoformat(),
|
||||||
"template": request.template,
|
"template": request.template,
|
||||||
@@ -862,9 +1095,19 @@ async def get_agent_build_status(agent_ref_id: str):
|
|||||||
elif action_status.get("status") == "no_runs":
|
elif action_status.get("status") == "no_runs":
|
||||||
overall_status = "pending"
|
overall_status = "pending"
|
||||||
|
|
||||||
# 计算域名
|
# 计算域名 - 优先使用存储的 k8s_name
|
||||||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
if agent_info and agent_info.get("k8s_name"):
|
||||||
|
k8s_name = agent_info.get("k8s_name")
|
||||||
|
else:
|
||||||
|
# 无法从 repo_name 准确推断,使用 agent 名称
|
||||||
|
agent_name = agent_info.get("name") if agent_info else None
|
||||||
|
if agent_name:
|
||||||
|
k8s_name = agent_name.lower().replace("_", "-").replace(" ", "-")
|
||||||
|
else:
|
||||||
|
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||||||
|
|
||||||
expected_domain = f"{k8s_name}.taijiagnet.com"
|
expected_domain = f"{k8s_name}.taijiagnet.com"
|
||||||
|
namespace = f"agent-{k8s_name}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -887,7 +1130,7 @@ async def get_agent_build_status(agent_ref_id: str):
|
|||||||
"access_info": {
|
"access_info": {
|
||||||
"expected_domain": expected_domain,
|
"expected_domain": expected_domain,
|
||||||
"expected_url": f"http://{expected_domain}",
|
"expected_url": f"http://{expected_domain}",
|
||||||
"expected_namespace": f"agent-{k8s_name}"
|
"expected_namespace": namespace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -913,14 +1156,21 @@ async def get_agent_deployment_info(agent_ref_id: str):
|
|||||||
- 访问 URL
|
- 访问 URL
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 从 agent_ref_id 推断 repo_name
|
# 检查是否有存储的 agent 信息
|
||||||
if agent_ref_id.startswith("agent-"):
|
agent_info = AGENT_REFS.get(agent_ref_id)
|
||||||
repo_name = agent_ref_id.replace("agent-", "", 1)
|
|
||||||
else:
|
|
||||||
repo_name = agent_ref_id
|
|
||||||
|
|
||||||
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
# 从 agent_ref_id 推断 repo_name
|
||||||
namespace = f"agent-{k8s_name}"
|
if agent_info:
|
||||||
|
repo_name = agent_info.get("repo_name")
|
||||||
|
k8s_name = agent_info.get("k8s_name")
|
||||||
|
namespace = agent_info.get("namespace")
|
||||||
|
else:
|
||||||
|
if agent_ref_id.startswith("agent-"):
|
||||||
|
repo_name = agent_ref_id.replace("agent-", "", 1)
|
||||||
|
else:
|
||||||
|
repo_name = agent_ref_id
|
||||||
|
k8s_name = repo_name.lower().replace("_", "-").replace(" ", "-")
|
||||||
|
namespace = f"agent-{k8s_name}"
|
||||||
|
|
||||||
# 导入 K8sManager 查询实际状态
|
# 导入 K8sManager 查询实际状态
|
||||||
from k8s_manager import K8sManager
|
from k8s_manager import K8sManager
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pydantic==2.5.0
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
sqlalchemy==2.0.23
|
sqlalchemy==2.0.23
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
|
httpx>=0.25.0
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"tool_ref_id": "tool-jina_reader_v5-d57dcc49",
|
||||||
|
"name": "jina_reader_v5",
|
||||||
|
"description": "调用 jina_reader_v5 API,参数: url",
|
||||||
|
"config": {
|
||||||
|
"name": "jina_reader_v5",
|
||||||
|
"description": "调用 jina_reader_v5 API,参数: url",
|
||||||
|
"url": "https://r.jina.ai/",
|
||||||
|
"method": "POST",
|
||||||
|
"headers": null,
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"key": "jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI",
|
||||||
|
"token": "jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI",
|
||||||
|
"username": null,
|
||||||
|
"password": null,
|
||||||
|
"in": "header",
|
||||||
|
"name": "X-API-Key"
|
||||||
|
},
|
||||||
|
"request_body": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要爬取的网页URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"request_params": null,
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要爬取的网页URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeout": 30,
|
||||||
|
"_original_request": {
|
||||||
|
"name": "jina_reader_v5",
|
||||||
|
"description": null,
|
||||||
|
"url": "https://r.jina.ai/",
|
||||||
|
"method": "POST",
|
||||||
|
"user_id": "test-user",
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"key": null,
|
||||||
|
"token": "jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI",
|
||||||
|
"username": null,
|
||||||
|
"password": null,
|
||||||
|
"in_location": "header",
|
||||||
|
"name": "X-API-Key"
|
||||||
|
},
|
||||||
|
"request_body_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要爬取的网页URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"request_params": null,
|
||||||
|
"headers": null,
|
||||||
|
"api_key": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_id": "test-user",
|
||||||
|
"tenant_id": null,
|
||||||
|
"created_at": "2026-01-30T17:25:13.854736",
|
||||||
|
"updated_at": "2026-01-30T17:25:13.854746",
|
||||||
|
"status": "created"
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
工具: jina_reader_v5
|
||||||
|
描述: 调用 jina_reader_v5 API,参数: url
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Optional, Any
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async def jina_reader_v5(url: str) -> str:
|
||||||
|
"""
|
||||||
|
调用 jina_reader_v5 API 爬取网页内容
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 要爬取的网页URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON 字符串格式的响应结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
api_key = os.getenv("TOOL_API_KEY", "jina_e26dc30420a44a1e859216528065b203TkMRmsoz-FgMDQC5FZX9jr5oF2CI")
|
||||||
|
|
||||||
|
# 将目标URL拼接到API路径中
|
||||||
|
api_url = f"https://r.jina.ai/{url}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"X-API-Key": api_key,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
api_url,
|
||||||
|
headers=headers
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# 尝试解析为JSON,如果失败则返回文本内容
|
||||||
|
try:
|
||||||
|
result = response.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
result = {"content": response.text, "status": "success"}
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
error_detail = {
|
||||||
|
"error": "HTTP错误",
|
||||||
|
"status_code": e.response.status_code,
|
||||||
|
"message": str(e),
|
||||||
|
"response": e.response.text
|
||||||
|
}
|
||||||
|
return json.dumps(error_detail, ensure_ascii=False)
|
||||||
|
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
error_detail = {
|
||||||
|
"error": "请求错误",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
return json.dumps(error_detail, ensure_ascii=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_detail = {
|
||||||
|
"error": "未知错误",
|
||||||
|
"message": str(e)
|
||||||
|
}
|
||||||
|
return json.dumps(error_detail, ensure_ascii=False)
|
||||||
Reference in New Issue
Block a user