This commit is contained in:
2025-09-05 19:00:24 +02:00
parent 793213a834
commit 630c6317a1
5 changed files with 331 additions and 44 deletions

View File

@@ -1,6 +1,9 @@
import asyncio import asyncio
import os import os
import pickle
from pprint import pprint from pprint import pprint
from datetime import datetime
from typing import Optional, List
import yaml import yaml
from pathlib import Path from pathlib import Path
@@ -8,14 +11,102 @@ from dotenv import load_dotenv
from living_agents import CharacterAgent, PromptManager from living_agents import CharacterAgent, PromptManager
import logging import logging
from llm_connector import LLMMessage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def save_character_to_cache(agent: CharacterAgent, template_path: Path):
"""Save a character agent to cache"""
cache_path = Path('.cache') / f"{template_path.stem}.pkl"
try:
# Create cache data with metadata
cache_data = {
'character': agent.character,
'memories': agent.memory_stream.memories,
'template_path': template_path,
'cached_at': datetime.now(),
'cache_version': '1.0'
}
with open(cache_path, 'wb') as f:
pickle.dump(cache_data, f)
print(f"💾 Character saved to cache: {cache_path.name}")
except Exception as e:
print(f"⚠️ Warning: Could not save character to cache: {e}")
def load_character_from_cache(template_path: Path) -> Optional[CharacterAgent]:
"""Load a character agent from cache, return None if not found or invalid"""
cache_path = Path('.cache') / f"{template_path.stem}.pkl"
if not cache_path.exists():
return None
try:
with open(cache_path, 'rb') as f:
cache_data = pickle.load(f)
# Verify cache data structure
required_keys = ['character', 'memories', 'template_path', 'cached_at']
if not all(key in cache_data for key in required_keys):
print(f"⚠️ Invalid cache file format: {cache_path.name}")
return None
# Check if template file is newer than cache
template_mtime = Path(template_path).stat().st_mtime
cache_time = cache_data['cached_at'].timestamp()
if template_mtime > cache_time:
print(f"📝 Template file is newer than cache, will recreate character")
return None
# Reconstruct CharacterAgent
from living_agents import LLMAgent
agent = CharacterAgent(cache_data['character'], LLMAgent())
agent.memory_stream.memories = cache_data['memories']
cached_date = cache_data['cached_at'].strftime('%Y-%m-%d %H:%M:%S')
print(f"💾 Loaded character from cache (created: {cached_date})")
return agent
except Exception as e:
print(f"⚠️ Could not load character from cache: {e}")
return None
def get_cache_info(template_path: Path) -> Optional[dict]:
"""Get information about cached character"""
cache_path = Path('.cache') / f"{template_path.stem}.pkl"
if not cache_path.exists():
return None
try:
with open(cache_path, 'rb') as f:
cache_data = pickle.load(f)
return {
'cache_file': cache_path.name,
'cached_at': cache_data['cached_at'],
'character_name': cache_data['character'].name,
'memory_count': len(cache_data['memories'])
}
except:
return None
class CharacterExplorer: class CharacterExplorer:
"""Interactive explorer for testing CharacterAgent functionality""" """Interactive explorer for testing CharacterAgent functionality"""
chat_history: List[LLMMessage]
def __init__(self, character_agent: CharacterAgent): def __init__(self, character_agent: CharacterAgent):
self.agent = character_agent self.agent = character_agent
self.chat_history = []
async def start_interactive_session(self): async def start_interactive_session(self):
"""Start the interactive exploration menu""" """Start the interactive exploration menu"""
@@ -27,23 +118,29 @@ class CharacterExplorer:
self._show_menu() self._show_menu()
try: try:
choice = input("\nChoose option (1-6): ").strip() choice = input("\nChoose option (1-8): ").strip()
if choice == "1": if choice == "1":
await self._handle_ask_question() await self._handle_ask_question()
elif choice == "2": if choice == "2":
await self._handle_list_memories() await self._handle_chat()
elif choice == "3": elif choice == "3":
await self._handle_view_memory() await self._handle_list_memories()
elif choice == "4": elif choice == "4":
await self._handle_memory_stats() await self._handle_view_memory()
elif choice == "5": elif choice == "5":
await self._handle_character_summary() await self._handle_memory_stats()
elif choice == "6": elif choice == "6":
await self._handle_character_summary()
elif choice == "7":
# Regenerate character
if await self._handle_regenerate_character():
break # Exit to main menu after regeneration
elif choice == "8":
print("👋 Goodbye!") print("👋 Goodbye!")
break break
else: else:
print("❌ Invalid choice. Please enter 1-6.") print("❌ Invalid choice. Please enter 1-8.")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n👋 Goodbye!") print("\n👋 Goodbye!")
@@ -51,16 +148,31 @@ class CharacterExplorer:
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {e}")
def clear_character_cache(self) -> bool:
"""Clear cached character for a template"""
cache_file = Path('.cache') / f"{self.agent.character.template_file.stem}.pkl"
if cache_file.exists():
try:
cache_file.unlink()
return True
except Exception as e:
print(f"❌ Could not clear cache: {e}")
return False
return False
def _show_menu(self): def _show_menu(self):
"""Display the interactive menu options""" """Display the interactive menu options"""
print(f"\n🎭 Character Explorer Menu") print(f"\n🎭 Character Explorer Menu")
print("=" * 30) print("=" * 30)
print("1. 💬 Ask a question") print(f"1. 💬 Ask {self.agent.character.name} a question")
print("2. 📚 List all memories") print(f"2. 💬 Chat with {self.agent.character.name}")
print("3. 🔍 View specific memory (with related)") print("3. 📚 List all memories")
print("4. 📊 Memory statistics") print("4. 🔍 View specific memory (with related)")
print("5. 👤 Character summary") print("5. 📊 Memory statistics")
print("6. 🚪 Exit") print("6. 👤 Character summary")
print("7. 🔄 Regenerate character")
print("8. 🚪 Exit")
async def _handle_ask_question(self): async def _handle_ask_question(self):
"""Handle asking questions to the character""" """Handle asking questions to the character"""
@@ -82,6 +194,33 @@ class CharacterExplorer:
except Exception as e: except Exception as e:
print(f"❌ Error getting response: {e}") print(f"❌ Error getting response: {e}")
async def _handle_chat(self):
"""Handle chat with a character"""
if not self.chat_history:
scenario_content = input(f"\n💬 Set a scenario for a new chat with {self.agent.character.name}: ").strip()
if not scenario_content:
return
self.chat_history.append({'role': 'system', 'content': scenario_content})
scenario_content = input(f"\n💬 Ask {self.agent.character.name}: ").strip()
if not scenario_content:
return
print(f"\n🤔 {self.agent.character.name} is thinking...")
try:
response = await self.agent.react_to_situation(scenario_content)
print(f"💬 {self.agent.character.name}: {response}")
# Show which memories were retrieved for this response
relevant_memories = await self.agent.memory_stream.retrieve_related_memories(scenario_content, k=5)
print(f"\n🧠 Memories used for this response:")
for i, mem in enumerate(relevant_memories, 1):
print(f" {i}. [{mem.memory_type}] {mem.description[:80]}{'...' if len(mem.description) > 80 else ''}")
except Exception as e:
print(f"❌ Error getting response: {e}")
async def _handle_list_memories(self): async def _handle_list_memories(self):
"""List all memories with filtering options""" """List all memories with filtering options"""
print("\n📚 Memory Filter Options:") print("\n📚 Memory Filter Options:")
@@ -204,6 +343,7 @@ class CharacterExplorer:
print(f" Occupation: {self.agent.character.occupation}") print(f" Occupation: {self.agent.character.occupation}")
print(f" Location: {self.agent.character.location}") print(f" Location: {self.agent.character.location}")
print("") print("")
print(f" - File: {self.agent.character.template_file}")
print(f" Traits") print(f" Traits")
traits_summary = "\n".join([f" - {trait.strength}/10 {trait.name} ({trait.description})" for trait in self.agent.character.traits]) if self.agent.character.traits else "No traits yet." traits_summary = "\n".join([f" - {trait.strength}/10 {trait.name} ({trait.description})" for trait in self.agent.character.traits]) if self.agent.character.traits else "No traits yet."
print(traits_summary) print(traits_summary)
@@ -220,32 +360,128 @@ class CharacterExplorer:
except Exception as e: except Exception as e:
print(f"❌ Error generating summary: {e}") print(f"❌ Error generating summary: {e}")
async def _handle_regenerate_character(self) -> bool:
"""Handle regenerating the current character"""
character_name = self.agent.character.name
async def load_and_explore_character(template_path: str): print(f"\n🔄 Regenerate {character_name}")
print("This will clear the cache and recreate the character from scratch.")
print("All current memories and traits will be lost!")
# Confirm regeneration
confirm = input(f"Regenerate {character_name}? (y/N): ").strip().lower()
if confirm not in ['y', 'yes']:
print("❌ Regeneration cancelled")
return False
template_file = self.agent.character.template_file
if self.agent.character.template_file is None:
print(f"❌ Could not find template file for {character_name}")
return False
# Clear the cache
if self.clear_character_cache():
print(f"🗑️ Cleared cache for {character_name}")
else:
print("❌ Failed to clear cache")
return False
await load_and_explore_character(template_file)
return True
def select_character_template(show_cache_info: bool = True) -> Path:
"""Show character template selection menu and return chosen template path"""
templates_dir = Path(__file__).parent / 'character_templates'
# Find all .yml files in the character_templates directory
template_files = list(templates_dir.glob('*.yml'))
if not template_files:
print("❌ No character templates found in character_templates/")
return None
print("\n🎭 Available Character Templates:")
print("=" * 60)
# Display template options with cache info
for i, template_file in enumerate(template_files, 1):
# Try to load the template to get the name
try:
with open(template_file, 'r', encoding='utf-8') as f:
template_data = yaml.safe_load(f)
character_name = template_data.get('name', template_file.stem)
except:
character_name = template_file.stem
# Check for cached version if requested
if show_cache_info:
cache_info = get_cache_info(template_file)
if cache_info:
cache_status = f"💾 (cached {cache_info['cached_at'].strftime('%m/%d %H:%M')})"
else:
cache_status = "🔄 (will create new)"
print(f"{i}. {character_name} ({template_file.name}) {cache_status}")
else:
print(f"{i}. {character_name} ({template_file.name})")
print(f"{len(template_files) + 1}. 🚪 Exit")
# Get user choice
while True:
try:
choice = input(f"\nChoose a character template (1-{len(template_files) + 1}): ").strip()
choice_num = int(choice)
if choice_num == len(template_files) + 1:
return None # Exit choice
elif 1 <= choice_num <= len(template_files):
return template_files[choice_num - 1]
else:
print(f"❌ Invalid choice. Please enter 1-{len(template_files) + 1}.")
except ValueError:
print(f"❌ Please enter a valid number (1-{len(template_files) + 1}).")
except KeyboardInterrupt:
return None
async def load_and_explore_character(template_path: Path):
"""Load a character template and start exploration""" """Load a character template and start exploration"""
# Load environment # Load environment
# env_path = Path(__file__).parent.parent / '.env'
load_dotenv() load_dotenv()
# Load template # Try to load from cache first
if not Path(template_path).exists(): print(f"📁 Loading character from {template_path.name}...")
print(f"❌ Template file not found: {template_path}") agent = load_character_from_cache(template_path)
return None
if agent is None:
# No cache or cache invalid, create from template
try: try:
with open(template_path, 'r', encoding='utf-8') as f: with open(template_path, 'r', encoding='utf-8') as f:
template = yaml.safe_load(f) template = yaml.safe_load(f)
template['yaml_file'] = template_path
except Exception as e: except Exception as e:
print(f"❌ Error loading template: {e}") print(f"❌ Error loading template: {e}")
return None return None
print(f"📁 Loading character from {template_path}...")
print(f"🤖 Creating memories and scoring importance...") print(f"🤖 Creating memories and scoring importance...")
try: try:
# Create character agent # Create character agent from template
agent = await CharacterAgent.create_from_template(template) agent = await CharacterAgent.create_from_template(template)
print(f"{agent.character.name} created successfully!")
# Save to cache for next time
save_character_to_cache(agent, template_path)
except Exception as e:
print(f"❌ Error creating character: {e}")
import traceback
traceback.print_exc()
return None
else:
print(f"{agent.character.name} loaded successfully!") print(f"{agent.character.name} loaded successfully!")
# Start explorer # Start explorer
@@ -254,13 +490,20 @@ async def load_and_explore_character(template_path: str):
return agent return agent
except Exception as e:
print(f"❌ Error creating character: {e}") async def main():
import traceback """Main function with character selection"""
traceback.print_exc() print("🎭 Welcome to the Living Agents Character Explorer!")
return None
selected_template = select_character_template(show_cache_info=True)
if selected_template is None:
print("👋 Goodbye!")
return
# Load and explore the selected character
await load_and_explore_character(selected_template)
if __name__ == '__main__': if __name__ == '__main__':
template = Path(__file__).parent / 'character_templates' / 'Alice.yml' asyncio.run(main())
asyncio.run(load_and_explore_character(str(template)))

