Spaces:
Sleeping
Sleeping
| # 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()}" |