This commit is contained in:
rjb
2026-01-23 09:49:45 +08:00
parent 32ce289b3b
commit 171a6edf94
24 changed files with 7317 additions and 72 deletions

View 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"]
}
}
}

View File

@@ -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服务实例

View 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()

View File

@@ -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: