""" LoreService for retrieving contextual lore for NPC conversations. This module provides an interface for lore retrieval that will be implemented with Weaviate in Phase 6. For now, it provides a mock implementation that returns embedded lore from quest definitions. The service follows a three-tier knowledge hierarchy: 1. World Lore - Global history, mythology, kingdoms 2. Regional Lore - Local history, landmarks, rumors 3. NPC Persona - Individual NPC knowledge (already in NPC YAML) """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Dict, List, Any, Optional, Protocol import structlog from app.models.quest import Quest, QuestLoreContext from app.models.npc import NPC logger = structlog.get_logger(__name__) @dataclass class LoreEntry: """ A single piece of lore information. Attributes: content: The lore text content title: Optional title for the lore entry knowledge_type: Type of knowledge (common, academic, secret) source: Where this lore came from (world, regional, quest) metadata: Additional metadata for filtering """ content: str title: str = "" knowledge_type: str = "common" source: str = "quest" metadata: Dict[str, Any] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: """Serialize to dictionary.""" return { "content": self.content, "title": self.title, "knowledge_type": self.knowledge_type, "source": self.source, "metadata": self.metadata, } @dataclass class LoreContext: """ Aggregated lore context for AI prompts. Contains lore from multiple sources, filtered for the current NPC and conversation context. Attributes: world_lore: Global world knowledge entries regional_lore: Local/regional knowledge entries quest_lore: Quest-specific lore entries """ world_lore: List[LoreEntry] = field(default_factory=list) regional_lore: List[LoreEntry] = field(default_factory=list) quest_lore: List[LoreEntry] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: """Serialize to dictionary for AI prompt injection.""" return { "world": [entry.to_dict() for entry in self.world_lore], "regional": [entry.to_dict() for entry in self.regional_lore], "quest": [entry.to_dict() for entry in self.quest_lore], } def has_content(self) -> bool: """Check if any lore content is available.""" return bool(self.world_lore or self.regional_lore or self.quest_lore) class LoreServiceInterface(ABC): """ Abstract interface for lore services. This interface allows swapping between MockLoreService (current) and WeaviateLoreService (Phase 6) without changing calling code. """ @abstractmethod def get_lore_context( self, npc: NPC, quest: Optional[Quest] = None, topic: str = "", region_id: str = "", ) -> LoreContext: """ Get lore context for an NPC conversation. Args: npc: The NPC in the conversation quest: Optional quest being discussed topic: Topic of conversation for semantic search region_id: Region to filter lore by Returns: LoreContext with relevant lore entries """ pass @abstractmethod def filter_lore_for_npc( self, lore_entries: List[LoreEntry], npc: NPC, ) -> List[LoreEntry]: """ Filter lore entries based on what an NPC would know. Args: lore_entries: Raw lore entries npc: The NPC to filter for Returns: Filtered list of lore entries """ pass class MockLoreService(LoreServiceInterface): """ Mock implementation of LoreService using embedded quest lore. This implementation returns lore directly from quest YAML files until Weaviate is implemented in Phase 6. It provides a working lore system without external dependencies. """ # NPC roles that have access to academic knowledge ACADEMIC_ROLES = ["scholar", "wizard", "sage", "librarian", "priest", "mage"] # NPC roles that might know secrets SECRET_KEEPER_ROLES = ["mayor", "noble", "spy", "elder", "priest"] def __init__(self): """Initialize the mock lore service.""" logger.info("MockLoreService initialized") def get_lore_context( self, npc: NPC, quest: Optional[Quest] = None, topic: str = "", region_id: str = "", ) -> LoreContext: """ Get lore context for an NPC conversation. For the mock implementation, this primarily returns quest embedded lore and some static regional/world hints. Args: npc: The NPC in the conversation quest: Optional quest being discussed topic: Topic of conversation (unused in mock) region_id: Region to filter by Returns: LoreContext with available lore """ context = LoreContext() # Add quest-specific lore if a quest is provided if quest and quest.lore_context: quest_entries = self._extract_quest_lore(quest) context.quest_lore = self.filter_lore_for_npc(quest_entries, npc) # Add some mock regional lore based on NPC location regional_entries = self._get_mock_regional_lore(npc.location_id, region_id) context.regional_lore = self.filter_lore_for_npc(regional_entries, npc) # Add world lore if NPC is knowledgeable if npc.role in self.ACADEMIC_ROLES: world_entries = self._get_mock_world_lore() context.world_lore = self.filter_lore_for_npc(world_entries, npc) logger.debug( "Lore context built", npc_id=npc.npc_id, quest_lore_count=len(context.quest_lore), regional_lore_count=len(context.regional_lore), world_lore_count=len(context.world_lore), ) return context def filter_lore_for_npc( self, lore_entries: List[LoreEntry], npc: NPC, ) -> List[LoreEntry]: """ Filter lore entries based on what an NPC would know. Args: lore_entries: Raw lore entries npc: The NPC to filter for Returns: Filtered list of lore entries """ filtered = [] for entry in lore_entries: knowledge_type = entry.knowledge_type # Academic knowledge requires appropriate role if knowledge_type == "academic": if npc.role not in self.ACADEMIC_ROLES: continue # Secret knowledge requires special role or tag if knowledge_type == "secret": if npc.role not in self.SECRET_KEEPER_ROLES: if "secret_keeper" not in npc.tags: continue filtered.append(entry) return filtered def _extract_quest_lore(self, quest: Quest) -> List[LoreEntry]: """ Extract lore entries from a quest's embedded lore context. Args: quest: The quest to extract lore from Returns: List of LoreEntry objects """ entries = [] lore = quest.lore_context # Add backstory as a lore entry if lore.backstory: entries.append(LoreEntry( content=lore.backstory, title=f"Background: {quest.name}", knowledge_type="common", source="quest", metadata={"quest_id": quest.quest_id}, )) # Add world connections for connection in lore.world_connections: entries.append(LoreEntry( content=connection, title="World Connection", knowledge_type="common", source="quest", metadata={"quest_id": quest.quest_id}, )) # Add regional hints for hint in lore.regional_hints: entries.append(LoreEntry( content=hint, title="Local Knowledge", knowledge_type="common", source="quest", metadata={"quest_id": quest.quest_id}, )) return entries def _get_mock_regional_lore( self, location_id: str, region_id: str, ) -> List[LoreEntry]: """ Get mock regional lore for a location. This provides basic regional context until Weaviate is implemented. Args: location_id: Specific location region_id: Region identifier Returns: List of LoreEntry objects """ entries = [] # Crossville regional lore if "crossville" in location_id.lower() or region_id == "crossville": entries.extend([ LoreEntry( content="Crossville was founded two hundred years ago as a trading post.", title="Crossville History", knowledge_type="common", source="regional", ), LoreEntry( content="The Old Mines were sealed fifty years ago after a tragic accident.", title="The Old Mines", knowledge_type="common", source="regional", ), LoreEntry( content="The Thornwood family has led the village for three generations.", title="Village Leadership", knowledge_type="common", source="regional", ), ]) return entries def _get_mock_world_lore(self) -> List[LoreEntry]: """ Get mock world-level lore. This provides basic world context until Weaviate is implemented. Returns: List of LoreEntry objects """ return [ LoreEntry( content="The Five Kingdoms united against the Shadow Empire two hundred years ago in a great war.", title="The Great War", knowledge_type="academic", source="world", ), LoreEntry( content="Magic flows from the ley lines that crisscross the land, concentrated at ancient sites.", title="The Nature of Magic", knowledge_type="academic", source="world", ), LoreEntry( content="The ancient ruins predate human settlement and are believed to be from the First Age.", title="First Age Ruins", knowledge_type="academic", source="world", ), ] # Global singleton instance _service_instance: Optional[LoreServiceInterface] = None def get_lore_service() -> LoreServiceInterface: """ Get the global LoreService instance. Currently returns MockLoreService. In Phase 6, this will be updated to return WeaviateLoreService. Returns: Singleton LoreService instance """ global _service_instance if _service_instance is None: _service_instance = MockLoreService() return _service_instance