Validate structured utility outputs
This commit is contained in:
+13
-15
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from duck_core.structured_output import load_json_object, validate_json_object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -124,26 +125,23 @@ class MemoryPolicy:
|
||||
|
||||
def _parse_response(self, content: str, summary: str, task_id: str) -> MemoryDecision:
|
||||
try:
|
||||
data = json.loads(content)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data = validate_json_object(
|
||||
load_json_object(content, "memory policy"),
|
||||
self._RESPONSE_SCHEMA,
|
||||
"memory policy",
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.warning("MemoryPolicy: invalid JSON for %s: %s", task_id, content[:200])
|
||||
return MemoryDecision(
|
||||
should_store=False,
|
||||
memory_type="event",
|
||||
summary=summary,
|
||||
importance=0.0,
|
||||
metadata={"task_id": task_id, "source": "llm_policy_fallback"},
|
||||
)
|
||||
|
||||
required = ("should_store", "memory_type", "summary", "importance", "scope")
|
||||
if not all(key in data for key in required):
|
||||
logger.warning("MemoryPolicy: missing fields for %s: %s", task_id, list(data.keys()))
|
||||
return MemoryDecision(
|
||||
should_store=False,
|
||||
memory_type="event",
|
||||
summary=summary,
|
||||
importance=0.0,
|
||||
metadata={"task_id": task_id, "source": "llm_policy_fallback"},
|
||||
metadata={
|
||||
"task_id": task_id,
|
||||
"source": "llm_policy_fallback",
|
||||
"error": str(exc),
|
||||
},
|
||||
)
|
||||
|
||||
return MemoryDecision(
|
||||
|
||||
@@ -12,11 +12,13 @@ from duck_core.memory.store import MemoryStore
|
||||
from duck_core.memory.vector_memory import VectorMemory
|
||||
from duck_core.model_client import ModelClient, ReasoningMode
|
||||
from duck_core.reflection import Reflection
|
||||
from duck_core.structured_output import load_json_object, load_json_schema, validate_json_object
|
||||
from duck_core.tasks.store import TaskStore
|
||||
from duck_core.tools.base import ToolResult
|
||||
from duck_core.tools.gateway import ToolGateway
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ACTION_DIRECTIVE_SCHEMA = load_json_schema("duck_core/schemas/action_directive.schema.json")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -415,7 +417,11 @@ class RuntimeLoop:
|
||||
try:
|
||||
await self.event_store.append(task_id, "model_call_started", {"role": "action"})
|
||||
response = await self.model_client.chat("action", messages)
|
||||
directive = json.loads(response.content)
|
||||
directive = validate_json_object(
|
||||
load_json_object(response.content, "action directive"),
|
||||
ACTION_DIRECTIVE_SCHEMA,
|
||||
"action directive",
|
||||
)
|
||||
except Exception as exc:
|
||||
await self.event_store.append(
|
||||
task_id,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
|
||||
class StructuredOutputError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def load_json_object(content: str, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
data = json.loads(content)
|
||||
except (json.JSONDecodeError, TypeError) as exc:
|
||||
raise StructuredOutputError(f"{label}: invalid JSON: {exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
raise StructuredOutputError(f"{label}: expected JSON object")
|
||||
return data
|
||||
|
||||
|
||||
def load_json_schema(path: str | Path) -> dict[str, Any]:
|
||||
return json.loads(Path(path).read_text())
|
||||
|
||||
|
||||
def validate_json_object(
|
||||
data: dict[str, Any],
|
||||
schema: dict[str, Any],
|
||||
label: str,
|
||||
) -> dict[str, Any]:
|
||||
errors = sorted(Draft202012Validator(schema).iter_errors(data), key=lambda error: error.path)
|
||||
if errors:
|
||||
first = errors[0]
|
||||
location = ".".join(str(part) for part in first.absolute_path) or "<root>"
|
||||
raise StructuredOutputError(f"{label}: schema violation at {location}: {first.message}")
|
||||
return data
|
||||
Reference in New Issue
Block a user