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,54 @@
"""add tools table
Revision ID: 004
Revises: 003
Create Date: 2026-01-23 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.mysql import CHAR, JSON
# revision identifiers, used by Alembic.
revision = '004_add_tools_table'
down_revision = '003_add_rbac'
branch_labels = None
depends_on = None
def upgrade():
# 创建tools表
op.create_table(
'tools',
sa.Column('id', CHAR(36), primary_key=True, comment='工具ID'),
sa.Column('name', sa.String(100), nullable=False, unique=True, comment='工具名称'),
sa.Column('description', sa.Text, nullable=False, comment='工具描述'),
sa.Column('category', sa.String(50), nullable=True, comment='工具分类'),
sa.Column('function_schema', JSON, nullable=False, comment='函数定义JSON Schema'),
sa.Column('implementation_type', sa.String(50), nullable=False, comment='实现类型: builtin/http/workflow/code'),
sa.Column('implementation_config', JSON, nullable=True, comment='实现配置'),
sa.Column('is_public', sa.Boolean, default=False, comment='是否公开'),
sa.Column('user_id', CHAR(36), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment='创建者ID'),
sa.Column('use_count', sa.Integer, default=0, comment='使用次数'),
sa.Column('created_at', sa.DateTime, server_default=sa.func.now(), comment='创建时间'),
sa.Column('updated_at', sa.DateTime, server_default=sa.func.now(), onupdate=sa.func.now(), comment='更新时间'),
mysql_engine='InnoDB',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
# 创建索引
op.create_index('idx_tools_category', 'tools', ['category'])
op.create_index('idx_tools_is_public', 'tools', ['is_public'])
op.create_index('idx_tools_user_id', 'tools', ['user_id'])
def downgrade():
# 删除索引
op.drop_index('idx_tools_user_id', table_name='tools')
op.drop_index('idx_tools_is_public', table_name='tools')
op.drop_index('idx_tools_category', table_name='tools')
# 删除表
op.drop_table('tools')

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=解析JSONstringify=序列化为JSONvalidate=验证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:

View File

@@ -78,7 +78,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.3",
"max_tokens": "1000",
"max_tokens": "200",
"prompt": """你是一个专业的对话意图分析助手。请分析用户的输入,识别用户的意图和情感。
用户输入:{{user_input}}
@@ -129,7 +129,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.7",
"max_tokens": "500",
"max_tokens": "200",
"prompt": """你是一个温暖、友好的AI助手。用户向你打招呼请用自然、亲切的方式回应。
用户输入:{{user_input}}
@@ -150,19 +150,20 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.5",
"max_tokens": "2000",
"prompt": """你是一个知识渊博、乐于助人的AI助手。请回答用户的问题。
"max_tokens": "500",
"prompt": """你是一个知识渊博、乐于助人的AI助手。请简洁、准确地回答用户的问题。
用户问题:{{user_input}}
对话历史:{{memory.conversation_history}}
意图分析:{{output}}
请提供
1. 直接、准确的答案
2. 必要的解释和说明
3. 如果问题不明确,友好地询问更多信息
回答要求
1. 直接给出核心答案,避免冗长描述
2. 如果是介绍类问题(如"你能做什么"用简洁的要点列举控制在100字以内
3. 如果是知识性问题提供准确答案和简要说明控制在150字以内
4. 如果问题不明确友好地询问更多信息控制在50字以内
请以自然、易懂的方式回答,长度控制在200字以内。直接输出回答内容。"""
请以自然、简洁的方式回答,避免重复和冗余。直接输出回答内容,无需额外格式化"""
}
}
nodes.append(question_node)
@@ -177,7 +178,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.8",
"max_tokens": "1000",
"max_tokens": "500",
"prompt": """你是一个善解人意的AI助手。请根据用户的情感状态给予适当的回应。
用户输入:{{user_input}}
@@ -204,7 +205,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.4",
"max_tokens": "1500",
"max_tokens": "800",
"prompt": """你是一个专业的AI助手。用户提出了一个请求请分析并回应。
用户请求:{{user_input}}
@@ -231,7 +232,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.6",
"max_tokens": "300",
"max_tokens": "150",
"prompt": """你是一个友好的AI助手。用户要结束对话请给予温暖的告别。
用户输入:{{user_input}}
@@ -252,7 +253,7 @@ def generate_chat_agent(db: Session, user: User):
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.6",
"max_tokens": "1000",
"max_tokens": "500",
"prompt": """你是一个友好、专业的AI助手。请回应用户的输入。
用户输入:{{user_input}}
@@ -286,31 +287,24 @@ def generate_chat_agent(db: Session, user: User):
"label": "更新记忆",
"operation": "set",
"key": "user_memory_{user_id}",
"value": '{"conversation_history": {{memory.conversation_history}} + [{"role": "user", "content": "{{user_input}}", "timestamp": "{{timestamp}}"}, {"role": "assistant", "content": "{{output}}", "timestamp": "{{timestamp}}"}], "user_profile": {{memory.user_profile}}, "context": {{memory.context}}}',
"value": '{"conversation_history": ({{memory.conversation_history}} + [{"role": "user", "content": "{{user_input}}", "timestamp": "{{timestamp}}"}, {"role": "assistant", "content": "{{output}}", "timestamp": "{{timestamp}}"}]), "user_profile": {{memory.user_profile}}, "context": {{memory.context}}}',
"ttl": 86400
}
}
nodes.append(update_memory_node)
# ========== 14. 格式化最终回复 ==========
format_response_node = {
"id": "llm-format",
"type": "llm",
# ========== 14. JSON提取节点 - 提取最终回答文本 ==========
json_extract_node = {
"id": "json-extract",
"type": "json",
"position": {"x": 1650, "y": 400},
"data": {
"label": "格式化回复",
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.3",
"max_tokens": "500",
"prompt": """请将以下回复内容格式化为最终输出。确保回复自然、流畅。
原始回复:{{output}}
请直接输出格式化后的回复内容,不要包含其他说明或标记。如果原始回复已经是合适的格式,直接输出即可。"""
"label": "提取回答",
"operation": "extract",
"path": "right.right.right"
}
}
nodes.append(format_response_node)
nodes.append(json_extract_node)
# ========== 15. 结束节点 ==========
end_node = {
@@ -458,19 +452,19 @@ def generate_chat_agent(db: Session, user: User):
"targetHandle": "left"
})
# 更新记忆 -> 格式化回复
# 更新记忆 -> JSON提取
edges.append({
"id": "e8",
"source": "cache-update",
"target": "llm-format",
"target": "json-extract",
"sourceHandle": "right",
"targetHandle": "left"
})
# 格式化回复 -> 结束
# JSON提取 -> 结束
edges.append({
"id": "e9",
"source": "llm-format",
"source": "json-extract",
"target": "end-1",
"sourceHandle": "right",
"targetHandle": "left"

View File

@@ -0,0 +1,334 @@
#!/usr/bin/env python3
"""
生成知识库问答Agent示例
展示如何使用向量数据库和RAG技术构建知识库问答系统包含
- 文本向量化HTTP节点调用embedding API
- 向量数据库检索vector_db节点
- 基于检索结果的答案生成LLM节点
- 上下文整合和格式化
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy.orm import Session
from app.core.database import SessionLocal
from app.models.agent import Agent
from app.models.user import User
from datetime import datetime
import uuid
def generate_knowledge_base_qa_agent(db: Session, user: User):
"""生成知识库问答Agent"""
nodes = []
edges = []
# ========== 1. 开始节点 ==========
start_node = {
"id": "start-1",
"type": "start",
"position": {"x": 50, "y": 400},
"data": {
"label": "开始",
"output_format": "json"
}
}
nodes.append(start_node)
# ========== 2. 问题预处理节点 ==========
preprocess_node = {
"id": "transform-preprocess",
"type": "transform",
"position": {"x": 250, "y": 400},
"data": {
"label": "问题预处理",
"mode": "merge",
"mapping": {
"query": "{{query}}",
"user_id": "{{user_id}}",
"timestamp": "{{timestamp}}"
}
}
}
nodes.append(preprocess_node)
# ========== 3. 文本向量化节点HTTP调用embedding API==========
# 注意需要在HTTP节点配置中手动设置Authorization header使用环境变量DEEPSEEK_API_KEY
embedding_node = {
"id": "http-embedding",
"type": "http",
"position": {"x": 450, "y": 400},
"data": {
"label": "文本向量化",
"method": "POST",
"url": "https://api.deepseek.com/v1/embeddings",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer sk-fdf7cc1c73504e628ec0119b7e11b8cc"
},
"body": {
"model": "deepseek-embedding",
"input": "{{query}}"
},
"response_format": "json"
}
}
nodes.append(embedding_node)
# ========== 4. 提取embedding向量节点 ==========
extract_embedding_node = {
"id": "json-extract-embedding",
"type": "json",
"position": {"x": 650, "y": 400},
"data": {
"label": "提取向量",
"operation": "extract",
"path": "output.data.data[0].embedding"
}
}
nodes.append(extract_embedding_node)
# ========== 5. 准备向量搜索数据节点 ==========
prepare_search_node = {
"id": "transform-prepare-search",
"type": "transform",
"position": {"x": 850, "y": 400},
"data": {
"label": "准备搜索数据",
"mode": "merge",
"mapping": {
"embedding": "{{output}}",
"query": "{{query}}"
}
}
}
nodes.append(prepare_search_node)
# ========== 6. 向量数据库检索节点 ==========
vector_search_node = {
"id": "vector-search",
"type": "vector_db",
"position": {"x": 1050, "y": 400},
"data": {
"label": "知识库检索",
"operation": "search",
"collection": "knowledge_base",
"query_vector": "{{embedding}}",
"top_k": 5
}
}
nodes.append(vector_search_node)
# ========== 7. 整理检索结果节点 ==========
format_results_node = {
"id": "transform-format-results",
"type": "transform",
"position": {"x": 1250, "y": 400},
"data": {
"label": "整理检索结果",
"mode": "merge",
"mapping": {
"query": "{{query}}",
"search_results": "{{output}}"
}
}
}
nodes.append(format_results_node)
# ========== 8. 生成答案节点LLM==========
answer_node = {
"id": "llm-answer",
"type": "llm",
"position": {"x": 1650, "y": 400},
"data": {
"label": "生成答案",
"provider": "deepseek",
"model": "deepseek-chat",
"temperature": "0.7",
"max_tokens": "2000",
"prompt": """你是一个专业的知识库问答助手。请基于提供的知识库内容回答用户的问题。
用户问题:{{query}}
相关知识库内容(从向量搜索中检索到的相关文档):
{{search_results}}
请根据以上知识库内容回答用户的问题。要求:
1. 答案要准确、完整,基于知识库内容
2. 如果知识库中没有相关信息,请明确说明"根据知识库,未找到相关信息"
3. 答案要清晰、有条理使用Markdown格式
4. 如果知识库内容与问题不完全匹配,可以结合常识进行补充说明,但要标注哪些是知识库内容,哪些是补充说明
请直接输出答案,不要包含其他格式说明。"""
}
}
nodes.append(answer_node)
# ========== 9. 提取最终答案节点 ==========
extract_answer_node = {
"id": "json-extract-answer",
"type": "json",
"position": {"x": 1850, "y": 400},
"data": {
"label": "提取最终答案",
"operation": "extract",
"path": "output"
}
}
nodes.append(extract_answer_node)
# ========== 10. 结束节点 ==========
end_node = {
"id": "end-1",
"type": "end",
"position": {"x": 2050, "y": 400},
"data": {
"label": "结束"
}
}
nodes.append(end_node)
# ========== 连接边 ==========
edges.append({
"id": "e1",
"source": "start-1",
"target": "transform-preprocess",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e2",
"source": "transform-preprocess",
"target": "http-embedding",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e3",
"source": "http-embedding",
"target": "json-extract-embedding",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e4",
"source": "json-extract-embedding",
"target": "transform-prepare-search",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e5",
"source": "transform-prepare-search",
"target": "vector-search",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e6",
"source": "vector-search",
"target": "transform-format-results",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e7",
"source": "transform-format-results",
"target": "llm-answer",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e8",
"source": "llm-answer",
"target": "json-extract-answer",
"sourceHandle": "right",
"targetHandle": "left"
})
edges.append({
"id": "e9",
"source": "json-extract-answer",
"target": "end-1",
"sourceHandle": "right",
"targetHandle": "left"
})
return {
"name": "知识库问答助手",
"description": "基于向量数据库和RAG技术的知识库问答系统支持语义搜索和智能回答。需要先使用向量数据库节点将知识库文档向量化并存储。",
"workflow_config": {"nodes": nodes, "edges": edges}
}
def main():
"""主函数"""
db = SessionLocal()
try:
# 获取或创建测试用户
user = db.query(User).first()
if not user:
print("❌ 未找到用户,请先创建用户")
return
print(f"📝 使用用户: {user.username} (ID: {user.id})")
# 生成Agent数据
agent_data = generate_knowledge_base_qa_agent(db, user)
# 检查是否已存在
existing = db.query(Agent).filter(
Agent.name == agent_data["name"],
Agent.user_id == user.id
).first()
if existing:
print(f"Agent '{agent_data['name']}' 已存在,跳过创建")
return
# 创建Agent
agent = Agent(
name=agent_data["name"],
description=agent_data["description"],
workflow_config=agent_data["workflow_config"],
user_id=user.id,
status="draft"
)
db.add(agent)
db.commit()
db.refresh(agent)
print(f"✅ 成功创建Agent: {agent.name} (ID: {agent.id})")
print(f" 节点数量: {len(agent_data['workflow_config']['nodes'])}")
print(f" 连接数量: {len(agent_data['workflow_config']['edges'])}")
print(f"\n📝 使用说明:")
print(f" 1. 在Agent管理页面找到 '{agent.name}'")
print(f" 2. 点击'设计'按钮进入工作流编辑器")
print(f" 3. 配置HTTP节点的API密钥DeepSeek API Key")
print(f" 4. 使用向量数据库节点将知识库文档向量化并存储到 'knowledge_base' 集合")
print(f" 5. 点击'发布'按钮发布Agent")
print(f" 6. 点击'使用'按钮测试问答功能")
print(f"\n💡 提示:")
print(f" - 知识库文档需要先通过向量数据库节点的 'upsert' 操作存储")
print(f" - 每个文档需要包含 'text''embedding' 字段")
print(f" - 可以使用HTTP节点调用embedding API将文档文本转为向量")
except Exception as e:
print(f"❌ 创建Agent失败: {str(e)}")
import traceback
traceback.print_exc()
db.rollback()
finally:
db.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""
初始化内置工具脚本
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.core.database import SessionLocal
from app.models.tool import Tool
from app.services.tool_registry import tool_registry
from app.services.builtin_tools import (
http_request_tool,
file_read_tool,
HTTP_REQUEST_SCHEMA,
FILE_READ_SCHEMA
)
def init_builtin_tools():
"""初始化内置工具"""
db = SessionLocal()
try:
# 注册内置工具到注册表
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
)
print("✅ 内置工具已注册到工具注册表")
# 保存到数据库
tools_to_create = [
("http_request", HTTP_REQUEST_SCHEMA, "发送HTTP请求支持GET、POST、PUT、DELETE方法"),
("file_read", FILE_READ_SCHEMA, "读取文件内容,只能读取项目目录下的文件")
]
created_count = 0
for tool_name, tool_schema, description in tools_to_create:
existing = db.query(Tool).filter(Tool.name == tool_name).first()
if not existing:
tool = Tool(
name=tool_name,
description=description,
category="builtin",
function_schema=tool_schema,
implementation_type="builtin",
is_public=True
)
db.add(tool)
created_count += 1
print(f"✅ 创建工具: {tool_name}")
else:
# 更新工具定义
existing.function_schema = tool_schema
existing.description = description
print(f" 更新工具: {tool_name}")
db.commit()
print(f"\n✅ 内置工具初始化完成!创建了 {created_count} 个工具")
except Exception as e:
db.rollback()
print(f"❌ 初始化失败: {str(e)}")
import traceback
traceback.print_exc()
finally:
db.close()
if __name__ == "__main__":
init_builtin_tools()

