All agents now prefer Claude API when ANTHROPIC_API_KEY is configured, with automatic fallback to Ollama when offline or unconfigured. New src/anthropic/ module provides model selection via get_model() factory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
7.7 KiB
Python
268 lines
7.7 KiB
Python
"""
|
|
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)}"
|