""" Response parser for AI narrative responses. This module handles AI response parsing. Game state changes (items, gold, XP) are now handled exclusively through predetermined dice check outcomes in action templates, not through AI-generated JSON. """ from dataclasses import dataclass, field from typing import Any, Optional import structlog logger = structlog.get_logger(__name__) @dataclass class ItemGrant: """ Represents an item granted by the AI during gameplay. The AI can grant items in two ways: 1. By item_id - References an existing item from game data 2. By name/type/description - Creates a generic item """ item_id: Optional[str] = None # For existing items name: Optional[str] = None # For generic items item_type: Optional[str] = None # consumable, weapon, armor, quest_item description: Optional[str] = None value: int = 0 quantity: int = 1 def is_existing_item(self) -> bool: """Check if this references an existing item by ID.""" return self.item_id is not None def is_generic_item(self) -> bool: """Check if this is a generic item created by the AI.""" return self.item_id is None and self.name is not None @dataclass class GameStateChanges: """ Structured game state changes extracted from AI response. These changes are validated and applied to the character after the AI generates its narrative response. """ items_given: list[ItemGrant] = field(default_factory=list) items_taken: list[str] = field(default_factory=list) # item_ids to remove gold_given: int = 0 gold_taken: int = 0 experience_given: int = 0 quest_offered: Optional[str] = None # quest_id quest_completed: Optional[str] = None # quest_id location_change: Optional[str] = None @dataclass class ParsedAIResponse: """ Complete parsed AI response with narrative and game state changes. Attributes: narrative: The narrative text to display to the player game_changes: Structured game state changes to apply raw_response: The original unparsed response from AI parse_success: Whether parsing succeeded parse_errors: Any errors encountered during parsing """ narrative: str game_changes: GameStateChanges raw_response: str parse_success: bool = True parse_errors: list[str] = field(default_factory=list) class ResponseParserError(Exception): """Exception raised when response parsing fails critically.""" pass def parse_ai_response(response_text: str) -> ParsedAIResponse: """ Parse an AI response to extract the narrative text. Game state changes (items, gold, XP) are now handled exclusively through predetermined dice check outcomes, not through AI-generated structured data. Args: response_text: The raw AI response text Returns: ParsedAIResponse with narrative (game_changes will be empty) """ logger.debug("Parsing AI response", response_length=len(response_text)) # Return the full response as narrative # Game state changes come from predetermined check_outcomes, not AI return ParsedAIResponse( narrative=response_text.strip(), game_changes=GameStateChanges(), raw_response=response_text, parse_success=True, parse_errors=[] ) def _parse_game_actions(data: dict[str, Any]) -> GameStateChanges: """ Parse the game actions dictionary into a GameStateChanges object. Args: data: Dictionary from parsed JSON Returns: GameStateChanges object with parsed data """ changes = GameStateChanges() # Parse items_given if "items_given" in data and data["items_given"]: for item_data in data["items_given"]: if isinstance(item_data, dict): item_grant = ItemGrant( item_id=item_data.get("item_id"), name=item_data.get("name"), item_type=item_data.get("type"), description=item_data.get("description"), value=item_data.get("value", 0), quantity=item_data.get("quantity", 1) ) changes.items_given.append(item_grant) elif isinstance(item_data, str): # Simple string format - treat as item_id changes.items_given.append(ItemGrant(item_id=item_data)) # Parse items_taken if "items_taken" in data and data["items_taken"]: changes.items_taken = [ item_id for item_id in data["items_taken"] if isinstance(item_id, str) ] # Parse gold changes changes.gold_given = int(data.get("gold_given", 0)) changes.gold_taken = int(data.get("gold_taken", 0)) # Parse experience changes.experience_given = int(data.get("experience_given", 0)) # Parse quest changes changes.quest_offered = data.get("quest_offered") changes.quest_completed = data.get("quest_completed") # Parse location change changes.location_change = data.get("location_change") return changes