View File

@@ -71,6 +71,33 @@ class CharacterAgent:
return response return response
async def chat_with(self, messages: List[LLMMessage]) -> LLMMessage:
"""Generate reaction based on memory and character"""
# Retrieve relevant memories
relevant_memories = await self.memory_stream.retrieve_related_memories(situation, k=8)
memory_context = "\n".join([f"- {m.description}" for m in relevant_memories])
context = {
'character': self._get_character_prompt(),
'character_name': self.character.name,
'memory_context': memory_context,
'situation': situation}
prompt = PromptManager.get_prompt('react_to_situation', context)
response = await self.llm.chat([{"role": "user", "content": prompt}])
# create new memories from interaction
interaction_context = {
'situation': f'I reacted to: \n{situation}',
'response': f'My response was: \n{response}',
}
prompt, schema = PromptManager.get_prompt_with_schema('extract_interaction_memories', interaction_context)
memories_response = await self.llm.client.get_structured_response([{"role": "user", "content": prompt}], schema)
for new_memory in memories_response['memories']:
await self.perceive(new_memory)
return response
async def plan_day(self) -> List[str]: async def plan_day(self) -> List[str]:
"""Generate high-level daily plan""" """Generate high-level daily plan"""
# Retrieve relevant memories about goals, habits, schedule # Retrieve relevant memories about goals, habits, schedule
@@ -255,5 +282,7 @@ Summary:"""
memory.importance_score = await instance._score_memory_importance(memory) memory.importance_score = await instance._score_memory_importance(memory)
await instance._analyze_trait_impact(memory) await instance._analyze_trait_impact(memory)
instance.character.template_file = template['yaml_file']
logger.info(f"Character {instance.character.name} created successfully") logger.info(f"Character {instance.character.name} created successfully")
return instance return instance

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass, field
from typing import Dict, List, Optional, Literal, TypedDict from typing import Dict, List, Optional, Literal, TypedDict
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
import random from pathlib import Path
class CharacterTemplate(TypedDict): class CharacterTemplate(TypedDict):
@@ -10,7 +10,7 @@ class CharacterTemplate(TypedDict):
observations: List[str] observations: List[str]
reflections: List[str] reflections: List[str]
plans: List[str] plans: List[str]
yaml_file: str yaml_file: Path
@dataclass @dataclass
@@ -72,6 +72,7 @@ class Character:
traits: List[CharacterTrait] = field(default_factory=list) traits: List[CharacterTrait] = field(default_factory=list)
relationships: Dict[str, str] = field(default_factory=dict) relationships: Dict[str, str] = field(default_factory=dict)
goals: List[str] = field(default_factory=list) goals: List[str] = field(default_factory=list)
template_file: Optional[Path] = None
_id: str = field(default_factory=lambda: str(uuid4())[:8]) _id: str = field(default_factory=lambda: str(uuid4())[:8])
def get_trait(self, trait_name, trait_description) -> CharacterTrait: def get_trait(self, trait_name, trait_description) -> CharacterTrait:

View File

@@ -41,7 +41,6 @@ class MemoryStream:
# Track for reflection trigger # Track for reflection trigger
self.recent_importance_sum += memory.importance_score self.recent_importance_sum += memory.importance_score
print(f"Recent Importance Sum: {self.recent_importance_sum}")
# Trigger reflection if threshold exceeded # Trigger reflection if threshold exceeded
if self.recent_importance_sum >= self.importance_threshold: if self.recent_importance_sum >= self.importance_threshold:

View File

@@ -0,0 +1,15 @@
{{character}}
Relevant memories from your past:
{{memory_context}}
Respond as {{character_name}} describing what you did and how you reacted. Write in first person past tense as if this
just happened to you.
Examples of the response style:
- "I looked up from my book and smiled nervously..."
- "I felt my heart race and took a deep breath before I said..."
- "I hesitated for a moment, then decided to..."
Stay completely in character and be specific about your actions, thoughts, and words.