first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
"""
Action Prompt Loader Service
This module provides a service for loading and filtering action prompts from YAML.
It implements a singleton pattern to cache loaded prompts in memory.
Usage:
from app.services.action_prompt_loader import ActionPromptLoader
loader = ActionPromptLoader()
loader.load_from_yaml("app/data/action_prompts.yaml")
# Get available actions for a user at a location
actions = loader.get_available_actions(
user_tier=UserTier.FREE,
location_type=LocationType.TOWN
)
# Get specific action
action = loader.get_action_by_id("ask_locals")
"""
import os
from typing import List, Optional, Dict
import yaml
from app.models.action_prompt import ActionPrompt, LocationType
from app.ai.model_selector import UserTier
from app.utils.logging import get_logger
# Initialize logger
logger = get_logger(__file__)
class ActionPromptLoaderError(Exception):
"""Base exception for action prompt loader errors."""
pass
class ActionPromptNotFoundError(ActionPromptLoaderError):
"""Raised when a requested action prompt is not found."""
pass
class ActionPromptLoader:
"""
Service for loading and filtering action prompts.
This class loads action prompts from YAML files and provides methods
to filter them based on user tier and location type.
Uses singleton pattern to cache loaded prompts in memory.
Attributes:
_prompts: Dictionary of loaded action prompts keyed by prompt_id
_loaded: Flag indicating if prompts have been loaded
"""
_instance = None
_prompts: Dict[str, ActionPrompt] = {}
_loaded: bool = False
def __new__(cls):
"""Implement singleton pattern."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._prompts = {}
cls._instance._loaded = False
return cls._instance
def load_from_yaml(self, filepath: str) -> int:
"""
Load action prompts from a YAML file.
Args:
filepath: Path to the YAML file
Returns:
Number of prompts loaded
Raises:
ActionPromptLoaderError: If file cannot be read or parsed
"""
if not os.path.exists(filepath):
logger.error("Action prompts file not found", filepath=filepath)
raise ActionPromptLoaderError(f"File not found: {filepath}")
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
logger.error("Failed to parse YAML", filepath=filepath, error=str(e))
raise ActionPromptLoaderError(f"Invalid YAML in {filepath}: {e}")
except IOError as e:
logger.error("Failed to read file", filepath=filepath, error=str(e))
raise ActionPromptLoaderError(f"Cannot read {filepath}: {e}")
if not data or 'action_prompts' not in data:
logger.error("No action_prompts key in YAML", filepath=filepath)
raise ActionPromptLoaderError(f"Missing 'action_prompts' key in {filepath}")
# Clear existing prompts
self._prompts = {}
# Parse each prompt
prompts_data = data['action_prompts']
errors = []
for i, prompt_data in enumerate(prompts_data):
try:
prompt = ActionPrompt.from_dict(prompt_data)
self._prompts[prompt.prompt_id] = prompt
except (ValueError, KeyError) as e:
errors.append(f"Prompt {i}: {e}")
logger.warning(
"Failed to parse action prompt",
index=i,
error=str(e)
)
if errors:
logger.warning(
"Some action prompts failed to load",
error_count=len(errors),
errors=errors
)
self._loaded = True
loaded_count = len(self._prompts)
logger.info(
"Action prompts loaded",
filepath=filepath,
count=loaded_count,
errors=len(errors)
)
return loaded_count
def get_all_actions(self) -> List[ActionPrompt]:
"""
Get all loaded action prompts.
Returns:
List of all action prompts
"""
self._ensure_loaded()
return list(self._prompts.values())
def get_action_by_id(self, prompt_id: str) -> ActionPrompt:
"""
Get a specific action prompt by ID.
Args:
prompt_id: The unique identifier of the action
Returns:
The ActionPrompt object
Raises:
ActionPromptNotFoundError: If action not found
"""
self._ensure_loaded()
if prompt_id not in self._prompts:
logger.warning("Action prompt not found", prompt_id=prompt_id)
raise ActionPromptNotFoundError(f"Action prompt '{prompt_id}' not found")
return self._prompts[prompt_id]
def get_available_actions(
self,
user_tier: UserTier,
location_type: LocationType
) -> List[ActionPrompt]:
"""
Get actions available to a user at a specific location.
Args:
user_tier: The user's subscription tier
location_type: The current location type
Returns:
List of available action prompts
"""
self._ensure_loaded()
available = []
for prompt in self._prompts.values():
if prompt.is_available(user_tier, location_type):
available.append(prompt)
logger.debug(
"Filtered available actions",
user_tier=user_tier.value,
location_type=location_type.value,
count=len(available)
)
return available
def get_actions_by_tier(self, user_tier: UserTier) -> List[ActionPrompt]:
"""
Get all actions available to a user tier (ignoring location).
Args:
user_tier: The user's subscription tier
Returns:
List of action prompts available to the tier
"""
self._ensure_loaded()
available = []
for prompt in self._prompts.values():
if prompt._tier_meets_requirement(user_tier):
available.append(prompt)
return available
def get_actions_by_category(self, category: str) -> List[ActionPrompt]:
"""
Get all actions in a specific category.
Args:
category: The action category (e.g., "ask_question", "explore")
Returns:
List of action prompts in the category
"""
self._ensure_loaded()
return [
prompt for prompt in self._prompts.values()
if prompt.category.value == category
]
def get_locked_actions(
self,
user_tier: UserTier,
location_type: LocationType
) -> List[ActionPrompt]:
"""
Get actions that are locked due to tier restrictions.
Used to show locked actions with upgrade prompts in UI.
Args:
user_tier: The user's subscription tier
location_type: The current location type
Returns:
List of locked action prompts
"""
self._ensure_loaded()
locked = []
for prompt in self._prompts.values():
# Must match location but be tier-locked
if prompt._location_matches_filter(location_type) and prompt.is_locked(user_tier):
locked.append(prompt)
return locked
def reload(self, filepath: str) -> int:
"""
Force reload prompts from YAML file.
Args:
filepath: Path to the YAML file
Returns:
Number of prompts loaded
"""
self._loaded = False
return self.load_from_yaml(filepath)
def is_loaded(self) -> bool:
"""Check if prompts have been loaded."""
return self._loaded
def get_prompt_count(self) -> int:
"""Get the number of loaded prompts."""
return len(self._prompts)
def _ensure_loaded(self) -> None:
"""
Ensure prompts are loaded, auto-load from default path if not.
Raises:
ActionPromptLoaderError: If prompts cannot be loaded
"""
if not self._loaded:
# Try default path
default_path = os.path.join(
os.path.dirname(__file__),
'..', 'data', 'action_prompts.yaml'
)
default_path = os.path.normpath(default_path)
if os.path.exists(default_path):
self.load_from_yaml(default_path)
else:
raise ActionPromptLoaderError(
"Action prompts not loaded. Call load_from_yaml() first."
)
@classmethod
def reset_instance(cls) -> None:
"""
Reset the singleton instance.
Primarily for testing purposes.
"""
cls._instance = None