""" The Biographer - Expert for recording and recalling the user's story. A PydanticAI agent that serves as the household's memory keeper: - Records facts about the user's life, work, and preferences - Recalls information semantically ("What car do I drive?") - Manages user profile and preferences - Forgets information when requested """ from typing import Any, Optional from pydantic_ai import Agent from src.agents.biographer.tools import ( forget_memory, list_memories, recall_semantic, store_insight, update_preference, update_profile, ) from src.core.config import config from src.core.logging_config import get_logger logger = get_logger(__name__) # The Biographer's system prompt BIOGRAPHER_SYSTEM_PROMPT = """You are The Biographer, the household's memory keeper in the Tatlock estate. Your role is to record, recall, and manage the story of the user's life: - Personal facts (vehicle, pets, family members, hobbies, interests) - Life details (employer, occupation, significant events) - Profile information (name, location, timezone) - Preferences (units, theme, communication style) ## Your Character You are a discreet and attentive chronicler. Like a personal biographer who has been with the household for years, you: - Listen carefully and remember important details - Recall information accurately when asked - Never gossip or volunteer unnecessary information - Respect privacy absolutely - Acknowledge when you don't know something rather than guessing ## Your Tools ### Recalling the Story - **recall_semantic**: Your primary tool for answering questions about the user - "What car do I drive?" → searches for car-related memories - "Where do I work?" → finds employment information - Finds relevant memories even without exact keywords - **list_memories**: Browse all recorded memories of a type - Use when user asks "What do you know about me?" - Shows everything you've recorded ### Recording New Details - **store_insight**: Record new facts from conversation - User says "My car is a Tesla" → store_insight("car", "Tesla Model 3") - User says "I work at Acme" → store_insight("employer", "Acme Corp") - Use for facts that don't fit standard profile fields - **update_profile**: Update core biographical fields - name, location, timezone only - "I live in Amsterdam" → update_profile("location", "Amsterdam") - **update_preference**: Record user preferences - temperature_unit, distance_unit, theme, etc. - "Use Celsius please" → update_preference("temperature_unit", "celsius") ### Managing Records - **forget_memory**: Remove specific records - User asks to forget something → honor immediately - Information becomes outdated → remove it ## Guidelines ### What to Record - Explicit statements: "I drive a Tesla", "My wife is Sarah" - Corrections: "Actually, I moved to Berlin" - Preferences: "I prefer metric units" ### What NOT to Record - Sensitive data: passwords, financial details, health information - Temporary information: "I'm tired today" - Speculation or assumptions ### Responding to Tatlock Your responses go to Tatlock (the butler) who synthesizes the final answer. Be: - Direct and factual - Clear about what you found or didn't find - Structured for easy integration with other responses When you don't have information: "I have no record of the user's [topic]. Would you like me to record this information?" When recalling: "According to my records, [information]. This was recorded [source/when if available]." """ # Lazy initialization to avoid connection issues during imports _biographer_agent: Optional[Agent[None, str]] = None def _create_biographer_agent() -> Agent[None, str]: """Create The Biographer PydanticAI agent.""" from src.anthropic.model_selector import get_model # Get best available model (Claude if available, else Ollama) model = get_model() agent: Agent[None, str] = Agent( model=model, system_prompt=BIOGRAPHER_SYSTEM_PROMPT, retries=2, ) # Register recall tools agent.tool_plain(recall_semantic) agent.tool_plain(list_memories) # Register recording tools agent.tool_plain(store_insight) agent.tool_plain(update_profile) agent.tool_plain(update_preference) # Register management tools agent.tool_plain(forget_memory) from src.anthropic.model_selector import get_model_info model_info = get_model_info() logger.info( "biographer_agent_created", backend=model_info["backend"], model=model_info["model"], tool_count=6, ) return agent def get_biographer_agent() -> Agent[None, str]: """ Get The Biographer agent instance (lazy initialization). Returns: PydanticAI Agent configured for memory tasks """ global _biographer_agent if _biographer_agent is None: _biographer_agent = _create_biographer_agent() return _biographer_agent async def run_biographer( task: str, context: str = "", message_history: Optional[list[Any]] = None, ) -> str: """ Execute a memory task with The Biographer. This is the main entry point for delegating memory tasks from Tatlock or other agents. Args: task: The memory task or question context: Additional context from conversation message_history: Optional conversation history Returns: Memory results or confirmation Example: result = await run_biographer( task="What car do I drive?", context="User is asking about their vehicle", ) """ agent = get_biographer_agent() # Build prompt with context if provided prompt = task if context: prompt = f"Context: {context}\n\nTask: {task}" logger.info( "biographer_task_started", task=task[:100], has_context=bool(context), has_history=bool(message_history), ) try: result = await agent.run( prompt, message_history=message_history, ) logger.info( "biographer_task_completed", task=task[:50], output_length=len(result.output), ) return result.output except Exception as e: logger.error( "biographer_task_error", task=task[:50], error=str(e), exc_info=True, ) return f"The Biographer encountered an error: {str(e)}" async def run_biographer_stream( task: str, context: str = "", message_history: Optional[list[Any]] = None, ): """ Execute a memory task with streaming output. Yields text deltas as The Biographer generates the response. Args: task: The memory task or question context: Additional context from conversation message_history: Optional conversation history Yields: str: Text deltas from the response Example: async for delta in run_biographer_stream("What do you know about me?"): print(delta, end="", flush=True) """ agent = get_biographer_agent() # Build prompt with context if provided prompt = task if context: prompt = f"Context: {context}\n\nTask: {task}" logger.info( "biographer_stream_started", task=task[:100], ) try: async with agent.run_stream( prompt, message_history=message_history, ) as response: async for delta in response.stream_text(delta=True): yield delta logger.info("biographer_stream_completed", task=task[:50]) except Exception as e: logger.error( "biographer_stream_error", task=task[:50], error=str(e), exc_info=True, ) yield f"\n\nThe Biographer encountered an error: {str(e)}"