73 lines
3.3 KiB
Python
73 lines
3.3 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Callable
|
|
|
|
from band_aider.ai_client.completion_client import CompletionClient
|
|
from band_aider.ai_client import completion_client
|
|
from band_aider.ai_client.json_guarding_client import JsonGuardingClient
|
|
from band_aider.ai_client.json_response_decoder import strip_json_fence
|
|
from band_aider.context import validation_utils
|
|
from band_aider.judgement.prompt_formatter import PromptFormatterBuilder, format_prompt
|
|
|
|
|
|
def load_json_completion(
|
|
client: CompletionClient,
|
|
prompt: str,
|
|
*,
|
|
prompt_formatter_builder: PromptFormatterBuilder,
|
|
check_name: str = "",
|
|
files: tuple[str, ...] = (),
|
|
references: list[object] | None = None,
|
|
validate: Callable[[Any], object] | None = None,
|
|
) -> Any:
|
|
"""Correct rejected JSON indefinitely before returning an owner-validated value.
|
|
|
|
Owners supply their response shapes and optional semantic validator. Validators
|
|
raise ValueError for correctable output; transport exceptions escape unchanged.
|
|
Existing StabilizationDecision callers retain their own semantic validation.
|
|
"""
|
|
|
|
turn = prompt
|
|
with completion_client.scoped_completion(check_name, files):
|
|
# StabilizationDecision owns its corrections and receives the raw parse failure.
|
|
if references is None and validate is None:
|
|
response = JsonGuardingClient(
|
|
inner=client, prompt_formatter_builder=prompt_formatter_builder,
|
|
).complete(prompt)
|
|
return json.loads(response.strip())
|
|
# No rejected model output can exhaust the judgement's correction allowance.
|
|
while True:
|
|
response = client.complete(turn)
|
|
try:
|
|
# Null responses cannot supply a typed judgement.
|
|
if not isinstance(response, str):
|
|
raise ValueError("Return a JSON response, not an absent completion.")
|
|
data = json.loads(strip_json_fence(response))
|
|
# Admit the declared shape before any owner casts or reads fields.
|
|
if references is not None:
|
|
result = validation_utils.validate_shape(
|
|
data, references, location=validation_utils.ValidationLocation([]),
|
|
)
|
|
if not result.valid:
|
|
raise ValueError(result.stringify())
|
|
# Required explanations cannot be replaced by empty response fields.
|
|
if references is not None and isinstance(data, dict) and "reason" in data and (
|
|
not isinstance(data["reason"], str) or not data["reason"].strip()
|
|
):
|
|
raise ValueError("reason must be a nonempty string.")
|
|
# Owners enforce enum, identity and field-coherence requirements.
|
|
if validate is not None:
|
|
validate(data)
|
|
return data
|
|
except ValueError as error:
|
|
turn = prompt + "\n\n" + format_prompt(
|
|
prompt_formatter_builder, "RESPONSE CORRECTION",
|
|
"Return the complete response required by the original request.\n"
|
|
f"Rejected response:\n{response}\n\nValidation errors:\n{error}",
|
|
escape=False,
|
|
)
|