Implement AI orchestration wedding demo
This commit is contained in:
3
backend/app/validation/README.md
Normal file
3
backend/app/validation/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Validation
|
||||
|
||||
Schema validation and policy enforcement for planner decisions before any action enters pending review.
|
||||
1
backend/app/validation/__init__.py
Normal file
1
backend/app/validation/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Validation and policy services."""
|
||||
35
backend/app/validation/action_validator.py
Normal file
35
backend/app/validation/action_validator.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.domain.schemas import PlannerDecision, ValidationResult
|
||||
from app.ai.tool_registry import ToolRegistry
|
||||
|
||||
|
||||
class ActionValidator:
|
||||
def __init__(self, registry: ToolRegistry) -> None:
|
||||
self.registry = registry
|
||||
|
||||
def validate(self, decision: PlannerDecision) -> ValidationResult:
|
||||
if decision.intent == "no_action":
|
||||
return ValidationResult(valid=False, errors=["No actionable intent detected."])
|
||||
if not decision.tool:
|
||||
return ValidationResult(valid=False, errors=["Planner did not select a tool."])
|
||||
tool = self.registry.get(decision.tool)
|
||||
if not tool:
|
||||
return ValidationResult(valid=False, errors=[f"Unknown tool: {decision.tool}"])
|
||||
try:
|
||||
Draft202012Validator(tool.schema, format_checker=FormatChecker()).validate(decision.arguments)
|
||||
except Exception as exc:
|
||||
return ValidationResult(valid=False, errors=[str(exc)])
|
||||
extra = set(decision.arguments) - set(tool.schema.get("properties", {}))
|
||||
if extra:
|
||||
return ValidationResult(valid=False, errors=[f"Hallucinated parameters: {sorted(extra)}"])
|
||||
try:
|
||||
self._validate_dates(decision.arguments)
|
||||
except ValidationError as exc:
|
||||
return ValidationResult(valid=False, errors=[str(exc)])
|
||||
return ValidationResult(valid=True)
|
||||
|
||||
def _validate_dates(self, args: dict) -> None:
|
||||
# JSON Schema catches shape; FastAPI/Pydantic validates API models at execution time.
|
||||
return None
|
||||
22
backend/app/validation/policy_engine.py
Normal file
22
backend/app/validation/policy_engine.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from app.config import get_settings
|
||||
from app.domain.schemas import PlannerDecision, PolicyResult
|
||||
|
||||
|
||||
class PolicyEngine:
|
||||
def __init__(self, existing_payloads: list[dict] | None = None) -> None:
|
||||
self.threshold = get_settings().planner_confidence_threshold
|
||||
self.existing_payloads = existing_payloads or []
|
||||
|
||||
def evaluate(self, decision: PlannerDecision) -> PolicyResult:
|
||||
reasons: list[str] = []
|
||||
if decision.confidence < self.threshold:
|
||||
reasons.append(f"Confidence {decision.confidence:.2f} is below threshold {self.threshold:.2f}.")
|
||||
if decision.arguments in self.existing_payloads:
|
||||
reasons.append("Duplicate pending action payload.")
|
||||
if decision.tool == "create_guest_reservation" and not decision.arguments.get("event_date"):
|
||||
reasons.append("Reservation requires a date.")
|
||||
if decision.tool == "create_guest_reservation" and not decision.arguments.get("partner_id"):
|
||||
reasons.append("Reservation requires a Wed partner_id.")
|
||||
if decision.tool == "create_task" and not decision.arguments.get("title"):
|
||||
reasons.append("Task requires a title.")
|
||||
return PolicyResult(allowed=not reasons, reasons=reasons)
|
||||
Reference in New Issue
Block a user