fix(nodes): resolve KB path against the package and add raw debug outputs (#78)
Two ComfyUI issues found while testing. 1. Empty output with no error: the Knowledge Base Loader resolved a relative `path` against ComfyUI's working directory, not this package, so the default `knowledge_base/` loaded 0 entries (an empty KB is valid) and every concept became unknown -> empty prompts. Relative paths now resolve against the package root, and the loader warns when the Knowledge Base is empty. 2. Add a `raw` string output (a JSON dump of the node's data) to the Scene Analyzer, Scene Validator, Resolver, Category Splitter, and Prompt Builder nodes, plus a summary `raw` on the Knowledge Base Loader, for inspecting the pipeline in the graph. Adds a shared to_raw_json helper. Tests: package-relative path resolution (independent of cwd), empty-KB warning, raw output content, and updated node metadata/return-arity expectations. Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
kunail0804
Claude Opus 4.8
parent
c967327e8f
commit
de5edd7064
@@ -8,6 +8,12 @@ as typed objects; the inter-stage types are `SCENE`, `RESOLVED_TAGS`,
|
||||
Nodes are thin adapters: all behaviour lives in the `compiler` package, which
|
||||
never imports ComfyUI.
|
||||
|
||||
Every stage node also exposes a **`raw`** string output — a JSON dump of that
|
||||
node's data output — for inspecting the pipeline (wire it into a text/preview
|
||||
node). The Knowledge Base Loader's `raw` summarizes the loaded entries. A
|
||||
relative Knowledge Base `path` is resolved against the installed package, so the
|
||||
default `knowledge_base/` works regardless of ComfyUI's working directory.
|
||||
|
||||
## Pipeline nodes
|
||||
|
||||
| Node | Inputs | Outputs |
|
||||
|
||||
@@ -21,6 +21,26 @@ def format_messages(messages: Iterable[Message]) -> str:
|
||||
return "\n".join(f"{message.code}: {message.description}" for message in messages)
|
||||
|
||||
|
||||
def to_raw_json(data: Any) -> str:
|
||||
"""Serialize a stage's data output to a pretty JSON string for inspection.
|
||||
|
||||
Handles models (via their ``to_json``), tuples/lists of models, plain values,
|
||||
and ``None`` (returns an empty string). Used for the debug ``raw`` outputs.
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
try:
|
||||
if hasattr(data, "to_json"):
|
||||
payload: Any = data.to_json()
|
||||
elif isinstance(data, list | tuple):
|
||||
payload = [item.to_json() if hasattr(item, "to_json") else item for item in data]
|
||||
else:
|
||||
payload = data
|
||||
return json.dumps(payload, indent=2, ensure_ascii=False)
|
||||
except (TypeError, ValueError):
|
||||
return repr(data)
|
||||
|
||||
|
||||
def upstream_failure_message(what: str) -> str:
|
||||
"""A stage's error string when a required input is missing (upstream failed)."""
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import Any
|
||||
|
||||
from compiler.splitter.category_splitter import split_into_categories
|
||||
|
||||
from .adapters import format_messages, upstream_failure_message
|
||||
from .adapters import format_messages, to_raw_json, upstream_failure_message
|
||||
|
||||
|
||||
class CategorySplitterNode:
|
||||
@@ -23,15 +23,20 @@ class CategorySplitterNode:
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("CATEGORY_MAP", "STRING", "STRING")
|
||||
RETURN_NAMES = ("category_map", "warnings", "errors")
|
||||
RETURN_TYPES = ("CATEGORY_MAP", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("category_map", "warnings", "errors", "raw")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
return {"required": {"resolved_tags": ("RESOLVED_TAGS",)}}
|
||||
|
||||
def run(self, resolved_tags: Any) -> tuple[Any, str, str]:
|
||||
def run(self, resolved_tags: Any) -> tuple[Any, str, str, str]:
|
||||
if resolved_tags is None:
|
||||
return (None, "", upstream_failure_message("resolved tags"))
|
||||
return (None, "", upstream_failure_message("resolved tags"), "")
|
||||
result = split_into_categories(tuple(resolved_tags))
|
||||
return (result.data, format_messages(result.warnings), format_messages(result.errors))
|
||||
return (
|
||||
result.data,
|
||||
format_messages(result.warnings),
|
||||
format_messages(result.errors),
|
||||
to_raw_json(result.data),
|
||||
)
|
||||
|
||||
@@ -3,26 +3,38 @@
|
||||
A thin interface: it loads a Knowledge Base directory via the loader module and
|
||||
exposes it for the Resolver node. No compiler logic; it never imports ComfyUI.
|
||||
|
||||
Reloading is explicit: bumping the ``reload`` input changes the node's inputs so
|
||||
ComfyUI re-executes it (there is no automatic file watching).
|
||||
A relative ``path`` is resolved against this package (where the shipped
|
||||
``knowledge_base/`` lives), not ComfyUI's working directory, so the default works
|
||||
regardless of where ComfyUI is launched from. Reloading is explicit: bumping the
|
||||
``reload`` input re-executes the node (there is no automatic file watching).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from compiler.common.knowledge_base import KnowledgeBaseError, load_knowledge_base
|
||||
|
||||
from .adapters import format_messages
|
||||
|
||||
# Package root: the repository directory that ships knowledge_base/, prompts/, ...
|
||||
_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _resolve_path(path: str) -> Path:
|
||||
"""Resolve a relative Knowledge Base path against the package root."""
|
||||
candidate = Path(path)
|
||||
return candidate if candidate.is_absolute() else _PACKAGE_ROOT / candidate
|
||||
|
||||
|
||||
class KnowledgeBaseLoaderNode:
|
||||
"""Loads the Knowledge Base and exposes it for the Resolver."""
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("KNOWLEDGE_BASE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("knowledge_base", "warnings", "errors")
|
||||
RETURN_TYPES = ("KNOWLEDGE_BASE", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("knowledge_base", "warnings", "errors", "raw")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
@@ -31,9 +43,21 @@ class KnowledgeBaseLoaderNode:
|
||||
"optional": {"reload": ("INT", {"default": 0, "min": 0})},
|
||||
}
|
||||
|
||||
def run(self, path: str, reload: int = 0) -> tuple[Any, str, str]:
|
||||
def run(self, path: str, reload: int = 0) -> tuple[Any, str, str, str]:
|
||||
resolved = _resolve_path(path)
|
||||
try:
|
||||
knowledge_base = load_knowledge_base(path)
|
||||
knowledge_base = load_knowledge_base(resolved)
|
||||
except KnowledgeBaseError as error:
|
||||
return (None, "", format_messages([error.message, *error.findings]))
|
||||
return (knowledge_base, "", "")
|
||||
errors = format_messages([error.message, *error.findings])
|
||||
return (None, "", errors, f"Failed to load Knowledge Base from: {resolved}")
|
||||
|
||||
count = len(knowledge_base)
|
||||
warnings = ""
|
||||
if count == 0:
|
||||
warnings = (
|
||||
f"Knowledge Base at '{resolved}' is empty (0 entries); "
|
||||
"check the path. Every concept will be reported as unknown."
|
||||
)
|
||||
sample = ", ".join(sorted(knowledge_base.by_id)[:20])
|
||||
raw = f"Loaded {count} entries from {resolved}\nFirst entries: {sample}"
|
||||
return (knowledge_base, warnings, "", raw)
|
||||
|
||||
@@ -8,15 +8,17 @@ ComfyUI.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from compiler.builder.prompt_builder import RESERVED_OUTPUTS, build_prompts
|
||||
from compiler.common.categories import CANONICAL_CATEGORIES
|
||||
from compiler.common.config import Config
|
||||
|
||||
# The stable set of node outputs: one per canonical category, then the reserved
|
||||
# outputs. Matches the Prompt Outputs produced by build_prompts.
|
||||
# One output per canonical category, then the reserved outputs (matching the
|
||||
# Prompt Outputs produced by build_prompts), then a debug `raw` dump.
|
||||
_OUTPUT_NAMES: tuple[str, ...] = (*CANONICAL_CATEGORIES, *RESERVED_OUTPUTS)
|
||||
_RETURN_NAMES: tuple[str, ...] = (*_OUTPUT_NAMES, "raw")
|
||||
|
||||
|
||||
class PromptBuilderNode:
|
||||
@@ -24,8 +26,8 @@ class PromptBuilderNode:
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("STRING",) * len(_OUTPUT_NAMES)
|
||||
RETURN_NAMES = _OUTPUT_NAMES
|
||||
RETURN_TYPES = ("STRING",) * len(_RETURN_NAMES)
|
||||
RETURN_NAMES = _RETURN_NAMES
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
@@ -37,7 +39,9 @@ class PromptBuilderNode:
|
||||
def run(self, category_map: Any, config: Config | None = None) -> tuple[str, ...]:
|
||||
if category_map is None:
|
||||
# Upstream produced no Category Map; emit empty strings rather than crash.
|
||||
return tuple("" for _ in _OUTPUT_NAMES)
|
||||
return tuple("" for _ in _RETURN_NAMES)
|
||||
result = build_prompts(category_map, config or Config())
|
||||
by_name = {output.name: output.value for output in result.data}
|
||||
return tuple(by_name[name] for name in _OUTPUT_NAMES)
|
||||
values = tuple(by_name[name] for name in _OUTPUT_NAMES)
|
||||
raw = json.dumps(by_name, indent=2, ensure_ascii=False)
|
||||
return (*values, raw)
|
||||
|
||||
+11
-6
@@ -12,7 +12,7 @@ from typing import Any
|
||||
from compiler.common.config import Config
|
||||
from compiler.resolver.illustrious_resolver import resolve_scene
|
||||
|
||||
from .adapters import format_messages, upstream_failure_message
|
||||
from .adapters import format_messages, to_raw_json, upstream_failure_message
|
||||
|
||||
|
||||
class ResolverNode:
|
||||
@@ -20,8 +20,8 @@ class ResolverNode:
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("RESOLVED_TAGS", "STRING", "STRING")
|
||||
RETURN_NAMES = ("resolved_tags", "warnings", "errors")
|
||||
RETURN_TYPES = ("RESOLVED_TAGS", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("resolved_tags", "warnings", "errors", "raw")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
@@ -38,8 +38,13 @@ class ResolverNode:
|
||||
scene: Any,
|
||||
knowledge_base: Any,
|
||||
config: Config | None = None,
|
||||
) -> tuple[Any, str, str]:
|
||||
) -> tuple[Any, str, str, str]:
|
||||
if scene is None or knowledge_base is None:
|
||||
return (None, "", upstream_failure_message("scene or knowledge base"))
|
||||
return (None, "", upstream_failure_message("scene or knowledge base"), "")
|
||||
result = resolve_scene(scene, knowledge_base, config or Config())
|
||||
return (result.data, format_messages(result.warnings), format_messages(result.errors))
|
||||
return (
|
||||
result.data,
|
||||
format_messages(result.warnings),
|
||||
format_messages(result.errors),
|
||||
to_raw_json(result.data),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from compiler.analyzer.backend import OllamaBackend
|
||||
from compiler.analyzer.scene_analyzer import analyze
|
||||
from compiler.common.config import Config
|
||||
|
||||
from .adapters import format_messages
|
||||
from .adapters import format_messages, to_raw_json
|
||||
|
||||
|
||||
class SceneAnalyzerNode:
|
||||
@@ -21,8 +21,8 @@ class SceneAnalyzerNode:
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("SCENE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("scene", "warnings", "errors")
|
||||
RETURN_TYPES = ("SCENE", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("scene", "warnings", "errors", "raw")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
@@ -57,4 +57,9 @@ class SceneAnalyzerNode:
|
||||
config = Config.from_json({"analyzer": analyzer})
|
||||
backend = OllamaBackend.from_config(config)
|
||||
result = analyze(natural_language, backend, config)
|
||||
return (result.data, format_messages(result.warnings), format_messages(result.errors))
|
||||
return (
|
||||
result.data,
|
||||
format_messages(result.warnings),
|
||||
format_messages(result.errors),
|
||||
to_raw_json(result.data),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Any
|
||||
from compiler.common.config import Config
|
||||
from compiler.validator.scene_validator import validate_scene
|
||||
|
||||
from .adapters import format_messages, upstream_failure_message
|
||||
from .adapters import format_messages, to_raw_json, upstream_failure_message
|
||||
|
||||
|
||||
class SceneValidatorNode:
|
||||
@@ -20,8 +20,8 @@ class SceneValidatorNode:
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("SCENE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("scene", "warnings", "errors")
|
||||
RETURN_TYPES = ("SCENE", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("scene", "warnings", "errors", "raw")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
@@ -30,8 +30,13 @@ class SceneValidatorNode:
|
||||
"optional": {"config": ("COMPILER_CONFIG",)},
|
||||
}
|
||||
|
||||
def run(self, scene: Any, config: Config | None = None) -> tuple[Any, str, str]:
|
||||
def run(self, scene: Any, config: Config | None = None) -> tuple[Any, str, str, str]:
|
||||
if scene is None:
|
||||
return (None, "", upstream_failure_message("scene"))
|
||||
return (None, "", upstream_failure_message("scene"), "")
|
||||
result = validate_scene(scene.to_json(), config or Config())
|
||||
return (result.data, format_messages(result.warnings), format_messages(result.errors))
|
||||
return (
|
||||
result.data,
|
||||
format_messages(result.warnings),
|
||||
format_messages(result.errors),
|
||||
to_raw_json(result.data),
|
||||
)
|
||||
|
||||
@@ -49,15 +49,15 @@ def scene() -> Scene:
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert ResolverNode.RETURN_TYPES == ("RESOLVED_TAGS", "STRING", "STRING")
|
||||
assert ResolverNode.RETURN_NAMES == ("resolved_tags", "warnings", "errors")
|
||||
assert ResolverNode.RETURN_TYPES == ("RESOLVED_TAGS", "STRING", "STRING", "STRING")
|
||||
assert ResolverNode.RETURN_NAMES == ("resolved_tags", "warnings", "errors", "raw")
|
||||
inputs = ResolverNode.INPUT_TYPES()
|
||||
assert set(inputs["required"]) == {"scene", "knowledge_base"}
|
||||
assert "config" in inputs["optional"]
|
||||
|
||||
|
||||
def test_resolves_tags_with_real_resolver() -> None:
|
||||
resolved, warnings, errors = ResolverNode().run(scene(), kb())
|
||||
resolved, warnings, errors, *_ = ResolverNode().run(scene(), kb())
|
||||
assert [t.tag for t in resolved] == ["1girl", "long hair"]
|
||||
assert errors == ""
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_delegates_and_surfaces_messages(monkeypatch) -> None:
|
||||
return CompilerResult(data=()).add_warning(message("SC0001", "unknown 'x'"))
|
||||
|
||||
monkeypatch.setattr(node_module, "resolve_scene", fake_resolve)
|
||||
resolved, warnings, errors = ResolverNode().run(scene(), kb())
|
||||
resolved, warnings, errors, *_ = ResolverNode().run(scene(), kb())
|
||||
assert resolved == ()
|
||||
assert "SC0001: unknown 'x'" in warnings
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ def rtag(name: str, category: str) -> ResolvedTag:
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert CategorySplitterNode.RETURN_TYPES == ("CATEGORY_MAP", "STRING", "STRING")
|
||||
assert CategorySplitterNode.RETURN_NAMES == ("category_map", "warnings", "errors")
|
||||
assert CategorySplitterNode.RETURN_TYPES == ("CATEGORY_MAP", "STRING", "STRING", "STRING")
|
||||
assert CategorySplitterNode.RETURN_NAMES == ("category_map", "warnings", "errors", "raw")
|
||||
assert "resolved_tags" in CategorySplitterNode.INPUT_TYPES()["required"]
|
||||
|
||||
|
||||
def test_splits_with_real_module() -> None:
|
||||
tags = (rtag("1girl", "character"), rtag("long hair", "hair"))
|
||||
category_map, warnings, errors = CategorySplitterNode().run(tags)
|
||||
category_map, warnings, errors, *_ = CategorySplitterNode().run(tags)
|
||||
assert isinstance(category_map, CategoryMap)
|
||||
assert [t.tag for t in category_map.tags_for("character")] == ["1girl"]
|
||||
assert [t.tag for t in category_map.tags_for("hair")] == ["long hair"]
|
||||
@@ -35,7 +35,7 @@ def test_delegates_and_surfaces_errors(monkeypatch) -> None:
|
||||
"split_into_categories",
|
||||
lambda tags: CompilerResult().add_error(message("SC0008", "bad category")),
|
||||
)
|
||||
category_map, warnings, errors = CategorySplitterNode().run(())
|
||||
category_map, warnings, errors, *_ = CategorySplitterNode().run(())
|
||||
assert category_map is None
|
||||
assert "SC0008: bad category" in errors
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ def write_kb(directory, entries: list[dict]) -> str:
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert KnowledgeBaseLoaderNode.RETURN_TYPES == ("KNOWLEDGE_BASE", "STRING", "STRING")
|
||||
assert KnowledgeBaseLoaderNode.RETURN_NAMES == ("knowledge_base", "warnings", "errors")
|
||||
assert KnowledgeBaseLoaderNode.RETURN_TYPES == ("KNOWLEDGE_BASE", "STRING", "STRING", "STRING")
|
||||
assert KnowledgeBaseLoaderNode.RETURN_NAMES == ("knowledge_base", "warnings", "errors", "raw")
|
||||
inputs = KnowledgeBaseLoaderNode.INPUT_TYPES()
|
||||
assert "path" in inputs["required"]
|
||||
assert "reload" in inputs["optional"]
|
||||
@@ -24,7 +24,7 @@ def test_node_metadata() -> None:
|
||||
|
||||
def test_loads_knowledge_base(tmp_path) -> None:
|
||||
path = write_kb(tmp_path, [{"id": "long_hair", "tags": ["long hair"], "category": "hair"}])
|
||||
kb, warnings, errors = KnowledgeBaseLoaderNode().run(path)
|
||||
kb, warnings, errors, *_ = KnowledgeBaseLoaderNode().run(path)
|
||||
assert isinstance(kb, KnowledgeBase)
|
||||
assert kb.get("long_hair") is not None
|
||||
assert (warnings, errors) == ("", "")
|
||||
@@ -32,14 +32,31 @@ def test_loads_knowledge_base(tmp_path) -> None:
|
||||
|
||||
def test_invalid_knowledge_base_surfaces_sc0004(tmp_path) -> None:
|
||||
path = write_kb(tmp_path, [{"id": "x", "tags": ["x"], "category": "nope"}])
|
||||
kb, warnings, errors = KnowledgeBaseLoaderNode().run(path)
|
||||
kb, warnings, errors, *_ = KnowledgeBaseLoaderNode().run(path)
|
||||
assert kb is None
|
||||
assert "SC0004" in errors
|
||||
|
||||
|
||||
def test_relative_path_resolves_against_package(monkeypatch, tmp_path) -> None:
|
||||
# Even with an unrelated working directory (as under ComfyUI), the default
|
||||
# relative path must load the shipped Knowledge Base, not an empty one.
|
||||
monkeypatch.chdir(tmp_path)
|
||||
kb, warnings, errors, raw = KnowledgeBaseLoaderNode().run("knowledge_base/")
|
||||
assert isinstance(kb, KnowledgeBase)
|
||||
assert len(kb) > 100
|
||||
assert (warnings, errors) == ("", "")
|
||||
assert "entries" in raw
|
||||
|
||||
|
||||
def test_empty_knowledge_base_warns(tmp_path) -> None:
|
||||
kb, warnings, errors, raw = KnowledgeBaseLoaderNode().run(str(tmp_path))
|
||||
assert len(kb) == 0
|
||||
assert "empty" in warnings.lower()
|
||||
|
||||
|
||||
def test_reload_input_is_accepted(tmp_path) -> None:
|
||||
path = write_kb(tmp_path, [{"id": "long_hair", "tags": ["long hair"], "category": "hair"}])
|
||||
kb, _, _ = KnowledgeBaseLoaderNode().run(path, reload=5)
|
||||
kb, _, _, _ = KnowledgeBaseLoaderNode().run(path, reload=5)
|
||||
assert isinstance(kb, KnowledgeBase)
|
||||
|
||||
|
||||
|
||||
@@ -14,25 +14,25 @@ from nodes.scene_validator_node import SceneValidatorNode
|
||||
|
||||
|
||||
def test_validator_handles_none_scene() -> None:
|
||||
scene, warnings, errors = SceneValidatorNode().run(None)
|
||||
scene, warnings, errors, *_ = SceneValidatorNode().run(None)
|
||||
assert scene is None
|
||||
assert "no output" in errors.lower()
|
||||
|
||||
|
||||
def test_resolver_handles_none_scene() -> None:
|
||||
resolved, _, errors = ResolverNode().run(None, knowledge_base=object())
|
||||
resolved, _, errors, *_ = ResolverNode().run(None, knowledge_base=object())
|
||||
assert resolved is None
|
||||
assert errors
|
||||
|
||||
|
||||
def test_resolver_handles_none_knowledge_base() -> None:
|
||||
resolved, _, errors = ResolverNode().run(scene=object(), knowledge_base=None)
|
||||
resolved, _, errors, *_ = ResolverNode().run(scene=object(), knowledge_base=None)
|
||||
assert resolved is None
|
||||
assert errors
|
||||
|
||||
|
||||
def test_category_splitter_handles_none_tags() -> None:
|
||||
category_map, _, errors = CategorySplitterNode().run(None)
|
||||
category_map, _, errors, *_ = CategorySplitterNode().run(None)
|
||||
assert category_map is None
|
||||
assert errors
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ def outputs(result: tuple[str, ...]) -> dict[str, str]:
|
||||
|
||||
|
||||
def test_emits_one_output_per_category_plus_reserved() -> None:
|
||||
assert PromptBuilderNode.RETURN_NAMES == (*CANONICAL_CATEGORIES, "negative", "scene")
|
||||
assert PromptBuilderNode.RETURN_TYPES == ("STRING",) * 21
|
||||
assert PromptBuilderNode.RETURN_NAMES == (*CANONICAL_CATEGORIES, "negative", "scene", "raw")
|
||||
assert PromptBuilderNode.RETURN_TYPES == ("STRING",) * 22
|
||||
|
||||
|
||||
def test_formats_and_orders_category_strings() -> None:
|
||||
|
||||
@@ -21,8 +21,8 @@ def test_input_types_structure() -> None:
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert SceneAnalyzerNode.RETURN_TYPES == ("SCENE", "STRING", "STRING")
|
||||
assert SceneAnalyzerNode.RETURN_NAMES == ("scene", "warnings", "errors")
|
||||
assert SceneAnalyzerNode.RETURN_TYPES == ("SCENE", "STRING", "STRING", "STRING")
|
||||
assert SceneAnalyzerNode.RETURN_NAMES == ("scene", "warnings", "errors", "raw")
|
||||
assert SceneAnalyzerNode.FUNCTION == "run"
|
||||
assert SceneAnalyzerNode.CATEGORY == "Scene Compiler"
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_run_delegates_to_analyzer_and_surfaces_messages(monkeypatch) -> None:
|
||||
|
||||
monkeypatch.setattr(node_module, "analyze", fake_analyze)
|
||||
|
||||
scene, warnings, errors = SceneAnalyzerNode().run(
|
||||
scene, warnings, errors, *_ = SceneAnalyzerNode().run(
|
||||
natural_language="A girl.",
|
||||
model_name="mistral",
|
||||
temperature=0.0,
|
||||
@@ -56,7 +56,7 @@ def test_run_delegates_to_analyzer_and_surfaces_messages(monkeypatch) -> None:
|
||||
|
||||
def test_no_messages_yield_empty_strings(monkeypatch) -> None:
|
||||
monkeypatch.setattr(node_module, "analyze", lambda d, b, c: CompilerResult(data="S"))
|
||||
scene, warnings, errors = SceneAnalyzerNode().run("x", "llama3", 0.0, 60)
|
||||
scene, warnings, errors, *_ = SceneAnalyzerNode().run("x", "llama3", 0.0, 60)
|
||||
assert (warnings, errors) == ("", "")
|
||||
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ def sample_scene() -> Scene:
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert SceneValidatorNode.RETURN_TYPES == ("SCENE", "STRING", "STRING")
|
||||
assert SceneValidatorNode.RETURN_NAMES == ("scene", "warnings", "errors")
|
||||
assert SceneValidatorNode.RETURN_TYPES == ("SCENE", "STRING", "STRING", "STRING")
|
||||
assert SceneValidatorNode.RETURN_NAMES == ("scene", "warnings", "errors", "raw")
|
||||
assert SceneValidatorNode.CATEGORY == "Scene Compiler"
|
||||
inputs = SceneValidatorNode.INPUT_TYPES()
|
||||
assert "scene" in inputs["required"]
|
||||
@@ -46,11 +46,17 @@ def test_node_metadata() -> None:
|
||||
|
||||
|
||||
def test_valid_scene_passes_through() -> None:
|
||||
scene, warnings, errors = SceneValidatorNode().run(sample_scene())
|
||||
scene, warnings, errors, *_ = SceneValidatorNode().run(sample_scene())
|
||||
assert isinstance(scene, Scene)
|
||||
assert (warnings, errors) == ("", "")
|
||||
|
||||
|
||||
def test_raw_output_contains_scene_json() -> None:
|
||||
_, _, _, raw = SceneValidatorNode().run(sample_scene())
|
||||
assert '"characters"' in raw # the debug raw output is the scene as JSON
|
||||
assert "female" in raw
|
||||
|
||||
|
||||
def test_delegates_and_surfaces_messages(monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
@@ -64,7 +70,7 @@ def test_delegates_and_surfaces_messages(monkeypatch) -> None:
|
||||
|
||||
monkeypatch.setattr(node_module, "validate_scene", fake_validate)
|
||||
config = Config.from_json({"validator": {"allow_unknown_fields": True}})
|
||||
scene, warnings, errors = SceneValidatorNode().run(sample_scene(), config)
|
||||
scene, warnings, errors, *_ = SceneValidatorNode().run(sample_scene(), config)
|
||||
assert scene == "VALIDATED"
|
||||
assert captured["allow_unknown"] is True
|
||||
assert "SC0015: unexpected field removed" in warnings
|
||||
|
||||
Reference in New Issue
Block a user