Validate structured utility outputs

This commit is contained in:
2026-05-22 07:48:01 +08:00
parent 8452673994
commit eef010f227
7 changed files with 141 additions and 17 deletions
+13 -15
View File
@@ -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(
+7 -1
View File
@@ -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,
+38
View File
@@ -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