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

@@ -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
View 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": "工具已删除"}

View File

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

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

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: