feat: single-agent all-in-one

This commit is contained in:
ai-business-hql
2025-10-29 17:42:01 +08:00
parent 00f4c44e98
commit b1b252743c
12 changed files with 1238 additions and 469 deletions
+302 -453
View File
@@ -5,10 +5,7 @@ import json
import asyncio
import time
from typing import Optional, TypedDict, List, Union
import threading
from collections import defaultdict
from sqlalchemy.orm import identity
from ..utils.globals import set_language
from ..utils.auth_utils import extract_and_store_api_key
@@ -20,18 +17,15 @@ import base64
import os
import shutil
from ..service.debug_agent import debug_workflow_errors
from ..service.debug_agent import debug_workflow_errors, debug_workflow_errors_single
from ..dao.workflow_table import save_workflow_data, get_workflow_data_by_id, update_workflow_ui_by_id
from ..service.mcp_client import comfyui_agent_invoke
from ..service.all_in_one_agent import all_in_one_invoke
from ..utils.request_context import set_request_context, get_session_id
from ..utils.logger import log
from ..utils.modelscope_gateway import ModelScopeGateway
import folder_paths
# 全局下载进度存储
download_progress = {}
download_lock = threading.Lock()
# 不再使用内存存储会话消息,改为从前端传递历史消息
# 在文件开头添加
@@ -69,98 +63,6 @@ class ChatResponse(TypedDict):
format: str # 返回的格式
ext: Optional[List[ExtItem]] # 扩展信息
# 下载进度回调类
class DownloadProgressCallback:
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
self.id = id
self.filename = filename
self.file_size = file_size
self.download_id = download_id
self.progress = 0
self.status = "downloading" # downloading, completed, failed
self.error_message = None
self.start_time = time.time()
# 初始化进度记录
with download_lock:
download_progress[download_id] = {
"id": id,
"filename": filename,
"file_size": file_size,
"progress": 0,
"percentage": 0.0,
"status": "downloading",
"start_time": self.start_time,
"estimated_time": None,
"speed": 0.0,
"error_message": None
}
def update(self, size: int):
"""更新下载进度"""
self.progress += size
current_time = time.time()
elapsed_time = current_time - self.start_time
# 计算下载速度和预估时间
if elapsed_time > 0:
speed = self.progress / elapsed_time # bytes per second
if speed > 0 and self.progress < self.file_size:
remaining_bytes = self.file_size - self.progress
estimated_time = remaining_bytes / speed
else:
estimated_time = None
else:
speed = 0.0
estimated_time = None
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
# 更新全局进度
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["progress"] = self.progress
progress_dict["percentage"] = round(percentage, 2)
progress_dict["speed"] = round(speed, 2)
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
def end(self, success: bool = True, error_message: str = None):
"""下载结束回调"""
current_time = time.time()
total_time = current_time - self.start_time
if success:
self.status = "completed"
# 验证下载完整性
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
else:
self.status = "failed"
self.error_message = error_message
# 更新最终状态
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["status"] = self.status
progress_dict["progress"] = self.file_size if success else self.progress
if self.file_size > 0:
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
else:
progress_dict["percentage"] = 0.0
progress_dict["total_time"] = round(total_time, 2)
progress_dict["error_message"] = self.error_message
def fail(self, error_message: str):
"""下载失败回调"""
self.end(success=False, error_message=error_message)
# 生成唯一下载ID
def generate_download_id() -> str:
import uuid
return str(uuid.uuid4())
async def upload_to_oss(file_data: bytes, filename: str) -> str:
# TODO: Implement your OSS upload logic here
@@ -225,9 +127,7 @@ async def invoke_chat(request):
await response.prepare(request)
session_id = req_json.get('session_id')
prompt = req_json.get('prompt')
images = req_json.get('images', [])
intent = req_json.get('intent')
ext = req_json.get('ext')
historical_messages = req_json.get('messages', [])
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
@@ -279,7 +179,6 @@ async def invoke_chat(request):
accumulated_text = ""
ext_data = None
finished = True # Default to True
has_sent_response = False
previous_text_length = 0
log.info(f"config: {config}")
@@ -353,6 +252,306 @@ async def invoke_chat(request):
return response
@server.PromptServer.instance.routes.post("/api/chat-single/invoke")
async def invoke_chat_single(request):
log.info("Received invoke_chat_single request")
# Extract and store API key from Authorization header
extract_and_store_api_key(request)
req_json = await request.json()
log.info("Request JSON:", req_json)
response = web.StreamResponse(
status=200,
reason='OK',
headers={
'Content-Type': 'application/json',
'X-Content-Type-Options': 'nosniff'
}
)
await response.prepare(request)
session_id = req_json.get('session_id')
images = req_json.get('images', [])
ext = req_json.get('ext')
historical_messages = req_json.get('messages', [])
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
workflow_data = req_json.get('workflow_data') # Support debug-style payloads
# Language
language = request.headers.get('Accept-Language', 'en')
set_language(language)
# Config
config = {
"session_id": session_id,
"workflow_checkpoint_id": workflow_checkpoint_id,
"openai_api_key": request.headers.get('Openai-Api-Key'),
"openai_base_url": request.headers.get('Openai-Base-Url'),
# Optional Workflow LLM settings
"workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'),
"workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'),
"workflow_llm_model": request.headers.get('Workflow-LLM-Model'),
"model_select": next((x['data'][0] for x in ext if x['type'] == 'model_select' and x.get('data')), None) if ext else None
}
# Set request context (isolation per session)
set_request_context(session_id, workflow_checkpoint_id, config)
# If workflow_data provided (debug scenario), save it so tools like run_workflow/get_current_workflow can access
try:
if workflow_data:
version_id = save_workflow_data(
session_id=session_id,
workflow_data=workflow_data,
workflow_data_ui=None,
attributes={
"checkpoint_type": "debug_start_single",
"description": "Initial workflow save for single-agent invoke",
"timestamp": time.time()
}
)
log.info(f"[Single] Saved incoming workflow_data, version ID: {version_id}")
except Exception as save_err:
log.error(f"[Single] Failed to save workflow_data: {save_err}")
processed_images = []
if images and len(images) > 0:
log.info(f"Note: Received {len(images)} images in legacy format, but using OpenAI message format instead")
# Ensure messages exist and start with a user message (required by agents streaming)
openai_messages = historical_messages if isinstance(historical_messages, list) else []
needs_user_head = (len(openai_messages) == 0) or (openai_messages[0].get('role') != 'user')
if needs_user_head:
# If workflow_data present, default to a debug instruction; otherwise a generic chat starter
default_user_text = "Validate and debug this ComfyUI workflow." if workflow_data else "Start the conversation."
openai_messages = [{"role": "user", "content": default_user_text}] + openai_messages
try:
accumulated_text = ""
ext_data = None
finished = True
previous_text_length = 0
log.info(f"[Single] config: {config}")
async for result in all_in_one_invoke(openai_messages, None):
if isinstance(result, tuple) and len(result) == 2:
text, ext_with_finished = result
if text:
accumulated_text = text
if ext_with_finished:
ext_data = ext_with_finished.get("data")
finished = ext_with_finished.get("finished", True)
log.info(f"[Single] -- Received ext: {bool(ext_data)}, finished: {finished}")
else:
text_chunk = result
if text_chunk:
accumulated_text += text_chunk
log.info(f"[Single] -- Received text chunk: '{text_chunk}', total length: {len(accumulated_text)}")
if accumulated_text and len(accumulated_text) > previous_text_length:
chat_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=False,
type="message",
format="markdown",
ext=None
)
await response.write(json.dumps(chat_response).encode() + b"\n")
previous_text_length = len(accumulated_text)
await asyncio.sleep(0.01)
final_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=finished,
type="message",
format="markdown",
ext=ext_data
)
await response.write(json.dumps(final_response).encode() + b"\n")
except Exception as e:
log.error(f"Error in invoke_chat_single: {str(e)}")
error_response = ChatResponse(
session_id=session_id,
text=f"I apologize, but an error occurred: {str(e)}",
finished=True,
type="message",
format="text",
ext=None
)
await response.write(json.dumps(error_response).encode() + b"\n")
await response.write_eof()
return response
@server.PromptServer.instance.routes.post("/api/debug-agent-single")
async def invoke_debug_single(request):
"""
Single-agent Debug endpoint for analyzing ComfyUI workflow errors
"""
log.info("Received debug-agent-single request")
extract_and_store_api_key(request)
req_json = await request.json()
response = web.StreamResponse(
status=200,
reason='OK',
headers={
'Content-Type': 'application/json',
'X-Content-Type-Options': 'nosniff'
}
)
await response.prepare(request)
session_id = req_json.get('session_id')
workflow_data = req_json.get('workflow_data')
config = {
"session_id": session_id,
"model": "gemini-2.5-flash",
"openai_api_key": request.headers.get('Openai-Api-Key'),
"openai_base_url": request.headers.get('Openai-Base-Url'),
"workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'),
"workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'),
"workflow_llm_model": request.headers.get('Workflow-LLM-Model'),
}
language = request.headers.get('Accept-Language', 'en')
set_language(language)
set_request_context(session_id, None, config)
log.info(f"Single Debug agent config: {config}")
log.info(f"Session ID: {session_id}")
log.info(f"Workflow nodes: {list(workflow_data.keys()) if workflow_data else 'None'}")
try:
accumulated_text = ""
final_ext_data = None
finished = False
async for result in debug_workflow_errors_single(workflow_data):
if isinstance(result, tuple) and len(result) == 2:
text, ext = result
if text:
accumulated_text = text
if ext:
if isinstance(ext, dict) and "data" in ext and "finished" in ext:
final_ext_data = ext["data"]
finished = ext["finished"]
has_realtime_ext = False
if final_ext_data:
for ext_item in final_ext_data:
if ext_item.get("type") in ["workflow_update", "param_update"]:
has_realtime_ext = True
break
if has_realtime_ext or finished:
chat_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=finished,
type="message",
format="markdown",
ext=final_ext_data
)
await response.write(json.dumps(chat_response).encode() + b"\n")
elif not finished:
chat_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=False,
type="message",
format="markdown",
ext=None
)
await response.write(json.dumps(chat_response).encode() + b"\n")
else:
final_ext_data = ext
finished = False
chat_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=False,
type="message",
format="markdown",
ext=ext
)
await response.write(json.dumps(chat_response).encode() + b"\n")
else:
chat_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=False,
type="message",
format="markdown",
ext=None
)
await response.write(json.dumps(chat_response).encode() + b"\n")
await asyncio.sleep(0.01)
final_response = ChatResponse(
session_id=session_id,
text=accumulated_text,
finished=True,
type="message",
format="markdown",
ext=final_ext_data if final_ext_data else [{"type": "debug_complete", "data": {"status": "completed"}}]
)
if workflow_data and accumulated_text:
try:
current_session_id = get_session_id()
checkpoint_id = save_workflow_data(
session_id=current_session_id,
workflow_data=workflow_data,
workflow_data_ui=None,
attributes={
"checkpoint_type": "debug_complete",
"description": "Workflow state after single-agent debug completion",
"timestamp": time.time()
}
)
if final_response["ext"]:
final_response["ext"].append({
"type": "debug_checkpoint",
"data": {"checkpoint_id": checkpoint_id, "checkpoint_type": "debug_complete"}
})
else:
final_response["ext"] = [{
"type": "debug_checkpoint",
"data": {"checkpoint_id": checkpoint_id, "checkpoint_type": "debug_complete"}
}]
log.info(f"Single Debug completion checkpoint saved with ID: {checkpoint_id}")
except Exception as checkpoint_error:
log.error(f"Failed to save single debug completion checkpoint: {checkpoint_error}")
await response.write(json.dumps(final_response).encode() + b"\n")
log.info("Single Debug agent processing complete")
except Exception as e:
log.error(f"Error in single debug agent: {str(e)}")
import traceback
traceback.print_exc()
error_response = ChatResponse(
session_id=session_id,
text=f"❌ Single Debug agent error: {str(e)}",
finished=True,
type="message",
format="text",
ext=[{"type": "error", "data": {"error": str(e)}}]
)
await response.write(json.dumps(error_response).encode() + b"\n")
await response.write_eof()
return response
@server.PromptServer.instance.routes.post("/api/save-workflow-checkpoint")
async def save_workflow_checkpoint(request):
"""
@@ -723,354 +922,4 @@ async def update_workflow_ui(request):
return web.json_response({
"success": False,
"message": f"Failed to update workflow UI data: {str(e)}"
})
@server.PromptServer.instance.routes.post("/api/download-model")
async def download_model(request):
"""
Download model from ModelScope using SDK
"""
log.info("Received download-model request")
req_json = await request.json()
try:
id = req_json.get('id')
model_id = req_json.get('model_id')
model_type = req_json.get('model_type')
dest_dir = req_json.get('dest_dir')
# 验证必需参数
if not id:
return web.json_response({
"success": False,
"message": "Missing required parameter: id"
})
if not model_id:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_id"
})
if not model_type:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_type"
})
log.info(f"Downloading model: {model_id} (type: {model_type})")
# 生成下载ID
download_id = generate_download_id()
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
resolved_dest_dir = None
if dest_dir:
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
else:
try:
model_type_paths = folder_paths.get_folder_paths(model_type)
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type)
except Exception:
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
# 创建进度回调
progress_callback = DownloadProgressCallback(
id=id,
filename=f"{model_id}.{model_type}",
file_size=0, # 实际大小会在下载过程中获取
download_id=download_id
)
# 启动下载任务(异步执行)
async def download_task():
try:
# 调用下载方法 - 使用snapshot_download
from modelscope import snapshot_download
# 创建进度回调包装器 - 实现ModelScope期望的接口
class ProgressWrapper:
"""Factory that returns a per-file progress object with update/end."""
def __init__(self, download_id: str):
self.download_id = download_id
def __call__(self, file_name: str, file_size: int):
# Create a per-file progress tracker expected by ModelScope
download_id = self.download_id
class _PerFileProgress:
def __init__(self, fname: str, fsize: int):
self.file_name = fname
self.file_size = max(int(fsize or 0), 0)
self.progress = 0
self.last_update_time = time.time()
self.last_downloaded = 0
with download_lock:
if download_id in download_progress:
# If unknown size, keep 0 to avoid div-by-zero
download_progress[download_id]["file_size"] = self.file_size
def update(self, size: int):
try:
self.progress += int(size or 0)
now = time.time()
# Update global progress
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
dp["progress"] = self.progress
if self.file_size > 0:
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
# speed
elapsed = max(now - self.last_update_time, 1e-6)
speed = (self.progress - self.last_downloaded) / elapsed
dp["speed"] = round(speed, 2)
self.last_update_time = now
self.last_downloaded = self.progress
except Exception as e:
log.error(f"Error in progress update: {e}")
def end(self):
# Called by modelscope when a file finishes
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
if self.file_size > 0:
dp["progress"] = self.file_size
dp["percentage"] = 100.0
return _PerFileProgress(file_name, file_size)
progress_wrapper = ProgressWrapper(download_id)
# 添加调试日志
log.info(f"Starting download with progress wrapper: {download_id}")
# 在线程中执行阻塞的下载,避免阻塞事件循环
from functools import partial
local_dir = await asyncio.to_thread(
partial(
snapshot_download,
model_id=model_id,
cache_dir=resolved_dest_dir,
progress_callbacks=[progress_wrapper]
)
)
# 下载完成
progress_callback.end(success=True)
log.info(f"Model downloaded successfully to: {local_dir}")
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
try:
moved_count = 0
allowed_exts = {
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
}
for root, dirs, files in os.walk(local_dir):
for name in files:
ext = os.path.splitext(name)[1].lower()
if ext in allowed_exts:
src_path = os.path.join(root, name)
target_dir = resolved_dest_dir
os.makedirs(target_dir, exist_ok=True)
target_path = os.path.join(target_dir, name)
# 如果已经在目标目录则跳过
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
continue
# 处理重名情况:自动追加 _1, _2 ...
if os.path.exists(target_path):
base, ext_real = os.path.splitext(name)
idx = 1
while True:
candidate = f"{base}_{idx}{ext_real}"
candidate_path = os.path.join(target_dir, candidate)
if not os.path.exists(candidate_path):
target_path = candidate_path
break
idx += 1
shutil.move(src_path, target_path)
moved_count += 1
log.info(f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
except Exception as move_err:
log.error(f"Post-download move failed: {move_err}")
except Exception as e:
progress_callback.fail(str(e))
log.error(f"Download failed: {str(e)}")
# 启动异步下载任务
asyncio.create_task(download_task())
return web.json_response({
"success": True,
"data": {
"download_id": download_id,
"id": id,
"model_id": model_id,
"model_type": model_type,
"dest_dir": resolved_dest_dir,
"status": "started"
},
"message": f"Download started for model '{model_id}'"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error starting download: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Failed to start download: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
async def get_download_progress(request):
"""
Get download progress by download ID
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
progress_info = download_progress.get(download_id)
if not progress_info:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
return web.json_response({
"success": True,
"data": progress_info,
"message": "Download progress retrieved successfully"
})
@server.PromptServer.instance.routes.get("/api/download-progress")
async def list_downloads(request):
"""
List all active downloads
"""
with download_lock:
downloads = list(download_progress.keys())
return web.json_response({
"success": True,
"data": {
"downloads": downloads,
"count": len(downloads)
},
"message": "Download list retrieved successfully"
})
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
async def clear_download_progress(request):
"""
Clear download progress record (for cleanup)
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
if download_id in download_progress:
del download_progress[download_id]
return web.json_response({
"success": True,
"message": f"Download progress {download_id} cleared successfully"
})
else:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
@server.PromptServer.instance.routes.get("/api/model-searchs")
async def model_suggests(request):
"""
Get model search list by keyword
"""
log.info("Received model-search request")
try:
keyword = request.query.get('keyword')
if not keyword:
return web.json_response({
"success": False,
"message": "Missing required parameter: keyword"
})
# 创建ModelScope网关实例
gateway = ModelScopeGateway()
suggests = gateway.search(name=keyword)
list = suggests["data"] if suggests.get("data") else []
return web.json_response({
"success": True,
"data": {
"searchs": list,
"total": len(list)
},
"message": f"Get searchs successfully"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error get model searchs: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model searchs failed: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/model-paths")
async def model_paths(request):
"""
Get model paths by type
"""
log.info("Received model-paths request")
try:
model_paths = list(folder_paths.folder_names_and_paths.keys())
return web.json_response({
"success": True,
"data": {
"paths": model_paths,
},
"message": f"Get paths successfully"
})
except Exception as e:
log.error(f"Error get model path: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model failed: {str(e)}"
})
+468
View File
@@ -0,0 +1,468 @@
import threading
import asyncio
import time
import server
from aiohttp import web
import os
import shutil
from ..utils.logger import log
from ..utils.modelscope_gateway import ModelScopeGateway
import folder_paths
# 全局下载进度存储
download_progress = {}
download_lock = threading.Lock()
# 下载进度回调类
class DownloadProgressCallback:
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
self.id = id
self.filename = filename
self.file_size = file_size
self.download_id = download_id
self.progress = 0
self.status = "downloading" # downloading, completed, failed
self.error_message = None
self.start_time = time.time()
# 初始化进度记录
with download_lock:
download_progress[download_id] = {
"id": id,
"filename": filename,
"file_size": file_size,
"progress": 0,
"percentage": 0.0,
"status": "downloading",
"start_time": self.start_time,
"estimated_time": None,
"speed": 0.0,
"error_message": None
}
def update(self, size: int):
"""更新下载进度"""
self.progress += size
current_time = time.time()
elapsed_time = current_time - self.start_time
# 计算下载速度和预估时间
if elapsed_time > 0:
speed = self.progress / elapsed_time # bytes per second
if speed > 0 and self.progress < self.file_size:
remaining_bytes = self.file_size - self.progress
estimated_time = remaining_bytes / speed
else:
estimated_time = None
else:
speed = 0.0
estimated_time = None
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
# 更新全局进度
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["progress"] = self.progress
progress_dict["percentage"] = round(percentage, 2)
progress_dict["speed"] = round(speed, 2)
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
def end(self, success: bool = True, error_message: str = None):
"""下载结束回调"""
current_time = time.time()
total_time = current_time - self.start_time
if success:
self.status = "completed"
# 验证下载完整性
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
else:
self.status = "failed"
self.error_message = error_message
# 更新最终状态
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["status"] = self.status
progress_dict["progress"] = self.file_size if success else self.progress
if self.file_size > 0:
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
else:
progress_dict["percentage"] = 0.0
progress_dict["total_time"] = round(total_time, 2)
progress_dict["error_message"] = self.error_message
def fail(self, error_message: str):
"""下载失败回调"""
self.end(success=False, error_message=error_message)
# 生成唯一下载ID
def generate_download_id() -> str:
import uuid
return str(uuid.uuid4())
@server.PromptServer.instance.routes.post("/api/download-model")
async def download_model(request):
"""
Download model from ModelScope using SDK
"""
log.info("Received download-model request")
req_json = await request.json()
try:
id = req_json.get('id')
model_id = req_json.get('model_id')
model_type = req_json.get('model_type')
dest_dir = req_json.get('dest_dir')
# 验证必需参数
if not id:
return web.json_response({
"success": False,
"message": "Missing required parameter: id"
})
if not model_id:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_id"
})
if not model_type:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_type"
})
log.info(f"Downloading model: {model_id} (type: {model_type})")
# 生成下载ID
download_id = generate_download_id()
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
resolved_dest_dir = None
if dest_dir:
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
else:
try:
model_type_paths = folder_paths.get_folder_paths(model_type)
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir,
model_type)
except Exception:
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
# 创建进度回调
progress_callback = DownloadProgressCallback(
id=id,
filename=f"{model_id}.{model_type}",
file_size=0, # 实际大小会在下载过程中获取
download_id=download_id
)
# 启动下载任务(异步执行)
async def download_task():
try:
# 调用下载方法 - 使用snapshot_download
from modelscope import snapshot_download
# 创建进度回调包装器 - 实现ModelScope期望的接口
class ProgressWrapper:
"""Factory that returns a per-file progress object with update/end."""
def __init__(self, download_id: str):
self.download_id = download_id
def __call__(self, file_name: str, file_size: int):
# Create a per-file progress tracker expected by ModelScope
download_id = self.download_id
class _PerFileProgress:
def __init__(self, fname: str, fsize: int):
self.file_name = fname
self.file_size = max(int(fsize or 0), 0)
self.progress = 0
self.last_update_time = time.time()
self.last_downloaded = 0
with download_lock:
if download_id in download_progress:
# If unknown size, keep 0 to avoid div-by-zero
download_progress[download_id]["file_size"] = self.file_size
def update(self, size: int):
try:
self.progress += int(size or 0)
now = time.time()
# Update global progress
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
dp["progress"] = self.progress
if self.file_size > 0:
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
# speed
elapsed = max(now - self.last_update_time, 1e-6)
speed = (self.progress - self.last_downloaded) / elapsed
dp["speed"] = round(speed, 2)
self.last_update_time = now
self.last_downloaded = self.progress
except Exception as e:
log.error(f"Error in progress update: {e}")
def end(self):
# Called by modelscope when a file finishes
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
if self.file_size > 0:
dp["progress"] = self.file_size
dp["percentage"] = 100.0
return _PerFileProgress(file_name, file_size)
progress_wrapper = ProgressWrapper(download_id)
# 添加调试日志
log.info(f"Starting download with progress wrapper: {download_id}")
# 在线程中执行阻塞的下载,避免阻塞事件循环
from functools import partial
local_dir = await asyncio.to_thread(
partial(
snapshot_download,
model_id=model_id,
cache_dir=resolved_dest_dir,
progress_callbacks=[progress_wrapper]
)
)
# 下载完成
progress_callback.end(success=True)
log.info(f"Model downloaded successfully to: {local_dir}")
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
try:
moved_count = 0
allowed_exts = {
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
}
for root, dirs, files in os.walk(local_dir):
for name in files:
ext = os.path.splitext(name)[1].lower()
if ext in allowed_exts:
src_path = os.path.join(root, name)
target_dir = resolved_dest_dir
os.makedirs(target_dir, exist_ok=True)
target_path = os.path.join(target_dir, name)
# 如果已经在目标目录则跳过
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
continue
# 处理重名情况:自动追加 _1, _2 ...
if os.path.exists(target_path):
base, ext_real = os.path.splitext(name)
idx = 1
while True:
candidate = f"{base}_{idx}{ext_real}"
candidate_path = os.path.join(target_dir, candidate)
if not os.path.exists(candidate_path):
target_path = candidate_path
break
idx += 1
shutil.move(src_path, target_path)
moved_count += 1
log.info(
f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
except Exception as move_err:
log.error(f"Post-download move failed: {move_err}")
except Exception as e:
progress_callback.fail(str(e))
log.error(f"Download failed: {str(e)}")
# 启动异步下载任务
asyncio.create_task(download_task())
return web.json_response({
"success": True,
"data": {
"download_id": download_id,
"id": id,
"model_id": model_id,
"model_type": model_type,
"dest_dir": resolved_dest_dir,
"status": "started"
},
"message": f"Download started for model '{model_id}'"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error starting download: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Failed to start download: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
async def get_download_progress(request):
"""
Get download progress by download ID
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
progress_info = download_progress.get(download_id)
if not progress_info:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
return web.json_response({
"success": True,
"data": progress_info,
"message": "Download progress retrieved successfully"
})
@server.PromptServer.instance.routes.get("/api/download-progress")
async def list_downloads(request):
"""
List all active downloads
"""
with download_lock:
downloads = list(download_progress.keys())
return web.json_response({
"success": True,
"data": {
"downloads": downloads,
"count": len(downloads)
},
"message": "Download list retrieved successfully"
})
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
async def clear_download_progress(request):
"""
Clear download progress record (for cleanup)
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
if download_id in download_progress:
del download_progress[download_id]
return web.json_response({
"success": True,
"message": f"Download progress {download_id} cleared successfully"
})
else:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
@server.PromptServer.instance.routes.get("/api/model-searchs")
async def model_suggests(request):
"""
Get model search list by keyword
"""
log.info("Received model-search request")
try:
keyword = request.query.get('keyword')
if not keyword:
return web.json_response({
"success": False,
"message": "Missing required parameter: keyword"
})
# 创建ModelScope网关实例
gateway = ModelScopeGateway()
suggests = gateway.search(name=keyword)
list = suggests["data"] if suggests.get("data") else []
return web.json_response({
"success": True,
"data": {
"searchs": list,
"total": len(list)
},
"message": f"Get searchs successfully"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error get model searchs: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model searchs failed: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/model-paths")
async def model_paths(request):
"""
Get model paths by type
"""
log.info("Received model-paths request")
try:
model_paths = list(folder_paths.folder_names_and_paths.keys())
return web.json_response({
"success": True,
"data": {
"paths": model_paths,
},
"message": f"Get paths successfully"
})
except Exception as e:
log.error(f"Error get model path: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model failed: {str(e)}"
})
+254
View File
@@ -0,0 +1,254 @@
'''
All-in-one Single Agent for Chat + Debug + Rewrite (streaming)
'''
from typing import List, Dict, Any, Optional
import asyncio
import traceback
from ..utils.globals import BACKEND_BASE_URL, get_comfyui_copilot_api_key, WORKFLOW_MODEL_NAME
from ..utils.logger import log
from ..utils.request_context import get_session_id, get_config
from ..utils.key_utils import workflow_config_adapt
# OpenAI Agents SDK imports
try:
from agents._config import set_default_openai_api
from agents.items import ItemHelpers
from agents.mcp import MCPServerSse
from agents.run import Runner
from agents.tracing import set_tracing_disabled
except Exception:
raise ImportError(
"Detected incorrect or missing 'agents' package while loading All-in-one agent. "
"Please install 'openai-agents'. Commands:\n"
" python -m pip uninstall -y agents gym tensorflow\n"
" python -m pip install -U openai-agents"
)
from openai.types.responses import ResponseTextDeltaEvent
# Import tools from existing modules
from .debug_agent import run_workflow, analyze_error_type, save_current_workflow
from .link_agent_tools import analyze_missing_connections, apply_connection_fixes
from .workflow_rewrite_tools import get_current_workflow, get_node_info, update_workflow, remove_node
from .parameter_tools import (
find_matching_parameter_value,
get_model_files,
suggest_model_download,
update_workflow_parameter,
)
from ..agent_factory import create_agent
async def all_in_one_invoke(messages: List[Dict[str, Any]], images: Optional[List[Any]] = None):
"""
Invoke a single agent that can handle chat, debug, and rewrite in one place.
Yields tuples of (accumulated_text, {"data": ext_or_None, "finished": bool}).
"""
try:
session_id = get_session_id()
config = get_config()
if not session_id:
raise ValueError("No session_id found in request context")
if not config:
raise ValueError("No config found in request context")
# Adapt config if needed (keys, endpoints, etc.)
config = workflow_config_adapt(config)
# Prepare MCP servers to expose external tools (recall_workflow, gen_workflow, searches, etc.)
mcp_server = MCPServerSse(
params={
"url": BACKEND_BASE_URL + "/mcp-server/mcp",
"timeout": 300.0,
"headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"}
},
cache_tools_list=True,
client_session_timeout_seconds=300.0
)
bing_server = MCPServerSse(
params={
"url": "https://mcp.api-inference.modelscope.net/8c9fe550938e4f/sse",
"timeout": 300.0,
"headers": {"X-Session-Id": session_id, "Authorization": f"Bearer {get_comfyui_copilot_api_key()}"}
},
cache_tools_list=True,
client_session_timeout_seconds=300.0
)
async with mcp_server, bing_server:
agent = create_agent(
name="ComfyUI-AllInOne",
model=WORKFLOW_MODEL_NAME,
instructions=(
"You are a single powerful assistant for ComfyUI.\n\n"
"- Respond in the user's language.\n"
"- For workflow generation requests, ALWAYS call recall_workflow first, then gen_workflow, and merge results.\n"
"- For debug intents (workflow failed, node error): validate with run_workflow(), analyze with analyze_error_type(), then use link/parameter/rewrite tools accordingly and re-validate.\n"
"- For rewrite intents (modify current canvas): use get_current_workflow(), get_node_info(), update_workflow(), remove_node(), and ensure connections/types are valid.\n"
"- Prefer concise streaming updates.\n"
),
# Bind ALL local tools
tools=[
# Debug/validation
run_workflow, analyze_error_type, save_current_workflow,
# Link/connectivity
analyze_missing_connections, apply_connection_fixes,
# Rewrite/structure
get_current_workflow, get_node_info, update_workflow, remove_node,
# Parameters/models
find_matching_parameter_value, get_model_files, suggest_model_download, update_workflow_parameter,
],
# Expose MCP tool servers for chat/search/workflow tools
mcp_servers=[mcp_server, bing_server],
config={
"max_tokens": 8192,
**config
}
)
# Stream the session
agent_input = messages
result = Runner.run_streamed(
agent,
input=agent_input,
max_turns=30,
)
current_text = ''
last_yield_length = 0
tool_results: Dict[str, Dict[str, Any]] = {}
tool_call_queue: List[str] = []
workflow_update_ext = None # collect workflow_update/param_update ext from tools
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
delta_text = event.data.delta
if delta_text:
current_text += delta_text
if len(current_text) > last_yield_length:
last_yield_length = len(current_text)
yield (current_text, None)
continue
if event.type == "run_item_stream_event":
if event.item.type == "tool_call_item":
tool_name = getattr(event.item.raw_item, 'name', 'unknown_tool')
tool_call_queue.append(tool_name)
log.info(f"[AllInOne] Tool called: {tool_name}")
continue
if event.item.type == "tool_call_output_item":
tool_output_data_str = str(event.item.output)
# Associate with FIFO queue
if tool_call_queue:
tool_name = tool_call_queue.pop(0)
else:
tool_name = 'unknown_tool'
# Try to parse output as JSON and capture ext
try:
import json
tool_output_data = json.loads(tool_output_data_str)
if "ext" in tool_output_data and tool_output_data["ext"]:
tool_ext_items = tool_output_data["ext"]
# capture any workflow_update/param_update; keep entire list
for ext_item in tool_ext_items:
if ext_item.get("type") in ["workflow_update", "param_update"]:
workflow_update_ext = tool_ext_items
log.info(f"[AllInOne] Captured workflow-related ext from tool '{tool_name}'")
break
if "text" in tool_output_data and tool_output_data.get('text'):
parsed_output = json.loads(tool_output_data['text'])
if isinstance(parsed_output, dict):
answer = parsed_output.get("answer")
data = parsed_output.get("data")
tool_ext = parsed_output.get("ext")
else:
answer = None
data = parsed_output if isinstance(parsed_output, list) else None
tool_ext = None
tool_results[tool_name] = {
"answer": answer,
"data": data,
"ext": tool_ext,
"content_dict": parsed_output
}
except Exception:
# Non-JSON output; store raw preview
tool_results[tool_name] = {
"answer": tool_output_data_str,
"data": None,
"ext": None,
"content_dict": None
}
continue
# Post-stream processing: merge workflow tool results if present
ext = None
finished = True
workflow_tools_found = [t for t in ["recall_workflow", "gen_workflow"] if t in tool_results]
if workflow_tools_found:
if "recall_workflow" in tool_results and "gen_workflow" in tool_results:
successful_workflows: List[Dict[str, Any]] = []
recall_result = tool_results["recall_workflow"]
gen_result = tool_results["gen_workflow"]
if recall_result.get("data"):
successful_workflows.extend(recall_result["data"])
if gen_result.get("data"):
# prioritize generated ones first
successful_workflows = list(gen_result["data"]) + successful_workflows
# de-duplicate by id
seen_ids = set()
unique_workflows = []
for wf in successful_workflows:
wf_id = wf.get('id')
if wf_id and wf_id not in seen_ids:
seen_ids.add(wf_id)
unique_workflows.append(wf)
elif not wf_id:
unique_workflows.append(wf)
if unique_workflows:
ext = [{"type": "workflow", "data": unique_workflows}]
elif "gen_workflow" in tool_results:
gen_result = tool_results["gen_workflow"]
if gen_result.get("data"):
ext = [{"type": "workflow", "data": gen_result["data"]}]
else:
# only recall_workflow was called; keep unfinished to encourage gen_workflow next time
ext = None
finished = False
# Include any workflow_update/param_update ext captured during tool calls
final_ext = ext
if workflow_update_ext:
if isinstance(workflow_update_ext, list):
final_ext = workflow_update_ext + (ext if ext else [])
else:
final_ext = [workflow_update_ext] + (ext if ext else [])
ext_with_finished = {"data": final_ext, "finished": finished}
yield (current_text, ext_with_finished)
except Exception as e:
log.error(f"Error in all_in_one_invoke: {str(e)}")
log.error(f"Traceback: {traceback.format_exc()}")
error_message = f"I apologize, but an error occurred while processing your request: {str(e)}"
yield (error_message, {"data": None, "finished": True})
'''
Author: ai-business-hql ai.bussiness.hql@gmail.com
Date: 2025-10-29 16:51:18
LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
LastEditTime: 2025-10-29 17:01:17
FilePath: /ComfyUI-Copilot/backend/service/all_in_one_agent.py
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
'''
+196
View File
@@ -1,6 +1,8 @@
'''
Debug Agent for ComfyUI Workflow Error Analysis
'''
import json
from typing import Dict, Any
from ..utils.key_utils import workflow_config_adapt
from ..agent_factory import create_agent
from agents.items import ItemHelpers
@@ -706,6 +708,200 @@ Start by validating the workflow to see its current state.""",
yield (error_message, ext_with_finished)
async def debug_workflow_errors_single(workflow_data: Dict[str, Any]):
"""
Analyze and debug workflow errors using a single-agent architecture.
The single agent has access to all tools used across the multi-agent system
and iteratively validates, analyzes, fixes, and re-validates until success or limits.
Args:
workflow_data: Current workflow data from app.graphToPrompt()
Yields:
tuple: (text, ext) where text is accumulated text and ext is structured data
"""
try:
session_id = get_session_id()
config = get_config()
config = workflow_config_adapt(config)
if not session_id:
session_id = str(uuid.uuid4())
# Save initial workflow snapshot
log.info(f"[SingleAgent] Saving workflow data for session {session_id}")
save_result = save_workflow_data(
session_id,
workflow_data,
attributes={"action": "debug_start_single", "description": "Initial workflow save for single-agent debugging"}
)
log.info(f"[SingleAgent] Workflow saved with version ID: {save_result}")
single_agent = create_agent(
name="ComfyUI-Debug-SingleAgent",
instructions=f"""
You are a single ComfyUI workflow debugging expert with access to all specialized tools.
Your iterative loop (up to 10 cycles):
1) Validate workflow using run_workflow()
2) If errors: analyze with analyze_error_type() and decide fix strategy
- Connection errors: use analyze_missing_connections() then apply_connection_fixes()
- Parameter errors: use find_matching_parameter_value(), get_model_files(), update_workflow_parameter()
• For model downloads required: use suggest_model_download() and STOP (final response)
- Structural issues: use get_current_workflow(), get_node_info(), update_workflow()
3) After any fix, immediately re-run run_workflow() to verify
4) Repeat until validation succeeds or loop limit reached
Guidelines:
- Be concise and stream updates during the process
- Prefer high-confidence fixes first; add nodes only when necessary
- Do not modify unrelated parameters
- Language: {get_language()}
""",
model=WORKFLOW_MODEL_NAME,
tools=[
# Coordinator tools
run_workflow, analyze_error_type, save_current_workflow,
# Link/structure tools
analyze_missing_connections, apply_connection_fixes, get_current_workflow, get_node_info, update_workflow,
# Parameter tools
find_matching_parameter_value, get_model_files, suggest_model_download, update_workflow_parameter,
],
config={
"max_tokens": 8192,
**config
}
)
messages = [{"role": "user", "content": "Validate and debug this ComfyUI workflow using the iterative loop until success."}]
log.info(f"[SingleAgent] Starting validation process for session {session_id}")
result = Runner.run_streamed(
single_agent,
input=messages,
max_turns=30,
)
current_text = ''
current_agent = "ComfyUI-Debug-SingleAgent"
last_yielded_length = 0
debug_events = []
workflow_update_ext = None
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
delta_text = event.data.delta
if delta_text:
current_text += delta_text
if len(current_text) > last_yielded_length:
last_yielded_length = len(current_text)
yield (current_text, None)
elif event.type == "run_item_stream_event":
item_updated = False
if event.item.type == "tool_call_item":
tool_name = getattr(event.item.raw_item, 'name', 'unknown_tool')
log.info(f"[SingleAgent] -- Tool called: {tool_name}")
current_text += f"\n\n⚙ *{current_agent} is using {tool_name}...*\n\n"
item_updated = True
debug_events.append({
"type": "tool_call",
"tool": tool_name,
"agent": current_agent,
"timestamp": len(current_text)
})
elif event.item.type == "tool_call_output_item":
output = str(event.item.output)
output_preview = output[:200] + "..." if len(output) > 200 else output
current_text += f"\n\n● *Tool execution completed*\n\n```\n{output_preview}\n```\n\n"
item_updated = True
try:
tool_output_json = json.loads(output)
if "ext" in tool_output_json and tool_output_json["ext"]:
for ext_item in tool_output_json["ext"]:
if ext_item.get("type") == "workflow_update" or ext_item.get("type") == "param_update":
workflow_update_ext = ext_item
log.info(f"[SingleAgent] -- Captured {ext_item.get('type')} ext, yielding immediately")
ext_with_finished = {"data": [ext_item], "finished": False}
yield (current_text, ext_with_finished)
break
except (json.JSONDecodeError, TypeError):
pass
debug_events.append({
"type": "tool_result",
"output_preview": output_preview,
"agent": current_agent,
"timestamp": len(current_text)
})
elif event.item.type == "message_output_item":
try:
message_content = ItemHelpers.text_message_output(event.item)
if message_content and message_content.strip():
if message_content not in current_text:
current_text += f"\n\n{message_content}\n\n"
item_updated = True
debug_events.append({
"type": "message_complete",
"content_length": len(message_content),
"agent": current_agent,
"timestamp": len(current_text)
})
except Exception as e:
log.error(f"[SingleAgent] Error processing message output: {str(e)}")
if item_updated:
last_yielded_length = len(current_text)
yield (current_text, None)
log.info("[SingleAgent] === Debug process complete ===")
debug_completion_checkpoint_id = None
try:
current_workflow = get_workflow_data(session_id)
if current_workflow:
debug_completion_checkpoint_id = save_workflow_data(
session_id,
current_workflow,
workflow_data_ui=None,
attributes={
"checkpoint_type": "debug_complete",
"description": "Workflow state after single-agent debug completion",
"action": "debug_complete_single",
"final_agent": current_agent
}
)
log.info(f"[SingleAgent] Debug completion checkpoint saved with ID: {debug_completion_checkpoint_id}")
except Exception as checkpoint_error:
log.error(f"[SingleAgent] Failed to save debug completion checkpoint: {checkpoint_error}")
debug_ext = [{
"type": "debug_complete",
"data": {
"status": "completed",
"final_agent": current_agent,
"events": debug_events,
"total_events": len(debug_events)
}
}]
if debug_completion_checkpoint_id:
debug_ext.append({
"type": "debug_checkpoint",
"data": {
"checkpoint_id": debug_completion_checkpoint_id,
"checkpoint_type": "debug_complete"
}
})
final_ext = debug_ext
if workflow_update_ext:
final_ext = [workflow_update_ext] + debug_ext
log.info("[SingleAgent] -- Including workflow_update ext in final response")
ext_with_finished = {"data": final_ext, "finished": True}
yield (current_text, ext_with_finished)
except Exception as e:
log.error(f"[SingleAgent] Error in debug_workflow_errors_single: {str(e)}")
error_message = f"\n\n× Error occurred during single-agent debugging: {str(e)}\n\n"
ext_with_finished = {"finished": True}
yield (error_message, ext_with_finished)
# Test function
async def test_debug():
"""Test the debug agent with a sample workflow"""
@@ -1,7 +1,7 @@
function getImportPath(filename) {
return `./${filename}`;
}
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CTj3qusy.js","copilot_web/vendor-markdown-B4Av9P7l.js","copilot_web/vendor-react-ByKzdfMq.js","copilot_web/message-components-CzGdQ9TQ.js","copilot_web/input.js","copilot_web/assets/input-UcJyCCDl.css","copilot_web/fonts.css"].map(path => {
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-DCaYfVyA.js","copilot_web/vendor-markdown-B4Av9P7l.js","copilot_web/vendor-react-ByKzdfMq.js","copilot_web/message-components-CDlGtWLl.js","copilot_web/input.js","copilot_web/assets/input-UcJyCCDl.css","copilot_web/fonts.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
if (apiBase) {
// 有 API base 时,使用完整路径
@@ -11,4 +11,4 @@ function getImportPath(filename) {
return `./${path}`;
}
}))))=>i.map(i=>d[i]);
import{_ as a}from"./input.js";import{j as o}from"./vendor-markdown-B4Av9P7l.js";import{r as t,R as i}from"./vendor-react-ByKzdfMq.js";import{C as n}from"./message-components-CzGdQ9TQ.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-CTj3qusy.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
import{_ as a}from"./input.js";import{j as o}from"./vendor-markdown-B4Av9P7l.js";import{r as t,R as i}from"./vendor-react-ByKzdfMq.js";import{C as n}from"./message-components-CDlGtWLl.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-DCaYfVyA.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
@@ -1,5 +1,5 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as s}from"./vendor-markdown-B4Av9P7l.js";import{r as i}from"./vendor-react-ByKzdfMq.js";import{u as A,p as E,R as D,o as m,W as f,a as b}from"./message-components-CzGdQ9TQ.js";function P({content:h,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:_}){const{dispatch:u,abortControllerRef:l}=A(),[d,w]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,v]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(h)}catch(e){return console.error("Failed to parse message content:",e),null}},[h]),x=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[r])||j,N=async()=>{if(d)return;const e=m(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
import{j as s}from"./vendor-markdown-B4Av9P7l.js";import{r as i}from"./vendor-react-ByKzdfMq.js";import{u as A,p as E,R as D,o as m,W as f,a as b}from"./message-components-CDlGtWLl.js";function P({content:h,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:_}){const{dispatch:u,abortControllerRef:l}=A(),[d,w]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,v]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(h)}catch(e){return console.error("Failed to parse message content:",e),null}},[h]),x=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[r])||j,N=async()=>{if(d)return;const e=m(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
`,ext:[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};y?.(t);try{f.trackEvent({event_type:"debug_trigger",message_type:"debug",message_id:e,data:{}}),await C(),u({type:"SET_LOADING",payload:!0}),await I(e)}finally{w(!1)}},C=async()=>{try{const e=await b.graphToPrompt(),t=localStorage.getItem("sessionId")||"";if(t&&e){const n=await f.saveWorkflowCheckpoint(t,e.output,e.workflow,"debug_start");if(v(n.version_id),console.log(`Saved debug start checkpoint: ${n.version_id}`),c&&r){const o=[...r.ext||[]].filter(g=>g.type!=="debug_start_checkpoint"&&!(g.type==="debug_checkpoint"&&g.data?.checkpoint_type==="debug_start"));o.push({type:"debug_start_checkpoint",data:{checkpoint_id:n.version_id,checkpoint_type:"debug_start"}});const p={id:_||m(),role:"ai",content:JSON.stringify({text:r.text,ext:o}),format:"debug_guide",name:"Assistant"};c(p)}}}catch(e){console.error("Failed to save checkpoint before debug:",e)}},I=async e=>{try{const t=await b.graphToPrompt();let n="",a=null;l&&(l.current=new AbortController);for await(const o of f.streamDebugAgent(t,l?.current?.signal||void 0))if(o.text){n=o.text,o.ext&&(a=o.ext);const p={id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};c?.(p)}c?.({id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!0,debugGuide:!0}),u({type:"SET_LOADING",payload:!1}),l.current=null}catch(t){console.error("Error calling debug agent:",t),S(!1);const a={id:e,role:"ai",content:JSON.stringify({text:`I encountered an error while analyzing your workflow: ${t.message||"Unknown error"}`,ext:[]}),format:"markdown",name:"Assistant",finished:!0};c?.(a),u({type:"SET_LOADING",payload:!1}),l.current=null}};return r?s.jsx(E,{name:k,children:s.jsxs("div",{className:"bg-gray-100 pt-4 px-4 pb-3 rounded-lg",children:[s.jsx("div",{className:"flex justify-between items-start",children:s.jsx("p",{className:"text-gray-700 text-sm flex-1",children:r.text})}),s.jsx("div",{className:"flex justify-end mt-4",children:s.jsx("button",{onClick:N,disabled:d,className:`debug-btn bg-debug-btn text-sm text-[#fff] rounded-lg px-4 py-2 ${d?"cursor-not-allowed":"cursor-pointer"}`,children:d?"Analyzing...":"Debug Errors"})}),s.jsx("div",{className:"flex justify-end mt-2",children:!!x&&s.jsx("div",{className:"ml-2 flex-shrink-0",children:s.jsx(D,{checkpointId:x,onRestore:()=>{console.log("Workflow restored from checkpoint")}})})})]})}):null}export{P as DebugGuide};
@@ -1,4 +1,4 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as e}from"./vendor-markdown-B4Av9P7l.js";import{b as u,c as w,p as g,G as k,R as j}from"./message-components-CzGdQ9TQ.js";import{r as p}from"./vendor-react-ByKzdfMq.js";import{B as v,M as N}from"./workflowChat-CTj3qusy.js";import"./input.js";/* empty css */import"./App-fluNqnYs.js";const y=n=>{const{title:a="",isWorkflowUpdate:f=!1,className:d="",children:c}=n,[i,r]=p.useState(!1);return e.jsx(v,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${d} ${i?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{r(!i)},children:i?e.jsx(u,{className:"text-gray-700"}):e.jsx(w,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function E({content:n,name:a="Assistant",avatar:f,format:d="markdown"}){const c=p.useRef(null),i=()=>{let r=null,l=!1,o,x=[];try{if(o=JSON.parse(n),o.ext){let t=o.ext.find(s=>s.type==="workflow_rewrite_checkpoint"||s.type==="debug_checkpoint"&&s.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(r=t.data.checkpoint_id,l=!0):(t=o.ext.find(s=>s.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(r=t.data.version_id,l=!0):(t=o.ext.find(s=>s.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(r=t.data.checkpoint_id,l=!1))),o.ext.find(s=>s.type==="workflow_update")&&(l=!0),x=o.ext.find(s=>s.type==="param_update")?.data?.model_suggest||[]}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=l?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-gray-900",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),m=l?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{ref:c,className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(y,{title:h,isWorkflowUpdate:l,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[d==="markdown"?e.jsx(k,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),x?.length>0&&e.jsx(N,{modelList:x,showPagination:!1}),e.jsx("div",{className:"flex justify-end mt-2",children:!!r&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(j,{checkpointId:r,onRestore:()=>{console.log(`Workflow restored from ${l?"workflow update":"debug"} checkpoint`)},title:l?`Restore to this version (Version ${r})`:`Restore checkpoint ${r}`})})})]})};return e.jsx(g,{name:a,children:i()})}export{E as DebugResult};
import{j as e}from"./vendor-markdown-B4Av9P7l.js";import{b as u,c as w,p as g,G as k,R as j}from"./message-components-CDlGtWLl.js";import{r as p}from"./vendor-react-ByKzdfMq.js";import{B as v,M as N}from"./workflowChat-DCaYfVyA.js";import"./input.js";/* empty css */import"./App-TDLVaCDl.js";const y=n=>{const{title:a="",isWorkflowUpdate:f=!1,className:d="",children:c}=n,[i,r]=p.useState(!1);return e.jsx(v,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${d} ${i?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{r(!i)},children:i?e.jsx(u,{className:"text-gray-700"}):e.jsx(w,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function E({content:n,name:a="Assistant",avatar:f,format:d="markdown"}){const c=p.useRef(null),i=()=>{let r=null,l=!1,o,x=[];try{if(o=JSON.parse(n),o.ext){let t=o.ext.find(s=>s.type==="workflow_rewrite_checkpoint"||s.type==="debug_checkpoint"&&s.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(r=t.data.checkpoint_id,l=!0):(t=o.ext.find(s=>s.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(r=t.data.version_id,l=!0):(t=o.ext.find(s=>s.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(r=t.data.checkpoint_id,l=!1))),o.ext.find(s=>s.type==="workflow_update")&&(l=!0),x=o.ext.find(s=>s.type==="param_update")?.data?.model_suggest||[]}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const h=l?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-gray-900",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),m=l?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{ref:c,className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(y,{title:h,isWorkflowUpdate:l,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[d==="markdown"?e.jsx(k,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),m]})}),x?.length>0&&e.jsx(N,{modelList:x,showPagination:!1}),e.jsx("div",{className:"flex justify-end mt-2",children:!!r&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(j,{checkpointId:r,onRestore:()=>{console.log(`Workflow restored from ${l?"workflow update":"debug"} checkpoint`)},title:l?`Restore to this version (Version ${r})`:`Restore checkpoint ${r}`})})})]})};return e.jsx(g,{name:a,children:i()})}export{E as DebugResult};
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -2,7 +2,7 @@
* @Author: ai-business-hql qingli.hql@alibaba-inc.com
* @Date: 2025-06-24 16:29:05
* @LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
* @LastEditTime: 2025-10-15 14:49:15
* @LastEditTime: 2025-10-29 15:35:51
* @FilePath: /comfyui_copilot/ui/src/apis/workflowChatApi.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
@@ -287,7 +287,8 @@ export namespace WorkflowChatAPI {
});
}
let chatUrl = `/api/chat/invoke`
// let chatUrl = `/api/chat/invoke`
let chatUrl = `/api/chat-single/invoke`
if(intent && intent !== '') {
chatUrl = `${BASE_URL}/api/chat/invoke`
} else {
@@ -707,7 +708,8 @@ export namespace WorkflowChatAPI {
});
}
const response = await fetch('/api/debug-agent', {
// const response = await fetch('/api/debug-agent-single', {
const response = await fetch('/api/chat-single/invoke', {
method: 'POST',
headers: headers,
body: JSON.stringify({