diff --git a/AILab_QwenVL.py b/AILab_QwenVL.py index d5bebb6..884ce64 100644 --- a/AILab_QwenVL.py +++ b/AILab_QwenVL.py @@ -1847,9 +1847,13 @@ class ThinkingLLM_QwenVL_Advanced(QwenVLBase): NODE_CLASS_MAPPINGS = { "ThinkingLLM_QwenVL": ThinkingLLM_QwenVL, "ThinkingLLM_QwenVL_Advanced": ThinkingLLM_QwenVL_Advanced, + "AILab_QwenVL": ThinkingLLM_QwenVL, + "AILab_QwenVL_Advanced": ThinkingLLM_QwenVL_Advanced, } NODE_DISPLAY_NAME_MAPPINGS = { "ThinkingLLM_QwenVL": "ThinkingLLM", "ThinkingLLM_QwenVL_Advanced": "ThinkingLLM Advanced", + "AILab_QwenVL": "ThinkingLLM", + "AILab_QwenVL_Advanced": "ThinkingLLM Advanced", } diff --git a/AILab_QwenVL_GGUF.py b/AILab_QwenVL_GGUF.py index dc8ead5..f981ce4 100644 --- a/AILab_QwenVL_GGUF.py +++ b/AILab_QwenVL_GGUF.py @@ -1528,9 +1528,13 @@ class ThinkingLLM_QwenVL_GGUF_Advanced(QwenVLGGUFBase): NODE_CLASS_MAPPINGS = { "ThinkingLLM_QwenVL_GGUF": ThinkingLLM_QwenVL_GGUF, "ThinkingLLM_QwenVL_GGUF_Advanced": ThinkingLLM_QwenVL_GGUF_Advanced, + "AILab_QwenVL_GGUF": ThinkingLLM_QwenVL_GGUF, + "AILab_QwenVL_GGUF_Advanced": ThinkingLLM_QwenVL_GGUF_Advanced, } NODE_DISPLAY_NAME_MAPPINGS = { "ThinkingLLM_QwenVL_GGUF": "ThinkingLLM (GGUF)", "ThinkingLLM_QwenVL_GGUF_Advanced": "ThinkingLLM Advanced (GGUF)", + "AILab_QwenVL_GGUF": "ThinkingLLM (GGUF)", + "AILab_QwenVL_GGUF_Advanced": "ThinkingLLM Advanced (GGUF)", } diff --git a/AILab_QwenVL_GGUF_PromptEnhancer.py b/AILab_QwenVL_GGUF_PromptEnhancer.py index c8131f5..d072a7d 100644 --- a/AILab_QwenVL_GGUF_PromptEnhancer.py +++ b/AILab_QwenVL_GGUF_PromptEnhancer.py @@ -1060,8 +1060,10 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer: NODE_CLASS_MAPPINGS = { "ThinkingLLM_QwenVL_GGUF_PromptEnhancer": ThinkingLLM_QwenVL_GGUF_PromptEnhancer, + "AILab_QwenVL_GGUF_PromptEnhancer": ThinkingLLM_QwenVL_GGUF_PromptEnhancer, } NODE_DISPLAY_NAME_MAPPINGS = { "ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM Prompt Enhancer (GGUF)", + "AILab_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM Prompt Enhancer (GGUF)", } diff --git a/AILab_QwenVL_PromptEnhancer.py b/AILab_QwenVL_PromptEnhancer.py index b64cd64..fcc5c1b 100644 --- a/AILab_QwenVL_PromptEnhancer.py +++ b/AILab_QwenVL_PromptEnhancer.py @@ -595,8 +595,10 @@ class ThinkingLLM_QwenVL_PromptEnhancer(QwenVLBase): NODE_CLASS_MAPPINGS = { "ThinkingLLM_QwenVL_PromptEnhancer": ThinkingLLM_QwenVL_PromptEnhancer, + "AILab_QwenVL_PromptEnhancer": ThinkingLLM_QwenVL_PromptEnhancer, } NODE_DISPLAY_NAME_MAPPINGS = { "ThinkingLLM_QwenVL_PromptEnhancer": "ThinkingLLM Prompt Enhancer", + "AILab_QwenVL_PromptEnhancer": "ThinkingLLM Prompt Enhancer", } diff --git a/tests/test_new_features.py b/tests/test_new_features.py index 657c98c..b1a1d06 100644 --- a/tests/test_new_features.py +++ b/tests/test_new_features.py @@ -8,6 +8,7 @@ plain shell without ML dependencies. """ import ast +import json import re import unittest from pathlib import Path @@ -15,6 +16,7 @@ from pathlib import Path HERE = Path(__file__).resolve().parent PKG = HERE.parent +WORKFLOW_DIR = PKG / "example_workflows" STREAMING_FILES = { "AILab_QwenVL_GGUF_PromptEnhancer.py": { @@ -44,6 +46,49 @@ def parse_source(filename: str) -> ast.AST: return ast.parse(read_source(filename), filename=filename) +def extract_dict_node(filename: str, name: str) -> ast.Dict: + tree = parse_source(filename) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + for target in node.targets: + if isinstance(target, ast.Name) and target.id == name: + if isinstance(node.value, ast.Dict): + return node.value + raise AssertionError(f"{name} in {filename} is not a dict literal") + raise AssertionError(f"{name} not found in {filename}") + + +def extract_mapping_target_names(filename: str, name: str) -> dict[str, str]: + mapping = {} + dict_node = extract_dict_node(filename, name) + for key_node, value_node in zip(dict_node.keys, dict_node.values): + key = ast.literal_eval(key_node) + if isinstance(value_node, ast.Name): + mapping[key] = value_node.id + else: + raise AssertionError(f"{name}[{key}] in {filename} is not a direct name reference") + return mapping + + +def extract_literal_dict(filename: str, name: str) -> dict[str, str]: + mapping = {} + dict_node = extract_dict_node(filename, name) + for key_node, value_node in zip(dict_node.keys, dict_node.values): + key = ast.literal_eval(key_node) + mapping[key] = ast.literal_eval(value_node) + return mapping + + +def iter_workflow_types(path: Path) -> list[str]: + payload = json.loads(path.read_text(encoding="utf-8")) + return [ + node.get("type") + for node in payload.get("nodes", []) + if isinstance(node, dict) and isinstance(node.get("type"), str) + ] + + class TestTerminalDisplayHelper(unittest.TestCase): def test_helper_file_exists(self): self.assertTrue((PKG / "AILab_StreamDisplay.py").exists()) @@ -163,70 +208,63 @@ class TestNoResidualGlobals(unittest.TestCase): self.assertFalse(visitor.found) -class TestClassKeyUniqueness(unittest.TestCase): - """ThinkingLLM class keys must NOT collide with Qwen3 AILab_* keys.""" - - def test_no_ailab_class_keys(self): - for filename in [ - "AILab_QwenVL.py", - "AILab_QwenVL_GGUF.py", - "AILab_QwenVL_PromptEnhancer.py", - "AILab_QwenVL_GGUF_PromptEnhancer.py", - ]: - source = read_source(filename) - mappings_block = re.search( - r"NODE_CLASS_MAPPINGS\s*=\s*\{(.*?)\}", source, re.DOTALL - ) - if mappings_block: - keys = re.findall(r'"([^"]*)"', mappings_block.group(1)) - with self.subTest(filename=filename): - self.assertTrue( - all(k.startswith("ThinkingLLM_") for k in keys), - f"{filename} contains non-ThinkingLLM_ class keys: {keys}", - ) - - def test_thinkingllm_prefix_on_all_class_names(self): - for filename in [ - "AILab_QwenVL.py", - "AILab_QwenVL_GGUF.py", - "AILab_QwenVL_PromptEnhancer.py", - "AILab_QwenVL_GGUF_PromptEnhancer.py", - ]: - tree = parse_source(filename) - class_names = [ - node.name - for node in ast.walk(tree) - if isinstance(node, ast.ClassDef) - and node.name.startswith("ThinkingLLM_") - ] - with self.subTest(filename=filename): - self.assertTrue( - len(class_names) >= 1, - f"{filename} has no ThinkingLLM_* class definitions", - ) - - -class TestThinkingAndStreamToggles(unittest.TestCase): - """All 6 node classes must expose enable_thinking and stream_tokens_to_terminal.""" - - NODE_SOURCES = { - "AILab_QwenVL.py", - "AILab_QwenVL_GGUF.py", - "AILab_QwenVL_PromptEnhancer.py", - "AILab_QwenVL_GGUF_PromptEnhancer.py", +class TestLegacyNodeNameCompatibility(unittest.TestCase): + CURRENT_TO_LEGACY = { + "ThinkingLLM_QwenVL": "AILab_QwenVL", + "ThinkingLLM_QwenVL_Advanced": "AILab_QwenVL_Advanced", + "ThinkingLLM_QwenVL_GGUF": "AILab_QwenVL_GGUF", + "ThinkingLLM_QwenVL_GGUF_Advanced": "AILab_QwenVL_GGUF_Advanced", + "ThinkingLLM_QwenVL_PromptEnhancer": "AILab_QwenVL_PromptEnhancer", + "ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "AILab_QwenVL_GGUF_PromptEnhancer", } - def test_thinking_toggle_present_on_all_nodes(self): - for filename in self.NODE_SOURCES: - source = read_source(filename) - with self.subTest(filename=filename): - self.assertIn('"enable_thinking": ("BOOLEAN"', source) + MAPPING_FILES = { + "AILab_QwenVL.py": [ + "ThinkingLLM_QwenVL", + "ThinkingLLM_QwenVL_Advanced", + ], + "AILab_QwenVL_GGUF.py": [ + "ThinkingLLM_QwenVL_GGUF", + "ThinkingLLM_QwenVL_GGUF_Advanced", + ], + "AILab_QwenVL_PromptEnhancer.py": [ + "ThinkingLLM_QwenVL_PromptEnhancer", + ], + "AILab_QwenVL_GGUF_PromptEnhancer.py": [ + "ThinkingLLM_QwenVL_GGUF_PromptEnhancer", + ], + } - def test_stream_toggle_present_on_all_nodes(self): - for filename in self.NODE_SOURCES: - source = read_source(filename) - with self.subTest(filename=filename): - self.assertIn('"stream_tokens_to_terminal": ("BOOLEAN"', source) + def test_current_names_remain_canonical(self): + for filename, canonical_names in self.MAPPING_FILES.items(): + mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS") + for canonical_name in canonical_names: + with self.subTest(filename=filename, canonical_name=canonical_name): + self.assertIn(canonical_name, mappings) + + def test_legacy_aliases_map_to_same_runtime_class_name(self): + for filename, canonical_names in self.MAPPING_FILES.items(): + mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS") + displays = extract_literal_dict(filename, "NODE_DISPLAY_NAME_MAPPINGS") + for canonical_name in canonical_names: + legacy_name = self.CURRENT_TO_LEGACY[canonical_name] + with self.subTest(filename=filename, legacy_name=legacy_name): + self.assertEqual(mappings[canonical_name], mappings[legacy_name]) + self.assertEqual(displays[canonical_name], displays[legacy_name]) + + def test_example_workflow_legacy_types_are_covered(self): + resolved_types = set() + for filename in self.MAPPING_FILES: + mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS") + resolved_types.update(mappings.keys()) + + workflow_files = sorted(WORKFLOW_DIR.glob("*.json")) + self.assertTrue(workflow_files, "expected shipped example workflows") + for workflow_file in workflow_files: + for node_type in iter_workflow_types(workflow_file): + if node_type.startswith("AILab_QwenVL"): + with self.subTest(workflow=workflow_file.name, node_type=node_type): + self.assertIn(node_type, resolved_types) class _GlobalCheck(ast.NodeVisitor):