# --- Pydantic Models for GPC 3.0 --- from typing import List, Optional, Union, Literal, Any, Dict, ForwardRef, Annotated from pydantic import BaseModel, Field, validator, root_validator # --- Forward References for Recursive Predicates --- # Define ForwardRefs for types used before their full definition NaryQueryPredicateRef = ForwardRef("NaryQueryPredicate") UnaryQueryPredicateRef = ForwardRef("UnaryQueryPredicate") AttributeQueryPredicateRef = ForwardRef("AttributeQueryPredicate") SelectionQueryPredicateRef = ForwardRef("SelectionQueryPredicate") # --- Base Models & Enums --- class BaseGPCModel(BaseModel): """Base class for all GPC models.""" class Config: allow_population_by_field_name = True arbitrary_types_allowed = True extra = "allow" # Allow extra fields not in the model class User(BaseGPCModel): """User information.""" id: Optional[str] = None access_token: Optional[str] = None class Location(BaseGPCModel): """Location information.""" originating_locale: Optional[str] = Field(None, alias="originatingLocale") country_code: Optional[str] = Field(None, alias="countryCode") class Advertising(BaseGPCModel): """Advertising information.""" limit_ad_tracking: Optional[bool] = Field(None, alias="limitAdTracking") advertising_id: Optional[str] = Field(None, alias="advertisingId") class RequestContext(BaseGPCModel): """Request context.""" user: Optional[User] = None location: Optional[Location] = None advertising: Optional[Advertising] = None class ResolvedEntity(BaseGPCModel): """Represents a single potential resolution for an entity mentioned.""" entity_id: str = Field( ..., description="Skill-provided canonical identifier for this specific entity (e.g., artist ID, track ID).", ) entity_name: Optional[str] = Field( None, description="Optional: Human-readable name (e.g., 'Taylor Swift'). Useful for logging/debugging.", ) confidence: Optional[float] = Field( None, description="Optional: Alexa's confidence in this specific resolution (0.0-1.0).", ) class ResolvedSelectionCriteriaAttribute(BaseGPCModel): """ Represents a single piece of information (attribute) extracted from the user's utterance. Can represent catalog entities, media types, sort orders etc. """ id: str = Field( ..., description="Unique identifier for this attribute instance within the current request. Used for referencing in predicates.", ) type: str = Field( ..., description="The type of attribute (e.g., 'ARTIST', 'ALBUM', 'GENRE', 'MEDIA_TYPE', 'SORT', 'PLAYLIST', 'TRACK', 'STATION', 'PROGRAM_SERIES', 'PROGRAM', 'RADIO', 'BOOK', 'AUTHOR').", ) raw_value: Optional[str] = Field( None, description="The raw text spoken by the user that mapped to this attribute (e.g., 'Taylor'). Replaces deprecated RawSelectionCriteria.", ) value: Optional[str] = Field( None, description="For non-catalog attributes like MEDIA_TYPE ('SONGS', 'ALBUMS') or SORT ('POPULARITY', 'RECENT').", ) resolved_entities: Optional[List[ResolvedEntity]] = Field( None, description="For catalog attributes (ARTIST, ALBUM etc.): A ranked list of possible entity resolutions. The first item is the most likely.", ) @validator("type") def check_type_known(cls, v): # Add known types for better validation if desired, but keeping it flexible # known_types = {'ARTIST', 'ALBUM', ...} # if v not in known_types: print(f"Warning: Unknown attribute type: {v}") return v # --- Predicate Models (for building the Query Tree) --- class BaseQueryPredicate(BaseGPCModel): """Base class for all predicate types.""" type: Literal["NARY", "UNARY", "ATTRIBUTE"] = Field( ..., description="Discriminator field for the predicate type." ) class NaryQueryPredicate(BaseQueryPredicate): """Combines multiple sub-predicates with UNION or INTERSECTION logic.""" type: Literal["NARY"] = Field( "NARY", description="Indicates this is an N-ary predicate." ) condition: Literal["UNION", "INTERSECTION"] = Field( ..., description="How to combine the sub-predicates. UNION (OR logic), INTERSECTION (AND logic).", ) predicates: List["SelectionQueryPredicate"] = Field( ..., description="List of sub-predicates to combine." ) class UnaryQueryPredicate(BaseQueryPredicate): """Applies a condition (modifier) like SIMILAR or NOT to a single sub-predicate.""" type: Literal["UNARY"] = Field( "UNARY", description="Indicates this is a Unary predicate." ) condition: Literal["SIMILAR", "NOT"] = Field( ..., description="Condition to apply. SIMILAR (find related items), NOT (exclude items).", ) predicate: "SelectionQueryPredicate" = Field( ..., description="The sub-predicate being modified." ) class AttributeQueryPredicate(BaseQueryPredicate): """A leaf node in the query tree, referencing a specific attribute by its ID.""" type: Literal["ATTRIBUTE"] = Field( "ATTRIBUTE", description="Indicates this is an Attribute predicate (leaf node)." ) attribute_id: str = Field( ..., alias="attributeId", description="The 'id' of the ResolvedSelectionCriteriaAttribute to use.", ) # --- Union type for predicates --- SelectionQueryPredicate = Union[ NaryQueryPredicate, UnaryQueryPredicate, AttributeQueryPredicate ] # Update forward references now that all models are defined # NaryQueryPredicate.model_rebuild() # UnaryQueryPredicate.model_rebuild() # AttributeQueryPredicate needs no forward refs # SelectionQueryPredicate doesn't need model_rebuild itself, its members do # --- Action Model --- class GPCAction(BaseGPCModel): """Represents an explicit action requested within the payload.""" type: Literal["GENERATE_CONTENT"] # Add other potential action types if needed # --- Polymorphic Selection Criteria Models --- class BaseResolvedSelectionCriteria(BaseGPCModel): """Base for the different ways Alexa interprets the user query.""" id: str = Field( ..., description="Unique identifier for this specific interpretation of the user's query.", ) type: Literal["ATTRIBUTES", "NL_QUERY"] = Field( ..., description="Discriminator field for the criteria type." ) class Completeness(BaseGPCModel): """Indicates how well a MultiAttributeSelectionCriteria captures user intent.""" bin: Literal["HIGH", "MEDIUM", "LOW"] = Field( ..., description="Categorical assessment of completeness." ) score: Optional[float] = Field( None, description="Numerical score (0.0-1.0) indicating completeness. Higher is better.", ) class MultiAttributeSelectionCriteria(BaseResolvedSelectionCriteria): """Structured interpretation using attributes and optional query predicates.""" type: Literal["ATTRIBUTES"] = Field( "ATTRIBUTES", description="Indicates structured attribute-based criteria." ) attributes: List[ResolvedSelectionCriteriaAttribute] = Field( ..., description="List of attributes extracted. Relationships defined by the 'query' field (or default INTERSECTION).", ) query: Optional[SelectionQueryPredicate] = Field( None, description="Optional predicate tree defining relationships (INTERSECTION, UNION, SIMILAR, NOT) between attributes. If None, default behavior is INTERSECTION of all attributes.", ) completeness: Optional[Completeness] = Field( None, description="How well this structured interpretation captures the full user intent.", ) request_context: Optional[RequestContext] = Field( None, description="The request context associated with this selection criteria.", ) class NaturalLanguageSelectionCriteria(BaseResolvedSelectionCriteria): """Free-form text interpretation of the user query.""" type: Literal["NL_QUERY"] = Field( "NL_QUERY", description="Indicates natural language query criteria." ) query: str = Field( ..., description="The natural language query string generated by Alexa (not the raw user utterance). Use this with your own NLU/search if supported.", ) # --- Union type for the ranked list items --- # Use Annotated and Field discriminator for robust type differentiation ResolvedSelectionCriteria = Annotated[ Union[MultiAttributeSelectionCriteria, NaturalLanguageSelectionCriteria], Field(discriminator="type"), ] # --- Request Header Model --- class GPCRequestHeader(BaseGPCModel): """Structure of the 'header' object.""" message_id: str = Field(..., alias="messageId") namespace: Literal["Alexa.Media.Search"] name: Literal["GetPlayableContent", "GetDisplayableContent"] payload_version: Literal["3.0"] = Field(..., alias="payloadVersion") # --- Filters Model --- class Filters(BaseGPCModel): """Placeholder for filters - add relevant fields.""" explicit_content_filter: Optional[bool] = Field( None, alias="explicitLanguageAllowed" ) # Corrected alias and type based on example # TODO: Add other filters like 'territory', 'musicStreamingProvider' etc. if used # --- Main Request Payload Model --- class GPCRequestPayload(BaseGPCModel): """Structure of the 'payload' object in GetPlayableContent/GetDisplayableContent v3.0 requests.""" ranked_selection_criteria: List[ResolvedSelectionCriteria] = Field( ..., alias="rankedSelectionCriteria", description="Ranked list of interpretations of the user's query. Process these in order.", ) request_context: Optional[RequestContext] = Field(None, alias="requestContext") filters: Optional[Filters] = Field(None) action: Optional[GPCAction] = Field( None, description="Optional action requested, e.g., generate content." ) # Added action field # Fields specific to GetDisplayableContent max_result_limit: Optional[int] = Field(None, alias="maxResultLimit") play_queue_preview_criteria: Optional[Any] = Field( None, alias="playQueuePreviewCriteria" ) # Define if needed endpoints: Optional[List[Any]] = Field(None) # Define if needed # Other observed fields (optional, add if needed for logic) policies: Optional[Any] = Field(None) response_options: Optional[Any] = Field(None, alias="responseOptions") raw_text: Optional[str] = Field(None, alias="rawText") experience: Optional[Any] = Field(None) # --- Full GPC Request Model --- class GPCRequest(BaseGPCModel): """The complete incoming request object for GPC v3.0.""" header: GPCRequestHeader payload: GPCRequestPayload # --- Pydantic Models for GPC 3.0 Response Payload --- class MatchedCriteria(BaseGPCModel): """Included in the response to indicate which criteria interpretation was used.""" criteria_id: str = Field( ..., alias="criteriaId", description="The 'id' of the ResolvedSelectionCriteria object from the request that you used to generate the response content.", ) class GPCResponsePayload(BaseGPCModel): """Base structure for V3.0 response payloads.""" content: Optional[Any] # For GetPlayableContent - Define your content structure content_groups: Optional[ Any ] # For GetDisplayableContent - Define your content structure matched_criteria: Optional[MatchedCriteria] = Field( None, alias="matchedCriteria", description="Reference to the request criteria used.", ) # Removed Config from here as it's inherited from BaseGPCModel # Example specific response (adapt content structure as needed) class GetPlayableContentResponsePayload(GPCResponsePayload): content: List[Dict[str, Any]] # Replace Any with your actual Content object model class GetDisplayableContentResponsePayload(GPCResponsePayload): content_groups: List[ Dict[str, Any] ] # Replace Any with your actual ContentGroup object model