Files
kunail0804-ComfyUI-Scene-Co…/compiler/analyzer/response_parser.py
T
c967327e8f Fix ComfyUI runtime issues: fenced-JSON responses + upstream-failure crash (#77)
* fix(nodes): handle a failed upstream stage without crashing

When a stage fails it passes None down its data output. Downstream nodes called
`.to_json()` / iterated the None and raised an unhelpful AttributeError (found in
ComfyUI: a failed Scene Analyzer crashed the Scene Validator on
`None.to_json()`).

Each consuming node now short-circuits on a missing required input: the
Validator, Resolver, and Category Splitter return None plus an error string that
points at the upstream node's errors output; the Prompt Builder returns empty
strings. Adds a shared upstream_failure_message helper and regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(analyzer): unwrap Markdown code fences in model responses

Most local models (e.g. gemma) return valid Scene JSON wrapped in a ```json
... ``` code fence despite the system prompt forbidding markdown. Strict
json.loads then rejected it (SC0011), and at temperature 0 every retry returned
the identical fenced output, so the Analyzer always failed and produced a None
scene.

The response parser now unwraps a leading/trailing code fence before parsing.
This is not the heuristic JSON repair forbidden by MASTER_SPEC §12.9: the JSON
inside is left byte-for-byte unchanged; only the surrounding ``` markers are
removed. Malformed JSON inside a fence is still rejected.

Verified end-to-end against a live Ollama (gemma): the fenced response now parses
and the full pipeline produces prompt outputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:38:17 +02:00

90 lines
3.4 KiB
Python

"""Parsing and validation of the Analyzer's model response (MASTER_SPEC §12.2, §12.8).
Turns the raw model response text into a validated :class:`Scene`. The response
MUST be a single valid JSON document conforming to the Scene schema; invalid
output MUST NOT continue through the compiler.
JSON is never repaired heuristically (§12.9): malformed or markdown-wrapped output
fails here, which the retry orchestration (issue #18) uses to request a corrected
response. Any parse or validation failure is reported as SC0011.
"""
from __future__ import annotations
import json
from typing import Any
from compiler.common.log import StructuredLogger
from compiler.common.message_codes import message
from compiler.common.result import CompilerResult
from schemas.models import Scene
from schemas.validation import validate_document
# The Scene schema version stamped onto compiler-owned metadata when the model
# leaves it empty (the model is instructed to emit `"metadata": {}`).
SCENE_SCHEMA_VERSION = "1.0"
def parse_scene_response(text: str, logger: StructuredLogger | None = None) -> CompilerResult:
"""Parse and validate an Analyzer response into a Scene.
Returns:
A CompilerResult whose ``data`` is the validated :class:`Scene` on
success, or ``None`` with an SC0011 error when the response is not a
valid Scene JSON document.
"""
try:
data = json.loads(_strip_code_fence(text))
except json.JSONDecodeError as exc:
return _reject("json_decode", f"Analyzer response is not valid JSON: {exc}", logger)
if not isinstance(data, dict):
return _reject(
"not_object",
f"Analyzer response must be a JSON object, got {type(data).__name__}.",
logger,
)
_stamp_metadata(data)
issues = validate_document(data, "scene")
if issues:
detail = "; ".join(f"{issue.path or '<root>'}: {issue.message}" for issue in issues)
return _reject("schema", f"Analyzer response failed Scene validation: {detail}", logger)
scene = Scene.from_json(data)
if logger is not None:
logger.verbose("analyzer_response_parsed", characters=len(scene.characters))
return CompilerResult(data=scene)
def _strip_code_fence(text: str) -> str:
"""Unwrap a Markdown code fence around the response, if present.
Most local models wrap their JSON in a ```json ... ``` (or plain ```) fence
despite being told not to. Unwrapping the fence is not JSON repair (§12.9):
the JSON inside is left byte-for-byte unchanged; only the surrounding code-
block markers are removed. Text without a fence is returned unchanged.
"""
stripped = text.strip()
if not stripped.startswith("```"):
return text
lines = stripped.splitlines()
lines = lines[1:] # drop the opening ``` / ```json line
if lines and lines[-1].strip() == "```":
lines = lines[:-1] # drop the closing ``` line
return "\n".join(lines)
def _stamp_metadata(data: dict[str, Any]) -> None:
"""Ensure compiler-owned metadata carries a schema_version before validation."""
metadata = data.get("metadata")
if isinstance(metadata, dict):
metadata.setdefault("schema_version", SCENE_SCHEMA_VERSION)
def _reject(kind: str, description: str, logger: StructuredLogger | None) -> CompilerResult:
if logger is not None:
logger.basic("analyzer_response_rejected", kind=kind)
return CompilerResult().add_error(message("SC0011", description, kind=kind))