19 KiB
19 KiB
我需要参考另外一个工程的代码,来实现我当前的后端能力,当前的工程是个ComfyUI的插件,我在这里实现了一个mcp_client,来替代掉原来的workflowChatApi.ts里的${BASE_URL}/api/chat/invoke接口。
核心代码facade.py实现了agent-tools的能力:
from typing import Optional, Generator, Dict
import json
import uuid
from agent.tools.node_explain_agent_tool import explain_node
from agent.tools.node_search_tool import search_node
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.postgres import PostgresSaver
from context import get_user_id
from logger import logger
from psycopg_pool import ConnectionPool
from agent.llms import llm_4_chatbot, llm_4o, llm_deepseek, llm_4o_mini, llm_dashscope, get_openai_client, llm_dashscope_deepseek_v3, llm_qwen3, llm_4_1, llm_gemini2_5_flash
from agent.tools.workflow_recall_tool import recall_workflow
from agent.tools.gen_workflow_tool import gen_workflow
from agent.tools.model_search_tool import search_model
from initializer.db_initializer import Session, db_url
from utils.os_util import get_root_path
from dao.message_dao import Message, MessageSource
from dao.node_dao import Node
from dao.workflow_dao import Workflow
tools = [
recall_workflow, # 工作流召回
gen_workflow, # 工作流生成
search_node, # 节点搜索
search_model, # 模型召回
explain_node, # 节点解释
# analyze_workflow, # 工作流分析
]
system_prompt_path = get_root_path('prompts/system_prompt.txt')
with open(system_prompt_path, 'r', encoding='utf-8') as f:
system_prompt_content = f.read()
new_system_prompt = SystemMessage(content=system_prompt_content)
connection_kwargs = {
"autocommit": True,
"prepare_threshold": 0,
}
# with ConnectionPool(
# conninfo=db_url,
# max_size=20,
# kwargs=connection_kwargs,
# ) as pool:
pool = ConnectionPool(db_url, max_size=50, kwargs=connection_kwargs)
checkpointer = PostgresSaver(pool)
checkpointer.setup()
# checkpointer = MemorySaver()
# 修改 LLM 映射字典,使用正确的配置方式
llm_config: Dict = {
"gemini-2.5-flash": {
"client":llm_gemini2_5_flash,
"image_enable":True,
},
"gpt-4o": {
"client":llm_4_1,
"image_enable":True,
},
"gpt-4.1": {
"client":llm_4_1,
"image_enable":True,
},
"qwen-plus": {
"client":llm_dashscope,
"image_enable":False,
},
}
llm_mapping = {k: llm_config[k]["client"] for k in llm_config}
# 获取所有llm列表
def get_model_list():
model_list = []
remove_models = ["gpt-4o"]
for model, config in llm_config.items():
if model not in remove_models:
model_list.append({
"name": model,
"image_enable": config["image_enable"],
})
return model_list
# 创建一个函数用于限制对话历史
def limit_conversation_history(state, max_turns=5):
"""
限制对话历史只保留最近的几轮对话
Args:
state: 当前的状态对象
max_turns: 保留的最大对话轮数
Returns:
修改后的状态
"""
messages = state.get("messages", [])
system_messages = []
conversation_messages = []
# 分离系统消息和对话消息
for msg in messages:
if isinstance(msg, SystemMessage):
system_messages.append(msg)
else:
conversation_messages.append(msg)
# 保留对话消息的最后max_turns*2条(每轮包含用户消息和AI回复)
limited_conversation = conversation_messages[-max_turns*2:] if len(conversation_messages) > max_turns*2 else conversation_messages
# 重新组合消息
state["messages"] = system_messages + limited_conversation
return state
# 将系统提示和历史限制组合在一起
def combined_state_modifier(state):
"""
组合状态修改器,添加系统提示并限制对话历史
Args:
state: 当前的状态对象
Returns:
修改后的消息列表
"""
messages = state.get("messages", [])
# 分离系统消息和对话消息
system_messages = []
conversation_messages = []
for msg in messages:
if isinstance(msg, SystemMessage):
system_messages.append(msg)
else:
conversation_messages.append(msg)
# 如果没有系统消息,添加系统提示
if not system_messages and isinstance(new_system_prompt, SystemMessage):
system_messages = [new_system_prompt]
# 保留对话消息的最后5轮(每轮包含用户消息和AI回复)
max_turns = 10
limited_conversation = conversation_messages[-max_turns*2:] if len(conversation_messages) > max_turns*2 else conversation_messages
# 返回系统消息和限制后的对话消息的组合
return system_messages + limited_conversation
# 创建基础 agent 时使用 combined_state_modifier
agent_executor = create_react_agent(
llm_4o,
tools,
checkpointer=checkpointer,
state_modifier=combined_state_modifier,
)
def is_return_direct_tool(tool_name: str) -> bool:
return any([tool_name == t.name and t.return_direct for t in tools])
def is_json_str(value: str) -> bool:
try:
json.loads(value)
return True
except ValueError:
return False
def chat(
query: str,
config: Optional[RunnableConfig] = None,
session_id: str=None,
debug: bool = False,
) -> (Generator[tuple[str, any], None, None]):
logger.info(f"agent query: {query}")
assert session_id is not None, "session_id is required"
common_info = {
"user_id": get_user_id() or -1,
"session_id": session_id,
"sub_session_id": str(uuid.uuid4()).replace('-', ''),
}
model = config.get("model") if config else None
with Session() as session:
Message.create(session, **common_info, source=MessageSource.USER.value, content=query, attributes={"model": model})
if config and config.get("images"):
images = config["images"]
content = [{"type": "text", "text": query}]
for image in images:
content.append({
"type": "image_url",
"image_url": {"url": image.data}
})
agent_input = {"messages": [HumanMessage(content=content)]}
else:
agent_input = {"messages": [HumanMessage(content=query)]}
# 检查自定义 OpenAI 凭据
custom_api_key = config.get("openai_api_key") if config else None
custom_base_url = config.get("openai_base_url") if config else None
# 创建 LLM 映射
if custom_api_key:
# 使用客户端管理器获取客户端
custom_llm_mapping = {
"gpt-4o": get_openai_client("gpt-4o", custom_api_key, custom_base_url),
# "gpt-4o-mini": get_openai_client("gpt-4o-mini", custom_api_key, custom_base_url),
}
# 对于其他模型,使用默认实例
custom_llm_mapping.update({
"DeepSeek-V3": llm_deepseek,
"qwen-plus": llm_dashscope
})
else:
custom_llm_mapping = llm_mapping
# 根据 config 中的 model 参数选择 LLM
if config and config.get("model") and config["model"] in custom_llm_mapping:
selected_llm = custom_llm_mapping[config["model"]]
agent = create_react_agent(selected_llm, tools, checkpointer=checkpointer, state_modifier=combined_state_modifier)
else:
agent = agent_executor
agent.step_timeout = 60 # 将单步超时时间增加到60秒
try:
agent_response = agent.stream(agent_input, config, stream_mode="messages")
current_text = ''
# data = None
ext = None
tool_results = {} # 存储不同工具的结果
workflow_tools_called = set() # 跟踪调用的工作流工具
for chunk, _ in agent_response:
content = chunk.content
chunk_type = chunk.type
if not content or not chunk_type:
continue
if chunk_type == 'tool':
tool_name = chunk.name
attributes = {
"tool_name": tool_name,
"model": model,
}
with Session() as session:
Message.create(session, **common_info, source=MessageSource.TOOL.value, content=content, attributes=attributes)
if is_json_str(content):
content_dict = json.loads(content)
answer = content_dict.get("answer")
data = content_dict.get("data")
tool_ext = content_dict.get("ext")
# 存储工具结果
tool_results[tool_name] = {
"answer": answer,
"data": data,
"ext": tool_ext,
"content_dict": content_dict
}
# 跟踪工作流相关工具的调用
if tool_name in ["recall_workflow", "gen_workflow"]:
workflow_tools_called.add(tool_name)
else:
answer = content
data = None
tool_ext = None
tool_results[tool_name] = {
"answer": answer,
"data": data,
"ext": tool_ext,
"content_dict": None
}
# 跟踪工作流相关工具的调用
if tool_name in ["recall_workflow", "gen_workflow"]:
workflow_tools_called.add(tool_name)
if is_return_direct_tool(chunk.name):
logger.info(f"agent response answer: {answer}")
if tool_ext:
logger.info(f"agent response ext: {tool_ext}")
yield answer, tool_ext
return
elif chunk_type == 'AIMessageChunk':
current_text += content
if not debug:
yield current_text, None
# 处理工作流工具的结果合并
workflow_tools_found = [tool for tool in ["recall_workflow", "gen_workflow"] if tool in tool_results]
if workflow_tools_found:
logger.info(f"Workflow tools called: {workflow_tools_found}")
# 检查是否同时调用了两个工作流工具
if "recall_workflow" in tool_results and "gen_workflow" in tool_results:
logger.info("Both recall_workflow and gen_workflow were called, merging results")
# 检查每个工具是否成功执行
successful_workflows = []
recall_result = tool_results["recall_workflow"]
if recall_result["data"] and len(recall_result["data"]) > 0:
successful_workflows.extend(recall_result["data"])
logger.info(f"recall_workflow succeeded with {len(recall_result['data'])} workflows")
else:
logger.info("recall_workflow failed or returned no data")
gen_result = tool_results["gen_workflow"]
if gen_result["data"] and len(gen_result["data"]) > 0:
successful_workflows.extend(gen_result["data"])
logger.info(f"gen_workflow succeeded with {len(gen_result['data'])} workflows")
else:
logger.info("gen_workflow failed or returned no data")
# 创建最终的ext
if successful_workflows:
ext = [{
"type": "workflow",
"data": successful_workflows
}]
logger.info(f"Returning {len(successful_workflows)} workflows from successful tools")
else:
ext = None
logger.info("No successful workflow data to return")
elif "recall_workflow" in tool_results and "gen_workflow" not in tool_results:
# 只调用了recall_workflow,不返回ext,保持finished=false
logger.info("Only recall_workflow was called, waiting for gen_workflow, not returning ext")
ext = None
elif "gen_workflow" in tool_results and "recall_workflow" not in tool_results:
# 只调用了gen_workflow的情况,正常返回结果
logger.info("Only gen_workflow was called, returning its result")
gen_result = tool_results["gen_workflow"]
if gen_result["data"] and len(gen_result["data"]) > 0:
ext = [{
"type": "workflow",
"data": gen_result["data"]
}]
logger.info(f"Returning {len(gen_result['data'])} workflows from gen_workflow")
else:
ext = None
logger.info("gen_workflow failed or returned no data")
else:
# 没有调用工作流工具,检查是否有其他工具返回了ext
for tool_name, result in tool_results.items():
if result["ext"]:
ext = result["ext"]
logger.info(f"Using ext from {tool_name}")
break
with Session() as session:
filtered_images = []
if config and config.get("images"):
filtered_images = [{k: v for k, v in img.dict().items() if k != 'data'} for img in config["images"]] if config["images"] else []
Message.create(session, **common_info, source=MessageSource.AI.value, content=current_text, attributes={"ext": ext, "images": filtered_images, "model": model})
logger.info(f"agent response answer: {current_text}")
if ext:
logger.info(f"agent response ext: {ext}")
yield current_text, ext
except TimeoutError:
logger.error("Agent execution timed out", exc_info=True)
yield "I apologize, but the request timed out. Please try again or rephrase your question.", None
return
except Exception as e:
logger.error(f"Agent execution failed: {str(e)}", exc_info=True)
yield f"I apologize, but an error occurred: {str(e)}", None
return
接口调用侧代码如下:
router = APIRouter()
# conversations: dict[str, any] = {}
@router.post('/invoke')
def invoke_post(request: ChatRequest, req: Request):
return _handle_invoke(request, req)
@router.get('/invoke')
def invoke_get(req: Request):
request = ChatRequest(**req.query_params)
return _handle_invoke(request, req)
def _handle_invoke(request: ChatRequest, req: Request):
# Extract headers
accept_language = req.headers.get('Accept-Language')
openai_base_url = req.headers.get('Openai-Base-Url')
encrypted_api_key = req.headers.get('Encrypted-Openai-Api-Key')
model = req.headers.get('Model')
user_type = getattr(req.state, 'user_type', None)
if user_type == "new":
def new_user_response_generator():
if accept_language == "zh-CN":
text = """
您好!我是ComfyUI-Copilot,您的AI工作流助手。我可以帮您快速上手ComfyUI、提高工作流调优效率,包括:
- 创建ComfyUI工作流、提供工作流模板和示例;
- 提供节点使用建议和最佳实践
- 解答ComfyUI相关问题
- 帮助调试和优化现有工作流
- 查询相关模型信息等
请告诉我你需要什么帮助,我会尽我所能协助你完成ComfyUI相关任务。
第一次访问我们的服务请访问[链接](https://form.typeform.com/to/tkg91K8D)完成Key申请(免费),并点击⚙️配置Key。
"""
else:
text = """Hello! I'm ComfyUI-Copilot, your AI workflow assistant. I can help you quickly get started with ComfyUI and improve your workflow optimization efficiency, including:
- Creating ComfyUI workflows and providing workflow templates and examples
- Offering node usage recommendations and best practices
- Answering ComfyUI-related questions
- Helping debug and optimize existing workflows
- Querying relevant model information
Please let me know what assistance you need, and I'll do my best to help you with your ComfyUI-related tasks.
For first-time visitors, please visit [link](https://form.typeform.com/to/tkg91K8D) to complete your Key application (free), and click ⚙️ to configure your Key.
"""
response = ChatResponse(
session_id=request.session_id,
text=text,
format="markdown",
finished=True
)
yield response.model_dump_json()
yield '\n'
return StreamingResponse(new_user_response_generator())
if user_type == "unregister":
def unregister_response_generator():
if accept_language == "zh-CN":
text = "ComfyUI-Copilot服务升级,请访问[链接](https://form.typeform.com/to/tkg91K8D)输入邮箱完成Key申请(免费),并点击⚙️配置Key。若原先已有Key,需要修改成邮箱新收到的Key。"
else:
text = "ComfyUI-Copilot service upgrade, please visit [link](https://form.typeform.com/to/tkg91K8D) to input your email and complete your Key application (free), and click 🔨 to configure your Key. If you already have a Key, you need to modify it to the new Key."
response = ChatResponse(
session_id=request.session_id,
text=text,
format="markdown",
finished=True
)
yield response.model_dump_json()
yield '\n'
return StreamingResponse(unregister_response_generator())
# Build config
config = {}
if accept_language:
config["language"] = accept_language
if openai_base_url:
config["openai_base_url"] = openai_base_url
# Handle encrypted API key if provided
if encrypted_api_key:
try:
openai_api_key = crypto_util.decrypt_with_private_key(encrypted_api_key)
logger.info("Successfully decrypted OpenAI API key")
config["openai_api_key"] = openai_api_key
except Exception as e:
logger.error(f"Failed to decrypt OpenAI API key: {str(e)}")
# Continue with potentially None openai_api_key
if model:
config["model"] = model
return do_invoke(request, config)