53
backend/test_cors.py Normal file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
测试CORS配置
"""
import requests
# 测试CORS配置
def test_cors():
base_url = "http://101.43.95.130:8037"
# 测试OPTIONS请求CORS预检
print("测试CORS预检请求...")
headers = {
"Origin": "http://101.43.95.130:8038",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "Content-Type"
}
try:
response = requests.options(f"{base_url}/api/v1/tools/builtin", headers=headers)
print(f"OPTIONS请求状态码: {response.status_code}")
print(f"CORS头信息:")
for key, value in response.headers.items():
if key.lower().startswith('access-control'):
print(f" {key}: {value}")
except Exception as e:
print(f"OPTIONS请求失败: {e}")
# 测试GET请求
print("\n测试GET请求...")
headers = {
"Origin": "http://101.43.95.130:8038"
}
try:
response = requests.get(f"{base_url}/api/v1/tools/builtin", headers=headers)
print(f"GET请求状态码: {response.status_code}")
print(f"CORS头信息:")
for key, value in response.headers.items():
if key.lower().startswith('access-control'):
print(f" {key}: {value}")
if response.status_code == 200:
print(f"\n✅ 请求成功!")
data = response.json()
print(f"返回工具数量: {len(data)}")
else:
print(f"\n❌ 请求失败: {response.text}")
except Exception as e:
print(f"GET请求失败: {e}")
if __name__ == "__main__":
test_cors()

File diff suppressed because it is too large Load Diff

102
publish_agent.py Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
发布Agent脚本
"""
import requests
import sys
BASE_URL = "http://localhost:8037"
def login(username="admin", password="123456"):
"""用户登录"""
login_data = {
"username": username,
"password": password
}
try:
response = requests.post(f"{BASE_URL}/api/v1/auth/login", data=login_data)
if response.status_code != 200:
print(f"❌ 登录失败: {response.status_code}")
return None, None
token = response.json().get("access_token")
if not token:
print("❌ 登录失败: 未获取到token")
return None, None
print(f"✅ 登录成功 (用户: {username})")
headers = {"Authorization": f"Bearer {token}"}
return token, headers
except Exception as e:
print(f"❌ 登录异常: {str(e)}")
return None, None
def deploy_agent(agent_id, headers):
"""发布Agent"""
try:
response = requests.post(
f"{BASE_URL}/api/v1/agents/{agent_id}/deploy",
headers=headers
)
if response.status_code == 200:
agent = response.json()
print(f"✅ Agent发布成功: {agent.get('name')} (状态: {agent.get('status')})")
return True
else:
print(f"❌ 发布失败: {response.status_code}")
print(f"响应: {response.text}")
return False
except Exception as e:
print(f"❌ 发布异常: {str(e)}")
return False
def find_agent_by_name(agent_name, headers):
"""通过名称查找Agent"""
try:
response = requests.get(
f"{BASE_URL}/api/v1/agents",
headers=headers,
params={"search": agent_name, "limit": 100}
)
if response.status_code != 200:
print(f"❌ 获取Agent列表失败: {response.status_code}")
return None
agents = response.json()
for agent in agents:
if agent.get("name") == agent_name:
return agent
return None
except Exception as e:
print(f"❌ 查找Agent异常: {str(e)}")
return None
if __name__ == "__main__":
agent_name = sys.argv[1] if len(sys.argv) > 1 else "知识库问答助手"
print(f"📝 发布Agent: {agent_name}\n")
# 登录
token, headers = login()
if not token:
sys.exit(1)
# 查找Agent
print(f"🔍 查找Agent: {agent_name}")
agent = find_agent_by_name(agent_name, headers)
if not agent:
print(f"❌ 未找到Agent: {agent_name}")
sys.exit(1)
print(f"✅ 找到Agent: {agent.get('name')} (ID: {agent.get('id')}, 状态: {agent.get('status')})")
# 发布Agent
print(f"\n🚀 发布Agent...")
if deploy_agent(agent.get('id'), headers):
print("\n✅ 完成!")
else:
sys.exit(1)

View File

@@ -294,15 +294,100 @@ def get_execution_result(execution_id, headers):
if output_data:
if isinstance(output_data, dict):
# 尝试提取文本输出
text_output = (
output_data.get("output") or
output_data.get("text") or
output_data.get("content") or
output_data.get("result") or
json.dumps(output_data, ensure_ascii=False, indent=2)
)
print(text_output)
# 如果 result 字段是字符串尝试解析它类似JSON节点的parse操作
if "result" in output_data and isinstance(output_data["result"], str):
try:
# 尝试使用 ast.literal_eval 解析Python字典字符串
import ast
parsed_result = ast.literal_eval(output_data["result"])
output_data = parsed_result
except:
# 如果解析失败尝试作为JSON解析
try:
parsed_result = json.loads(output_data["result"])
output_data = parsed_result
except:
pass
# 使用类似JSON节点的extract操作来提取文本
def json_extract(data, path):
"""类似JSON节点的extract操作使用路径提取数据"""
if not path or not isinstance(data, dict):
return None
# 支持 $.right.right.right 格式的路径
path = path.replace('$.', '').replace('$', '')
keys = path.split('.')
result = data
for key in keys:
if isinstance(result, dict) and key in result:
result = result[key]
else:
return None
return result
# 尝试使用路径提取:递归查找 right 字段直到找到字符串
def extract_text_by_path(data, depth=0, max_depth=10):
"""递归提取嵌套在right字段中的文本"""
if depth > max_depth:
return None
if isinstance(data, str):
# 如果是字符串且不是JSON格式返回它
if len(data) > 10 and not data.strip().startswith('{') and not data.strip().startswith('['):
return data
return None
if isinstance(data, dict):
# 优先查找 right 字段
if "right" in data:
right_value = data["right"]
# 如果 right 的值是字符串,直接返回
if isinstance(right_value, str) and len(right_value) > 10:
return right_value
# 否则递归查找
result = extract_text_by_path(right_value, depth + 1, max_depth)
if result:
return result
# 查找其他常见的输出字段
for key in ["output", "text", "content"]:
if key in data:
result = extract_text_by_path(data[key], depth + 1, max_depth)
if result:
return result
return None
return None
# 优先检查 result 字段JSON节点提取后的结果
if "result" in output_data and isinstance(output_data["result"], str):
text_output = output_data["result"]
else:
# 先尝试使用路径提取类似JSON节点的extract操作
# 尝试多个可能的路径
paths_to_try = [
"right.right.right", # 最常见的嵌套路径
"right.right",
"right",
"output",
"text",
"content"
]
text_output = None
for path in paths_to_try:
extracted = json_extract(output_data, f"$.{path}")
if extracted and isinstance(extracted, str) and len(extracted) > 10:
text_output = extracted
break
# 如果路径提取失败,使用递归提取
if not text_output:
text_output = extract_text_by_path(output_data)
if text_output and isinstance(text_output, str):
print(text_output)
print()
print_info(f"回答长度: {len(text_output)} 字符")
else:
# 如果无法提取显示格式化的JSON
print(json.dumps(output_data, ensure_ascii=False, indent=2))
else:
print(output_data)
else:

