主要更新: - 新增 external_tool_api.py: 外部工具管理 API - 新增 tool_storage.py: 工具存储管理器 - 新增回调功能用于计费 (agent_callback_utils) - 支持多工具创建 Agent - 新增 CI/CD 构建状态查询 API - 新增部署信息查询 API - 更新文档 (EXTERNAL_TOOL_API.md v2.0) - 更新 Dockerfile 添加新模块 - 更新 app.py 集成外部工具路由
315 lines
9.2 KiB
Python
315 lines
9.2 KiB
Python
"""
|
|
工具存储管理器
|
|
负责存储和管理外部数据工具的配置和代码
|
|
符合 MCP-Server 调用规范
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ToolStorage:
|
|
"""工具存储管理器 - 管理外部数据工具的配置和代码文件"""
|
|
|
|
def __init__(self, storage_path: str = None):
|
|
"""
|
|
初始化工具存储
|
|
|
|
Args:
|
|
storage_path: 工具存储路径,默认使用 ./tool_storage
|
|
"""
|
|
self.storage_path = Path(storage_path or os.getenv("TOOL_STORAGE_PATH", "./tool_storage"))
|
|
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 内存缓存
|
|
self._cache: Dict[str, Dict] = {}
|
|
|
|
# 加载已有工具
|
|
self._load_existing_tools()
|
|
|
|
def _load_existing_tools(self):
|
|
"""从文件系统加载已有的工具配置"""
|
|
try:
|
|
for tool_dir in self.storage_path.iterdir():
|
|
if tool_dir.is_dir():
|
|
config_file = tool_dir / "config.json"
|
|
if config_file.exists():
|
|
with open(config_file, "r", encoding="utf-8") as f:
|
|
config = json.load(f)
|
|
self._cache[config["tool_ref_id"]] = config
|
|
|
|
logger.info(f"✅ 已加载 {len(self._cache)} 个工具配置")
|
|
except Exception as e:
|
|
logger.error(f"加载工具配置失败: {e}")
|
|
|
|
def _get_tool_dir(self, tool_ref_id: str) -> Path:
|
|
"""获取工具的存储目录"""
|
|
return self.storage_path / tool_ref_id
|
|
|
|
def save_tool(
|
|
self,
|
|
tool_ref_id: str,
|
|
name: str,
|
|
description: str,
|
|
config: Dict,
|
|
code: str,
|
|
user_id: str,
|
|
tenant_id: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
保存工具配置和代码
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
name: 工具名称
|
|
description: 工具描述
|
|
config: 工具配置(含认证、参数等)
|
|
code: 生成的 Python 代码
|
|
user_id: 用户 ID
|
|
tenant_id: 租户 ID
|
|
|
|
Returns:
|
|
保存结果
|
|
"""
|
|
tool_dir = self._get_tool_dir(tool_ref_id)
|
|
tool_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 保存配置
|
|
tool_data = {
|
|
"tool_ref_id": tool_ref_id,
|
|
"name": name,
|
|
"description": description,
|
|
"config": config,
|
|
"user_id": user_id,
|
|
"tenant_id": tenant_id,
|
|
"created_at": datetime.utcnow().isoformat(),
|
|
"updated_at": datetime.utcnow().isoformat(),
|
|
"status": "created"
|
|
}
|
|
|
|
config_file = tool_dir / "config.json"
|
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
json.dump(tool_data, f, ensure_ascii=False, indent=2)
|
|
|
|
# 保存代码文件
|
|
code_file = tool_dir / f"{self._to_python_name(name)}.py"
|
|
with open(code_file, "w", encoding="utf-8") as f:
|
|
f.write(code)
|
|
|
|
# 更新缓存
|
|
tool_data["code_file"] = str(code_file)
|
|
self._cache[tool_ref_id] = tool_data
|
|
|
|
logger.info(f"✅ 工具已保存: {tool_ref_id}")
|
|
return {"success": True, "tool_ref_id": tool_ref_id, "code_file": str(code_file)}
|
|
|
|
def get_tool(self, tool_ref_id: str) -> Optional[Dict]:
|
|
"""
|
|
获取工具配置
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
|
|
Returns:
|
|
工具配置,不存在返回 None
|
|
"""
|
|
# 先从缓存获取
|
|
if tool_ref_id in self._cache:
|
|
return self._cache[tool_ref_id]
|
|
|
|
# 从文件系统加载
|
|
tool_dir = self._get_tool_dir(tool_ref_id)
|
|
config_file = tool_dir / "config.json"
|
|
|
|
if config_file.exists():
|
|
with open(config_file, "r", encoding="utf-8") as f:
|
|
tool_data = json.load(f)
|
|
self._cache[tool_ref_id] = tool_data
|
|
return tool_data
|
|
|
|
return None
|
|
|
|
def get_tool_code(self, tool_ref_id: str) -> Optional[str]:
|
|
"""
|
|
获取工具代码
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
|
|
Returns:
|
|
工具代码,不存在返回 None
|
|
"""
|
|
tool = self.get_tool(tool_ref_id)
|
|
if not tool:
|
|
return None
|
|
|
|
name = tool.get("name", "tool")
|
|
tool_dir = self._get_tool_dir(tool_ref_id)
|
|
code_file = tool_dir / f"{self._to_python_name(name)}.py"
|
|
|
|
if code_file.exists():
|
|
with open(code_file, "r", encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
return None
|
|
|
|
def update_tool(self, tool_ref_id: str, updates: Dict) -> Optional[Dict]:
|
|
"""
|
|
更新工具配置
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
updates: 更新内容
|
|
|
|
Returns:
|
|
更新后的配置
|
|
"""
|
|
tool = self.get_tool(tool_ref_id)
|
|
if not tool:
|
|
return None
|
|
|
|
# 更新配置
|
|
if "description" in updates:
|
|
tool["description"] = updates["description"]
|
|
tool["config"]["description"] = updates["description"]
|
|
|
|
for key in ["url", "method", "headers", "auth", "request_params", "request_body", "timeout"]:
|
|
if key in updates and updates[key] is not None:
|
|
tool["config"][key] = updates[key]
|
|
|
|
tool["updated_at"] = datetime.utcnow().isoformat()
|
|
|
|
# 保存到文件
|
|
tool_dir = self._get_tool_dir(tool_ref_id)
|
|
config_file = tool_dir / "config.json"
|
|
|
|
save_data = {k: v for k, v in tool.items() if k != "code_file"}
|
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
json.dump(save_data, f, ensure_ascii=False, indent=2)
|
|
|
|
# 更新缓存
|
|
self._cache[tool_ref_id] = tool
|
|
|
|
return tool
|
|
|
|
def delete_tool(self, tool_ref_id: str) -> bool:
|
|
"""
|
|
删除工具
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
|
|
Returns:
|
|
是否删除成功
|
|
"""
|
|
tool_dir = self._get_tool_dir(tool_ref_id)
|
|
|
|
if tool_dir.exists():
|
|
import shutil
|
|
shutil.rmtree(tool_dir)
|
|
|
|
if tool_ref_id in self._cache:
|
|
del self._cache[tool_ref_id]
|
|
|
|
logger.info(f"✅ 工具已删除: {tool_ref_id}")
|
|
return True
|
|
|
|
def list_tools(self, user_id: Optional[str] = None, tenant_id: Optional[str] = None) -> List[Dict]:
|
|
"""
|
|
列出工具
|
|
|
|
Args:
|
|
user_id: 用户 ID 过滤
|
|
tenant_id: 租户 ID 过滤
|
|
|
|
Returns:
|
|
工具列表
|
|
"""
|
|
tools = list(self._cache.values())
|
|
|
|
if user_id:
|
|
tools = [t for t in tools if t.get("user_id") == user_id]
|
|
|
|
if tenant_id:
|
|
tools = [t for t in tools if t.get("tenant_id") == tenant_id]
|
|
|
|
return tools
|
|
|
|
def get_tools_by_refs(self, tool_refs: List[str]) -> List[Dict]:
|
|
"""
|
|
根据 tool_ref_id 列表获取工具
|
|
|
|
Args:
|
|
tool_refs: tool_ref_id 列表
|
|
|
|
Returns:
|
|
工具列表(仅返回存在的工具)
|
|
"""
|
|
tools = []
|
|
for ref in tool_refs:
|
|
tool = self.get_tool(ref)
|
|
if tool:
|
|
tools.append(tool)
|
|
return tools
|
|
|
|
def check_tool_in_use(self, tool_ref_id: str) -> bool:
|
|
"""
|
|
检查工具是否被 Agent 使用
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
|
|
Returns:
|
|
是否被使用
|
|
"""
|
|
tool = self.get_tool(tool_ref_id)
|
|
if not tool:
|
|
return False
|
|
|
|
return tool.get("status") == "in_use" or bool(tool.get("used_by_agents"))
|
|
|
|
def mark_tool_in_use(self, tool_ref_id: str, agent_name: str) -> bool:
|
|
"""
|
|
标记工具被 Agent 使用
|
|
|
|
Args:
|
|
tool_ref_id: 工具唯一标识
|
|
agent_name: Agent 名称
|
|
|
|
Returns:
|
|
是否成功
|
|
"""
|
|
tool = self.get_tool(tool_ref_id)
|
|
if not tool:
|
|
return False
|
|
|
|
if "used_by_agents" not in tool:
|
|
tool["used_by_agents"] = []
|
|
|
|
if agent_name not in tool["used_by_agents"]:
|
|
tool["used_by_agents"].append(agent_name)
|
|
|
|
tool["status"] = "in_use"
|
|
|
|
# 保存更新
|
|
self.update_tool(tool_ref_id, {"status": "in_use"})
|
|
return True
|
|
|
|
def _to_python_name(self, name: str) -> str:
|
|
"""将名称转换为 Python 函数名格式"""
|
|
import re
|
|
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
|
if name and name[0].isdigit():
|
|
name = '_' + name
|
|
return name.lower()
|
|
|
|
|
|
# 全局实例
|
|
tool_storage = ToolStorage()
|