diff --git a/nodes/json_builder.py b/nodes/json_builder.py new file mode 100644 index 0000000..dd72cb1 --- /dev/null +++ b/nodes/json_builder.py @@ -0,0 +1,66 @@ +import json +import re +from typing import Tuple, Optional + + +class JSONBuilder: + """ + Build JSON object from multiple key-value inputs. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "optional": { + "key_1": ("STRING", {"default": ""}), + "value_1": ("STRING", {"default": "", "forceInput": True}), + "key_2": ("STRING", {"default": ""}), + "value_2": ("STRING", {"default": "", "forceInput": True}), + "key_3": ("STRING", {"default": ""}), + "value_3": ("STRING", {"default": "", "forceInput": True}), + "key_4": ("STRING", {"default": ""}), + "value_4": ("STRING", {"default": "", "forceInput": True}), + "key_5": ("STRING", {"default": ""}), + "value_5": ("STRING", {"default": "", "forceInput": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("json_string",) + FUNCTION = "build" + CATEGORY = "🚦ComfyUI_LLMs_Toolkit/JSON" + + def build( + self, + key_1: str = "", value_1: str = "", + key_2: str = "", value_2: str = "", + key_3: str = "", value_3: str = "", + key_4: str = "", value_4: str = "", + key_5: str = "", value_5: str = "" + ) -> Tuple[str]: + """Build JSON from key-value pairs.""" + result = {} + + pairs = [ + (key_1, value_1), (key_2, value_2), (key_3, value_3), + (key_4, value_4), (key_5, value_5) + ] + + for key, value in pairs: + if key and key.strip(): + # Try to parse value as JSON, otherwise use as string + try: + parsed_value = json.loads(value) + result[key.strip()] = parsed_value + except (json.JSONDecodeError, TypeError): + result[key.strip()] = value + + json_str = json.dumps(result, ensure_ascii=False, indent=2) + print(f"# [🚦 LLMs_Toolkit] built JSON with {len(result)} keys") + return (json_str,) + + +# Register the node +NODE_CLASS_MAPPINGS = {"JSONBuilder": JSONBuilder} +NODE_DISPLAY_NAME_MAPPINGS = {"JSONBuilder": "JSON Builder"} diff --git a/nodes/json_extractor.py b/nodes/json_extractor.py index 14ec0bd..18ef792 100644 --- a/nodes/json_extractor.py +++ b/nodes/json_extractor.py @@ -12,8 +12,7 @@ class JSONExtractor: return { "required": { "json_string": ("STRING", { - "multiline": True, - "default": "{}" + "forceInput": True }), "key": ("STRING", { "default": "", diff --git a/nodes/json_fixer.py b/nodes/json_fixer.py new file mode 100644 index 0000000..02d67a0 --- /dev/null +++ b/nodes/json_fixer.py @@ -0,0 +1,111 @@ +import json +import re +from typing import Tuple + + +class JSONFixer: + """ + Fix malformed JSON from LLM outputs. + + Common issues fixed: + - Trailing commas + - Single quotes instead of double quotes + - Unquoted keys + - Missing quotes around string values + - Markdown code block wrappers + - Extra text before/after JSON + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"forceInput": True}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("fixed_json",) + FUNCTION = "fix" + CATEGORY = "🚦ComfyUI_LLMs_Toolkit/JSON" + + def fix(self, text: str) -> Tuple[str]: + """ + Attempt to fix malformed JSON. + + Args: + text: Raw text that should contain JSON + + Returns: + Fixed JSON string, or error message if unfixable + """ + original = text + + # Step 1: Try direct parse first (already valid) + try: + parsed = json.loads(text) + print(f"# [🚦 LLMs_Toolkit] JSON already valid") + return (json.dumps(parsed, ensure_ascii=False),) + except json.JSONDecodeError: + pass + + # Step 2: Extract JSON from markdown code blocks + code_block_pattern = r"```(?:json)?\s*([\s\S]*?)```" + matches = re.findall(code_block_pattern, text) + if matches: + text = matches[0].strip() + + # Step 3: Find JSON-like content (starts with { or [) + json_pattern = r'(\{[\s\S]*\}|\[[\s\S]*\])' + json_matches = re.findall(json_pattern, text) + if json_matches: + # Try each match + for match in json_matches: + text = match.strip() + try: + parsed = json.loads(text) + print(f"# [🚦 LLMs_Toolkit] extracted JSON from text") + return (json.dumps(parsed, ensure_ascii=False),) + except json.JSONDecodeError: + pass + + # Step 4: Apply common fixes + fixed = text + + # Remove trailing commas before } or ] + fixed = re.sub(r',\s*([}\]])', r'\1', fixed) + + # Replace single quotes with double quotes (careful with apostrophes) + # Only replace quotes that look like JSON string delimiters + fixed = re.sub(r"'([^']*)':", r'"\1":', fixed) # Keys + fixed = re.sub(r":\s*'([^']*)'([,}\]])", r': "\1"\2', fixed) # Values + + # Try parse after fixes + try: + parsed = json.loads(fixed) + print(f"# [🚦 LLMs_Toolkit] fixed JSON (trailing commas, quotes)") + return (json.dumps(parsed, ensure_ascii=False),) + except json.JSONDecodeError as e: + pass + + # Step 5: Last resort - try to find any valid JSON substring + for i in range(len(original)): + if original[i] in '{[': + for j in range(len(original), i, -1): + try: + candidate = original[i:j] + parsed = json.loads(candidate) + print(f"# [🚦 LLMs_Toolkit] extracted valid JSON substring") + return (json.dumps(parsed, ensure_ascii=False),) + except json.JSONDecodeError: + continue + + # Failed to fix + error_msg = f"Unable to fix JSON: {str(e)}" + print(f"# [🚦 LLMs_Toolkit] ✗ {error_msg}") + return (error_msg,) + + +# Register the node +NODE_CLASS_MAPPINGS = {"JSONFixer": JSONFixer} +NODE_DISPLAY_NAME_MAPPINGS = {"JSONFixer": "JSON Fixer"}