287
内置工具列表.md Normal file
View File

@@ -0,0 +1,287 @@
# 内置工具列表
平台目前提供 **8个内置工具**可以在LLM节点中启用工具调用来使用。
## 📋 工具列表
### 1. 🌐 http_request - HTTP请求工具
**功能**: 发送HTTP请求支持GET、POST、PUT、DELETE方法
**用途**:
- 调用外部API
- 获取网页内容
- 发送数据到服务器
**参数**:
- `url` (必需): 请求的URL地址
- `method` (可选): HTTP方法默认GET
- `headers` (可选): HTTP请求头
- `body` (可选): 请求体POST/PUT时使用
**示例**:
```json
{
"url": "https://api.github.com/users/octocat",
"method": "GET"
}
```
---
### 2. 📖 file_read - 文件读取工具
**功能**: 读取文件内容
**用途**:
- 读取配置文件
- 读取文档内容
- 读取数据文件
**参数**:
- `file_path` (必需): 文件路径(只能读取项目目录下的文件)
**示例**:
```json
{
"file_path": "backend/app/core/config.py"
}
```
---
### 3. ✍️ file_write - 文件写入工具
**功能**: 写入文件内容
**用途**:
- 保存处理结果
- 创建配置文件
- 写入日志文件
**参数**:
- `file_path` (必需): 文件路径(只能写入项目目录下的文件)
- `content` (必需): 要写入的内容
- `mode` (可选): 写入模式w=覆盖a=追加默认w
**示例**:
```json
{
"file_path": "output/result.txt",
"content": "处理结果",
"mode": "w"
}
```
---
### 4. 📊 text_analyze - 文本分析工具
**功能**: 分析文本内容
**用途**:
- 统计文本字数、行数等
- 提取关键词
- 生成文本摘要
**参数**:
- `text` (必需): 要分析的文本内容
- `operation` (可选): 操作类型
- `count`: 统计字数、字符数、行数、段落数
- `keywords`: 提取关键词(基于词频)
- `summary`: 生成摘要取前3句
**示例**:
```json
{
"text": "这是一段很长的文本...",
"operation": "count"
}
```
---
### 5. 🕐 datetime - 日期时间工具
**功能**: 获取和处理日期时间信息
**用途**:
- 获取当前时间
- 格式化时间
- 时间戳转换
**参数**:
- `operation` (可选): 操作类型
- `now`: 获取当前时间(默认)
- `format`: 格式化时间
- `format` (可选): 时间格式字符串,默认 "%Y-%m-%d %H:%M:%S"
**示例**:
```json
{
"operation": "now",
"format": "%Y-%m-%d %H:%M:%S"
}
```
---
### 6. 🔢 math_calculate - 数学计算工具
**功能**: 执行数学计算
**用途**:
- 基本数学运算(加减乘除)
- 数学函数计算sqrt, sin, cos, log等
- 复杂数学表达式计算
**参数**:
- `expression` (必需): 数学表达式
**支持的函数**:
- `sqrt(x)`: 平方根
- `sin(x)`, `cos(x)`, `tan(x)`: 三角函数
- `log(x)`: 自然对数
- `exp(x)`: 指数函数
- `abs(x)`: 绝对值
- `pow(x, y)`: 幂运算
- `pi`: 圆周率
- `e`: 自然常数
**示例**:
```json
{
"expression": "sqrt(16) + sin(0) * cos(0)"
}
```
---
### 7. 💻 system_info - 系统信息工具
**功能**: 获取系统信息
**用途**:
- 查看操作系统信息
- 查看Python版本
- 查看系统架构
**参数**: 无
**返回信息**:
- 操作系统平台
- 系统版本
- 处理器架构
- Python版本
**示例**:
```json
{}
```
---
### 8. 📦 json_process - JSON处理工具
**功能**: 处理JSON数据
**用途**:
- 解析JSON字符串
- 序列化数据为JSON
- 验证JSON格式
**参数**:
- `json_string` (必需): JSON字符串
- `operation` (可选): 操作类型
- `parse`: 解析JSON默认
- `stringify`: 序列化为JSON
- `validate`: 验证JSON格式
**示例**:
```json
{
"json_string": "{\"name\": \"test\"}",
"operation": "parse"
}
```
---
## 🎯 使用场景示例
### 场景1: 数据获取和处理
```
用户: "查询GitHub用户信息并保存到文件"
→ LLM调用 http_request 获取数据
→ LLM调用 json_process 解析数据
→ LLM调用 file_write 保存结果
```
### 场景2: 文本分析
```
用户: "分析这段文本的字数和关键词"
→ LLM调用 text_analyze (count) 统计字数
→ LLM调用 text_analyze (keywords) 提取关键词
```
### 场景3: 数学计算
```
用户: "计算 2的10次方 加上 16的平方根"
→ LLM调用 math_calculate("pow(2, 10) + sqrt(16)")
```
### 场景4: 文件处理
```
用户: "读取config.json文件并解析"
→ LLM调用 file_read 读取文件
→ LLM调用 json_process 解析内容
```
---
## ⚠️ 安全限制
1. **文件操作限制**:
- 只能读写项目目录下的文件
- 不允许访问系统敏感文件
2. **数学计算限制**:
- 只允许安全的数学函数
- 不允许执行任意代码
3. **HTTP请求限制**:
- 超时时间30秒
- 建议在生产环境中添加域名白名单
---
## 📝 如何启用工具
1. 在工作流编辑器中选择LLM节点
2. 打开"工具"标签页
3. 启用"启用工具调用"开关
4. 选择需要的工具(可多选)
5. 保存配置
---
## 🔄 工具调用流程
```
用户输入
LLM节点启用工具
LLM分析需求决定调用哪个工具
执行工具
工具返回结果
LLM基于结果生成最终回复
```
---
**最后更新**: 2026-01-23
**工具总数**: 8个

View File

@@ -0,0 +1,177 @@
# 工具调用功能实施完成总结
## ✅ 已完成的工作
### 后端实现100%完成)
1. **工具定义模型** (`backend/app/models/tool.py`)
- ✅ 完整的工具数据模型
- ✅ 支持工具分类、函数定义、实现类型等
2. **工具注册表** (`backend/app/services/tool_registry.py`)
- ✅ 管理所有可用工具
- ✅ 支持注册内置工具和从数据库加载
3. **内置工具实现** (`backend/app/services/builtin_tools.py`)
-`http_request`: HTTP请求工具
-`file_read`: 文件读取工具
- ✅ 完整的工具定义OpenAI Function格式
4. **LLM服务扩展** (`backend/app/services/llm_service.py`)
-`call_openai_with_tools`: 支持工具调用的OpenAI调用
-`call_deepseek_with_tools`: 支持工具调用的DeepSeek调用
-`call_llm_with_tools`: 通用工具调用接口
-`_execute_tool`: 工具执行方法
- ✅ 支持多轮工具调用循环最多5次迭代
5. **工作流引擎集成** (`backend/app/services/workflow_engine.py`)
- ✅ LLM节点执行时检查工具配置
- ✅ 自动加载工具定义并传递给LLM服务
6. **工具管理API** (`backend/app/api/tools.py`)
-`GET /api/v1/tools` - 获取工具列表
-`GET /api/v1/tools/builtin` - 获取内置工具列表
-`GET /api/v1/tools/{tool_id}` - 获取工具详情
-`POST /api/v1/tools` - 创建工具
-`PUT /api/v1/tools/{tool_id}` - 更新工具
-`DELETE /api/v1/tools/{tool_id}` - 删除工具
- ✅ 已注册到main.py
7. **初始化脚本** (`backend/scripts/init_builtin_tools.py`)
- ✅ 已执行成功创建了2个内置工具
8. **数据库迁移** (`backend/alembic/versions/004_add_tools_table.py`)
- ✅ 创建tools表的迁移文件
### 前端实现100%完成)
1. **工具配置界面** (`frontend/src/components/WorkflowEditor/WorkflowEditor.vue`)
- ✅ 在LLM节点配置面板中添加"工具"标签页
- ✅ "启用工具调用"开关
- ✅ 工具选择器(多选,支持筛选)
- ✅ 工具分组显示(内置工具/自定义工具)
- ✅ 显示选中工具的详细信息(描述、参数定义)
- ✅ 工具移除功能
- ✅ 自动加载工具列表
2. **默认配置**
- ✅ LLM节点创建时自动初始化工具配置
-`enable_tools: false`, `tools: []`
## 📊 功能特性
### 1. 工具调用流程
```
用户输入 → LLM节点启用工具 → LLM API带tools参数
LLM返回tool_call → 工具执行器 → 执行工具
工具结果 → 返回LLM → 生成最终回复
```
### 2. 支持的工具
**内置工具**
- `http_request`: 发送HTTP请求GET/POST/PUT/DELETE
- `file_read`: 读取文件内容(限制在项目目录内)
**自定义工具**
- 支持通过API创建自定义工具
- 支持多种实现类型builtin/http/workflow/code
### 3. 安全特性
- 文件读取限制在项目目录内
- 工具参数验证
- 工具执行超时30秒
- 最大工具调用迭代次数5次
## 🎯 使用方法
### 1. 在LLM节点中启用工具调用
1. 选择LLM节点
2. 打开"工具"标签页
3. 启用"启用工具调用"开关
4. 选择需要的工具(如:`http_request`, `file_read`
5. 保存配置
### 2. 测试工具调用
创建一个简单的Agent
- 开始节点
- LLM节点启用工具调用选择`http_request`工具)
- 结束节点
测试输入:`"查询 https://api.github.com/users/octocat 的信息"`
LLM应该会
1. 识别需要调用工具
2. 调用 `http_request` 工具获取GitHub API数据
3. 基于获取的数据生成回复
## 📝 配置示例
### LLM节点配置JSON格式
```json
{
"id": "llm-1",
"type": "llm",
"data": {
"label": "智能助手",
"provider": "openai",
"model": "gpt-4",
"prompt": "请帮助用户解决问题,可以使用工具获取信息。",
"enable_tools": true,
"tools": ["http_request", "file_read"]
}
}
```
## 🔧 API使用示例
### 获取工具列表
```bash
curl http://localhost:8037/api/v1/tools
```
### 获取内置工具
```bash
curl http://localhost:8037/api/v1/tools/builtin
```
## ⚠️ 注意事项
1. **模型支持**
- 当前支持OpenAI和DeepSeek兼容OpenAI API格式
- 需要模型支持function calling功能
- 推荐使用:`gpt-4`, `gpt-3.5-turbo`, `deepseek-chat`
2. **工具调用限制**
- 最大迭代次数5次
- 工具执行超时30秒
- 如果达到最大迭代次数,会返回最后一次的结果
3. **安全考虑**
- 文件读取限制在项目目录内
- HTTP工具没有域名限制生产环境建议添加
- 工具参数需要验证(当前为基本验证)
## 🎉 完成状态
- ✅ 后端核心功能100%
- ✅ API接口100%
- ✅ 前端界面100%
- ✅ 内置工具100%
- ✅ 文档100%
**工具调用功能已完全实施完成!** 🎊
---
**完成时间**: 2026-01-23
**实施状态**: ✅ 全部完成

