我需要参考另外一个工程的代码,来实现我当前的后端能力,当前的工程是个ComfyUI的插件,我在这里实现了一个mcp_client,来替代掉原来的workflowChatApi.ts里的`${BASE_URL}/api/chat/invoke`接口。 核心代码facade.py实现了agent-tools的能力: ```python 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, llm_volcengine, get_openai_client, llm_dashscope_deepseek_v3, llm_qwen3, llm_4_1 from agent.tools.workflow_recall_tool import recall_workflow, generate_workflow_step2 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, # 工作流召回 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, } pool = ConnectionPool(db_url, max_size=50, kwargs=connection_kwargs) checkpointer = PostgresSaver(pool) checkpointer.setup() # 修改 LLM 映射字典,使用正确的配置方式 llm_config: Dict = { "gpt-4o": { "client":llm_4_1, "image_enable":True, }, "gpt-4.1": { "client":llm_4_1, "image_enable":True, }, "gpt-4o-mini": { "client":llm_4o_mini, "image_enable":True, }, "DeepSeek-V3": { "client":llm_deepseek, "image_enable":False, }, "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('-', ''), } with Session() as session: Message.create(session, **common_info, source=MessageSource.USER.value, content=query) 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 recall_workflow_data = None 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, } 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") ext = content_dict.get("ext") # 检查是否是 recall_workflow 的第一步响应 step = content_dict.get("step") if tool_name == "recall_workflow" and step == 1: # 保存第一步数据以便后续处理 recall_workflow_data = { "instruction": content_dict.get("instruction"), "keywords": content_dict.get("keywords"), "workflows": data } else: answer = content data = None ext = None if is_return_direct_tool(chunk.name): logger.info(f"agent response answer: {answer}") if ext: logger.info(f"agent response ext: {ext}") yield answer, ext return elif chunk_type == 'AIMessageChunk': current_text += content if not debug: yield current_text, None # 处理完第一步后,如果有 recall_workflow 的数据,执行第二步 if recall_workflow_data: logger.info("Executing step 2 of recall_workflow") workflow_values = [] with Session() as session: # 获取原始工作流数据 if recall_workflow_data["workflows"]: workflow_ids = [w.get("id") for w in recall_workflow_data["workflows"]] workflow_dos = Workflow.query_workflow_by_ids(session, workflow_ids) workflow_values = [workflow_do.to_dict().get('workflow') for workflow_do in workflow_dos] # 执行步骤二:生成工作流 if workflow_values: try: step2_result = generate_workflow_step2( recall_workflow_data["instruction"], recall_workflow_data["keywords"], workflow_values ) # 将步骤二的结果添加到原有数据中 if recall_workflow_data["workflows"]: recall_workflow_data["workflows"].append(step2_result) else: recall_workflow_data["workflows"] = [step2_result] except Exception as e: logger.error(f"Error in step 2 of recall_workflow: {str(e)}", exc_info=True) # 如果步骤2执行过程中报错,就直接使用原始workflows # 创建新的ext数据 if ext and isinstance(ext, list): for item in ext: if item.get("type") == "workflow": item["data"] = recall_workflow_data["workflows"] # 返回完整结果 yield current_text, ext 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}) 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) if config and config.get("model") and config["model"] == "DeepSeek-V3": yield "I apologize, but our DeepSeek service provider is currently unstable. Please try using GPT-4 instead.", None else: 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 ``` 为了能跟理解原始的架构,展示一recall_workflow的代码: ```python from hashlib import md5 import json from typing import List, Dict, Any from langchain_core.tools import tool from pydantic import BaseModel, Field from agent.tools.tool_funcs.recall_workflow import query_workflows_from_db, generate_workflow_step2, summary from logger import logger class RecallWorkflowParamSchema(BaseModel): name: str = Field( description=''' Extract the core intent, key technical terms from user input. ''') description: str = Field( description=''' Concisely describe the user's core requirements, no extra words, including: 1. Performance requirements (e.g. low VRAM, fast generation) 2. Source preferences (e.g. official workflows) 3. Technical specifications and constraints 4. Model requirements ''') keywords: List[str] = Field( description=''' Extract all core keywords from user input, make keywords shorter. Do not include too generic keywords, like "workflow"|"model"|"apply"|"工作流"|"模型". ''') instruction: str = Field( description=''' Convert the user's question into a non-technical problem statement from a user-centric perspective, reply use English. It articulates the desired functionality or outcome the user wishes to achieve, often in layman terms, without specifying technical or implementation details. The instruction should: 1. State a concrete creative or computational goal. 2. Specify input requirements (e.g., "a photo of my cat"). 3. Define desired output characteristics (e.g., "watercolor painting style"). 4. Avoid implementation terms such as nodes, models, or parameters. 5. Be phrased as a natural language request rather than a technical specification. Example: Build a workflow that turns my portrait photos into vintage comic book art. It should preserve facial features accurately while applying bold ink outlines and halftone shading effects. ''') @tool(args_schema=RecallWorkflowParamSchema, return_direct=False) def recall_workflow(name: str, description: str, keywords: List[str], instruction: str) -> str: """ Recall_workflow Tool is designed to select the most suitable workflow from a pool of candidate workflows based on user requirements. Call this tool when the user's intent is to want a workflow or to generate an image with specific requirements. Respond in the language used by the user in their question. For each workflow, only one representative image will be returned to avoid excessive image data. Must add a respond to the last: "You can choose from the recommended workflows above, or let AI generate a personalized solution based on your input (approximately 20 seconds, please wait). If generation fails, the new solution will be automatically hidden." """ # keywords = ['LTX', 'Videos', '高质量'] logger.info(f"recall_workflow args: {name}| {description} | {keywords}") # 步骤一:召回工作流 workflows = query_workflows_from_db(f'{name} | {description}', keywords) answer = summary(workflows) # Format the response if workflows: workflow_list = [{ "type": "workflow", "data": workflows }] ext_items = workflow_list else: ext_items = None return json.dumps({ "answer": answer, "data": workflows, "ext": ext_items, "instruction": instruction, "keywords": keywords, "step": 1 }, ensure_ascii=False) # 原来的函数内容已经移到 tool_funcs/recall_workflow.py # 这里只保留对tool的定义和调用 # 导出函数以保持与facade.py的兼容性 __all__ = ['recall_workflow', 'generate_workflow_step2'] ``` 接口实现层如下: ```python import base64 from typing import List, Generator from agent import facade from agent.intent_call import intent_call from agent.tools.workflow_param_optimize2 import get_optimized_params from fastapi import APIRouter from starlette.requests import Request from starlette.responses import StreamingResponse import time from base.core import app from logger import logger from base.result import Result from model.chat import * from agent.facade import chat from utils import oss_util, crypto_util import requests import openai from dao.track_dao import Track from initializer.db_initializer import Session 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) def do_invoke(request: ChatRequest, params: dict = None) -> StreamingResponse: logger.info(f"chat request: {request.model_dump_json()}") session_id = request.session_id prompt = request.prompt if request.mock: return StreamingResponse(mock_response(session_id)) if not request.prompt or request.prompt.strip() == "": return StreamingResponse(iter([])) func_chat = chat config = { "configurable": { "thread_id": session_id, } } # Add language from Accept-Language header if available if params: config.update(params) if request.images and len(request.images) > 0: for image in request.images: image_data = image.data # Extract base64 data after the comma if it's a data URL if image_data.startswith('data:'): image_data = image_data.split(',')[1] image_data = image_data.encode('utf-8') image_data = base64.b64decode(image_data) image_url = oss_util.upload_data(image_data, image.filename) image.url = image_url config["images"] = request.images if request.ext and isinstance(request.ext, list): for ext_item in request.ext: if ext_item.type == "model_select" and len(ext_item.data) > 0: config["model"] = ext_item.data[0] if request.intent and request.intent != "": config = {"intent": request.intent, "ext": request.ext, "language": config.get("language", "zh-CN")} func_chat = intent_call def response_generator(): last_text = None ext = None for text, ext in func_chat(prompt, config, session_id=session_id): response = ChatResponse( session_id=session_id, text=text, finished=ext is not None ) # If we have ext information, convert it to list[ExtItem] if needed if ext: if isinstance(ext, dict): ext = [ExtItem(**ext)] elif isinstance(ext, list): ext = [item if isinstance(item, ExtItem) else ExtItem(**item) for item in ext] response.ext = ext last_text = text yield response.model_dump_json() yield '\n' # Send final response with finished=True if we haven't already if not ext: final_response = ChatResponse( session_id=session_id, text=last_text, finished=True ) yield final_response.model_dump_json() yield '\n' return StreamingResponse(response_generator()) ```