Files
olivv-cs-ComfyUI-FunPack/modules/conditioning/prompt_enhancer/tests/test_prompt_enhancer.py
T

263 lines
11 KiB
Python

"""The prompt enhancer: what the model is asked, how its answer is cleaned, and
that every failure hands the ORIGINAL prompt on."""
import json
import pytest
from core import config, shortcuts
@pytest.fixture(autouse=True)
def _library(tmp_path, monkeypatch):
monkeypatch.setattr(config, "SHORTCUTS_FILE", tmp_path / "s.json")
monkeypatch.setattr(config, "SHORTCUT_CATEGORIES_FILE", tmp_path / "c.json")
monkeypatch.setattr(config, "ROOT", tmp_path)
def en():
from modules.conditioning.prompt_enhancer import enhance
return enhance
class FakeClip:
"""ComfyUI's generate/decode pair, recording what it was asked."""
def __init__(self, answer="A fox walks.", fail=None):
self.answer, self.fail, self.seen = answer, fail, {}
def tokenize(self, text, **kw):
self.seen = {"text": text, **kw}
return "tok"
def generate(self, tokens, **kw):
if self.fail:
raise self.fail
self.seen["gen"] = kw
return "ids"
def decode(self, ids, skip_special_tokens=True):
return self.answer
# --- cleaning ---------------------------------------------------------------
@pytest.mark.parametrize("raw,want", [
("<think>hmm</think>A fox.", "A fox."),
("<think>never ends and eats the answer", ""),
("```text\nA fox.\n```", "A fox."),
("Here is the prompt: A fox.", "A fox."),
('"A fox says \\"hi\\" and runs."', 'A fox says \\"hi\\" and runs.'),
('A fox says "hi" and runs.', 'A fox says "hi" and runs.'),
])
def test_clean(raw, want):
assert en().clean(raw) == want
def test_thinking_is_pulled_out_even_unterminated_or_headless():
e = en()
assert e.extract_thinking("<think>a</think>b") == "a"
assert e.extract_thinking("<think>still going") == "still going"
assert e.extract_thinking("opened by template</think>answer") == "opened by template"
assert e.extract_thinking("plain") == ""
def test_a_looping_model_is_cut_at_a_sentence_and_a_long_one_by_the_users_limit():
e = en()
loop = "A fox walks across the snowy field. " * 5
out, cut = e.trim_runaway("Intro sentence here. " + loop)
assert cut
assert out.count("A fox walks across the snowy field.") == 2
long_text = "A sentence that goes on a bit. " * 40
out, cut = e.trim_runaway(long_text, max_length=16) # cap 64 chars
assert cut and len(out) <= 64 and out.endswith(".")
assert e.trim_runaway("short", max_length=400) == ("short", False)
# --- chat -------------------------------------------------------------------
def test_chat_shows_only_the_latest_rewrite_and_every_comment():
e = en()
rounds = [{"rewrites": {"whole": "v1"}, "comment": "warmer"},
{"rewrites": {"whole": "v2"}, "comment": "no rain"}]
block = e.chat_block(rounds)
assert "v2" in block and "v1" not in block and "- warmer\n- no rain" in block
assert e.chat_block([{"comment": " "}, "junk", None]) == ""
assert e.chat_block([{"rewrites": {"3": "only scene 3"}, "comment": "x"}], scene=3).count("only scene 3") == 1
def test_an_echoed_conversation_is_cut():
e = en()
assert e.cut_chat_echo("The fox runs.\n\nUSER FEEDBACK (oldest first):\n- warmer") == "The fox runs."
assert e.cut_chat_echo("YOUR LATEST REWRITE: The fox runs.") == "The fox runs."
# --- reference --------------------------------------------------------------
def test_reference_uses_what_the_prompt_mentions_else_the_whole_group():
e = en()
shortcuts.save({"name": "Rain city", "triggers": ["rc"], "replacements": ["heavy neon rain on asphalt"]})
shortcuts.save({"name": "Fox", "triggers": ["fx"], "replacements": ["a red fox"]})
groups = e.sources(["Rain city", "Fox", "Missing"], [], say=lambda m: None)
assert len(groups) == 1 and len(groups[0]) == 2
only_rain = e.reference("a walk in the neon street", groups)
assert "[Shortcut] Rain city" in only_rain and "[Shortcut] Fox" not in only_rain # a word from its content
both = e.reference("nothing related", groups)
assert "Rain city" in both and "Fox" in both # none mentioned: all
assert e.reference("x", []) == ""
def test_a_missing_shortcut_or_file_is_said_not_silent():
said = []
assert en().sources(["Nope"], ["/no/such/file.json"], say=said.append) == []
assert len(said) == 2 and "Nope" in said[0] and "/no/such/file.json" in said[1]
def test_lorebooks_match_by_keyword_whole_word_and_constants_ride_along(tmp_path):
e = en()
book = tmp_path / "lore.json"
book.write_text(json.dumps({"entries": [
{"keys": ["sea"], "content": "The sea is cold.", "comment": "Sea"},
{"keys": ["/ra+in/"], "content": "Rain is soft."},
{"keys": ["x"], "content": "Always true.", "constant": True}]}))
groups = e.sources([], [str(book)])
got = e.reference("a season of rain", groups) # "sea" is NOT in "season"
assert "Rain is soft." in got and "The sea is cold." not in got and "Always true." in got
# --- enhance ----------------------------------------------------------------
def test_the_model_gets_instructions_then_the_prompt_and_the_answer_is_returned():
clip = FakeClip("Here is the prompt: A red fox walks through snow.")
out, status, info = en().enhance(clip, "a fox", seed=5, max_length=100, do_sample=False)
assert out == "A red fox walks through snow." and info["ok"]
assert clip.seen["text"].startswith(en().SYSTEM_PROMPT) and clip.seen["text"].endswith("a fox")
assert clip.seen["gen"]["seed"] == 5 and clip.seen["gen"]["do_sample"] is False
assert clip.seen["skip_template"] is False and clip.seen["min_length"] == 1
@pytest.mark.parametrize("clip", [FakeClip(fail=RuntimeError("boom")), FakeClip(""), FakeClip("<think>x</think>"),
None, object()])
def test_every_failure_returns_the_original_prompt(clip):
out, status, info = en().enhance(clip, "a fox")
assert out == "a fox" and not info["ok"] and status
@pytest.mark.parametrize("answer", [",,,,,,,,,,,,,,,,,,,,", "... --- !!!", "1234 5678", "x ,,,,,,,,,,,,,,,,,,,, ,,,,,"])
def test_an_answer_with_no_words_is_a_failure_not_a_prompt(answer):
"""A text encoder that is not a chat model answered with 400 commas; the run used them as the prompt."""
out, status, info = en().enhance(FakeClip(answer), "a red fox in the snow")
assert out == "a red fox in the snow" and not info["ok"] and "no usable words" in status
def test_an_empty_prompt_is_skipped_not_sent():
clip = FakeClip()
out, status, _ = en().enhance(clip, " ")
assert out == " " and "empty" in status and clip.seen == {}
class _Tok:
"""ComfyUI's H3 tokenizer shape: takes `images`, swallows everything else."""
def tokenize_with_weights(self, text, return_word_ids=False, images=[], **kwargs):
return []
class RealShapeClip(FakeClip):
"""sd.CLIP.generate's real signature: no no_repeat_ngram_size, seed=None default."""
tokenizer = _Tok()
def generate(self, tokens, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.95,
min_p=0.0, repetition_penalty=1.0, seed=None, presence_penalty=0.0):
assert seed is not None or not do_sample, "torch.Generator.manual_seed(None) raises"
self.seen["gen"] = dict(do_sample=do_sample, seed=seed, temperature=temperature)
return "ids"
def test_the_real_generate_signature_gets_a_seed_and_no_unknown_arguments():
clip = RealShapeClip("A fox runs.")
out, status, info = en().enhance(clip, "fox", seed=0)
assert out == "A fox runs." and info["ok"] and clip.seen["gen"]["seed"]
assert en().enhance(RealShapeClip("A fox runs."), "fox", seed=7)[2]["ok"]
def test_temperature_zero_is_greedy_not_a_crash():
clip = RealShapeClip("A fox runs.")
assert en().enhance(clip, "fox", temperature=0.0)[2]["ok"] and clip.seen["gen"]["do_sample"] is False
def test_a_tokenizer_without_picture_or_template_input_says_so():
clip = RealShapeClip("A fox runs.")
_, status, _ = en().enhance(clip, "fox", image=object())
assert "takes no picture" in status and "no chat template" in status
assert "image" not in clip.seen and "skip_template" not in clip.seen
def test_chat_revision_goes_in_the_system_turn_and_an_echo_is_cut():
e = en()
clip = FakeClip("The fox runs.\nUSER FEEDBACK: warmer")
chat = e.chat_block([{"rewrites": {"whole": "old"}, "comment": "warmer"}])
out, _, info = e.enhance(clip, "a fox", chat=chat)
assert out == "The fox runs." and e.CHAT_SYSTEM in clip.seen["text"]
assert "ORIGINAL PROMPT:\na fox" in clip.seen["text"] and info["sent"]
# --- the node ---------------------------------------------------------------
@pytest.fixture
def node(comfyui):
from modules.conditioning.prompt_enhancer import nodes
nodes.RUNS.clear()
return nodes
def test_the_node_passes_through_when_off_and_never_touches_the_model(node):
clip = FakeClip()
out = node.FunPackEnhancePrompt.execute(clip, "a fox", enabled=False)
assert tuple(out.args) == ("a fox", "off") and clip.seen == {}
assert node.RUNS[-1]["after"] == "a fox", "the Enhance tab still shows the final prompt"
def test_the_node_enhances_records_the_run_and_warns_on_failure(node, caplog):
out = node.FunPackEnhancePrompt.execute(FakeClip("A red fox."), "a fox", enabled=True, seed=3)
assert out.args[0] == "A red fox." and node.RUNS[-1]["after"] == "A red fox."
from core import log
log._reset()
bad = node.FunPackEnhancePrompt.execute(FakeClip(fail=RuntimeError("x")), "a fox", enabled=True)
assert bad.args[0] == "a fox" and node.RUNS[-1]["ok"] is False
assert any("original prompt" in r["message"] for r in log.history())
def test_seed_zero_is_never_cached_a_set_seed_is(node):
f = node.FunPackEnhancePrompt.fingerprint_inputs
assert isinstance(f(seed=0, enabled=True), float)
assert f(seed=7, enabled=True) == "" and f(seed=0, enabled=False) == ""
def test_the_readout_routes_are_provided_and_answer():
from modules.conditioning import prompt_enhancer as mod
assert mod.PROVIDES["routes"] is mod.routes
table = {}
class T:
def get(self, path):
return lambda fn: table.setdefault(path, fn)
class W:
@staticmethod
def json_response(d):
return d
mod.routes(T(), "/m", W)
assert set(table) == {"/m/defaults", "/m/runs"}
def test_a_tokenizer_that_reads_the_picture_out_of_kwargs_gets_it():
class Qwen: # Qwen3-VL's shape: names only `images`, reads kwargs.get("image")
def tokenize_with_weights(self, text, return_word_ids=False, images=[], **kwargs):
return kwargs.get("image")
class Clip(RealShapeClip):
tokenizer = Qwen()
clip = Clip("A fox runs.")
_, status, _ = en().enhance(clip, "fox", image=object())
assert "takes no picture" not in status and "image" in clip.seen