183
工具调用实施总结.md Normal file
View File

@@ -0,0 +1,183 @@
# 工具调用功能实施总结
## ✅ 已完成的工作
### 阶段1: 后端核心功能 ✅
#### 1.1 工具定义模型 ✅
- **文件**: `backend/app/models/tool.py`
- **功能**: 定义了工具数据模型,包括工具名称、描述、函数定义等
- **状态**: 已完成
#### 1.2 工具注册表 ✅
- **文件**: `backend/app/services/tool_registry.py`
- **功能**: 管理所有可用工具,支持注册内置工具和从数据库加载工具
- **状态**: 已完成
#### 1.3 内置工具实现 ✅
- **文件**: `backend/app/services/builtin_tools.py`
- **功能**: 实现了两个内置工具:
- `http_request`: HTTP请求工具
- `file_read`: 文件读取工具
- **状态**: 已完成
#### 1.4 LLM服务扩展 ✅
- **文件**: `backend/app/services/llm_service.py`
- **功能**:
- 添加了 `call_openai_with_tools` 方法
- 添加了 `call_deepseek_with_tools` 方法
- 添加了 `call_llm_with_tools` 通用方法
- 添加了 `_execute_tool` 工具执行方法
- 支持多轮工具调用循环
- **状态**: 已完成
#### 1.5 工作流引擎扩展 ✅
- **文件**: `backend/app/services/workflow_engine.py`
- **功能**: 在LLM节点执行时检查工具配置如果启用则调用工具调用接口
- **状态**: 已完成
### 阶段2: 数据库迁移 ✅
- **文件**: `backend/alembic/versions/004_add_tools_table.py`
- **功能**: 创建tools表的数据库迁移
- **状态**: 已完成(需要执行迁移)
### 阶段3: API接口 ✅
- **文件**: `backend/app/api/tools.py`
- **功能**:
- `GET /api/v1/tools` - 获取工具列表
- `GET /api/v1/tools/builtin` - 获取内置工具列表
- `GET /api/v1/tools/{tool_id}` - 获取工具详情
- `POST /api/v1/tools` - 创建工具
- `PUT /api/v1/tools/{tool_id}` - 更新工具
- `DELETE /api/v1/tools/{tool_id}` - 删除工具
- **状态**: 已完成已注册到main.py
### 阶段4: 初始化脚本 ✅
- **文件**: `backend/scripts/init_builtin_tools.py`
- **功能**: 初始化内置工具到数据库和注册表
- **状态**: 已完成并执行成功
## 📋 下一步工作
### 阶段5: 前端实现(待实施)
#### 5.1 LLM节点配置扩展
- 在LLM节点配置面板中添加"工具"标签页
- 添加"启用工具调用"开关
- 添加工具选择器(多选)
- 显示选中工具的详细信息
#### 5.2 工具调用可视化
- 在工作流执行时显示工具调用过程
- 显示工具名称、参数、执行结果
- 显示工具调用状态(成功/失败)
#### 5.3 工具管理界面(可选)
- 工具列表页面
- 工具创建/编辑页面
- 工具详情页面
## 🔧 使用说明
### 1. 执行数据库迁移
```bash
cd /home/renjianbo/aiagent/backend
alembic upgrade head
```
### 2. 初始化内置工具(已完成)
```bash
python3 backend/scripts/init_builtin_tools.py
```
### 3. 在LLM节点中启用工具调用
在LLM节点的配置中需要设置
```json
{
"enable_tools": true,
"tools": ["http_request", "file_read"]
}
```
### 4. 测试工具调用
创建一个简单的Agent包含
- 开始节点
- LLM节点启用工具调用
- 结束节点
测试输入:`"查询 https://api.github.com/users/octocat 的信息"`
LLM应该会
1. 识别需要调用工具
2. 调用 `http_request` 工具
3. 获取结果后生成回复
## 📊 当前状态
- ✅ 后端核心功能100% 完成
- ✅ 数据库迁移100% 完成
- ✅ API接口100% 完成
- ✅ 初始化脚本100% 完成
- ⏳ 前端实现0% 完成(待实施)
## 🎯 测试建议
1. **API测试**
```bash
# 获取工具列表
curl http://localhost:8037/api/v1/tools
# 获取内置工具
curl http://localhost:8037/api/v1/tools/builtin
```
2. **工作流测试**
- 创建一个包含LLM节点的Agent
- 在LLM节点配置中启用工具调用
- 选择 `http_request` 工具
- 测试执行
## ⚠️ 注意事项
1. **数据库迁移**
- 需要执行 `alembic upgrade head` 创建tools表
- 如果表已存在,迁移会失败(需要先检查)
2. **工具调用限制**
- 当前支持OpenAI和DeepSeek兼容OpenAI API
- 最大工具调用迭代次数5次
- 工具执行超时30秒HTTP工具
3. **安全考虑**
- 文件读取工具限制在项目目录内
- HTTP工具没有域名限制生产环境需要添加
- 工具参数需要验证(当前为基本验证)
## 📝 后续优化
1. **更多内置工具**
- 数据库查询工具
- 邮件发送工具
- 文件写入工具
2. **工具调用优化**
- 并行工具执行
- 工具结果缓存
- 工具调用日志
3. **前端增强**
- 工具调用可视化
- 工具参数配置界面
- 工具执行历史
---
**实施时间**: 2026-01-23
**实施状态**: 后端核心功能已完成,前端待实施

767
工具调用实现方案.md Normal file
View File

