band-aider-pub/band_aider/ai_client/json_completion.py
2026-10-17 11:57:37 -07:00

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,
)