工具
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
执行日志API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from app.core.database import get_db
|
||||
from app.models.execution_log import ExecutionLog
|
||||
@@ -14,6 +14,10 @@ from app.models.agent import Agent
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.core.exceptions import NotFoundError
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/execution-logs", tags=["execution-logs"])
|
||||
|
||||
@@ -266,3 +270,153 @@ async def get_execution_performance(
|
||||
for log in timeline_logs
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/executions/{execution_id}/nodes/{node_id}/data")
|
||||
async def get_node_execution_data(
|
||||
execution_id: str,
|
||||
node_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取特定节点的执行数据(输入和输出)
|
||||
|
||||
从执行日志中提取节点的输入和输出数据
|
||||
"""
|
||||
# 验证执行记录是否存在
|
||||
execution = db.query(Execution).filter(Execution.id == execution_id).first()
|
||||
|
||||
if not execution:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 验证权限:检查workflow或agent的所有权
|
||||
has_permission = False
|
||||
if execution.workflow_id:
|
||||
workflow = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first()
|
||||
if workflow and workflow.user_id == current_user.id:
|
||||
has_permission = True
|
||||
elif execution.agent_id:
|
||||
agent = db.query(Agent).filter(Agent.id == execution.agent_id).first()
|
||||
if agent and (agent.user_id == current_user.id or agent.status in ["published", "running"]):
|
||||
has_permission = True
|
||||
|
||||
if not has_permission:
|
||||
raise NotFoundError("执行记录", execution_id)
|
||||
|
||||
# 查找节点的开始和完成日志
|
||||
start_log = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution_id,
|
||||
ExecutionLog.node_id == node_id,
|
||||
ExecutionLog.message.like('%开始执行%')
|
||||
).order_by(ExecutionLog.timestamp.asc()).first()
|
||||
|
||||
complete_log = db.query(ExecutionLog).filter(
|
||||
ExecutionLog.execution_id == execution_id,
|
||||
ExecutionLog.node_id == node_id,
|
||||
ExecutionLog.message.like('%执行完成%')
|
||||
).order_by(ExecutionLog.timestamp.desc()).first()
|
||||
|
||||
input_data = None
|
||||
output_data = None
|
||||
|
||||
# 从开始日志中提取输入数据
|
||||
if start_log and start_log.data:
|
||||
input_data = start_log.data.get('input', start_log.data)
|
||||
|
||||
# 从完成日志中提取输出数据
|
||||
if complete_log and complete_log.data:
|
||||
output_data = complete_log.data.get('output', complete_log.data)
|
||||
|
||||
return {
|
||||
"execution_id": execution_id,
|
||||
"node_id": node_id,
|
||||
"input": input_data,
|
||||
"output": output_data,
|
||||
"start_time": start_log.timestamp.isoformat() if start_log else None,
|
||||
"complete_time": complete_log.timestamp.isoformat() if complete_log else None,
|
||||
"duration": complete_log.duration if complete_log else None
|
||||
}
|
||||
|
||||
|
||||
@router.get("/cache/{key:path}")
|
||||
async def get_cache_value(
|
||||
key: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
获取缓存值(记忆数据)
|
||||
|
||||
从Redis或内存缓存中获取指定key的值
|
||||
"""
|
||||
try:
|
||||
from app.core.redis_client import get_redis_client
|
||||
|
||||
redis_client = get_redis_client()
|
||||
value = None
|
||||
|
||||
if redis_client:
|
||||
# 从Redis获取
|
||||
try:
|
||||
cached_data = redis_client.get(key)
|
||||
if cached_data:
|
||||
value = json.loads(cached_data)
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON,直接返回字符串
|
||||
value = cached_data
|
||||
except Exception as e:
|
||||
logger.warning(f"从Redis获取缓存失败: {str(e)}")
|
||||
|
||||
if value is None:
|
||||
# 如果Redis中没有,返回空
|
||||
raise HTTPException(status_code=404, detail=f"缓存键 '{key}' 不存在")
|
||||
|
||||
return {
|
||||
"key": key,
|
||||
"value": value,
|
||||
"exists": True
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取缓存值失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"获取缓存值失败: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/cache/{key:path}")
|
||||
async def delete_cache_value(
|
||||
key: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
删除缓存值(记忆数据)
|
||||
|
||||
从Redis或内存缓存中删除指定key的值
|
||||
"""
|
||||
try:
|
||||
from app.core.redis_client import get_redis_client
|
||||
|
||||
redis_client = get_redis_client()
|
||||
deleted = False
|
||||
|
||||
if redis_client:
|
||||
# 从Redis删除
|
||||
try:
|
||||
result = redis_client.delete(key)
|
||||
deleted = result > 0
|
||||
except Exception as e:
|
||||
logger.warning(f"从Redis删除缓存失败: {str(e)}")
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail=f"缓存键 '{key}' 不存在")
|
||||
|
||||
return {
|
||||
"key": key,
|
||||
"deleted": True,
|
||||
"message": "缓存已删除"
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"删除缓存值失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"删除缓存值失败: {str(e)}")
|
||||
|
||||
176
backend/app/api/tools.py
Normal file
176
backend/app/api/tools.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
工具管理API
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from app.core.database import get_db
|
||||
from app.models.tool import Tool
|
||||
from app.services.tool_registry import tool_registry
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/v1/tools", tags=["tools"])
|
||||
|
||||
|
||||
class ToolCreate(BaseModel):
|
||||
"""创建工具请求"""
|
||||
name: str
|
||||
description: str
|
||||
category: Optional[str] = None
|
||||
function_schema: dict
|
||||
implementation_type: str
|
||||
implementation_config: Optional[dict] = None
|
||||
is_public: bool = False
|
||||
|
||||
|
||||
class ToolResponse(BaseModel):
|
||||
"""工具响应"""
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
category: Optional[str]
|
||||
function_schema: dict
|
||||
implementation_type: str
|
||||
implementation_config: Optional[dict]
|
||||
is_public: bool
|
||||
use_count: int
|
||||
user_id: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("", response_model=List[ToolResponse])
|
||||
async def list_tools(
|
||||
category: Optional[str] = Query(None, description="工具分类"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取工具列表"""
|
||||
query = db.query(Tool).filter(Tool.is_public == True)
|
||||
|
||||
if category:
|
||||
query = query.filter(Tool.category == category)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
Tool.name.contains(search) |
|
||||
Tool.description.contains(search)
|
||||
)
|
||||
|
||||
tools = query.order_by(Tool.use_count.desc(), Tool.created_at.desc()).all()
|
||||
return tools
|
||||
|
||||
|
||||
@router.get("/builtin")
|
||||
async def list_builtin_tools():
|
||||
"""获取内置工具列表"""
|
||||
schemas = tool_registry.get_all_tool_schemas()
|
||||
return schemas
|
||||
|
||||
|
||||
@router.get("/{tool_id}", response_model=ToolResponse)
|
||||
async def get_tool(
|
||||
tool_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取工具详情"""
|
||||
tool = db.query(Tool).filter(Tool.id == tool_id).first()
|
||||
if not tool:
|
||||
raise HTTPException(status_code=404, detail="工具不存在")
|
||||
return tool
|
||||
|
||||
|
||||
@router.post("", response_model=ToolResponse, status_code=201)
|
||||
async def create_tool(
|
||||
tool_data: ToolCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""创建工具"""
|
||||
# 检查工具名称是否已存在
|
||||
existing = db.query(Tool).filter(Tool.name == tool_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail=f"工具名称 '{tool_data.name}' 已存在")
|
||||
|
||||
tool = Tool(
|
||||
name=tool_data.name,
|
||||
description=tool_data.description,
|
||||
category=tool_data.category,
|
||||
function_schema=tool_data.function_schema,
|
||||
implementation_type=tool_data.implementation_type,
|
||||
implementation_config=tool_data.implementation_config,
|
||||
is_public=tool_data.is_public,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
db.add(tool)
|
||||
db.commit()
|
||||
db.refresh(tool)
|
||||
|
||||
return tool
|
||||
|
||||
|
||||
@router.put("/{tool_id}", response_model=ToolResponse)
|
||||
async def update_tool(
|
||||
tool_id: str,
|
||||
tool_data: ToolCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""更新工具"""
|
||||
tool = db.query(Tool).filter(Tool.id == tool_id).first()
|
||||
if not tool:
|
||||
raise HTTPException(status_code=404, detail="工具不存在")
|
||||
|
||||
# 检查权限(只有创建者可以更新)
|
||||
if tool.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权更新此工具")
|
||||
|
||||
# 检查名称冲突
|
||||
if tool_data.name != tool.name:
|
||||
existing = db.query(Tool).filter(Tool.name == tool_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail=f"工具名称 '{tool_data.name}' 已存在")
|
||||
|
||||
tool.name = tool_data.name
|
||||
tool.description = tool_data.description
|
||||
tool.category = tool_data.category
|
||||
tool.function_schema = tool_data.function_schema
|
||||
tool.implementation_type = tool_data.implementation_type
|
||||
tool.implementation_config = tool_data.implementation_config
|
||||
tool.is_public = tool_data.is_public
|
||||
|
||||
db.commit()
|
||||
db.refresh(tool)
|
||||
|
||||
return tool
|
||||
|
||||
|
||||
@router.delete("/{tool_id}", status_code=200)
|
||||
async def delete_tool(
|
||||
tool_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""删除工具"""
|
||||
tool = db.query(Tool).filter(Tool.id == tool_id).first()
|
||||
if not tool:
|
||||
raise HTTPException(status_code=404, detail="工具不存在")
|
||||
|
||||
# 检查权限(只有创建者可以删除)
|
||||
if tool.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权删除此工具")
|
||||
|
||||
# 内置工具不允许删除
|
||||
if tool.implementation_type == "builtin":
|
||||
raise HTTPException(status_code=400, detail="内置工具不允许删除")
|
||||
|
||||
db.delete(tool)
|
||||
db.commit()
|
||||
|
||||
return {"message": "工具已删除"}
|
||||
@@ -90,12 +90,15 @@ Authorization: Bearer <your_token>
|
||||
|
||||
# CORS 配置
|
||||
cors_origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",")]
|
||||
# 添加调试日志
|
||||
logger.info(f"CORS允许的源: {cors_origins}")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册全局异常处理器
|
||||
@@ -149,7 +152,7 @@ async def health_check():
|
||||
"""健康检查"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
# 应用启动时初始化数据库
|
||||
# 应用启动时初始化数据库和工具
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""应用启动事件"""
|
||||
@@ -160,9 +163,45 @@ async def startup_event():
|
||||
except Exception as e:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
# 不抛出异常,允许应用继续启动
|
||||
|
||||
# 注册内置工具
|
||||
try:
|
||||
from app.services.tool_registry import tool_registry
|
||||
from app.services.builtin_tools import (
|
||||
http_request_tool,
|
||||
file_read_tool,
|
||||
file_write_tool,
|
||||
text_analyze_tool,
|
||||
datetime_tool,
|
||||
math_calculate_tool,
|
||||
system_info_tool,
|
||||
json_process_tool,
|
||||
HTTP_REQUEST_SCHEMA,
|
||||
FILE_READ_SCHEMA,
|
||||
FILE_WRITE_SCHEMA,
|
||||
TEXT_ANALYZE_SCHEMA,
|
||||
DATETIME_SCHEMA,
|
||||
MATH_CALCULATE_SCHEMA,
|
||||
SYSTEM_INFO_SCHEMA,
|
||||
JSON_PROCESS_SCHEMA
|
||||
)
|
||||
|
||||
tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA)
|
||||
tool_registry.register_builtin_tool("file_read", file_read_tool, FILE_READ_SCHEMA)
|
||||
tool_registry.register_builtin_tool("file_write", file_write_tool, FILE_WRITE_SCHEMA)
|
||||
tool_registry.register_builtin_tool("text_analyze", text_analyze_tool, TEXT_ANALYZE_SCHEMA)
|
||||
tool_registry.register_builtin_tool("datetime", datetime_tool, DATETIME_SCHEMA)
|
||||
tool_registry.register_builtin_tool("math_calculate", math_calculate_tool, MATH_CALCULATE_SCHEMA)
|
||||
tool_registry.register_builtin_tool("system_info", system_info_tool, SYSTEM_INFO_SCHEMA)
|
||||
tool_registry.register_builtin_tool("json_process", json_process_tool, JSON_PROCESS_SCHEMA)
|
||||
|
||||
logger.info("内置工具注册完成(共8个工具)")
|
||||
except Exception as e:
|
||||
logger.error(f"内置工具注册失败: {e}")
|
||||
# 不抛出异常,允许应用继续启动
|
||||
|
||||
# 注册路由
|
||||
from app.api import auth, workflows, executions, websocket, execution_logs, data_sources, agents, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates
|
||||
from app.api import auth, workflows, executions, websocket, execution_logs, data_sources, agents, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(workflows.router)
|
||||
@@ -181,6 +220,7 @@ app.include_router(monitoring.router)
|
||||
app.include_router(alert_rules.router)
|
||||
app.include_router(node_test.router)
|
||||
app.include_router(node_templates.router)
|
||||
app.include_router(tools.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
38
backend/app/models/tool.py
Normal file
38
backend/app/models/tool.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
工具定义模型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, JSON, DateTime, Boolean, ForeignKey, Integer, func
|
||||
from sqlalchemy.dialects.mysql import CHAR
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.core.database import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class Tool(Base):
|
||||
"""工具定义表"""
|
||||
__tablename__ = "tools"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="工具ID")
|
||||
name = Column(String(100), nullable=False, unique=True, comment="工具名称")
|
||||
description = Column(Text, nullable=False, comment="工具描述")
|
||||
category = Column(String(50), comment="工具分类")
|
||||
|
||||
# 工具定义(OpenAI Function格式)
|
||||
function_schema = Column(JSON, nullable=False, comment="函数定义(JSON Schema)")
|
||||
|
||||
# 工具实现类型
|
||||
implementation_type = Column(String(50), nullable=False, comment="实现类型: builtin/http/workflow/code")
|
||||
implementation_config = Column(JSON, comment="实现配置")
|
||||
|
||||
# 元数据
|
||||
is_public = Column(Boolean, default=False, comment="是否公开")
|
||||
user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="创建者ID")
|
||||
use_count = Column(Integer, default=0, comment="使用次数")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
# 关系
|
||||
user = relationship("User", backref="tools")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tool(id={self.id}, name={self.name})>"
|
||||
608
backend/app/services/builtin_tools.py
Normal file
608
backend/app/services/builtin_tools.py
Normal file
@@ -0,0 +1,608 @@
|
||||
"""
|
||||
内置工具实现
|
||||
"""
|
||||
from typing import Dict, Any, Optional, List
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
import platform
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def http_request_tool(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
body: Any = None
|
||||
) -> str:
|
||||
"""
|
||||
HTTP请求工具
|
||||
|
||||
Args:
|
||||
url: 请求URL
|
||||
method: HTTP方法 (GET, POST, PUT, DELETE)
|
||||
headers: 请求头
|
||||
body: 请求体(POST/PUT时使用)
|
||||
|
||||
Returns:
|
||||
JSON格式的响应结果
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
method_upper = method.upper()
|
||||
|
||||
if method_upper == "GET":
|
||||
response = await client.get(url, headers=headers)
|
||||
elif method_upper == "POST":
|
||||
response = await client.post(url, json=body, headers=headers)
|
||||
elif method_upper == "PUT":
|
||||
response = await client.put(url, json=body, headers=headers)
|
||||
elif method_upper == "DELETE":
|
||||
response = await client.delete(url, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"不支持的HTTP方法: {method}")
|
||||
|
||||
# 尝试解析JSON响应
|
||||
try:
|
||||
response_body = response.json()
|
||||
except:
|
||||
response_body = response.text
|
||||
|
||||
result = {
|
||||
"status_code": response.status_code,
|
||||
"headers": dict(response.headers),
|
||||
"body": response_body
|
||||
}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"HTTP请求工具执行失败: {str(e)}")
|
||||
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def file_read_tool(file_path: str) -> str:
|
||||
"""
|
||||
文件读取工具
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
|
||||
Returns:
|
||||
文件内容或错误信息
|
||||
"""
|
||||
try:
|
||||
# 安全检查:限制可读取的文件路径
|
||||
# 只允许读取项目目录下的文件
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
abs_path = os.path.abspath(file_path)
|
||||
|
||||
if not abs_path.startswith(project_root):
|
||||
return json.dumps({
|
||||
"error": f"不允许读取项目目录外的文件: {file_path}"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
return json.dumps({
|
||||
"file_path": file_path,
|
||||
"content": content,
|
||||
"size": len(content)
|
||||
}, ensure_ascii=False)
|
||||
except FileNotFoundError:
|
||||
return json.dumps({
|
||||
"error": f"文件不存在: {file_path}"
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"文件读取工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def file_write_tool(file_path: str, content: str, mode: str = "w") -> str:
|
||||
"""
|
||||
文件写入工具
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
content: 要写入的内容
|
||||
mode: 写入模式(w=覆盖,a=追加)
|
||||
|
||||
Returns:
|
||||
写入结果
|
||||
"""
|
||||
try:
|
||||
# 安全检查:限制可写入的文件路径
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
abs_path = os.path.abspath(file_path)
|
||||
|
||||
if not abs_path.startswith(project_root):
|
||||
return json.dumps({
|
||||
"error": f"不允许写入项目目录外的文件: {file_path}"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
write_mode = "a" if mode == "a" else "w"
|
||||
with open(file_path, write_mode, encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"file_path": file_path,
|
||||
"mode": write_mode,
|
||||
"content_length": len(content)
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"文件写入工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def text_analyze_tool(text: str, operation: str = "count") -> str:
|
||||
"""
|
||||
文本分析工具
|
||||
|
||||
Args:
|
||||
text: 要分析的文本
|
||||
operation: 操作类型(count=统计, keywords=提取关键词, summary=摘要)
|
||||
|
||||
Returns:
|
||||
分析结果
|
||||
"""
|
||||
try:
|
||||
if operation == "count":
|
||||
# 统计字数、字符数、行数等
|
||||
char_count = len(text)
|
||||
char_count_no_spaces = len(text.replace(" ", ""))
|
||||
word_count = len(text.split())
|
||||
line_count = len(text.splitlines())
|
||||
paragraph_count = len([p for p in text.split("\n\n") if p.strip()])
|
||||
|
||||
return json.dumps({
|
||||
"char_count": char_count,
|
||||
"char_count_no_spaces": char_count_no_spaces,
|
||||
"word_count": word_count,
|
||||
"line_count": line_count,
|
||||
"paragraph_count": paragraph_count
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif operation == "keywords":
|
||||
# 简单的关键词提取(基于词频)
|
||||
words = re.findall(r'\b\w+\b', text.lower())
|
||||
word_freq = {}
|
||||
for word in words:
|
||||
if len(word) > 2: # 忽略太短的词
|
||||
word_freq[word] = word_freq.get(word, 0) + 1
|
||||
|
||||
# 取频率最高的10个词
|
||||
top_keywords = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
|
||||
return json.dumps({
|
||||
"keywords": [{"word": k, "frequency": v} for k, v in top_keywords]
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif operation == "summary":
|
||||
# 简单的摘要(取前3句)
|
||||
sentences = re.split(r'[.!?。!?]\s+', text)
|
||||
summary = ". ".join(sentences[:3]) + "."
|
||||
|
||||
return json.dumps({
|
||||
"summary": summary,
|
||||
"original_length": len(text),
|
||||
"summary_length": len(summary)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
return json.dumps({
|
||||
"error": f"不支持的操作类型: {operation}"
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"文本分析工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def datetime_tool(operation: str = "now", format: str = "%Y-%m-%d %H:%M:%S",
|
||||
timezone: Optional[str] = None) -> str:
|
||||
"""
|
||||
日期时间工具
|
||||
|
||||
Args:
|
||||
operation: 操作类型(now=当前时间, format=格式化, parse=解析)
|
||||
format: 时间格式
|
||||
timezone: 时区(暂未实现)
|
||||
|
||||
Returns:
|
||||
日期时间结果
|
||||
"""
|
||||
try:
|
||||
if operation == "now":
|
||||
now = datetime.now()
|
||||
return json.dumps({
|
||||
"datetime": now.strftime(format),
|
||||
"timestamp": now.timestamp(),
|
||||
"iso_format": now.isoformat()
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif operation == "format":
|
||||
now = datetime.now()
|
||||
return json.dumps({
|
||||
"formatted": now.strftime(format)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
return json.dumps({
|
||||
"error": f"不支持的操作类型: {operation}"
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"日期时间工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def math_calculate_tool(expression: str) -> str:
|
||||
"""
|
||||
数学计算工具(安全限制:只允许基本数学运算)
|
||||
|
||||
Args:
|
||||
expression: 数学表达式(如 "2+2", "sqrt(16)", "sin(0)")
|
||||
|
||||
Returns:
|
||||
计算结果
|
||||
"""
|
||||
try:
|
||||
# 安全检查:只允许数字、基本运算符和安全的数学函数
|
||||
allowed_chars = set("0123456789+-*/.() ")
|
||||
allowed_functions = ['sqrt', 'sin', 'cos', 'tan', 'log', 'exp', 'abs', 'pow']
|
||||
|
||||
# 检查表达式是否安全
|
||||
if not all(c in allowed_chars or any(f in expression for f in allowed_functions) for c in expression):
|
||||
return json.dumps({
|
||||
"error": "表达式包含不安全的字符"
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# 构建安全的数学环境
|
||||
safe_dict = {
|
||||
"__builtins__": {},
|
||||
"abs": abs,
|
||||
"sqrt": math.sqrt,
|
||||
"sin": math.sin,
|
||||
"cos": math.cos,
|
||||
"tan": math.tan,
|
||||
"log": math.log,
|
||||
"exp": math.exp,
|
||||
"pow": pow,
|
||||
"pi": math.pi,
|
||||
"e": math.e
|
||||
}
|
||||
|
||||
result = eval(expression, safe_dict)
|
||||
|
||||
return json.dumps({
|
||||
"expression": expression,
|
||||
"result": result,
|
||||
"result_type": type(result).__name__
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"数学计算工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def system_info_tool() -> str:
|
||||
"""
|
||||
系统信息工具
|
||||
|
||||
Returns:
|
||||
系统信息
|
||||
"""
|
||||
try:
|
||||
info = {
|
||||
"platform": platform.system(),
|
||||
"platform_version": platform.version(),
|
||||
"architecture": platform.machine(),
|
||||
"processor": platform.processor(),
|
||||
"python_version": sys.version,
|
||||
"python_version_info": {
|
||||
"major": sys.version_info.major,
|
||||
"minor": sys.version_info.minor,
|
||||
"micro": sys.version_info.micro
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(info, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"系统信息工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def json_process_tool(json_string: str, operation: str = "parse") -> str:
|
||||
"""
|
||||
JSON处理工具
|
||||
|
||||
Args:
|
||||
json_string: JSON字符串
|
||||
operation: 操作类型(parse=解析, stringify=序列化, validate=验证)
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
"""
|
||||
try:
|
||||
if operation == "parse":
|
||||
data = json.loads(json_string)
|
||||
return json.dumps({
|
||||
"parsed": data,
|
||||
"type": type(data).__name__
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif operation == "stringify":
|
||||
# 如果输入已经是字符串,尝试解析后再序列化
|
||||
try:
|
||||
data = json.loads(json_string)
|
||||
except:
|
||||
data = json_string
|
||||
|
||||
return json.dumps({
|
||||
"stringified": json.dumps(data, ensure_ascii=False, indent=2)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif operation == "validate":
|
||||
try:
|
||||
json.loads(json_string)
|
||||
return json.dumps({
|
||||
"valid": True
|
||||
}, ensure_ascii=False)
|
||||
except json.JSONDecodeError as e:
|
||||
return json.dumps({
|
||||
"valid": False,
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
return json.dumps({
|
||||
"error": f"不支持的操作类型: {operation}"
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"JSON处理工具执行失败: {str(e)}")
|
||||
return json.dumps({
|
||||
"error": str(e)
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
async def database_query_tool(query: str, database: str = "default") -> str:
|
||||
"""
|
||||
数据库查询工具(占位实现)
|
||||
|
||||
Args:
|
||||
query: SQL查询语句
|
||||
database: 数据库名称
|
||||
|
||||
Returns:
|
||||
查询结果
|
||||
"""
|
||||
# TODO: 实现数据库查询逻辑
|
||||
return json.dumps({
|
||||
"error": "数据库查询工具尚未实现",
|
||||
"query": query,
|
||||
"database": database
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
# 工具定义(OpenAI Function格式)
|
||||
HTTP_REQUEST_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "http_request",
|
||||
"description": "发送HTTP请求,支持GET、POST、PUT、DELETE方法。可以用于调用API、获取网页内容等。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "请求的URL地址"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST", "PUT", "DELETE"],
|
||||
"description": "HTTP请求方法",
|
||||
"default": "GET"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "HTTP请求头(可选)",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"type": "object",
|
||||
"description": "请求体(POST/PUT时使用,可选)"
|
||||
}
|
||||
},
|
||||
"required": ["url", "method"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FILE_READ_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "file_read",
|
||||
"description": "读取文件内容。只能读取项目目录下的文件,确保文件路径正确。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "要读取的文件路径(相对于项目根目录或绝对路径)"
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FILE_WRITE_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "file_write",
|
||||
"description": "写入文件内容。只能写入项目目录下的文件,确保文件路径正确。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "要写入的文件路径(相对于项目根目录或绝对路径)"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "要写入的内容"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["w", "a"],
|
||||
"description": "写入模式:w=覆盖写入,a=追加写入",
|
||||
"default": "w"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "content"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEXT_ANALYZE_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "text_analyze",
|
||||
"description": "分析文本内容,支持统计字数、提取关键词、生成摘要等功能。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "要分析的文本内容"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["count", "keywords", "summary"],
|
||||
"description": "操作类型:count=统计字数等信息,keywords=提取关键词,summary=生成摘要",
|
||||
"default": "count"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DATETIME_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "datetime",
|
||||
"description": "获取和处理日期时间信息,支持获取当前时间、格式化时间等。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["now", "format"],
|
||||
"description": "操作类型:now=获取当前时间,format=格式化时间",
|
||||
"default": "now"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "时间格式字符串(如:%Y-%m-%d %H:%M:%S)",
|
||||
"default": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MATH_CALCULATE_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "math_calculate",
|
||||
"description": "执行数学计算,支持基本运算和常用数学函数(如sqrt, sin, cos, log等)。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "数学表达式(如:2+2, sqrt(16), sin(0), pow(2,3))"
|
||||
}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SYSTEM_INFO_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "system_info",
|
||||
"description": "获取系统信息,包括操作系统、Python版本等。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSON_PROCESS_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "json_process",
|
||||
"description": "处理JSON数据,支持解析、序列化、验证等功能。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"json_string": {
|
||||
"type": "string",
|
||||
"description": "JSON字符串"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": ["parse", "stringify", "validate"],
|
||||
"description": "操作类型:parse=解析JSON,stringify=序列化为JSON,validate=验证JSON格式",
|
||||
"default": "parse"
|
||||
}
|
||||
},
|
||||
"required": ["json_string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DATABASE_QUERY_SCHEMA = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "database_query",
|
||||
"description": "执行数据库查询(暂未实现)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "SQL查询语句"
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"description": "数据库名称",
|
||||
"default": "default"
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
"""
|
||||
LLM服务 - 处理各种LLM提供商的调用
|
||||
"""
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.config import settings
|
||||
from app.services.tool_registry import tool_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMService:
|
||||
@@ -219,6 +224,257 @@ class LLMService:
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"不支持的LLM提供商: {provider},目前支持: openai, deepseek")
|
||||
|
||||
async def call_openai_with_tools(
|
||||
self,
|
||||
prompt: str,
|
||||
tools: List[Dict[str, Any]],
|
||||
model: str = "gpt-3.5-turbo",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
max_iterations: int = 5
|
||||
) -> str:
|
||||
"""
|
||||
调用OpenAI API,支持工具调用
|
||||
|
||||
Args:
|
||||
prompt: 提示词
|
||||
tools: 工具定义列表(OpenAI Function格式)
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
api_key: API密钥
|
||||
base_url: API地址
|
||||
max_iterations: 最大工具调用迭代次数
|
||||
|
||||
Returns:
|
||||
LLM返回的最终文本
|
||||
"""
|
||||
# 获取客户端
|
||||
if api_key is not None or base_url is not None:
|
||||
final_api_key = api_key if api_key else settings.OPENAI_API_KEY
|
||||
final_base_url = base_url if base_url else settings.OPENAI_BASE_URL
|
||||
if not final_api_key:
|
||||
raise ValueError("OpenAI API Key未配置")
|
||||
client = AsyncOpenAI(api_key=final_api_key, base_url=final_base_url)
|
||||
else:
|
||||
if not self.openai_client:
|
||||
if settings.OPENAI_API_KEY:
|
||||
self.openai_client = AsyncOpenAI(
|
||||
api_key=settings.OPENAI_API_KEY,
|
||||
base_url=settings.OPENAI_BASE_URL
|
||||
)
|
||||
else:
|
||||
raise ValueError("OpenAI API Key未配置")
|
||||
client = self.openai_client
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
try:
|
||||
for iteration in range(max_iterations):
|
||||
# 准备工具参数(只在第一次调用时传递tools)
|
||||
create_kwargs = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
|
||||
if iteration == 0:
|
||||
# 转换工具格式为OpenAI格式
|
||||
openai_tools = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, dict):
|
||||
if "type" in tool and tool["type"] == "function":
|
||||
openai_tools.append(tool)
|
||||
elif "function" in tool:
|
||||
openai_tools.append(tool)
|
||||
else:
|
||||
# 假设是function格式,包装一下
|
||||
openai_tools.append({
|
||||
"type": "function",
|
||||
"function": tool
|
||||
})
|
||||
create_kwargs["tools"] = openai_tools
|
||||
create_kwargs["tool_choice"] = "auto"
|
||||
|
||||
# 调用LLM
|
||||
response = await client.chat.completions.create(**create_kwargs)
|
||||
|
||||
message = response.choices[0].message
|
||||
|
||||
# 添加助手回复到消息历史
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": message.content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": tc.type,
|
||||
"function": {
|
||||
"name": tc.function.name,
|
||||
"arguments": tc.function.arguments
|
||||
}
|
||||
} for tc in (message.tool_calls or [])
|
||||
]
|
||||
})
|
||||
|
||||
# 检查是否有工具调用
|
||||
if message.tool_calls and len(message.tool_calls) > 0:
|
||||
logger.info(f"检测到 {len(message.tool_calls)} 个工具调用")
|
||||
# 处理每个工具调用
|
||||
for tool_call in message.tool_calls:
|
||||
tool_name = tool_call.function.name
|
||||
try:
|
||||
tool_args = json.loads(tool_call.function.arguments)
|
||||
except:
|
||||
tool_args = {}
|
||||
|
||||
logger.info(f"执行工具: {tool_name}, 参数: {tool_args}")
|
||||
|
||||
# 执行工具
|
||||
tool_result = await self._execute_tool(tool_name, tool_args)
|
||||
|
||||
# 添加工具结果到消息历史
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": tool_result
|
||||
})
|
||||
else:
|
||||
# 没有工具调用,返回最终回复
|
||||
final_content = message.content or ""
|
||||
if final_content:
|
||||
logger.info("LLM返回最终回复,工具调用完成")
|
||||
return final_content
|
||||
|
||||
# 达到最大迭代次数
|
||||
logger.warning(f"达到最大工具调用迭代次数 ({max_iterations})")
|
||||
last_message = messages[-1] if messages else {}
|
||||
return last_message.get("content", "达到最大工具调用次数")
|
||||
except Exception as e:
|
||||
logger.error(f"工具调用过程中出错: {str(e)}")
|
||||
raise Exception(f"OpenAI工具调用失败: {str(e)}")
|
||||
|
||||
async def call_deepseek_with_tools(
|
||||
self,
|
||||
prompt: str,
|
||||
tools: List[Dict[str, Any]],
|
||||
model: str = "deepseek-chat",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
max_iterations: int = 5
|
||||
) -> str:
|
||||
"""
|
||||
调用DeepSeek API,支持工具调用(DeepSeek兼容OpenAI API格式)
|
||||
"""
|
||||
# DeepSeek使用相同的实现
|
||||
return await self.call_openai_with_tools(
|
||||
prompt=prompt,
|
||||
tools=tools,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
api_key=api_key or settings.DEEPSEEK_API_KEY,
|
||||
base_url=base_url or settings.DEEPSEEK_BASE_URL,
|
||||
max_iterations=max_iterations
|
||||
)
|
||||
|
||||
async def call_llm_with_tools(
|
||||
self,
|
||||
prompt: str,
|
||||
tools: List[Dict[str, Any]],
|
||||
provider: str = "openai",
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
**kwargs
|
||||
) -> str:
|
||||
"""
|
||||
通用LLM调用接口(支持工具)
|
||||
|
||||
Args:
|
||||
prompt: 提示词
|
||||
tools: 工具定义列表
|
||||
provider: 提供商,支持openai、deepseek
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
LLM返回的最终文本
|
||||
"""
|
||||
if provider == "openai":
|
||||
if not model:
|
||||
model = "gpt-3.5-turbo"
|
||||
return await self.call_openai_with_tools(
|
||||
prompt=prompt,
|
||||
tools=tools,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
**kwargs
|
||||
)
|
||||
elif provider == "deepseek":
|
||||
if not model:
|
||||
model = "deepseek-chat"
|
||||
return await self.call_deepseek_with_tools(
|
||||
prompt=prompt,
|
||||
tools=tools,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"不支持的LLM提供商: {provider},目前支持: openai, deepseek")
|
||||
|
||||
async def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
|
||||
"""
|
||||
执行工具
|
||||
|
||||
Args:
|
||||
tool_name: 工具名称
|
||||
tool_args: 工具参数
|
||||
|
||||
Returns:
|
||||
工具执行结果(JSON字符串)
|
||||
"""
|
||||
# 从注册表获取工具函数
|
||||
tool_func = tool_registry.get_tool_function(tool_name)
|
||||
|
||||
if not tool_func:
|
||||
error_msg = f"工具 {tool_name} 未找到"
|
||||
logger.error(error_msg)
|
||||
return json.dumps({"error": error_msg}, ensure_ascii=False)
|
||||
|
||||
try:
|
||||
logger.info(f"执行工具 {tool_name},参数: {tool_args}")
|
||||
|
||||
# 执行工具(支持异步函数)
|
||||
if asyncio.iscoroutinefunction(tool_func):
|
||||
result = await tool_func(**tool_args)
|
||||
else:
|
||||
# 同步函数在事件循环中执行
|
||||
result = tool_func(**tool_args)
|
||||
|
||||
# 将结果转换为字符串
|
||||
if isinstance(result, (dict, list)):
|
||||
result_str = json.dumps(result, ensure_ascii=False)
|
||||
else:
|
||||
result_str = str(result)
|
||||
|
||||
logger.info(f"工具 {tool_name} 执行成功,结果长度: {len(result_str)}")
|
||||
return result_str
|
||||
except Exception as e:
|
||||
error_msg = f"工具 {tool_name} 执行失败: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return json.dumps({"error": error_msg}, ensure_ascii=False)
|
||||
|
||||
|
||||
# 全局LLM服务实例
|
||||
|
||||
115
backend/app/services/tool_registry.py
Normal file
115
backend/app/services/tool_registry.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
工具注册表 - 管理所有可用工具
|
||||
"""
|
||||
from typing import Dict, Any, Callable, Optional, List
|
||||
import json
|
||||
import logging
|
||||
from app.models.tool import Tool
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""工具注册表 - 管理所有可用工具"""
|
||||
|
||||
def __init__(self):
|
||||
self._builtin_tools: Dict[str, Callable] = {}
|
||||
self._tool_schemas: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def register_builtin_tool(self, name: str, func: Callable, schema: Dict[str, Any]):
|
||||
"""
|
||||
注册内置工具
|
||||
|
||||
Args:
|
||||
name: 工具名称
|
||||
func: 工具函数(可以是同步或异步函数)
|
||||
schema: 工具定义(OpenAI Function格式)
|
||||
"""
|
||||
self._builtin_tools[name] = func
|
||||
self._tool_schemas[name] = schema
|
||||
logger.info(f"注册内置工具: {name}")
|
||||
|
||||
def get_tool_schema(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取工具定义"""
|
||||
return self._tool_schemas.get(name)
|
||||
|
||||
def get_tool_function(self, name: str) -> Optional[Callable]:
|
||||
"""获取工具函数"""
|
||||
return self._builtin_tools.get(name)
|
||||
|
||||
def get_all_tool_schemas(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有工具定义(用于LLM)"""
|
||||
return list(self._tool_schemas.values())
|
||||
|
||||
def load_tools_from_db(self, db: Session, tool_names: List[str] = None):
|
||||
"""
|
||||
从数据库加载工具
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
tool_names: 工具名称列表(可选,如果为None则加载所有公开工具)
|
||||
"""
|
||||
query = db.query(Tool).filter(Tool.is_public == True)
|
||||
if tool_names:
|
||||
query = query.filter(Tool.name.in_(tool_names))
|
||||
|
||||
tools = query.all()
|
||||
for tool in tools:
|
||||
self._tool_schemas[tool.name] = tool.function_schema
|
||||
# 根据implementation_type加载工具实现
|
||||
if tool.implementation_type == 'builtin':
|
||||
# 从内置工具中查找
|
||||
if tool.name in self._builtin_tools:
|
||||
logger.debug(f"工具 {tool.name} 已在内置工具中注册")
|
||||
else:
|
||||
logger.warning(f"工具 {tool.name} 标记为builtin但未在内置工具中找到")
|
||||
elif tool.implementation_type == 'http':
|
||||
# HTTP工具需要特殊处理
|
||||
self._register_http_tool(tool)
|
||||
elif tool.implementation_type == 'workflow':
|
||||
# 工作流工具
|
||||
self._register_workflow_tool(tool)
|
||||
elif tool.implementation_type == 'code':
|
||||
# 代码执行工具
|
||||
self._register_code_tool(tool)
|
||||
|
||||
logger.info(f"从数据库加载了 {len(tools)} 个工具")
|
||||
|
||||
def _register_http_tool(self, tool: Tool):
|
||||
"""注册HTTP工具"""
|
||||
# TODO: 实现HTTP工具的动态注册
|
||||
logger.warning(f"HTTP工具 {tool.name} 的动态注册尚未实现")
|
||||
|
||||
def _register_workflow_tool(self, tool: Tool):
|
||||
"""注册工作流工具"""
|
||||
# TODO: 实现工作流工具的动态注册
|
||||
logger.warning(f"工作流工具 {tool.name} 的动态注册尚未实现")
|
||||
|
||||
def _register_code_tool(self, tool: Tool):
|
||||
"""注册代码执行工具"""
|
||||
# TODO: 实现代码执行工具的动态注册
|
||||
logger.warning(f"代码执行工具 {tool.name} 的动态注册尚未实现")
|
||||
|
||||
def get_tools_by_names(self, tool_names: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
根据工具名称列表获取工具定义
|
||||
|
||||
Args:
|
||||
tool_names: 工具名称列表
|
||||
|
||||
Returns:
|
||||
工具定义列表(OpenAI Function格式)
|
||||
"""
|
||||
tools = []
|
||||
for name in tool_names:
|
||||
schema = self.get_tool_schema(name)
|
||||
if schema:
|
||||
tools.append(schema)
|
||||
else:
|
||||
logger.warning(f"工具 {name} 未找到")
|
||||
return tools
|
||||
|
||||
|
||||
# 全局工具注册表实例
|
||||
tool_registry = ToolRegistry()
|
||||
@@ -725,19 +725,44 @@ class WorkflowEngine:
|
||||
# 记录实际发送给LLM的prompt
|
||||
logger.info(f"[rjb] 准备调用LLM: node_id={node_id}, provider={provider}, model={model}, prompt前200字符='{prompt[:200] if len(prompt) > 200 else prompt}'")
|
||||
|
||||
# 检查是否启用工具调用
|
||||
enable_tools = node_data.get('enable_tools', False)
|
||||
tools_config = node_data.get('tools', []) # 工具名称列表
|
||||
|
||||
# 如果启用了工具,加载工具定义
|
||||
tools = []
|
||||
if enable_tools and tools_config:
|
||||
from app.services.tool_registry import tool_registry
|
||||
# 从注册表加载工具定义
|
||||
tools = tool_registry.get_tools_by_names(tools_config)
|
||||
logger.info(f"[rjb] LLM节点启用工具调用: {len(tools)} 个工具, 工具列表: {tools_config}")
|
||||
|
||||
# 调用LLM服务
|
||||
try:
|
||||
if self.logger:
|
||||
logger.debug(f"[rjb] LLM节点配置: provider={provider}, model={model}, 使用系统默认API Key配置")
|
||||
logger.debug(f"[rjb] LLM节点配置: provider={provider}, model={model}, 使用系统默认API Key配置, 工具调用: {'启用' if tools else '禁用'}")
|
||||
self.logger.info(f"调用LLM服务: {provider}/{model}", node_id=node_id, node_type=node_type)
|
||||
result = await llm_service.call_llm(
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
# 不传递 api_key 和 base_url,使用系统默认配置
|
||||
)
|
||||
|
||||
# 根据是否启用工具选择不同的调用方式
|
||||
if tools:
|
||||
result = await llm_service.call_llm_with_tools(
|
||||
prompt=prompt,
|
||||
tools=tools,
|
||||
provider=provider,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
else:
|
||||
result = await llm_service.call_llm(
|
||||
prompt=prompt,
|
||||
provider=provider,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
# 不传递 api_key 和 base_url,使用系统默认配置
|
||||
)
|
||||
|
||||
exec_result = {'output': result, 'status': 'success'}
|
||||
if self.logger:
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
@@ -748,6 +773,7 @@ class WorkflowEngine:
|
||||
if self.logger:
|
||||
duration = int((time.time() - start_time) * 1000)
|
||||
self.logger.log_node_error(node_id, node_type, e, duration)
|
||||
logger.error(f"[rjb] LLM节点执行失败: {str(e)}", exc_info=True)
|
||||
return {
|
||||
'output': None,
|
||||
'status': 'failed',
|
||||
@@ -2438,6 +2464,15 @@ class WorkflowEngine:
|
||||
logger.info(f"[rjb] user_input: {user_input[:50]}, output: {str(output)[:50]}, timestamp: {timestamp}")
|
||||
value = eval(value_str, {"__builtins__": {}}, safe_dict)
|
||||
logger.info(f"[rjb] Cache节点 {node_id} value模板执行成功,类型: {type(value)}")
|
||||
|
||||
# 确保conversation_history只保留最近的20条(性能优化)
|
||||
if isinstance(value, dict) and 'conversation_history' in value:
|
||||
if isinstance(value['conversation_history'], list):
|
||||
max_history_length = 20
|
||||
if len(value['conversation_history']) > max_history_length:
|
||||
value['conversation_history'] = value['conversation_history'][-max_history_length:]
|
||||
logger.info(f"[rjb] 对话历史已截断,保留最近 {max_history_length} 条")
|
||||
|
||||
if isinstance(value, dict):
|
||||
logger.info(f"[rjb] keys: {list(value.keys())}")
|
||||
if 'conversation_history' in value:
|
||||
|
||||
Reference in New Issue
Block a user