@@ -0,0 +1,767 @@
# 工具调用Function Calling实现方案
## 📋 方案概述
本方案实现LLM工具调用功能允许LLM节点调用预定义的工具函数实现更强大的AI能力。
## 🎯 功能目标
1. **LLM节点支持工具调用**
- 在LLM节点配置中定义可用工具
- LLM自动选择并调用合适的工具
- 支持多轮工具调用Tool Calling Loop
2. **工具定义和管理**
- 支持内置工具HTTP请求、数据库查询、文件操作等
- 支持自定义工具Python函数、工作流节点等
- 工具参数验证和类型转换
3. **工具执行**
- 异步执行工具
- 错误处理和重试
- 结果格式化返回给LLM
4. **前端配置界面**
- 工具选择器
- 工具参数配置
- 工具调用可视化
## 🏗️ 架构设计
### 1. 系统架构
```
┌─────────────────┐
│ LLM节点配置 │
│ (工具列表) │
└────────┬────────┘
┌─────────────────┐
│ LLM服务调用 │
│ (传递tools参数) │
└────────┬────────┘
┌─────────────────┐ ┌──────────────┐
│ 工具调用解析器 │─────▶│ 工具执行器 │
│ (解析tool_call) │ │ (执行工具) │
└────────┬────────┘ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ 工具注册表 │
│ │ (工具定义) │
│ └──────────────┘
┌─────────────────┐
│ 结果返回LLM │
│ (继续对话) │
└─────────────────┘
```
### 2. 数据流
```
1. 用户输入 → LLM节点
2. LLM节点 → LLM API (带tools参数)
3. LLM API → 返回tool_call请求
4. 工具调用解析器 → 解析tool_call
5. 工具执行器 → 执行工具
6. 工具结果 → 返回LLM (tool message)
7. LLM → 生成最终回复
```
## 📝 实现步骤
### 阶段1: 后端核心功能
#### 1.1 工具定义模型
**文件**: `backend/app/models/tool.py`
```python
from sqlalchemy import Column, String, Text, JSON, DateTime, Boolean, ForeignKey, Integer
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()))
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())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
user = relationship("User", backref="tools")
```
#### 1.2 工具注册表
**文件**: `backend/app/services/tool_registry.py`
```python
from typing import Dict, Any, Callable, Optional
import json
from app.models.tool import Tool
from sqlalchemy.orm import Session
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]):
"""注册内置工具"""
self._builtin_tools[name] = func
self._tool_schemas[name] = schema
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:
"""获取所有工具定义用于LLM"""
return list(self._tool_schemas.values())
def load_tools_from_db(self, db: Session, tool_names: list = 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:
pass # 已注册
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)
# 全局工具注册表实例
tool_registry = ToolRegistry()
```
#### 1.3 内置工具实现
**文件**: `backend/app/services/builtin_tools.py`
```python
from typing import Dict, Any
import httpx
import json
async def http_request_tool(url: str, method: str = "GET", headers: Dict = None, body: Any = None) -> str:
"""HTTP请求工具"""
try:
async with httpx.AsyncClient() as client:
if method.upper() == "GET":
response = await client.get(url, headers=headers)
elif method.upper() == "POST":
response = await client.post(url, json=body, headers=headers)
else:
raise ValueError(f"不支持的HTTP方法: {method}")
return json.dumps({
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response.text
}, ensure_ascii=False)
except Exception as e:
return json.dumps({"error": str(e)}, ensure_ascii=False)
async def database_query_tool(query: str, database: str = "default") -> str:
"""数据库查询工具"""
# 实现数据库查询逻辑
pass
async def file_read_tool(file_path: str) -> str:
"""文件读取工具"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
return f"错误: {str(e)}"
# 工具定义OpenAI Function格式
HTTP_REQUEST_SCHEMA = {
"type": "function",
"function": {
"name": "http_request",
"description": "发送HTTP请求支持GET和POST方法",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "请求URL"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE"],
"description": "HTTP方法"
},
"headers": {
"type": "object",
"description": "请求头"
},
"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"]
}
}
}
```
#### 1.4 LLM服务扩展
**文件**: `backend/app/services/llm_service.py` (扩展)
```python
from typing import List, Dict, Any, Optional
from app.services.tool_registry import tool_registry
class LLMService:
# ... 现有代码 ...
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返回的最终文本
"""
messages = [{"role": "user", "content": prompt}]
for iteration in range(max_iterations):
# 调用LLM
response = await client.chat.completions.create(
model=model,
messages=messages,
tools=tools if iteration == 0 else None, # 只在第一次调用时传递tools
tool_choice="auto",
temperature=temperature,
max_tokens=max_tokens
)
message = response.choices[0].message
# 添加助手回复到消息历史
messages.append(message)
# 检查是否有工具调用
if message.tool_calls:
# 处理每个工具调用
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# 执行工具
tool_result = await self._execute_tool(tool_name, tool_args)
# 添加工具结果到消息历史
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_result
})
else:
# 没有工具调用,返回最终回复
return message.content or ""
# 达到最大迭代次数
return messages[-1].get("content", "达到最大工具调用次数")
async def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
"""执行工具"""
# 从注册表获取工具函数
tool_func = tool_registry.get_tool_function(tool_name)
if not tool_func:
return json.dumps({"error": f"工具 {tool_name} 未找到"}, ensure_ascii=False)
try:
# 执行工具(支持异步函数)
if asyncio.iscoroutinefunction(tool_func):
result = await tool_func(**tool_args)
else:
result = tool_func(**tool_args)
# 将结果转换为字符串
if isinstance(result, (dict, list)):
return json.dumps(result, ensure_ascii=False)
return str(result)
except Exception as e:
return json.dumps({"error": str(e)}, ensure_ascii=False)
```
#### 1.5 工作流引擎扩展
**文件**: `backend/app/services/workflow_engine.py` (扩展)
```python
# 在LLM节点执行部分添加工具调用支持
elif node_type == 'llm' or node_type == 'template':
node_data = node.get('data', {})
prompt = node_data.get('prompt', '')
# 获取工具配置
tools_config = node_data.get('tools', []) # 工具名称列表
enable_tools = node_data.get('enable_tools', False)
# 如果启用了工具,加载工具定义
tools = []
if enable_tools and tools_config:
# 从注册表加载工具定义
for tool_name in tools_config:
tool_schema = tool_registry.get_tool_schema(tool_name)
if tool_schema:
tools.append(tool_schema)
# 调用LLM带工具
if tools:
result = await llm_service.call_openai_with_tools(
prompt=formatted_prompt,
tools=tools,
provider=provider,
model=model,
temperature=temperature,
max_tokens=max_tokens
)
else:
# 普通调用
result = await llm_service.call_llm(
prompt=formatted_prompt,
provider=provider,
model=model,
temperature=temperature,
max_tokens=max_tokens
)
```
### 阶段2: 数据库迁移
**文件**: `backend/alembic/versions/xxxx_add_tools_table.py`
```python
def upgrade():
op.create_table(
'tools',
sa.Column('id', sa.CHAR(36), primary_key=True),
sa.Column('name', sa.String(100), nullable=False, unique=True),
sa.Column('description', sa.Text, nullable=False),
sa.Column('category', sa.String(50)),
sa.Column('function_schema', sa.JSON, nullable=False),
sa.Column('implementation_type', sa.String(50), nullable=False),
sa.Column('implementation_config', sa.JSON),
sa.Column('is_public', sa.Boolean, default=False),
sa.Column('user_id', sa.CHAR(36), sa.ForeignKey('users.id')),
sa.Column('use_count', sa.Integer, default=0),
sa.Column('created_at', sa.DateTime, default=func.now()),
sa.Column('updated_at', sa.DateTime, default=func.now(), onupdate=func.now())
)
```
### 阶段3: API接口
**文件**: `backend/app/api/tools.py`
```python
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
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
router = APIRouter(prefix="/api/v1/tools", tags=["tools"])
@router.get("")
async def list_tools(
category: str = None,
db: Session = Depends(get_db)
):
"""获取工具列表"""
query = db.query(Tool).filter(Tool.is_public == True)
if category:
query = query.filter(Tool.category == category)
tools = query.all()
return [{
"id": tool.id,
"name": tool.name,
"description": tool.description,
"category": tool.category,
"function_schema": tool.function_schema
} for tool in tools]
@router.post("")
async def create_tool(
tool_data: dict,
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""创建工具"""
tool = Tool(
name=tool_data["name"],
description=tool_data["description"],
category=tool_data.get("category"),
function_schema=tool_data["function_schema"],
implementation_type=tool_data["implementation_type"],
implementation_config=tool_data.get("implementation_config"),
user_id=current_user.id
)
db.add(tool)
db.commit()
return tool
@router.get("/builtin")
async def list_builtin_tools():
"""获取内置工具列表"""
schemas = tool_registry.get_all_tool_schemas()
return schemas
```
### 阶段4: 前端实现
#### 4.1 LLM节点配置扩展
**文件**: `frontend/src/components/WorkflowEditor/WorkflowEditor.vue`
```vue
<template>
<!-- 在LLM节点配置面板中添加工具配置 -->
<el-tab-pane label="工具" name="tools" v-if="selectedNode.type === 'llm'">
<el-switch
v-model="selectedNode.data.enable_tools"
label="启用工具调用"
/>
<div v-if="selectedNode.data.enable_tools" style="margin-top: 20px;">
<el-select
v-model="selectedNode.data.tools"
multiple
placeholder="选择可用工具"
style="width: 100%"
>
<el-option
v-for="tool in availableTools"
:key="tool.name"
:label="tool.name"
:value="tool.name"
>
<div>
<strong>{{ tool.name }}</strong>
<p style="margin: 0; color: #999; font-size: 12px;">
{{ tool.description }}
</p>
</div>
</el-option>
</el-select>
<div v-for="toolName in selectedNode.data.tools" :key="toolName" style="margin-top: 10px;">
<el-card>
<template #header>
<span>{{ toolName }}</span>
</template>
<div v-if="getToolSchema(toolName)">
<p><strong>描述:</strong> {{ getToolSchema(toolName).function.description }}</p>
<p><strong>参数:</strong></p>
<pre>{{ JSON.stringify(getToolSchema(toolName).function.parameters, null, 2) }}</pre>
</div>
</el-card>
</div>
</div>
</el-tab-pane>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import api from '@/api'
const availableTools = ref([])
const toolSchemas = ref({})
onMounted(async () => {
// 加载可用工具
const response = await api.get('/api/v1/tools')
availableTools.value = response.data
// 加载内置工具
const builtinResponse = await api.get('/api/v1/tools/builtin')
availableTools.value = [...availableTools.value, ...builtinResponse.data]
// 构建工具schema映射
availableTools.value.forEach(tool => {
if (tool.function_schema) {
toolSchemas.value[tool.name] = tool.function_schema
}
})
})
const getToolSchema = (toolName: string) => {
return toolSchemas.value[toolName]
}
</script>
```
#### 4.2 工具调用可视化
在工作流执行时,显示工具调用过程:
```vue
<template>
<div class="tool-call-visualization" v-if="toolCalls.length > 0">
<h4>工具调用过程:</h4>
<div v-for="(call, index) in toolCalls" :key="index" class="tool-call-item">
<div class="tool-call-header">
<span class="tool-name">{{ call.name }}</span>
<span class="tool-status" :class="call.status">
{{ call.status === 'success' ? '✓' : '✗' }}
</span>
</div>
<div class="tool-call-args">
<strong>参数:</strong>
<pre>{{ JSON.stringify(call.arguments, null, 2) }}</pre>
</div>
<div class="tool-call-result" v-if="call.result">
<strong>结果:</strong>
<pre>{{ call.result }}</pre>
</div>
</div>
</div>
</template>
```
### 阶段5: 初始化内置工具
**文件**: `backend/scripts/init_builtin_tools.py`
```python
from app.core.database import SessionLocal
from app.models.tool import Tool
from app.services.tool_registry import tool_registry
from app.services.builtin_tools import (
http_request_tool,
file_read_tool,
HTTP_REQUEST_SCHEMA,
FILE_READ_SCHEMA
)
def init_builtin_tools():
"""初始化内置工具"""
db = SessionLocal()
try:
# 注册内置工具到注册表
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
)
# 保存到数据库
for tool_name, tool_schema in [
("http_request", HTTP_REQUEST_SCHEMA),
("file_read", FILE_READ_SCHEMA)
]:
existing = db.query(Tool).filter(Tool.name == tool_name).first()
if not existing:
tool = Tool(
name=tool_name,
description=tool_schema["function"]["description"],
category="builtin",
function_schema=tool_schema,
implementation_type="builtin",
is_public=True
)
db.add(tool)
db.commit()
print("✅ 内置工具初始化完成")
except Exception as e:
db.rollback()
print(f"❌ 初始化失败: {str(e)}")
finally:
db.close()
if __name__ == "__main__":
init_builtin_tools()
```
## 📊 使用示例
### 示例1: 配置LLM节点使用工具
```json
{
"id": "llm-1",
"type": "llm",
"data": {
"label": "智能助手",
"provider": "openai",
"model": "gpt-4",
"prompt": "请帮助用户解决问题,可以使用工具获取信息。",
"enable_tools": true,
"tools": ["http_request", "file_read"]
}
}
```
### 示例2: 工作流示例
```
开始 → LLM节点(启用工具) → 结束
```
用户输入: "查询北京的天气"
1. LLM识别需要调用工具
2. 调用 `http_request` 工具查询天气API
3. 获取结果后生成回复
## 🔒 安全考虑
1. **工具权限控制**
- 限制可执行的文件路径
- 限制HTTP请求的目标域名
- 限制数据库查询权限
2. **参数验证**
- 验证工具参数类型和范围
- 防止注入攻击
3. **执行超时**
- 设置工具执行超时时间
- 防止无限循环
4. **资源限制**
- 限制工具调用次数
- 限制工具执行时间
## 📈 后续优化
1. **工具市场**
- 用户可分享自定义工具
- 工具评分和评论
2. **工具组合**
- 支持工具链(一个工具调用另一个工具)
- 工具依赖管理
3. **性能优化**
- 工具结果缓存
- 并行工具执行
4. **监控和日志**
- 工具调用统计
- 工具执行日志
- 性能分析
## 🎯 实施优先级
### 高优先级MVP
1. ✅ 工具注册表
2. ✅ 内置工具HTTP请求、文件读取
3. ✅ LLM服务工具调用支持
4. ✅ 工作流引擎集成
5. ✅ 前端工具配置界面
### 中优先级
1. 数据库工具
2. 工具调用可视化
3. 工具执行日志
4. 错误处理和重试
### 低优先级
1. 工具市场
2. 工具组合
3. 性能优化
4. 高级安全控制
---
**方案版本**: v1.0
**创建时间**: 2026-01-23
**作者**: AI Assistant

View File

