Files
yawiii-ComfyUI-Prompt-Assis…/node/expand_node.py
T
2026-04-25 10:38:46 +08:00

338 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
提示词增强节点 - V3 版本
V3 迁移说明:
- 继承 LLMNodeBase(工具基类 Mixin)+ io.ComfyNode(V3 节点基类)
- INPUT_TYPES → define_schema(),返回 io.Schema
- IS_CHANGED → fingerprint_inputs()
- def enhance(self, ...) → @classmethod execute(cls, ...)
- 返回 io.NodeOutput(val) 代替 (val,)
- hidden unique_id 通过 cls.hidden.unique_id 访问
- 不再导出 NODE_CLASS_MAPPINGS,由顶层 __init__.py 的 ComfyExtension 统一注册
"""
import hashlib
from comfy.model_management import InterruptProcessingException
from comfy_api.latest import io
from ..services.llm import LLMService
from ..utils.common import (
format_api_error, format_model_with_thinking, generate_request_id,
log_prepare, log_error, TASK_EXPAND, SOURCE_NODE
)
from ..services.thinking_control import build_thinking_suppression
from .base import LLMNodeBase
class PromptExpand(LLMNodeBase, io.ComfyNode):
"""
提示词增强节点(V3)
- 输入 "source_text",根据所选规则模板或自定义规则进行增强/扩写
- 仅包含一个字符串输入和一个字符串输出
"""
@classmethod
def define_schema(cls):
"""定义节点 Schema(V3 替代 INPUT_TYPES + 类属性)"""
# 从 config_manager 获取系统提示词配置
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
# 获取所有 expand_prompts 作为下拉选项
expand_prompts = {}
active_expand_id = None
if system_prompts:
expand_prompts = system_prompts.get('expand_prompts', {}) or {}
active_expand_id = system_prompts.get('active_prompts', {}).get('expand')
# 构建提示词模板选项(支持分类格式:类别/规则名称)
prompt_template_options = []
id_to_display_name = {}
for key, value in expand_prompts.items():
# 过滤掉不在后端显示的规则
show_in = value.get('showIn', ["frontend", "node"])
if 'node' not in show_in:
continue
name = value.get('name', key)
category = value.get('category', '')
display_name = f"{category}/{name}" if category else name
id_to_display_name[key] = display_name
prompt_template_options.append(display_name)
# 默认选项回退
default_template_name = prompt_template_options[0] if prompt_template_options else "扩写-自然语言"
if active_expand_id and active_expand_id in id_to_display_name:
default_template_name = id_to_display_name[active_expand_id]
if not prompt_template_options:
prompt_template_options = ["扩写-自然语言"]
# ---动态获取 LLM 服务/模型列表---
service_options = cls.get_llm_service_options()
default_service = service_options[0] if service_options else "智谱"
return io.Schema(
node_id="PromptExpand",
display_name="✨Prompt Enhance",
category="✨Prompt Assistant",
description="Enhance and expand prompts using LLM services",
inputs=[
# 规则模板:来自系统配置的所有扩写规则
io.Combo.Input(
"rule",
options=prompt_template_options,
default=default_template_name,
tooltip="Choose a preset rule for prompt enhancement",
),
# 临时规则开关
io.Boolean.Input(
"custom_rule",
default=False,
label_on="Enable",
label_off="Disable",
tooltip="Enable to use custom rule content below instead of preset",
),
# 临时规则内容输入框
io.String.Input(
"custom_rule_content",
multiline=True,
default="",
placeholder="Enter custom rule here, only effective when 'Custom Rule' is enabled",
tooltip="Enter your custom rule content here",
),
# 用户提示词
io.String.Input(
"user_prompt",
multiline=True,
default="",
placeholder="Enter the prompt to enhance; if source_text is also connected, both will be merged",
tooltip="The original prompt to enhance",
),
# 扩写服务
io.Combo.Input(
"llm_service",
options=service_options,
default=default_service,
tooltip="Select LLM service and model",
),
# Ollama 自动释放显存
io.Boolean.Input(
"ollama_auto_unload",
default=True,
label_on="Enable",
label_off="Disable",
tooltip="Auto unload Ollama model after generation",
),
io.Int.Input(
"seed",
default=0,
min=0,
max=0xffffffffffffffff,
control_after_generate=True,
),
# 原文输入端口(可选),默认为连接端口
io.String.Input(
"source_text",
optional=True,
multiline=True,
default="",
force_input=True,
placeholder="Input text to enhance...",
tooltip="Optional input text",
),
],
outputs=[
io.String.Output("enhanced_text"),
],
hidden=[io.Hidden.unique_id],
)
@classmethod
def fingerprint_inputs(
cls,
rule=None, custom_rule=None, custom_rule_content=None,
user_prompt=None, llm_service=None, ollama_auto_unload=None,
seed=None, source_text=None
):
"""
替代 V1 IS_CHANGED,只在输入内容真正变化时才触发重新执行
使用输入参数的哈希值作为判断依据
"""
text_hash = hashlib.md5(((source_text or "")).encode('utf-8')).hexdigest()
temp_rule_hash = hashlib.md5((custom_rule_content or "").encode('utf-8')).hexdigest()
user_hint_hash = hashlib.md5((user_prompt or "").encode('utf-8')).hexdigest()
input_hash = hash((
rule,
bool(custom_rule),
temp_rule_hash,
user_hint_hash,
llm_service,
bool(ollama_auto_unload),
seed,
text_hash,
))
return input_hash
@classmethod
def execute(
cls,
rule, custom_rule, custom_rule_content, user_prompt,
llm_service, ollama_auto_unload, seed=None, source_text=None
):
"""
增强/扩写文本函数(V3 classmethod 版本)
通过 cls.hidden.unique_id 访问节点唯一 ID
"""
# 从 cls.hidden 获取节点唯一 ID
unique_id = cls.hidden.unique_id
request_id = None
try:
# 允许原文为空,但原文与用户提示词至少有一项非空
source_text = (source_text or "").strip()
user_prompt = (user_prompt or "").strip()
if not source_text and not user_prompt:
return io.NodeOutput("")
# ---准备系统提示词(规则)---
system_message = None
rule_name = "Custom Rule" if (custom_rule and custom_rule_content) else rule
if custom_rule and custom_rule_content:
# 使用临时规则
system_message = {"role": "system", "content": custom_rule_content}
else:
# 使用模板:从 config_manager 获取系统提示词配置
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
expand_prompts = system_prompts.get('expand_prompts', {}) if system_prompts else {}
# 查找选定的提示词模板(按显示名称匹配)
template_found = False
for key, value in expand_prompts.items():
name = value.get('name', key)
category = value.get('category', '')
display_name = f"{category}/{name}" if category else name
if display_name == rule:
system_message = {"role": value.get('role', 'system'), "content": value.get('content', '')}
template_found = True
break
if not template_found:
# 允许用规则名称或键名直接匹配(兼容旧格式)
for key, value in expand_prompts.items():
if value.get('name') == rule or key == rule:
system_message = {"role": value.get('role', 'system'), "content": value.get('content', '')}
template_found = True
break
if not template_found or not system_message or not system_message.get('content'):
# 回退到默认
system_message = {"role": "system", "content": "你是一名提示词扩写专家,请将用户给定文本扩写为更完整、更具可读性和可执行性的提示词。"}
rule_name = "Default Rule"
# ---解析服务/模型字符串---
service_id, model_name = cls.parse_service_model(llm_service)
if not service_id:
raise ValueError(f"Invalid service selection: {llm_service}")
# ---获取服务配置---
from ..config_manager import config_manager
service = config_manager.get_service(service_id)
if not service:
raise ValueError(f"Service config not found: {llm_service}")
# ---构建 provider_config---
llm_models = service.get('llm_models', [])
target_model = None
if model_name:
target_model = next((m for m in llm_models if m.get('name') == model_name), None)
if not target_model:
target_model = next(
(m for m in llm_models if m.get('is_default')),
llm_models[0] if llm_models else None
)
if not target_model:
raise ValueError(f"Service {llm_service} has no available models")
provider_config = {
'provider': service_id,
'model': target_model.get('name', ''),
'base_url': service.get('base_url', ''),
'api_key': service.get('api_key', ''),
'temperature': target_model.get('temperature', 0.7),
'max_tokens': target_model.get('max_tokens', 1000),
'top_p': target_model.get('top_p', 0.9),
}
# Ollama 特殊处理:添加 auto_unload 配置
if service.get('type') == 'ollama':
provider_config['auto_unload'] = ollama_auto_unload
# 生成请求 ID
request_id = generate_request_id("exp", None, unique_id)
# 合并原文与用户提示词(输入端口在前,节点内文本框在后)
combined_text = (
user_prompt if not source_text
else (f"{source_text}\n\n{user_prompt}" if user_prompt else source_text)
)
# 检查是否关闭思维链
model_full_name = provider_config.get('model')
disable_thinking_enabled = service.get('disable_thinking', True)
thinking_extra = build_thinking_suppression(service_id, model_full_name) if disable_thinking_enabled else None
model_display = format_model_with_thinking(model_full_name, bool(thinking_extra))
# 获取服务显示名称
service_display_name = service.get('name', service_id)
# 准备阶段日志
log_prepare(TASK_EXPAND, request_id, SOURCE_NODE, service_display_name, model_display, rule_name, {"长度": len(combined_text)})
# 检查 API 密钥和模型
if not provider_config.get('model', ''):
raise ValueError(f"Please configure model for {llm_service}")
if cls._service_requires_api_key(service) and not provider_config.get('api_key', ''):
raise ValueError(f"Please configure API key and model for {llm_service}")
# 执行扩写(异步线程 + 可中断)
result = cls._run_llm_task(
LLMService.expand_prompt,
service_id,
prompt=combined_text,
request_id=request_id,
stream_callback=None,
custom_provider=service_id,
custom_provider_config=provider_config,
system_message_override=system_message,
task_type=TASK_EXPAND,
source=SOURCE_NODE
)
if result and result.get('success'):
expanded_text = result.get('data', {}).get('expanded', '').strip()
if not expanded_text:
error_msg = 'API returned empty result'
log_error(TASK_EXPAND, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"Enhancement failed: {error_msg}")
# 结果阶段日志由服务层统一输出,节点层不再重复打印
return io.NodeOutput(expanded_text)
else:
error_msg = result.get('error', 'Unknown error') if result else 'No result returned'
if error_msg == "任务被中断":
raise InterruptProcessingException()
log_error(TASK_EXPAND, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"Enhancement failed: {error_msg}")
except InterruptProcessingException:
# 不打印日志,由基类统一打印
raise
except Exception as e:
error_msg = format_api_error(e, llm_service)
log_error(TASK_EXPAND, request_id, error_msg, source=SOURCE_NODE)
raise RuntimeError(f"Enhancement error: {error_msg}")