Files

225 lines
7.6 KiB
Python

import json
import time
import folder_paths
from llama_cpp import Llama
from .llm_utils import resolve_gpu_layers
# Reuse the folder registration from llm_gguf_node (already registered at import time)
class LLMChatStart:
"""Initialize a chat conversation with a system prompt and first user message."""
_model_cache = {}
@classmethod
def INPUT_TYPES(cls):
model_list = folder_paths.get_filename_list("LLMs")
return {
"required": {
"model": (model_list, {"tooltip": "Select a .gguf model from ComfyUI/models/LLM"}),
"system_prompt": ("STRING", {
"multiline": True,
"default": "You are a helpful assistant.",
"tooltip": "System prompt to set the model's behavior",
}),
"user_message": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "First user message to start the conversation",
}),
"max_tokens": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.05}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05}),
"context_length": ("INT", {"default": 2048, "min": 512, "max": 32768, "step": 256}),
"gpu_layers": ("INT", {"default": -1, "min": -1, "max": 200, "step": 1}),
"keep_model_loaded": ("BOOLEAN", {"default": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("response", "chat_history",)
FUNCTION = "start_chat"
CATEGORY = "LLM"
def start_chat(
self, model, system_prompt, user_message,
max_tokens, temperature, top_p,
context_length, gpu_layers, keep_model_loaded, seed,
):
llm = _get_or_load_model(
self._model_cache, model, context_length, gpu_layers,
)
messages = []
if system_prompt.strip():
messages.append({"role": "system", "content": system_prompt.strip()})
messages.append({"role": "user", "content": user_message})
response_text, elapsed = _run_inference(
llm, messages, max_tokens, temperature, top_p, seed,
)
messages.append({"role": "assistant", "content": response_text})
chat_history = json.dumps(messages, ensure_ascii=False)
if not keep_model_loaded:
_evict_cache(self._model_cache)
return (response_text, chat_history,)
class LLMChatContinue:
"""Continue an existing chat conversation with a new user message."""
_model_cache = {}
@classmethod
def INPUT_TYPES(cls):
model_list = folder_paths.get_filename_list("LLMs")
return {
"required": {
"model": (model_list, {"tooltip": "Select a .gguf model from ComfyUI/models/LLM"}),
"chat_history": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Chat history JSON from a previous Chat Start or Chat Continue node",
}),
"user_message": ("STRING", {
"multiline": True,
"default": "",
"tooltip": "Next user message in the conversation",
}),
"max_tokens": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.05}),
"top_p": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05}),
"context_length": ("INT", {"default": 2048, "min": 512, "max": 32768, "step": 256}),
"gpu_layers": ("INT", {"default": -1, "min": -1, "max": 200, "step": 1}),
"keep_model_loaded": ("BOOLEAN", {"default": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
},
}
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("response", "chat_history",)
FUNCTION = "continue_chat"
CATEGORY = "LLM"
def continue_chat(
self, model, chat_history, user_message,
max_tokens, temperature, top_p,
context_length, gpu_layers, keep_model_loaded, seed,
):
try:
messages = json.loads(chat_history)
if not isinstance(messages, list):
raise ValueError("chat_history must be a JSON array")
except (json.JSONDecodeError, ValueError) as e:
raise ValueError(
f"Invalid chat_history: {e}. "
"Connect this to the chat_history output of a Chat Start or Chat Continue node."
)
messages.append({"role": "user", "content": user_message})
llm = _get_or_load_model(
self._model_cache, model, context_length, gpu_layers,
)
response_text, elapsed = _run_inference(
llm, messages, max_tokens, temperature, top_p, seed,
)
messages.append({"role": "assistant", "content": response_text})
updated_history = json.dumps(messages, ensure_ascii=False)
if not keep_model_loaded:
_evict_cache(self._model_cache)
return (response_text, updated_history,)
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _get_or_load_model(cache, model, context_length, gpu_layers):
model_path = folder_paths.get_full_path_or_raise("LLMs", model)
actual_gpu_layers = resolve_gpu_layers(model_path, context_length, gpu_layers)
cache_key = (model_path, context_length, actual_gpu_layers)
llm = cache.get(cache_key)
if llm is None:
_evict_cache(cache)
print(f"[LLM-Tools] Loading model: {model} gpu_layers={actual_gpu_layers}")
llm = Llama(
model_path=model_path,
n_ctx=context_length,
n_gpu_layers=actual_gpu_layers,
verbose=False,
)
cache[cache_key] = llm
return llm
def _run_inference(llm, messages, max_tokens, temperature, top_p, seed):
start = time.perf_counter()
result = llm.create_chat_completion(
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
seed=seed if seed != 0 else None,
)
elapsed = time.perf_counter() - start
response_text = ""
if result and "choices" in result and len(result["choices"]) > 0:
response_text = result["choices"][0].get("message", {}).get("content", "")
tokens_used = result.get("usage", {})
print(
f"[LLM-Tools] Generated in {elapsed:.1f}s | "
f"prompt_tokens={tokens_used.get('prompt_tokens', '?')} "
f"completion_tokens={tokens_used.get('completion_tokens', '?')}"
)
return response_text, elapsed
def _evict_cache(cache):
for key, llm in list(cache.items()):
try:
llm.close()
except Exception:
pass
del cache[key]
cache.clear()
import gc
gc.collect()
gc.collect()
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
except ImportError:
pass
NODE_CLASS_MAPPINGS = {
"LLMChatStart": LLMChatStart,
"LLMChatContinue": LLMChatContinue,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LLMChatStart": "LLM Chat Start",
"LLMChatContinue": "LLM Chat Continue",
}