futurecafe-voice-core / app /intent_schema.py
Eyob-Sol's picture
Upload 41 files
ac1f51b verified
raw
history blame
2.58 kB
# app/intent_schema.py
from __future__ import annotations
from typing import List, Optional, Literal, Union, Tuple
from pydantic import BaseModel, Field, ValidationError
# ---- Canonical intent names ----
IntentName = Literal[
"reservation.create",
"order.create",
"hours.get",
"menu.search",
"smalltalk",
"other",
]
# ---- Slot models ----
class ReservationSlots(BaseModel):
name: Optional[str] = None
party_size: Optional[int] = Field(default=None, ge=1, le=20)
date: Optional[str] = None # ISO preferred (YYYY-MM-DD) or “today”
time: Optional[str] = None # “19:00” or “7 pm”
phone: Optional[str] = None
model_config = {"extra": "ignore"} # tolerate extra keys from LLM
class OrderItem(BaseModel):
name: str
qty: int = Field(default=1, ge=1)
model_config = {"extra": "ignore"}
class OrderSlots(BaseModel):
items: List[OrderItem] = Field(default_factory=list)
notes: Optional[str] = None
model_config = {"extra": "ignore"}
class MenuSlots(BaseModel):
query: Optional[str] = None
dietary: List[str] = Field(default_factory=list) # e.g., ["vegan","gluten-free"]
model_config = {"extra": "ignore"}
# ---- Envelope returned by the router/LLM ----
class IntentEnvelope(BaseModel):
intent: IntentName
need_more_info: bool = False
ask_user: Optional[str] = None # a single, polite follow-up question if info missing
# Keep it loose at the API boundary; we’ll coerce it with helper below.
slots: dict = Field(default_factory=dict)
model_config = {"extra": "ignore"}
# ---- Helpers to validate slots into the right model ----
SlotsUnion = Union[ReservationSlots, OrderSlots, MenuSlots, dict]
def coerce_slots(intent: IntentName, slots: dict | None) -> Tuple[SlotsUnion, Optional[str]]:
"""
Try to convert a loose slots dict into the correct typed model based on 'intent'.
Returns (slots_obj, error_message). If cannot validate, returns (original_dict, message).
This keeps the pipeline resilient while giving you typed access when possible.
"""
raw = slots or {}
try:
if intent == "reservation.create":
return ReservationSlots(**raw), None
if intent == "order.create":
return OrderSlots(**raw), None
if intent == "menu.search":
return MenuSlots(**raw), None
# 'hours.get', 'smalltalk', 'other' often don’t need slots
return raw, None
except ValidationError as ve:
return raw, f"slot_validation_failed: {ve.errors()}"