honcho-api / src /llm /structured_output.py
rrizwan98
Honcho self-hosted deployment for HF Spaces
66227af
Raw
History Blame Contribute Delete
4.08 kB
from __future__ import annotations
import json
from collections.abc import Awaitable, Callable
from typing import Literal
from pydantic import BaseModel, ValidationError
from src.utils.json_parser import validate_and_repair_json
from src.utils.representation import PromptRepresentation
from .backend import CompletionResult
StructuredOutputFailurePolicy = Literal[
"raise",
"repair_then_raise",
"repair_then_empty",
]
class StructuredOutputError(ValueError):
"""Raised when structured output cannot be validated or repaired."""
def repair_response_model_json(
raw_content: str,
response_model: type[BaseModel],
_model: str,
) -> BaseModel:
"""Repair truncated or malformed JSON and validate against the response model."""
try:
final = validate_and_repair_json(raw_content)
repaired_data = json.loads(final)
if (
response_model is PromptRepresentation
and "deductive" in repaired_data
and isinstance(repaired_data["deductive"], list)
):
for item in repaired_data["deductive"]:
if isinstance(item, dict):
if "conclusion" not in item and "premises" in item:
if item["premises"]:
item["conclusion"] = (
f"[Incomplete reasoning from premises: {item['premises'][0][:100]}...]"
)
else:
item["conclusion"] = (
"[Incomplete reasoning - conclusion missing]"
)
if "premises" not in item:
item["premises"] = []
final = json.dumps(repaired_data)
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
final = ""
try:
return response_model.model_validate_json(final)
except ValidationError:
if response_model is PromptRepresentation:
return PromptRepresentation(explicit=[])
raise
def validate_structured_output(
content: object,
response_model: type[BaseModel],
) -> BaseModel:
if isinstance(content, response_model):
return content
if isinstance(content, str):
return response_model.model_validate_json(content)
if isinstance(content, dict):
return response_model.model_validate(content)
raise StructuredOutputError(
f"Unsupported structured output payload: {type(content).__name__}"
)
def attempt_structured_output_repair(
content: object,
response_model: type[BaseModel],
model: str,
) -> BaseModel | None:
if not isinstance(content, str):
return None
try:
return repair_response_model_json(content, response_model, model)
except (StructuredOutputError, ValidationError):
return None
def empty_structured_output(response_model: type[BaseModel]) -> BaseModel:
if response_model is PromptRepresentation:
return PromptRepresentation(explicit=[])
return response_model.model_validate({})
async def execute_structured_output_call(
executor: Callable[[], Awaitable[CompletionResult]],
*,
response_model: type[BaseModel],
model_name: str,
failure_policy: StructuredOutputFailurePolicy = "repair_then_raise",
) -> CompletionResult:
result = await executor()
try:
result.content = validate_structured_output(result.content, response_model)
return result
except (StructuredOutputError, ValidationError):
if failure_policy == "raise":
raise
repaired = attempt_structured_output_repair(
result.content,
response_model,
model_name,
)
if repaired is not None:
result.content = repaired
return result
if failure_policy == "repair_then_empty":
result.content = empty_structured_output(response_model)
return result
raise StructuredOutputError(
f"Failed to produce valid structured output for {model_name}"
)