338 lines
14 KiB
Python
338 lines
14 KiB
Python
"""
|
||
提示词增强节点 - 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}")
|