@@ -0,0 +1,167 @@
# 智能聊天助手性能优化实施报告
## 📊 优化完成情况
### ✅ 已完成的优化
#### 1. 删除 llm-format 节点 ⭐⭐⭐⭐⭐
- **状态**:✅ 已完成
- **优化内容**
- 删除了 `llm-format` 节点原第14个节点
- 优化了 `llm-question` 节点的 prompt直接生成格式化好的回复
- 更新了工作流边连接:`cache-update` 直接连接到 `end-1`
- **效果**:减少 1 个 LLM 调用,节省 **1-2 秒**
#### 2. 优化 max_tokens 配置 ⭐⭐⭐⭐
- **状态**:✅ 已完成
- **优化内容**
- `llm-intent`: 1000 → **200** (减少 80%)
- `llm-greeting`: 500 → **200** (减少 60%)
- `llm-question`: 2000 → **1000** (减少 50%)
- `llm-emotion`: 1000 → **500** (减少 50%)
- `llm-request`: 1500 → **800** (减少 47%)
- `llm-goodbye`: 300 → **150** (减少 50%)
- `llm-general`: 1000 → **500** (减少 50%)
- **效果**:减少 token 生成时间,节省 **0.5-1 秒**,降低 **30-40%** token 消耗
#### 3. 对话历史截断 ⭐⭐⭐⭐
- **状态**:✅ 已完成
- **优化内容**
-`workflow_engine.py` 中添加了对话历史截断逻辑
- 自动保留最近 **20 条**对话记录
-`cache-update` 节点执行后自动截断
- **效果**
- 减少 prompt 长度,节省 **0.2-0.5 秒**
- 降低 token 消耗
- 提升 LLM 响应速度
## 📈 性能提升预期
### 优化前
- **平均响应时间**5-6 秒
- **LLM 调用次数**3 次(意图理解 + 问题回答 + 格式化)
- **Token 消耗**:约 3500-4500 tokens/对话
- **节点数量**15 个
### 优化后
- **平均响应时间****3-4 秒**(提升 **40%**
- **LLM 调用次数****2 次**(意图理解 + 问题回答)
- **Token 消耗**:约 **2000-2800 tokens/对话**(减少 **30-40%**
- **节点数量****14 个**(减少 1 个)
## 🔧 技术实现细节
### 1. 工作流结构优化
**优化前流程**
```
开始 → 查询记忆 → 合并上下文 → 意图理解 → 意图路由 →
[5个分支] → 合并回复 → 更新记忆 → 格式化回复 → 结束
```
**优化后流程**
```
开始 → 查询记忆 → 合并上下文 → 意图理解 → 意图路由 →
[5个分支直接生成最终回复] → 合并回复 → 更新记忆 → 结束
```
### 2. LLM 节点 Prompt 优化
**llm-question 节点优化**
- 添加了"确保回复格式自然、完整,无需额外格式化"的指令
- 让 LLM 直接生成最终格式的回复,避免二次格式化
### 3. 对话历史截断实现
**位置**`backend/app/services/workflow_engine.py`
**实现逻辑**
```python
# 确保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}")
```
## 📋 优化清单
| 优化项 | 状态 | 预期效果 | 实际效果 |
|--------|------|----------|----------|
| 删除 llm-format 节点 | ✅ 完成 | 节省 1-2 秒 | 待测试 |
| 优化 max_tokens | ✅ 完成 | 节省 0.5-1 秒 | 待测试 |
| 对话历史截断 | ✅ 完成 | 节省 0.2-0.5 秒 | 待测试 |
## 🚀 下一步建议
### 中优先级优化(可选)
1. **使用 WebSocket 替代轮询** ⭐⭐⭐⭐
- 实施难度:中
- 效果:提升实时性,减少服务器负载
- 预计时间2-3 小时
2. **智能轮询(如果不用 WebSocket** ⭐⭐⭐
- 实施难度:低
- 效果:减少 30-50% 无效请求
- 预计时间1 小时
### 高级优化(可选)
3. **流式响应** ⭐⭐⭐⭐⭐
- 实施难度:高
- 效果:首字响应时间降低 50-70%
- 预计时间4-6 小时
4. **LLM 响应缓存** ⭐⭐⭐
- 实施难度:中
- 效果:重复问题响应时间 < 100ms
- 预计时间2-3 小时
## 📝 测试建议
1. **性能测试**
- 测试优化前后的响应时间对比
- 监控 LLM API 调用次数和耗时
- 检查对话历史是否正确截断
2. **功能测试**
- 验证删除 llm-format 节点后,回复格式是否正常
- 验证各分支节点的回复质量
- 验证记忆功能是否正常
3. **压力测试**
- 测试长时间对话超过20条时的性能
- 测试并发请求的处理能力
## ⚠️ 注意事项
1. **向后兼容**:优化后的配置已更新到数据库,现有对话会使用新配置
2. **对话历史**超过20条的旧对话历史会被自动截断
3. **回复格式**:由于删除了格式化节点,需要确保各分支节点生成的回复格式正确
## 📊 监控指标
建议监控以下指标以验证优化效果:
1. **响应时间**
- P50中位数
- P9595% 分位数)
- P9999% 分位数)
2. **LLM 调用**
- 各节点的平均调用时间
- Token 消耗统计
3. **系统资源**
- CPU 使用率
- 内存使用率
- 数据库连接数
---
**优化完成时间**2024年
**优化版本**v1.0
**维护人员**AI Assistant

View File

@@ -0,0 +1,178 @@
# 知识库问答助手测试总结
## 测试时间
2026-01-23 00:00
## Agent信息
- **名称**: 知识库问答助手
- **ID**: `45c56398-ad1d-4533-89e0-ba02f9c47932`
- **状态**: published已发布
- **节点数量**: 10个
- **连接数量**: 9条
## 工作流结构
1. **开始节点** - 接收用户问题
2. **问题预处理** - 整理输入数据
3. **文本向量化** - HTTP节点调用DeepSeek embedding API
4. **提取向量** - JSON节点提取embedding向量
5. **准备搜索数据** - 合并向量和查询文本
6. **知识库检索** - 向量数据库节点进行语义搜索
7. **整理检索结果** - 合并搜索结果和查询
8. **生成答案** - LLM节点基于检索结果生成答案
9. **提取最终答案** - JSON节点提取最终文本
10. **结束节点** - 返回答案
## 测试结果
### 测试输入
```
问题: 什么是人工智能?
```
### 执行状态
- **状态**: failed执行失败
- **执行时间**: 2240ms
- **执行ID**: `e775395c-a306-4544-abaf-357c9245f56e`
### 错误分析
#### 1. HTTP节点调用embedding API失败
- **错误信息**: "Not Found. Please check the configuration."
- **节点**: `http-embedding`
- **API URL**: `https://api.deepseek.com/v1/embeddings`
- **模型**: `deepseek-embedding`
- **问题**: DeepSeek可能不支持embedding API或者URL/模型名称不正确
#### 2. 向量数据库节点无法获取查询向量
- **错误信息**: "节点 vector-search 执行失败: 向量数据库操作失败: 无法获取查询向量"
- **节点**: `vector-search`
- **原因**: 由于embedding API调用失败没有获取到向量数据
### 执行日志关键信息
```
[3] 2026-01-22 16:00:25 [INFO]
节点: http-embedding (http)
消息: 节点 http-embedding (http) 开始执行
[10] 2026-01-22 16:00:26 [ERROR]
节点: http-embedding (http)
消息: HTTP请求失败: 404
数据: {
"error_msg": "Not Found. Please check the configuration."
}
[11] 2026-01-22 16:00:26 [INFO]
节点: vector-search (vector_db)
消息: 节点 vector-search (vector_db) 开始执行
[13] 2026-01-22 16:00:26 [ERROR]
节点: (无) ((无))
消息: 工作流任务执行失败
错误: 节点 vector-search 执行失败: 向量数据库操作失败: 无法获取查询向量
```
## 问题根因
1. **DeepSeek Embedding API不支持或配置错误**
- DeepSeek可能不提供embedding API
- 或者需要使用不同的API端点/模型名称
- 需要验证DeepSeek是否支持embedding功能
2. **缺少备选方案**
- 当前工作流完全依赖embedding API
- 没有fallback机制处理API调用失败的情况
## 解决方案
### 方案1: 使用其他embedding服务
1. **使用OpenAI Embedding API**
- URL: `https://api.openai.com/v1/embeddings`
- 模型: `text-embedding-ada-002``text-embedding-3-small`
- 需要配置OpenAI API Key
2. **使用本地embedding模型**
- 使用sentence-transformers等库
- 在服务器端运行embedding模型
- 通过HTTP节点调用本地服务
### 方案2: 简化工作流(用于测试)
1. **跳过embedding步骤**
- 直接使用文本关键词匹配
- 或者使用LLM节点进行语义理解
- 简化向量数据库的使用
2. **使用预计算的向量**
- 预先将知识库文档向量化
- 存储向量数据
- 查询时只需要搜索,不需要实时向量化
### 方案3: 修复DeepSeek配置
1. **验证DeepSeek API文档**
- 确认是否支持embedding
- 确认正确的API端点和模型名称
- 更新HTTP节点配置
## 下一步行动
1. **验证DeepSeek Embedding API**
- 查阅DeepSeek官方文档
- 测试API端点是否可用
- 确认模型名称是否正确
2. **准备测试数据**
- 创建知识库文档
- 将文档向量化并存储到向量数据库
- 使用`knowledge_base`集合
3. **修复或替换embedding节点**
- 根据验证结果修复DeepSeek配置
- 或替换为其他embedding服务
- 或使用简化方案
4. **重新测试**
- 修复后重新执行测试
- 验证完整工作流是否正常
- 测试知识库检索和答案生成
## 测试命令
```bash
# 发布Agent
python3 publish_agent.py "知识库问答助手"
# 测试Agent
python3 test_workflow_tool.py -a "知识库问答助手" -i "什么是人工智能?"
# 查看执行日志
python3 check_execution_logs.py
```
## 相关文件
- **Agent生成脚本**: `backend/scripts/generate_knowledge_base_qa_agent.py`
- **测试工具**: `test_workflow_tool.py`
- **日志查看工具**: `check_execution_logs.py`
- **发布脚本**: `publish_agent.py`
## 注意事项
1. **知识库数据准备**
- 需要先准备知识库文档
- 文档需要向量化并存储
- 集合名称必须是`knowledge_base`
2. **API配置**
- 确保embedding API可用
- 配置正确的API Key
- 验证API端点和模型名称
3. **向量数据库**
- 当前使用内存存储(简化实现)
- 生产环境应使用ChromaDB、Pinecone等
- 数据在服务重启后会丢失
---
测试完成时间: 2026-01-23 00:02
测试人员: AI Assistant

View File

@@ -0,0 +1,260 @@
# 节点配置页面增强方案 - 完成情况报告
## 📊 总体完成度:约 75%
---
## ✅ 已完成功能(高优先级)
### 1. 数据流转可视化面板 ⭐⭐⭐⭐⭐
**完成度85%**
| 功能点 | 状态 | 说明 |
|--------|------|------|
| ✅ 显示上游节点的输出变量 | **已完成** | 在"数据流"标签页中显示所有上游节点及其输出变量 |
| ✅ 显示数据流转路径 | **已完成** | 显示上游节点列表和下游节点列表,清晰展示数据流转 |
| ⚠️ 提供数据预览功能 | **部分完成** | 有独立的"数据预览"标签页,但数据流面板中上游节点的实时数据预览未实现 |
| ✅ 一键插入变量 | **已完成** | 点击变量标签即可插入到配置字段 |
**实现位置:**
- 标签页:`数据流` (name="dataflow")
- 文件:`frontend/src/components/WorkflowEditor/WorkflowEditor.vue` (1439-1530行)
**已实现功能:**
- ✅ 上游节点列表展示
- ✅ 上游节点输出变量展示(带类型和描述)
- ✅ 当前节点输出字段说明
- ✅ 下游节点列表展示
- ✅ 变量一键插入功能
**待完善:**
- ⚠️ 上游节点的实时数据预览(需要执行记录)
---
### 2. 记忆信息展示 ⭐⭐⭐⭐⭐
**完成度100%**
| 功能点 | 状态 | 说明 |
|--------|------|------|
| ✅ 实时显示记忆内容 | **已完成** | 通过API获取并显示记忆数据 |
| ✅ 对话历史预览 | **已完成** | 对话框形式展示完整对话历史 |
| ✅ 用户画像展示 | **已完成** | 表格形式展示用户画像字段 |
| ✅ TTL 和过期时间 | **已完成** | 自动计算并显示过期时间(天/小时/分钟) |
**实现位置:**
- 标签页:`记忆信息` (name="memory",仅 Cache 节点显示)
- 文件:`frontend/src/components/WorkflowEditor/WorkflowEditor.vue` (1532-1644行)
- 后端API`backend/app/api/execution_logs.py` (缓存查询接口)
**已实现功能:**
- ✅ 记忆键显示
- ✅ 记忆状态显示(存在/不存在)
- ✅ 对话历史统计和详情查看
- ✅ 用户画像统计和详情查看
- ✅ TTL 信息显示
- ✅ 刷新记忆功能连接实际API
- ✅ 删除记忆功能
- ✅ 清空记忆功能
**后端API**
-`GET /api/v1/execution-logs/cache/{key}` - 获取缓存值
-`DELETE /api/v1/execution-logs/cache/{key}` - 删除缓存值
---
### 3. 变量智能提示增强 ⭐⭐⭐⭐⭐
**完成度80%**
| 功能点 | 状态 | 说明 |
|--------|------|------|
| ✅ 按来源分组显示变量 | **已完成** | 基础变量、上游节点变量、记忆变量分组显示 |
| ✅ 类型提示和描述 | **已完成** | 不同变量类型用不同颜色标签,鼠标悬停显示描述 |
| ❌ 自动补全功能 | **未实现** | 输入 `{{` 时自动提示变量(需要实现) |
| ✅ 一键插入变量 | **已完成** | 点击变量标签直接插入到提示词字段 |
**实现位置:**
- 标签页:`基础` (name="basic") - LLM节点提示词字段下方
- 文件:`frontend/src/components/WorkflowEditor/WorkflowEditor.vue` (396-476行)
**已实现功能:**
- ✅ 变量分组显示(基础变量、上游变量、记忆变量)
- ✅ 变量类型标签(不同颜色区分)
- ✅ 变量描述提示Tooltip
- ✅ 一键插入变量功能
- ✅ 可折叠面板
**待完善:**
- ❌ 自动补全:输入 `{{` 时自动弹出变量选择器
---
## ⚠️ 部分完成功能(中优先级)
### 4. 执行时数据预览 ⭐⭐⭐⭐
**完成度70%**
| 功能点 | 状态 | 说明 |
|--------|------|------|
| ✅ 显示实际输入/输出数据 | **已完成** | JSON格式化显示节点的输入和输出数据 |
| ✅ 执行时间和状态 | **已完成** | 显示执行时间、开始时间、完成时间 |
| ❌ 缓存命中情况 | **未实现** | 未显示缓存命中信息 |
**实现位置:**
- 标签页:`数据预览` (name="preview")
- 文件:`frontend/src/components/WorkflowEditor/WorkflowEditor.vue` (1645-1783行)
- 后端API`backend/app/api/execution_logs.py` (节点执行数据接口)
**已实现功能:**
- ✅ 执行记录选择器
- ✅ 输入数据展示JSON格式化
- ✅ 输出数据展示JSON格式化
- ✅ 执行时间信息
- ✅ 复制到剪贴板功能
- ✅ 自动加载最近的执行记录
**后端API**
-`GET /api/v1/execution-logs/executions/{execution_id}/nodes/{node_id}/data` - 获取节点执行数据
**待完善:**
- ❌ 缓存命中情况显示(需要从执行日志中提取 cache_hit 信息)
---
## ❌ 未实现功能(低优先级)
### 5. 智能配置助手 ⭐⭐⭐⭐
**完成度30%**
| 功能点 | 状态 | 说明 |
|--------|------|------|
| ❌ 场景化配置向导 | **未实现** | 分步骤引导用户完成配置 |
| ⚠️ 配置模板库 | **部分实现** | 有快速模板功能,但不是完整的模板库 |
| ✅ 一键应用模板 | **已完成** | 快速模板可以一键应用 |
**已实现功能:**
- ✅ 快速模板选择(在"基础"标签页中)
- ✅ 模板一键应用功能
- ✅ 支持多种节点类型的模板HTTP、LLM、JSON、文本、缓存等
**待实现:**
- ❌ 场景化配置向导(分步骤引导)
- ❌ 完整的配置模板库(分类、搜索、收藏等)
- ❌ 配置模板的导入/导出功能
---
## 📋 详细功能清单
### ✅ 已实现的核心功能
1. **数据流转可视化**
- ✅ 上游节点列表
- ✅ 上游节点变量展示
- ✅ 输出字段说明
- ✅ 下游节点列表
- ✅ 变量一键插入
2. **记忆信息管理**
- ✅ 记忆内容实时查看
- ✅ 对话历史预览
- ✅ 用户画像展示
- ✅ TTL 信息显示
- ✅ 记忆操作(刷新、删除、清空)
3. **变量智能提示**
- ✅ 变量分组显示
- ✅ 类型和描述提示
- ✅ 一键插入功能
4. **执行数据预览**
- ✅ 输入/输出数据展示
- ✅ 执行时间信息
- ✅ 执行记录选择
5. **基础功能**
- ✅ 快速模板功能
- ✅ 节点测试功能(已移到独立标签页)
### ⚠️ 待完善的功能
1. **数据流转可视化**
- ⚠️ 上游节点的实时数据预览(需要执行记录支持)
2. **变量智能提示**
- ❌ 自动补全功能(输入 `{{` 时自动提示)
3. **执行数据预览**
- ❌ 缓存命中情况显示
4. **智能配置助手**
- ❌ 场景化配置向导
- ❌ 完整的配置模板库
---
## 🎯 下一步建议
### 高优先级(建议优先完成)
1. **变量自动补全功能**
- 实现输入 `{{` 时自动弹出变量选择器
- 支持键盘导航和选择
- 预计时间2-3小时
2. **上游节点数据预览**
- 在数据流面板中显示上游节点的实际输出数据
- 需要从执行记录中提取数据
- 预计时间2-3小时
3. **缓存命中情况显示**
- 在执行数据预览中显示缓存命中信息
- 需要从执行日志中提取 cache_hit 字段
- 预计时间1-2小时
### 中优先级(后续优化)
4. **场景化配置向导**
- 为复杂节点提供分步骤配置向导
- 根据使用场景提供预设配置
- 预计时间4-6小时
5. **配置模板库**
- 完整的模板管理系统
- 模板分类、搜索、收藏功能
- 预计时间6-8小时
---
## 📊 完成度统计
| 优先级 | 功能模块 | 完成度 | 状态 |
|--------|----------|--------|------|
| 高 | 数据流转可视化面板 | 85% | ✅ 基本完成 |
| 高 | 记忆信息展示 | 100% | ✅ 已完成 |
| 高 | 变量智能提示增强 | 80% | ✅ 基本完成 |
| 中 | 执行时数据预览 | 70% | ⚠️ 部分完成 |
| 低 | 智能配置助手 | 30% | ❌ 待实现 |
**总体完成度:约 75%**
---
## 🎉 已实现的亮点功能
1. **完整的数据流转可视化** - 用户可以清楚看到数据如何流转
2. **实时记忆信息管理** - Cache 节点可以实时查看和管理记忆
3. **智能变量提示** - 分组显示、类型提示、一键插入
4. **执行数据预览** - 查看实际执行时的输入/输出数据
5. **后端API完整支持** - 所有功能都有对应的后端API支持
---
**文档版本**v1.0
**更新时间**2024年
**维护人员**AI Assistant

View File

@@ -0,0 +1,632 @@
# 节点配置页面增强方案
## 一、您的想法完全正确!
您的建议非常有价值,这正是提升用户体验和开发效率的关键改进点。当前系统虽然有一些基础功能,但确实需要增强以下方面:
### 当前系统的不足
1. **变量可见性不足**
- 虽然有 `availableVariables` 计算属性,但用户看不到:
- 上游节点实际输出了哪些变量
- 变量的数据结构(嵌套字段)
- 变量的实时值(执行时)
2. **记忆信息不直观**
- Cache 节点配置时,用户看不到:
- 当前记忆的内容conversation_history、user_profile
- 记忆的键名和存储位置
- TTL 和过期时间
3. **数据流转不透明**
- 用户不知道:
- 数据从哪个节点来
- 经过哪些转换
- 最终输出什么格式
4. **配置指导不足**
- 缺少:
- 节点使用示例
- 常见配置模式
- 最佳实践提示
## 二、增强方案设计
### 方案 1数据流转可视化面板 ⭐⭐⭐⭐⭐
#### 1.1 新增"数据流"标签页
在节点配置面板中添加"数据流"标签页,显示:
```vue
<el-tab-pane label="数据流" name="dataflow">
<!-- 上游数据源 -->
<el-card shadow="never" class="dataflow-card">
<template #header>
<div class="card-header">
<span>📥 输入数据源</span>
<el-tag size="small" type="info">{{ upstreamNodes.length }} 个上游节点</el-tag>
</div>
</template>
<!-- 上游节点列表 -->
<div v-for="edge in upstreamEdges" :key="edge.id" class="upstream-item">
<div class="node-info">
<el-icon><ArrowLeft /></el-icon>
<span class="node-name">{{ getNodeLabel(edge.source) }}</span>
<el-tag size="small" :type="getNodeTypeColor(edge.source)">
{{ getNodeType(edge.source) }}
</el-tag>
</div>
<!-- 可用变量列表 -->
<div class="variables-list">
<div
v-for="var in getUpstreamVariables(edge.source)"
:key="var.name"
class="variable-item"
@click="insertVariable(var.name)"
>
<el-icon><Document /></el-icon>
<span class="var-name">{{ var.name }}</span>
<span class="var-type">{{ var.type }}</span>
<el-tooltip :content="var.description" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</div>
</div>
<!-- 数据预览如果有执行记录 -->
<el-collapse v-if="hasExecutionData(edge.source)">
<el-collapse-item title="数据预览" name="preview">
<pre class="data-preview">{{ formatExecutionData(edge.source) }}</pre>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
<!-- 当前节点输出 -->
<el-card shadow="never" class="dataflow-card">
<template #header>
<div class="card-header">
<span>📤 输出数据</span>
</div>
</template>
<!-- 输出字段说明 -->
<div class="output-fields">
<div
v-for="field in getOutputFields(selectedNode.type)"
:key="field.name"
class="output-field"
>
<span class="field-name">{{ field.name }}</span>
<span class="field-desc">{{ field.description }}</span>
</div>
</div>
<!-- 下游节点 -->
<div v-if="downstreamNodes.length > 0" class="downstream-section">
<div class="section-title">下游节点</div>
<div
v-for="node in downstreamNodes"
:key="node.id"
class="downstream-item"
>
<el-icon><ArrowRight /></el-icon>
<span>{{ node.data.label }}</span>
</div>
</div>
</el-card>
</el-tab-pane>
```
#### 1.2 功能特性
- **上游节点追踪**:显示所有连接到当前节点的上游节点
- **变量自动推断**:根据上游节点类型自动推断可用变量
- **数据预览**:如果有执行记录,显示实际数据
- **一键插入**:点击变量名直接插入到配置字段
- **类型提示**:显示变量的数据类型和结构
### 方案 2记忆信息展示 ⭐⭐⭐⭐⭐
#### 2.1 Cache 节点增强
对于 Cache 节点,显示详细的记忆信息:
```vue
<el-tab-pane label="记忆信息" name="memory" v-if="selectedNode.type === 'cache'">
<!-- 当前记忆内容 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>💾 记忆内容</span>
<el-button size="small" @click="refreshMemory">刷新</el-button>
</div>
</template>
<!-- 记忆键 -->
<el-form-item label="记忆键">
<el-input
v-model="memoryKey"
placeholder="user_memory_{user_id}"
readonly
/>
<el-tag size="small" style="margin-left: 8px;">
{{ memoryStatus }}
</el-tag>
</el-form-item>
<!-- 对话历史 -->
<el-form-item label="对话历史">
<el-tag
v-if="memoryData?.conversation_history"
type="info"
>
{{ memoryData.conversation_history.length }} 条记录
</el-tag>
<el-button
size="small"
text
@click="showConversationHistory"
>
查看详情
</el-button>
</el-form-item>
<!-- 用户画像 -->
<el-form-item label="用户画像">
<el-tag
v-if="memoryData?.user_profile"
type="success"
>
{{ Object.keys(memoryData.user_profile).length }} 个字段
</el-tag>
<el-button
size="small"
text
@click="showUserProfile"
>
查看详情
</el-button>
</el-form-item>
<!-- TTL 信息 -->
<el-form-item label="过期时间">
<el-tag type="warning">
{{ ttlInfo }}
</el-tag>
</el-form-item>
</el-card>
<!-- 记忆操作 -->
<el-card shadow="never" style="margin-top: 15px;">
<template #header>
<span>🔧 记忆操作</span>
</template>
<el-button-group>
<el-button size="small" @click="testMemoryQuery">测试查询</el-button>
<el-button size="small" @click="clearMemory">清空记忆</el-button>
<el-button size="small" type="danger" @click="deleteMemory">删除记忆</el-button>
</el-button-group>
</el-card>
</el-tab-pane>
```
#### 2.2 功能特性
- **实时记忆查看**:显示当前记忆的实际内容
- **对话历史预览**:显示最近的对话记录
- **用户画像展示**:显示用户画像字段
- **TTL 倒计时**:显示记忆过期时间
- **记忆操作**:提供测试、清空、删除等操作
### 方案 3智能配置助手 ⭐⭐⭐⭐
#### 3.1 配置向导
为复杂节点提供配置向导:
```vue
<el-tab-pane label="配置助手" name="assistant">
<!-- 配置模式选择 -->
<el-card shadow="never">
<template #header>
<span>🎯 快速配置</span>
</template>
<el-radio-group v-model="configMode">
<el-radio label="simple">简单模式</el-radio>
<el-radio label="advanced">高级模式</el-radio>
<el-radio label="template">模板模式</el-radio>
</el-radio-group>
<!-- 简单模式 -->
<div v-if="configMode === 'simple'" class="config-wizard">
<el-steps :active="wizardStep" simple>
<el-step title="选择场景" />
<el-step title="配置参数" />
<el-step title="完成" />
</el-steps>
<!-- 场景选择 -->
<div v-if="wizardStep === 0" class="wizard-content">
<div class="scenario-list">
<div
v-for="scenario in getScenarios(selectedNode.type)"
:key="scenario.id"
class="scenario-item"
@click="selectScenario(scenario)"
>
<el-icon><component :is="scenario.icon" /></el-icon>
<div class="scenario-info">
<div class="scenario-name">{{ scenario.name }}</div>
<div class="scenario-desc">{{ scenario.description }}</div>
</div>
</div>
</div>
</div>
<!-- 参数配置 -->
<div v-if="wizardStep === 1" class="wizard-content">
<el-form :model="wizardConfig" label-width="120px">
<el-form-item
v-for="field in selectedScenario.fields"
:key="field.name"
:label="field.label"
>
<el-input
v-if="field.type === 'text'"
v-model="wizardConfig[field.name]"
:placeholder="field.placeholder"
/>
<el-select
v-else-if="field.type === 'select'"
v-model="wizardConfig[field.name]"
>
<el-option
v-for="opt in field.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</div>
<!-- 模板模式 -->
<div v-else-if="configMode === 'template'" class="template-selector">
<el-select
v-model="selectedTemplate"
placeholder="选择配置模板"
@change="applyTemplate"
>
<el-option
v-for="template in getTemplates(selectedNode.type)"
:key="template.id"
:label="template.name"
:value="template.id"
>
<div class="template-option">
<span>{{ template.name }}</span>
<el-tag size="small" type="info">{{ template.category }}</el-tag>
</div>
</el-option>
</el-select>
<el-card v-if="selectedTemplate" shadow="never" style="margin-top: 15px;">
<div class="template-preview">
<div class="preview-title">模板预览</div>
<pre>{{ getTemplatePreview(selectedTemplate) }}</pre>
</div>
</el-card>
</div>
</el-card>
</el-tab-pane>
```
#### 3.2 功能特性
- **场景化配置**:根据使用场景提供预设配置
- **配置向导**:分步骤引导用户完成配置
- **模板库**:提供常用配置模板
- **一键应用**:快速应用模板配置
### 方案 4变量智能提示 ⭐⭐⭐⭐⭐
#### 4.1 增强变量插入功能
```vue
<!-- 在配置字段中添加智能提示 -->
<el-form-item label="提示词">
<el-input
v-model="selectedNode.data.prompt"
type="textarea"
:rows="6"
placeholder="输入提示词..."
/>
<!-- 变量提示面板 -->
<el-collapse v-model="variablePanelActive" style="margin-top: 10px;">
<el-collapse-item title="可用变量" name="variables">
<div class="variable-suggestions">
<!-- 基础变量 -->
<div class="var-group">
<div class="group-title">基础变量</div>
<div class="var-items">
<el-tag
v-for="var in basicVariables"
:key="var"
class="var-tag"
@click="insertVariable('prompt', var)"
>
{{ var }}
</el-tag>
</div>
</div>
<!-- 上游变量 -->
<div class="var-group" v-if="upstreamVariables.length > 0">
<div class="group-title">上游节点变量</div>
<div class="var-items">
<el-tag
v-for="var in upstreamVariables"
:key="var.name"
class="var-tag"
@click="insertVariable('prompt', var.name)"
>
{{ var.name }}
<el-tooltip :content="var.description">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</el-tag>
</div>
</div>
<!-- 记忆变量 -->
<div class="var-group" v-if="memoryVariables.length > 0">
<div class="group-title">记忆变量</div>
<div class="var-items">
<el-tag
v-for="var in memoryVariables"
:key="var.name"
class="var-tag"
@click="insertVariable('prompt', var.name)"
>
{{ var.name }}
<el-tooltip :content="var.description">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</el-tag>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</el-form-item>
```
#### 4.2 功能特性
- **变量分组**:按来源分组显示变量
- **类型提示**:显示变量的数据类型
- **描述信息**:鼠标悬停显示变量说明
- **一键插入**:点击变量标签直接插入
- **自动补全**:输入 `{{` 时自动提示变量
### 方案 5执行时数据预览 ⭐⭐⭐⭐
#### 5.1 实时数据流追踪
```vue
<el-tab-pane label="数据预览" name="preview">
<!-- 执行记录选择 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>📊 执行数据</span>
<el-select
v-model="selectedExecutionId"
placeholder="选择执行记录"
size="small"
style="width: 200px;"
>
<el-option
v-for="exec in recentExecutions"
:key="exec.id"
:label="`${exec.created_at} - ${exec.status}`"
:value="exec.id"
/>
</el-select>
</div>
</template>
<!-- 输入数据 -->
<el-card shadow="never" style="margin-bottom: 15px;">
<template #header>
<span>📥 输入数据</span>
</template>
<el-scrollbar height="200px">
<pre class="data-viewer">{{ formatInputData(executionData?.input) }}</pre>
</el-scrollbar>
</el-card>
<!-- 输出数据 -->
<el-card shadow="never">
<template #header>
<span>📤 输出数据</span>
</template>
<el-scrollbar height="200px">
<pre class="data-viewer">{{ formatOutputData(executionData?.output) }}</pre>
</el-scrollbar>
</el-card>
<!-- 执行信息 -->
<el-descriptions :column="2" border style="margin-top: 15px;">
<el-descriptions-item label="执行时间">
{{ executionData?.duration }}ms
</el-descriptions-item>
<el-descriptions-item label="执行状态">
<el-tag :type="getStatusType(executionData?.status)">
{{ executionData?.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="缓存命中" v-if="executionData?.cache_hit !== undefined">
<el-tag :type="executionData.cache_hit ? 'success' : 'info'">
{{ executionData.cache_hit ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-tab-pane>
```
## 三、实施优先级
### 🔥 高优先级(立即实施)
1. **数据流转可视化面板** ⭐⭐⭐⭐⭐
- 实施难度:中
- 效果:显著提升用户体验
- 预计时间3-4 小时
2. **变量智能提示增强** ⭐⭐⭐⭐⭐
- 实施难度:低
- 效果:降低配置错误率
- 预计时间2-3 小时
### 🟡 中优先级(近期实施)
3. **记忆信息展示** ⭐⭐⭐⭐⭐
- 实施难度:中
- 效果:直观了解记忆状态
- 预计时间2-3 小时
4. **执行时数据预览** ⭐⭐⭐⭐
- 实施难度:中
- 效果:便于调试和优化
- 预计时间2-3 小时
### 🟢 低优先级(长期优化)
5. **智能配置助手** ⭐⭐⭐⭐
- 实施难度:高
- 效果:降低学习成本
- 预计时间4-6 小时
## 四、技术实现要点
### 1. 数据获取
```typescript
// 获取上游节点变量
const getUpstreamVariables = (nodeId: string) => {
const node = nodes.value.find(n => n.id === nodeId)
if (!node) return []
// 根据节点类型推断输出变量
const variables = []
switch (node.type) {
case 'llm':
variables.push({ name: 'output', type: 'string', description: 'LLM生成的回复' })
variables.push({ name: 'response', type: 'string', description: '完整响应' })
break
case 'cache':
variables.push({ name: 'memory', type: 'object', description: '记忆数据' })
variables.push({ name: 'conversation_history', type: 'array', description: '对话历史' })
variables.push({ name: 'user_profile', type: 'object', description: '用户画像' })
break
case 'transform':
// 从mapping中提取
const mapping = node.data?.mapping || {}
Object.keys(mapping).forEach(key => {
variables.push({ name: key, type: 'any', description: `映射字段: ${key}` })
})
break
}
return variables
}
```
### 2. 记忆数据获取
```typescript
// 获取记忆数据
const fetchMemoryData = async (nodeId: string) => {
if (selectedNode.value?.type !== 'cache') return
const key = selectedNode.value.data.key
// 调用API获取记忆数据
try {
const response = await api.get(`/api/v1/cache/${encodeURIComponent(key)}`)
memoryData.value = response.data
} catch (error) {
console.error('获取记忆数据失败:', error)
}
}
```
### 3. 执行数据获取
```typescript
// 获取节点执行数据
const fetchNodeExecutionData = async (nodeId: string, executionId: string) => {
try {
const response = await api.get(
`/api/v1/execution-logs/executions/${executionId}/nodes/${nodeId}`
)
executionData.value = response.data
} catch (error) {
console.error('获取执行数据失败:', error)
}
}
```
## 五、预期效果
### 用户体验提升
1. **降低学习成本**
- 新用户无需查阅文档即可理解数据流转
- 配置错误率降低 50%+
2. **提升开发效率**
- 节点配置时间减少 40%+
- 调试时间减少 60%+
3. **增强可视化**
- 数据流转一目了然
- 记忆状态实时可见
- 执行结果直观展示
### 开发体验提升
1. **快速搭建**
- 通过模板和向导快速创建工作流
- 减少重复配置工作
2. **错误预防**
- 变量类型检查
- 配置验证提示
- 常见错误预警
## 六、总结
您的想法完全正确!这些增强功能将显著提升用户体验和开发效率。建议优先实施:
1. **数据流转可视化面板** - 让用户清楚看到数据如何流转
2. **变量智能提示** - 降低配置错误率
3. **记忆信息展示** - 直观了解记忆状态
这些功能将使平台更加易用和专业!
---
**文档版本**v1.0
**创建时间**2024年
**维护人员**AI Assistant