first commit
This commit is contained in:
160
api/app/ai/response_parser.py
Normal file
160
api/app/ai/response_parser.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user