Add compiler/analyzer/response_parser.py: parse_scene_response(text, logger=None) -> CompilerResult, per MASTER_SPEC §12.2, §12.8. - Strictly json.loads the model response; JSON is never repaired heuristically (§12.9), so malformed or markdown-fenced output fails here (feeding the retry path in #18). - Validates the parsed document against the Scene schema; any failure (malformed JSON, non-object, missing field, wrong type) is reported as SC0011 and stops the compilation -- invalid responses are never silently accepted. - Handles both object and string concept forms via the Scene model; confidence stays metadata-only. - Stamps a default schema_version onto compiler-owned metadata (the model emits `"metadata": {}`) before validation. Tests: valid response -> Scene, metadata stamping, object/string concepts + confidence, and rejection (SC0011) of malformed JSON, non-object JSON, missing fields, wrong types, and markdown-fenced (unrepaired) output. Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.7 KiB
Python
72 lines
2.7 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(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 _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))
|