Add nodes/configuration_node.py: ConfigurationNode, a thin adapter that
centralizes compiler configuration (MASTER_SPEC §20, §23).
- Exposes all §23.2 options as node inputs (analyzer model/temperature/
max_retries/timeout + optional system_prompt; resolver knowledge_base/
strict_mode/allow_aliases/expansion_enabled/max_expansion_depth; validator
allow_unknown_fields; prompt_builder target/separator/trim_empty_outputs/
remove_duplicate_tags; debug enabled/level).
- Builds a Config via Config.from_json and outputs (config, errors) as a
COMPILER_CONFIG. The Validator, Resolver, and Prompt Builder nodes already
accept this via their optional config input, so behaviour changes without
editing the workflow. No compiler logic in the node.
- Registered as SceneCompilerConfiguration ("Configuration").
Tests: metadata/inputs, defaults reproduce Config(), inputs flow into the config,
system-prompt override + omission, all debug levels valid, and registration.
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
845ab5005e
commit
8b26c1e67f
@@ -8,6 +8,7 @@ Nodes are thin interfaces; all compiler logic lives in the ``compiler`` package.
|
||||
from __future__ import annotations
|
||||
|
||||
from .category_splitter_node import CategorySplitterNode
|
||||
from .configuration_node import ConfigurationNode
|
||||
from .debug_viewer_node import DebugViewerNode
|
||||
from .knowledge_base_loader_node import KnowledgeBaseLoaderNode
|
||||
from .prompt_builder_node import PromptBuilderNode
|
||||
@@ -23,6 +24,7 @@ NODE_CLASS_MAPPINGS: dict[str, type] = {
|
||||
"SceneCompilerPromptBuilder": PromptBuilderNode,
|
||||
"SceneCompilerDebugViewer": DebugViewerNode,
|
||||
"SceneCompilerKnowledgeBaseLoader": KnowledgeBaseLoaderNode,
|
||||
"SceneCompilerConfiguration": ConfigurationNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
|
||||
@@ -33,6 +35,7 @@ NODE_DISPLAY_NAME_MAPPINGS: dict[str, str] = {
|
||||
"SceneCompilerPromptBuilder": "Prompt Builder",
|
||||
"SceneCompilerDebugViewer": "Debug Viewer",
|
||||
"SceneCompilerKnowledgeBaseLoader": "Knowledge Base Loader",
|
||||
"SceneCompilerConfiguration": "Configuration",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""ComfyUI node for the compiler Configuration (MASTER_SPEC §20, §23).
|
||||
|
||||
A thin interface: it collects the configuration options as node inputs and emits
|
||||
a single COMPILER_CONFIG that the other nodes consume, so behaviour can change
|
||||
without editing the workflow. No compiler logic; it never imports ComfyUI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from compiler.common.config import Config, ConfigError
|
||||
|
||||
from .adapters import format_messages
|
||||
|
||||
_DEBUG_LEVELS = ["none", "basic", "verbose", "developer"]
|
||||
|
||||
|
||||
class ConfigurationNode:
|
||||
"""Centralizes compiler configuration and emits it for the other nodes."""
|
||||
|
||||
CATEGORY = "Scene Compiler"
|
||||
FUNCTION = "run"
|
||||
RETURN_TYPES = ("COMPILER_CONFIG", "STRING")
|
||||
RETURN_NAMES = ("config", "errors")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"analyzer_model": ("STRING", {"default": "llama3"}),
|
||||
"analyzer_temperature": (
|
||||
"FLOAT",
|
||||
{"default": 0.0, "min": 0.0, "max": 2.0, "step": 0.1},
|
||||
),
|
||||
"analyzer_max_retries": ("INT", {"default": 3, "min": 0, "max": 10}),
|
||||
"analyzer_timeout": ("INT", {"default": 60, "min": 1, "max": 3600}),
|
||||
"knowledge_base": ("STRING", {"default": "knowledge_base/"}),
|
||||
"resolver_strict_mode": ("BOOLEAN", {"default": True}),
|
||||
"resolver_allow_aliases": ("BOOLEAN", {"default": True}),
|
||||
"resolver_expansion_enabled": ("BOOLEAN", {"default": True}),
|
||||
"resolver_max_expansion_depth": ("INT", {"default": 8, "min": 1, "max": 32}),
|
||||
"validator_allow_unknown_fields": ("BOOLEAN", {"default": False}),
|
||||
"prompt_target": ("STRING", {"default": "easy_illustrious"}),
|
||||
"prompt_separator": ("STRING", {"default": ","}),
|
||||
"prompt_trim_empty_outputs": ("BOOLEAN", {"default": True}),
|
||||
"prompt_remove_duplicate_tags": ("BOOLEAN", {"default": True}),
|
||||
"debug_enabled": ("BOOLEAN", {"default": False}),
|
||||
"debug_level": (_DEBUG_LEVELS, {"default": "basic"}),
|
||||
},
|
||||
"optional": {
|
||||
"analyzer_system_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
def run(
|
||||
self,
|
||||
analyzer_model: str,
|
||||
analyzer_temperature: float,
|
||||
analyzer_max_retries: int,
|
||||
analyzer_timeout: int,
|
||||
knowledge_base: str,
|
||||
resolver_strict_mode: bool,
|
||||
resolver_allow_aliases: bool,
|
||||
resolver_expansion_enabled: bool,
|
||||
resolver_max_expansion_depth: int,
|
||||
validator_allow_unknown_fields: bool,
|
||||
prompt_target: str,
|
||||
prompt_separator: str,
|
||||
prompt_trim_empty_outputs: bool,
|
||||
prompt_remove_duplicate_tags: bool,
|
||||
debug_enabled: bool,
|
||||
debug_level: str,
|
||||
analyzer_system_prompt: str = "",
|
||||
) -> tuple[Any, str]:
|
||||
analyzer: dict[str, Any] = {
|
||||
"model": analyzer_model,
|
||||
"temperature": analyzer_temperature,
|
||||
"max_retries": analyzer_max_retries,
|
||||
"timeout": analyzer_timeout,
|
||||
}
|
||||
if analyzer_system_prompt:
|
||||
analyzer["system_prompt"] = analyzer_system_prompt
|
||||
|
||||
document = {
|
||||
"analyzer": analyzer,
|
||||
"resolver": {
|
||||
"knowledge_base": knowledge_base,
|
||||
"strict_mode": resolver_strict_mode,
|
||||
"allow_aliases": resolver_allow_aliases,
|
||||
"expansion_enabled": resolver_expansion_enabled,
|
||||
"max_expansion_depth": resolver_max_expansion_depth,
|
||||
},
|
||||
"validator": {"allow_unknown_fields": validator_allow_unknown_fields},
|
||||
"prompt_builder": {
|
||||
"target": prompt_target,
|
||||
"separator": prompt_separator,
|
||||
"trim_empty_outputs": prompt_trim_empty_outputs,
|
||||
"remove_duplicate_tags": prompt_remove_duplicate_tags,
|
||||
},
|
||||
"debug": {"enabled": debug_enabled, "level": debug_level},
|
||||
}
|
||||
|
||||
try:
|
||||
config = Config.from_json(document)
|
||||
except ConfigError as error:
|
||||
return (None, format_messages([error.message]))
|
||||
return (config, "")
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Tests for the Configuration ComfyUI node (issue #26)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import nodes
|
||||
from compiler.common.config import Config
|
||||
from nodes.configuration_node import ConfigurationNode
|
||||
|
||||
|
||||
def default_inputs() -> dict:
|
||||
return {
|
||||
"analyzer_model": "llama3",
|
||||
"analyzer_temperature": 0.0,
|
||||
"analyzer_max_retries": 3,
|
||||
"analyzer_timeout": 60,
|
||||
"knowledge_base": "knowledge_base/",
|
||||
"resolver_strict_mode": True,
|
||||
"resolver_allow_aliases": True,
|
||||
"resolver_expansion_enabled": True,
|
||||
"resolver_max_expansion_depth": 8,
|
||||
"validator_allow_unknown_fields": False,
|
||||
"prompt_target": "easy_illustrious",
|
||||
"prompt_separator": ",",
|
||||
"prompt_trim_empty_outputs": True,
|
||||
"prompt_remove_duplicate_tags": True,
|
||||
"debug_enabled": False,
|
||||
"debug_level": "basic",
|
||||
}
|
||||
|
||||
|
||||
def test_node_metadata() -> None:
|
||||
assert ConfigurationNode.RETURN_TYPES == ("COMPILER_CONFIG", "STRING")
|
||||
assert ConfigurationNode.RETURN_NAMES == ("config", "errors")
|
||||
required = ConfigurationNode.INPUT_TYPES()["required"]
|
||||
assert "analyzer_model" in required
|
||||
assert "debug_level" in required
|
||||
assert "analyzer_system_prompt" in ConfigurationNode.INPUT_TYPES()["optional"]
|
||||
|
||||
|
||||
def test_default_inputs_produce_default_config() -> None:
|
||||
config, errors = ConfigurationNode().run(**default_inputs())
|
||||
assert isinstance(config, Config)
|
||||
assert config == Config()
|
||||
assert errors == ""
|
||||
|
||||
|
||||
def test_inputs_flow_into_config() -> None:
|
||||
inputs = default_inputs()
|
||||
inputs["analyzer_model"] = "mistral"
|
||||
inputs["resolver_max_expansion_depth"] = 4
|
||||
inputs["validator_allow_unknown_fields"] = True
|
||||
inputs["prompt_separator"] = " | "
|
||||
inputs["debug_enabled"] = True
|
||||
inputs["debug_level"] = "verbose"
|
||||
config, _ = ConfigurationNode().run(**inputs)
|
||||
assert config.analyzer.model == "mistral"
|
||||
assert config.resolver.max_expansion_depth == 4
|
||||
assert config.validator.allow_unknown_fields is True
|
||||
assert config.prompt_builder.separator == " | "
|
||||
assert config.debug.enabled is True
|
||||
assert config.debug.level == "verbose"
|
||||
|
||||
|
||||
def test_system_prompt_override_is_applied() -> None:
|
||||
config, _ = ConfigurationNode().run(**default_inputs(), analyzer_system_prompt="CUSTOM")
|
||||
assert config.analyzer.system_prompt == "CUSTOM"
|
||||
|
||||
|
||||
def test_empty_system_prompt_is_omitted() -> None:
|
||||
config, _ = ConfigurationNode().run(**default_inputs(), analyzer_system_prompt="")
|
||||
assert config.analyzer.system_prompt is None
|
||||
|
||||
|
||||
def test_debug_level_options_match_config_schema() -> None:
|
||||
options = ConfigurationNode.INPUT_TYPES()["required"]["debug_level"][0]
|
||||
for level in options:
|
||||
inputs = default_inputs()
|
||||
inputs["debug_level"] = level
|
||||
config, errors = ConfigurationNode().run(**inputs)
|
||||
assert isinstance(config, Config)
|
||||
assert errors == ""
|
||||
|
||||
|
||||
def test_registered_for_comfyui() -> None:
|
||||
assert nodes.NODE_CLASS_MAPPINGS["SceneCompilerConfiguration"] is ConfigurationNode
|
||||
assert nodes.NODE_DISPLAY_NAME_MAPPINGS["SceneCompilerConfiguration"] == "Configuration"
|
||||
Reference in New Issue
Block a user