diff --git a/CLAUDE.md b/CLAUDE.md index 29f8309..c070f18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,16 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a multi-agent roleplay system implementing Stanford's "Generative Agents" memory architecture for believable AI characters with emergent behaviors. The project currently uses OpenAI's API in the agent system but is transitioning to use a custom LLM connector that supports any OpenAI-compatible API endpoint. +This is a character development system implementing Stanford's "Generative Agents" memory architecture for believable AI characters with dynamic personality evolution. The project uses a custom LLM connector that supports any OpenAI-compatible API endpoint, allowing flexible backend configuration. ## Key Architecture Components -### Agent System (agents.py) +### Agent System (living_agents/) - **Memory Stream**: Stanford's memory architecture with observations, reflections, and plans - **Smart Retrieval**: Combines recency (exponential decay), importance (1-10 scale), and relevance (cosine similarity) - **Auto-Reflection**: Generates insights when importance threshold (150) is reached -- **Character Components**: Character, CharacterAgent, MemoryStream, SceneManager -- Currently uses OpenAI API directly but should be migrated to use llm_connector +- **Character Components**: Character, CharacterAgent, MemoryStream +- **Trait Development**: Dynamic personality evolution based on experiences +- Uses llm_connector for flexible backend support ### LLM Connector Package - **Custom LLM abstraction** that supports any OpenAI-compatible API @@ -21,14 +22,11 @@ This is a multi-agent roleplay system implementing Stanford's "Generative Agents - **Type definitions**: LLMBackend (base_url, api_token, model) and LLMMessage - Environment variables: BACKEND_BASE_URL, BACKEND_API_TOKEN, BACKEND_MODEL -### UI Framework -- **NiceGUI** for web interface (async components) -- **AsyncElement base class**: Simplified async UI component pattern - - Constructor accepts element_type (default: ui.column) and element args/kwargs - - Implement build() method for async initialization logic - - Use create() factory method which returns the NiceGUI element directly - - Supports method chaining on the returned element -- Pages are created in pages/ directory, main page is MainPage +### Character Explorer CLI +- **CLI Testing Tool**: Interactive character development and testing interface +- **Character Loading**: YAML template system for character initialization +- **Real-time Development**: Direct testing of memory, traits, and personality evolution +- Located in `character_explorer.py` for easy development iteration ## Development Commands @@ -36,9 +34,8 @@ This is a multi-agent roleplay system implementing Stanford's "Generative Agents # Install dependencies uv sync -# Run the application -uv run python main.py -# Application runs on http://localhost:8080 +# Run the character explorer CLI +uv run python character_explorer.py # Add new dependencies uv add @@ -49,43 +46,14 @@ uv python pin 3.12 # Pin to Python 3.12 ## Important Development Notes -### AsyncElement Usage -When creating UI components that extend AsyncElement: -```python -class MyComponent(AsyncElement): - async def build(self, param1: str, param2: int, *args, **kwargs) -> None: - # Build content directly in self.element - with self.element: - ui.label(f'{param1}: {param2}') - # Add more UI elements... - -# Usage - create() returns the NiceGUI element directly, supports method chaining -(await MyComponent.create(element_type=ui.card, param1="test", param2=123)).classes('w-full') - -# Can specify different element types -(await MyComponent.create(element_type=ui.row, param1="test", param2=456)).classes('gap-4') - -# Pass element constructor args/kwargs via special keys -await MyComponent.create( - element_type=ui.column, - element_args=(), # Positional args for element constructor - element_kwargs={'classes': 'p-4'}, # Kwargs for element constructor - param1="test", # Build method parameters - param2=789 -) -``` - -Key points: -- Constructor accepts element_type (default: ui.column) and element args/kwargs -- build() method receives component-specific parameters -- create() factory method returns the NiceGUI element directly (not the AsyncElement instance) -- Supports method chaining on the returned element -- Use `with self.element:` context manager to add content in build() +### Character Development Focus +The current focus is on perfecting single-agent character development: +- Characters evolve through experiences and interactions +- Memory system creates realistic personality development +- CLI tool allows rapid testing and iteration ### LLM Integration -The project has two LLM integration approaches: -1. **Legacy** (in agents.py): Direct OpenAI client usage -2. **Current** (llm_connector): Flexible backend supporting any OpenAI-compatible API +The project uses a flexible LLM connector supporting any OpenAI-compatible API. When implementing new features, use the llm_connector package: ```python @@ -114,23 +82,23 @@ async for chunk in await get_response(backend, messages, stream=True): ``` ### Project Structure -- `main.py`: Entry point, NiceGUI app configuration -- `agents.py`: Stanford memory architecture implementation (to be integrated) -- `llm_connector/`: Custom LLM integration package -- `components/`: Reusable UI components with AsyncElement base -- `pages/`: UI pages (currently only MainPage) +- `character_explorer.py`: CLI tool for character development and testing +- `living_agents/`: Core agent system with memory, traits, and prompt management +- `character_templates/`: YAML files defining character backgrounds +- `llm_connector/`: Custom LLM integration package for flexible backend support ### Environment Variables Required in `.env`: - `BACKEND_BASE_URL`: LLM API endpoint - `BACKEND_API_TOKEN`: API authentication token - `BACKEND_MODEL`: Model identifier -- `OPENAI_API_KEY`: Currently needed for agents.py (to be removed) -## Next Steps for Integration +## Current Development Status -The agents.py system needs to be: -1. Modified to use llm_connector instead of direct OpenAI client -2. Integrated into the NiceGUI web interface -3. Create UI components for character interaction, memory viewing, scene management -4. Implement real-time streaming of agent responses in the UI \ No newline at end of file +The system currently focuses on single-agent character development: +1. Character agents with dynamic personality evolution +2. Stanford-inspired memory architecture +3. CLI testing tool for rapid iteration +4. Flexible LLM backend configuration + +Future plans include multi-agent interactions and web interface integration. \ No newline at end of file diff --git a/agents.py b/agents.py deleted file mode 100644 index 20ae113..0000000 --- a/agents.py +++ /dev/null @@ -1,604 +0,0 @@ -import json -import os -import math -import time -from datetime import datetime, timedelta -from typing import Dict, List, Any, Optional, Tuple -from dataclasses import dataclass, field -from openai import OpenAI -import numpy as np -from sklearn.metrics.pairwise import cosine_similarity - -# Initialize OpenAI client -client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - - -@dataclass -class Memory: - """A single memory object with Stanford's architecture""" - description: str - creation_time: datetime - last_accessed: datetime - importance_score: int # 1-10 scale - embedding: Optional[List[float]] = None - memory_type: str = "observation" # observation, reflection, plan - related_memories: List[int] = field(default_factory=list) # IDs of supporting memories - - def __post_init__(self): - if self.last_accessed is None: - self.last_accessed = self.creation_time - - -class LLMAgent: - def __init__(self, model: str = "gpt-3.5-turbo", temperature: float = 0.8): - self.model = model - self.temperature = temperature - - def chat(self, messages: List[Dict[str, str]], max_tokens: int = 200) -> str: - try: - response = client.chat.completions.create( - model=self.model, - messages=messages, - temperature=self.temperature, - max_tokens=max_tokens - ) - return response.choices[0].message.content.strip() - except Exception as e: - return f"[LLM Error: {str(e)}]" - - def get_embedding(self, text: str) -> List[float]: - """Get embedding for memory relevance scoring""" - try: - response = client.embeddings.create( - model="text-embedding-ada-002", - input=text - ) - return response.data[0].embedding - except Exception as e: - print(f"Embedding error: {e}") - return [0.0] * 1536 # Default embedding size - - -@dataclass -class Character: - name: str - age: int - personality: str - occupation: str - location: str - relationships: Dict[str, str] = field(default_factory=dict) - goals: List[str] = field(default_factory=list) - - -class MemoryStream: - """Stanford's memory architecture with observation, reflection, and planning""" - - def __init__(self, llm_agent: LLMAgent): - self.memories: List[Memory] = [] - self.memory_counter = 0 - self.llm = llm_agent - self.importance_threshold = 150 # Reflection trigger threshold - self.recent_importance_sum = 0 - - def add_observation(self, description: str) -> int: - """Add a new observation with importance scoring""" - importance = self._score_importance(description) - - memory = Memory( - description=description, - creation_time=datetime.now(), - last_accessed=datetime.now(), - importance_score=importance, - memory_type="observation" - ) - - # Get embedding for retrieval - memory.embedding = self.llm.get_embedding(description) - - memory_id = self.memory_counter - self.memories.append(memory) - self.memory_counter += 1 - - # Track for reflection trigger - self.recent_importance_sum += importance - - # Trigger reflection if threshold exceeded - if self.recent_importance_sum >= self.importance_threshold: - self._generate_reflections() - self.recent_importance_sum = 0 - - return memory_id - - def _score_importance(self, description: str) -> int: - """Use LLM to score memory importance (Stanford approach)""" - prompt = f"""On the scale of 1 to 10, where 1 is purely mundane (e.g., brushing teeth, making bed) and 10 is extremely poignant (e.g., a break up, college acceptance), rate the likely poignancy of the following piece of memory. - -Memory: {description} -Rating: """ - - try: - response = self.llm.chat([{"role": "user", "content": prompt}], max_tokens=5) - # Extract number from response - score = int(''.join(filter(str.isdigit, response))[:1] or "5") - return max(1, min(10, score)) - except: - return 5 # Default moderate importance - - def _generate_reflections(self): - """Generate high-level reflections from recent memories""" - # Get recent high-importance memories - recent_memories = [m for m in self.memories[-20:] if m.memory_type == "observation"] - - if len(recent_memories) < 3: - return - - # Generate questions for reflection - memory_descriptions = "\n".join([f"{i+1}. {m.description}" for i, m in enumerate(recent_memories)]) - - questions_prompt = f"""Given only the information above, what are 3 most salient high-level questions we can answer about the subjects in the statements? - -{memory_descriptions} - -Questions:""" - - try: - questions_response = self.llm.chat([{"role": "user", "content": questions_prompt}]) - - # For each question, generate insights - insight_prompt = f"""Statements: -{memory_descriptions} - -What 5 high-level insights can you infer from the above statements? -Format: insight (because of 1, 3, 5)""" - - insights_response = self.llm.chat([{"role": "user", "content": insight_prompt}]) - - # Parse insights and create reflection memories - for line in insights_response.split('\n'): - if '(' in line and ')' in line: - insight = line.split('(')[0].strip() - if insight and len(insight) > 10: - # Create reflection memory - reflection = Memory( - description=f"Reflection: {insight}", - creation_time=datetime.now(), - last_accessed=datetime.now(), - importance_score=7, # Reflections are generally important - memory_type="reflection", - embedding=self.llm.get_embedding(insight) - ) - self.memories.append(reflection) - self.memory_counter += 1 - - except Exception as e: - print(f"Reflection generation error: {e}") - - def retrieve_memories(self, query: str, k: int = 10) -> List[Memory]: - """Retrieve relevant memories using recency, importance, relevance""" - if not self.memories: - return [] - - query_embedding = self.llm.get_embedding(query) - current_time = datetime.now() - scores = [] - - for i, memory in enumerate(self.memories): - # Update last accessed - memory.last_accessed = current_time - - # Calculate recency (exponential decay) - hours_since_accessed = (current_time - memory.last_accessed).total_seconds() / 3600 - recency = 0.995 ** hours_since_accessed - - # Importance (already scored 1-10) - importance = memory.importance_score / 10.0 - - # Relevance (cosine similarity) - if memory.embedding and query_embedding: - relevance = cosine_similarity([query_embedding], [memory.embedding])[0][0] - else: - relevance = 0.0 - - # Combined score (equal weighting as in Stanford paper) - score = recency + importance + relevance - scores.append((score, i, memory)) - - # Sort by score and return top k - scores.sort(reverse=True, key=lambda x: x[0]) - return [memory for _, _, memory in scores[:k]] - - -class CharacterAgent: - """Enhanced agent with Stanford's memory architecture""" - - def __init__(self, character: Character, llm: LLMAgent): - self.character = character - self.llm = llm - self.memory_stream = MemoryStream(llm) - self.current_plan: List[str] = [] - - # Initialize with character background - self._initialize_memories() - - def _initialize_memories(self): - """Initialize agent with background memories""" - background_facts = [ - f"My name is {self.character.name} and I am {self.character.age} years old", - f"My personality: {self.character.personality}", - f"My occupation: {self.character.occupation}", - f"I live in {self.character.location}" - ] - - for fact in background_facts: - self.memory_stream.add_observation(fact) - - for person, relationship in self.character.relationships.items(): - self.memory_stream.add_observation(f"My relationship with {person}: {relationship}") - - def perceive(self, observation: str) -> None: - """Add new observation to memory stream""" - self.memory_stream.add_observation(observation) - - def plan_day(self) -> List[str]: - """Generate high-level daily plan""" - # Retrieve relevant memories about goals, habits, schedule - relevant_memories = self.memory_stream.retrieve_memories( - f"{self.character.name} daily routine goals schedule", k=5 - ) - - memory_context = "\n".join([m.description for m in relevant_memories]) - - plan_prompt = f"""You are {self.character.name}. -Background: {self.character.personality} -Occupation: {self.character.occupation} - -Relevant memories: -{memory_context} - -Plan your day in broad strokes (5-8 activities with times): -1)""" - - try: - response = self.llm.chat([{"role": "user", "content": plan_prompt}], max_tokens=300) - plan_steps = [f"1){response}"] if response else ["1) Go about my daily routine"] - - # Add plan to memory - plan_description = f"Daily plan: {'; '.join(plan_steps)}" - self.memory_stream.add_observation(plan_description) - - return plan_steps - except: - return ["1) Go about my daily routine"] - - def react_to_situation(self, situation: str) -> str: - """Generate reaction based on memory and character""" - # Retrieve relevant memories - relevant_memories = self.memory_stream.retrieve_memories(situation, k=8) - memory_context = "\n".join([f"- {m.description}" for m in relevant_memories]) - - reaction_prompt = f"""You are {self.character.name}. -Age: {self.character.age} -Personality: {self.character.personality} -Current location: {self.character.location} - -Relevant memories from your past: -{memory_context} - -Current situation: {situation} - -How do you react? Stay completely in character and be specific about what you would do or say.""" - - try: - response = self.llm.chat([{"role": "user", "content": reaction_prompt}]) - - # Add reaction to memory - self.memory_stream.add_observation(f"I reacted to '{situation}' by: {response}") - - return response - except: - return "I'm not sure how to respond to that." - - def get_summary(self) -> str: - """Generate current summary based on memories and reflections""" - reflections = [m for m in self.memory_stream.memories if m.memory_type == "reflection"] - recent_observations = self.memory_stream.memories[-10:] - - summary_memories = reflections[-3:] + recent_observations[-5:] - memory_context = "\n".join([m.description for m in summary_memories]) - - summary_prompt = f"""Based on the following memories and reflections, provide a brief summary of who {self.character.name} is and what they care about: - -{memory_context} - -Summary:""" - - try: - return self.llm.chat([{"role": "user", "content": summary_prompt}], max_tokens=150) - except: - return f"{self.character.name} is a {self.character.age}-year-old {self.character.occupation}." - - -class SceneManager: - """Enhanced scene manager with better context filtering""" - - def __init__(self, main_llm: LLMAgent): - self.main_llm = main_llm - self.characters: Dict[str, Character] = {} - self.agents: Dict[str, CharacterAgent] = {} - self.scene_state = { - "location": "cozy coffee shop", - "time": "afternoon", - "atmosphere": "quiet and peaceful", - "active_conversations": [], - "events": [] - } - self.global_time = datetime.now() - - def add_character(self, character: Character): - self.characters[character.name] = character - agent = CharacterAgent(character, LLMAgent("gpt-3.5-turbo", temperature=0.9)) - self.agents[character.name] = agent - print(f"โœ“ Added {character.name} to the scene") - - def advance_time(self, hours: int = 1): - """Advance scene time and trigger agent planning""" - self.global_time += timedelta(hours=hours) - self.scene_state["time"] = self.global_time.strftime("%I:%M %p") - - # Each agent plans their next actions - for name, agent in self.agents.items(): - agent.perceive(f"Time is now {self.scene_state['time']}") - - def character_interaction(self, char1_name: str, char2_name: str, context: str) -> Dict[str, str]: - """Handle interaction between two characters""" - if char1_name not in self.agents or char2_name not in self.agents: - return {"error": "Character not found"} - - char1_agent = self.agents[char1_name] - char2_agent = self.agents[char2_name] - - # Both characters observe the interaction context - char1_agent.perceive(f"Interacting with {char2_name}: {context}") - char2_agent.perceive(f"Interacting with {char1_name}: {context}") - - # Generate responses - char1_response = char1_agent.react_to_situation(f"You are talking with {char2_name}. Context: {context}") - char2_response = char2_agent.react_to_situation(f"{char1_name} said: '{char1_response}'") - - # Both remember the conversation - char1_agent.perceive(f"Conversation with {char2_name}: I said '{char1_response}', they replied '{char2_response}'") - char2_agent.perceive(f"Conversation with {char1_name}: They said '{char1_response}', I replied '{char2_response}'") - - return { - char1_name: char1_response, - char2_name: char2_response - } - - -class EnhancedRoleplaySystem: - def __init__(self): - self.scene_manager = SceneManager(LLMAgent("gpt-4o-mini", temperature=0.7)) - self.setup_characters() - - def setup_characters(self): - # Create characters with rich backgrounds for testing memory - alice = Character( - name="Alice", - age=23, - personality="Introverted literature student who loves mystery novels and gets nervous in social situations but is very observant", - occupation="Graduate student studying Victorian literature", - location="coffee shop", - relationships={ - "Professor Wilson": "My thesis advisor - supportive but demanding", - "Emma": "Friendly barista I have a secret crush on" - }, - goals=["Finish thesis chapter", "Work up courage to talk to Emma", "Find rare book for research"] - ) - - bob = Character( - name="Bob", - age=28, - personality="Confident software developer, outgoing and helpful, loves solving technical problems", - occupation="Senior fullstack developer at local startup", - location="coffee shop", - relationships={ - "Alice": "Quiet regular I've seen around - seems nice", - "Emma": "Friendly barista, always remembers my order" - }, - goals=["Launch new feature this week", "Ask someone interesting on a date", "Learn more about AI"] - ) - - emma = Character( - name="Emma", - age=25, - personality="Energetic art student working as barista, cheerful and social, dreams of opening gallery", - occupation="Barista and art student", - location="coffee shop counter", - relationships={ - "Alice": "Sweet regular who seems shy - orders same drink daily", - "Bob": "Tech guy regular - always friendly and tips well" - }, - goals=["Save money for art supplies", "Organize local art show", "Connect with more creative people"] - ) - - for character in [alice, bob, emma]: - self.scene_manager.add_character(character) - - def get_character_response(self, character_name: str, user_input: str) -> str: - if character_name not in self.scene_manager.agents: - return f"โŒ Character {character_name} not found!" - - print(f"๐Ÿง  {character_name} accessing memories...") - agent = self.scene_manager.agents[character_name] - - # Agent perceives user interaction - agent.perceive(f"Someone asked me: '{user_input}'") - - # Generate response - response = agent.react_to_situation(user_input) - return response - - def character_chat(self, char1: str, char2: str, context: str) -> str: - """Make two characters interact with each other""" - interaction = self.scene_manager.character_interaction(char1, char2, context) - - if "error" in interaction: - return interaction["error"] - - result = f"\n๐Ÿ’ฌ **{char1}**: {interaction[char1]}\n๐Ÿ’ฌ **{char2}**: {interaction[char2]}\n" - return result - - def advance_scene_time(self, hours: int = 1): - """Advance time and let characters plan""" - self.scene_manager.advance_time(hours) - return f"โฐ Advanced time by {hours} hour(s). Current time: {self.scene_manager.scene_state['time']}" - - def get_character_memories(self, character_name: str, memory_type: str = "all") -> str: - """Show character's memory stream for debugging""" - if character_name not in self.scene_manager.agents: - return f"Character {character_name} not found" - - agent = self.scene_manager.agents[character_name] - memories = agent.memory_stream.memories - - if memory_type != "all": - memories = [m for m in memories if m.memory_type == memory_type] - - result = f"\n๐Ÿง  {character_name}'s {memory_type} memories ({len(memories)} total):\n" - for i, memory in enumerate(memories[-10:]): # Show last 10 - result += f"{i+1}. [{memory.memory_type}] {memory.description} (importance: {memory.importance_score})\n" - - return result - - def get_character_summary(self, character_name: str) -> str: - """Get AI-generated summary of character based on their memories""" - if character_name not in self.scene_manager.agents: - return f"Character {character_name} not found" - - agent = self.scene_manager.agents[character_name] - summary = agent.get_summary() - - return f"\n๐Ÿ“ Current summary of {character_name}:\n{summary}\n" - - -def main(): - print("๐ŸŽญ Advanced Multi-Agent Roleplay with Stanford Memory Architecture") - print("=" * 70) - print("This implements Stanford's proven memory system:") - print("โ€ข Memory Stream: observations, reflections, plans") - print("โ€ข Smart Retrieval: recency + importance + relevance") - print("โ€ข Auto Reflection: generates insights when importance threshold hit") - print("โ€ข Natural Forgetting: older memories become less accessible") - print() - print("๐ŸŽฏ COMMANDS:") - print(" talk - Character responds using their memories") - print(" chat - Two characters interact") - print(" time - Advance time, triggers planning") - print(" memories [type] - Show character's memories") - print(" summary - AI summary of character") - print(" status - Show scene status") - print(" quit - Exit") - print() - - if not os.getenv("OPENAI_API_KEY"): - print("โš ๏ธ Set OPENAI_API_KEY environment variable to use real LLMs") - print() - - system = EnhancedRoleplaySystem() - - # Give agents some initial experiences - print("๐ŸŒฑ Setting up initial memories...") - system.scene_manager.agents["Alice"].perceive("I spilled coffee on my notes yesterday - so embarrassing") - system.scene_manager.agents["Alice"].perceive("Emma helped me clean up and was really sweet about it") - system.scene_manager.agents["Bob"].perceive("Shipped a major feature at work - feeling accomplished") - system.scene_manager.agents["Emma"].perceive("A shy regular (Alice) has been coming in every day this week") - print("โœ“ Initial memories established") - print() - - print("๐Ÿงช TRY THESE EXPERIMENTS:") - print("1. talk Alice How are you feeling today?") - print("2. time 2 (advance time to trigger reflection)") - print("3. memories Alice reflection (see generated insights)") - print("4. chat Alice Emma You both seem to be here often") - print("5. summary Alice (see how memories shaped character)") - print() - - while True: - try: - command = input("> ").strip() - - if command == "quit": - print("๐Ÿ‘‹ Goodbye!") - break - elif command == "status": - print(f"\n๐Ÿ“ Scene: {system.scene_manager.scene_state['location']}") - print(f"โฐ Time: {system.scene_manager.scene_state['time']}") - print(f"๐Ÿ‘ฅ Characters: {', '.join(system.scene_manager.characters.keys())}") - for name, agent in system.scene_manager.agents.items(): - mem_count = len(agent.memory_stream.memories) - reflections = len([m for m in agent.memory_stream.memories if m.memory_type == "reflection"]) - print(f" {name}: {mem_count} memories ({reflections} reflections)") - print() - - elif command.startswith("talk "): - parts = command.split(" ", 2) - if len(parts) >= 3: - character, message = parts[1], parts[2] - print(f"\n๐Ÿ—ฃ๏ธ You to {character}: {message}") - response = system.get_character_response(character, message) - print(f"๐Ÿ’ฌ {character}: {response}\n") - else: - print("โ“ Usage: talk ") - - elif command.startswith("chat "): - parts = command.split(" ", 3) - if len(parts) >= 4: - char1, char2, context = parts[1], parts[2], parts[3] - print(f"\n๐ŸŽฌ Setting up interaction: {context}") - result = system.character_chat(char1, char2, context) - print(result) - else: - print("โ“ Usage: chat ") - - elif command.startswith("time "): - try: - hours = int(command.split()[1]) - result = system.advance_scene_time(hours) - print(result) - # Show what characters are planning - for name, agent in system.scene_manager.agents.items(): - plan = agent.plan_day() - print(f"๐Ÿ“… {name}'s plan: {plan[0] if plan else 'No specific plans'}") - except (IndexError, ValueError): - print("โ“ Usage: time ") - - elif command.startswith("memories "): - parts = command.split() - character = parts[1] if len(parts) > 1 else "" - memory_type = parts[2] if len(parts) > 2 else "all" - if character: - result = system.get_character_memories(character, memory_type) - print(result) - else: - print("โ“ Usage: memories [observation/reflection/plan/all]") - - elif command.startswith("summary "): - character = command.split()[1] if len(command.split()) > 1 else "" - if character: - result = system.get_character_summary(character) - print(result) - else: - print("โ“ Usage: summary ") - - else: - print("โ“ Commands: talk, chat, time, memories, summary, status, quit") - - except KeyboardInterrupt: - print("\n๐Ÿ‘‹ Goodbye!") - break - except Exception as e: - print(f"๐Ÿ’ฅ Error: {e}") - - -if __name__ == "__main__": - main() diff --git a/character_explorer.py b/character_explorer.py index 7d15f1d..c921903 100644 --- a/character_explorer.py +++ b/character_explorer.py @@ -94,31 +94,32 @@ class CharacterExplorer: filter_choice = input("Choose filter (1-6): ").strip() - memories = self.agent.memory_stream.memories.copy() + # memories = self.agent.memory_stream.memories.copy() if filter_choice == "2": - memories = [m for m in memories if m.memory_type == "observation"] + memories = [m for m in self.agent.memory_stream.memories if m.memory_type == "observation"] title = "Observations" elif filter_choice == "3": - memories = [m for m in memories if m.memory_type == "reflection"] + memories = [m for m in self.agent.memory_stream.memories if m.memory_type == "reflection"] title = "Reflections" elif filter_choice == "4": - memories = [m for m in memories if m.memory_type == "plan"] + memories = [m for m in self.agent.memory_stream.memories if m.memory_type == "plan"] title = "Plans" elif filter_choice == "5": - memories = sorted(memories, key=lambda m: m.importance_score, reverse=True) + memories = sorted(self.agent.memory_stream.memories, key=lambda m: m.importance_score, reverse=True) title = "All Memories (by importance)" elif filter_choice == "6": - memories = sorted(memories, key=lambda m: m.creation_time, reverse=True) + memories = sorted(self.agent.memory_stream.memories, key=lambda m: m.creation_time, reverse=True) title = "All Memories (by recency)" else: + memories = self.agent.memory_stream.memories title = "All Memories" print(f"\n๐Ÿ“‹ {title} ({len(memories)} total):") - for i, memory in enumerate(memories, 1): + for i, memory in enumerate(memories): age_hours = (memory.last_accessed - memory.creation_time).total_seconds() / 3600 print( - f"{i:3d}. [{memory.memory_type[:4]}] [imp:{memory.importance_score}] [age:{age_hours:.1f}h] {memory.description}") + f"[#{i:3d}] [{memory.memory_type[:4]}] [imp:{memory.importance_score}] [age:{age_hours:.1f}h] {memory.description}") if len(memories) > 20: print(f"\n... showing first 20 of {len(memories)} memories") @@ -126,43 +127,28 @@ class CharacterExplorer: async def _handle_view_memory(self): """View a specific memory with its related memories""" - try: - memory_num = int(input(f"\nEnter memory number (1-{len(self.agent.memory_stream.memories)}): ").strip()) + memory_num = int(input(f"\nEnter memory number (1-{len(self.agent.memory_stream.memories) - 1}): ").strip()) - if 1 <= memory_num <= len(self.agent.memory_stream.memories): - memory = self.agent.memory_stream.memories[memory_num - 1] + if 0 <= memory_num <= len(self.agent.memory_stream.memories) - 1: + memory = self.agent.memory_stream.memories[memory_num] - print(f"\n๐Ÿ” Memory #{memory_num} Details:") - print(f" Type: {memory.memory_type}") - print(f" Importance: {memory.importance_score}/10") - print(f" Created: {memory.creation_time.strftime('%Y-%m-%d %H:%M:%S')}") - print(f" Last accessed: {memory.last_accessed.strftime('%Y-%m-%d %H:%M:%S')}") - print(f" Description: {memory.description}") + print(f"\n๐Ÿ” Memory #{memory_num} Details:") + print(f" Type: {memory.memory_type}") + print(f" Importance: {memory.importance_score}/10") + print(f" Created: {memory.creation_time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f" Last accessed: {memory.last_accessed.strftime('%Y-%m-%d %H:%M:%S')}") + print(f" Description: {memory.description}") - # Show related memories using embeddings - print(f"\n๐Ÿ”— Related memories (by similarity):") - try: - related = await self.agent._get_related_memories_for_scoring( - memory.description, - exclude_self=memory, - k=5 - ) - for i, rel_mem in enumerate(related, 1): - rel_index = self.agent.memory_stream.memories.index(rel_mem) + 1 - print( - f" {i}. [#{rel_index}] [{rel_mem.memory_type}] {rel_mem.description[:70]}{'...' if len(rel_mem.description) > 70 else ''}") + # Show related memories using embeddings + if memory.memory_type == 'observation' or memory.memory_type == 'plan': + print(f"\n๐Ÿ”— Related memories:") - if not related: - print(" (No related memories found)") - - except Exception as e: - print(f" โŒ Error finding related memories: {e}") - - else: - print(f"โŒ Invalid memory number. Range: 1-{len(self.agent.memory_stream.memories)}") - - except ValueError: - print("โŒ Please enter a valid number") + for rel_mem in memory.related_memories: + rel_index = self.agent.memory_stream.memories.index(rel_mem) + print( + f" [#{rel_index:3d}] [{rel_mem.memory_type}] {rel_mem.description}") + else: + print(f"โŒ Invalid memory number. Range: 1-{len(self.agent.memory_stream.memories)}") async def _handle_memory_stats(self): """Show detailed memory statistics""" @@ -217,7 +203,10 @@ class CharacterExplorer: print(f" Personality: {self.agent.character.personality}") print(f" Occupation: {self.agent.character.occupation}") print(f" Location: {self.agent.character.location}") - + print("") + 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." + print(traits_summary) if self.agent.character.relationships: print(f" Relationships:") for person, relationship in self.agent.character.relationships.items(): diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..00a49e8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,53 @@ +# Living Agents - Character Development System + +A sophisticated AI-powered character development system featuring memory-based agents with dynamic personality evolution +based on Stanford's "Generative Agents" research. + +## ๐ŸŽฏ Project Goals + +- Create believable AI characters that remember past interactions +- Implement dynamic personality development based on experiences +- Build characters that evolve naturally through their experiences +- Provide an interactive CLI tool for testing and developing characters + +## ๐Ÿ—๏ธ System Architecture + +The system consists of several interconnected components: + +1. **Character Agents** - AI entities with memory and personality +2. **Memory Stream** - Stanford-inspired memory architecture +3. **Prompt Management** - Centralized, templated prompt system +4. **Character Explorer** - CLI tool for testing character development +5. **Trait System** - Dynamic personality development +6. **LLM Connector** - Flexible backend supporting any OpenAI-compatible API + +## ๐Ÿš€ Quick Start + +1. Set up environment variables in `.env` +2. Create character templates in YAML format +3. Run the character explorer CLI to interact with agents +4. Observe personality development over time + +## ๐Ÿ“ Project Structure + +``` +character_templates/ # YAML character definition files +living_agents/ # Core agent system +โ”œโ”€โ”€ character_agent.py # Main character agent implementation +โ”œโ”€โ”€ memory_stream.py # Memory architecture +โ”œโ”€โ”€ prompts/ # LLM prompt templates and JSON schemas +โ”œโ”€โ”€ datatypes.py # Data structures and models +โ”œโ”€โ”€ prompt_manager.py # Template management system +โ””โ”€โ”€ llmagent.py # LLM integration wrapper +llm_connector/ # LLM backend abstraction +character_explorer.py # CLI tool for testing characters +``` + +## ๐ŸŽญ Key Features + +- **Memory-Based Conversations**: Characters remember and reference past interactions +- **Dynamic Traits**: Personality develops incrementally from experiences +- **Structured Outputs**: JSON schemas for reliable LLM responses +- **Template System**: Easy character creation from YAML files +- **Flexible LLM Backend**: Support for any OpenAI-compatible API endpoint +- **CLI Testing Tool**: Interactive character exploration and development \ No newline at end of file diff --git a/docs/character-agents.md b/docs/character-agents.md new file mode 100644 index 0000000..5cac80c --- /dev/null +++ b/docs/character-agents.md @@ -0,0 +1,117 @@ +# Character Agent System + +Character agents are the core AI entities that combine memory, personality, and conversational ability into believable +characters that evolve through their experiences. + +## ๐ŸŽญ Agent Architecture + +### Core Components + +1. **Memory Stream**: Stanford-inspired memory architecture +2. **Character Data**: Structured personality and relationship info +3. **LLM Integration**: Natural language processing and generation +4. **Trait System**: Dynamic personality development +5. **Response Generation**: Context-aware conversation handling + +### Character Creation Process + +1. **Template Loading**: YAML files define initial memories +2. **Memory Initialization**: Observations, reflections, and plans loaded +3. **Importance Scoring**: All memories rated for significance +4. **Character Extraction**: LLM generates structured character data +5. **Agent Ready**: Fully functional roleplay partner + +## ๐Ÿ“ Character Templates + +### YAML Structure + +```yaml +observations: + - "My name is Alice and I am 23 years old" + - "I study Victorian literature at university" + - "I spilled coffee yesterday and felt embarrassed" + +reflections: + - "I have romantic feelings for Emma (evidence: daily visits, heart racing)" + - "I am naturally shy in social situations (evidence: nervous with strangers)" + +plans: + - "I want to work up courage to talk to Emma" + - "I need to finish my thesis chapter this week" +``` + +### Memory Types in Templates + +**Observations**: Factual experiences and basic information + +- Identity facts (name, age, occupation) +- Recent experiences and events +- Relationship interactions +- Physical descriptions and traits + +**Reflections**: Character insights and self-understanding + +- Personality trait recognition +- Relationship feelings and dynamics +- Behavioral pattern awareness +- Values and belief formation + +**Plans**: Future intentions and goals + +- Short-term objectives +- Long-term dreams and aspirations +- Relationship goals +- Personal development aims + +## ๐ŸŽฏ Response Generation + +### Context Building Process + +1. **Query Analysis**: Understand what user is asking +2. **Memory Retrieval**: Find relevant memories using smart scoring +3. **Context Assembly**: Combine character info + relevant memories +4. **Prompt Construction**: Use template system for consistency +5. **LLM Generation**: Natural language response in character +6. **Memory Update**: Store new experience from interaction + +### Response Style + +- **First Person Past Tense**: "I looked up and smiled nervously..." +- **Character Consistency**: Responses match established personality +- **Memory Integration**: References past experiences naturally +- **Emotional Authenticity**: Shows appropriate feelings and reactions + +## ๐Ÿ”„ Character Development + +### Dynamic Personality Growth + +Characters evolve through experience: + +1. **New Experiences**: Each interaction creates memories +2. **Trait Analysis**: System evaluates personality impact +3. **Trait Updates**: Strengths/weaknesses adjust over time +4. **Reflection Generation**: Insights emerge from patterns +5. **Behavioral Consistency**: Future responses reflect growth + +### Trait System + +- **Incremental Development**: Traits strengthen/weaken with evidence +- **Evidence-Based**: Every trait change linked to specific experiences +- **Single-Word Names**: Simple, clear personality descriptors +- **Strength Ratings**: 1-10 scale for trait intensity +- **Dynamic Descriptions**: How traits manifest in behavior + +## ๐Ÿ”ฎ Future Vision: Multi-Agent Interactions + +### Planned Features + +The system is designed with future multi-agent capabilities in mind: + +- Characters will be able to interact with each other +- Conversations will create memories for all participants +- Relationship dynamics will develop naturally +- Information will spread through character networks +- Emergent social behaviors will arise from interactions + +Currently, the focus is on perfecting single-agent character development and ensuring each character becomes genuinely +complex and believable through their individual growth. \ No newline at end of file diff --git a/docs/memory-system.md b/docs/memory-system.md new file mode 100644 index 0000000..a0ba3f7 --- /dev/null +++ b/docs/memory-system.md @@ -0,0 +1,87 @@ +# Memory Architecture + +The memory system is inspired by Stanford's "Generative Agents" research, implementing a sophisticated memory model that +enables realistic long-term character development. + +## ๐Ÿง  Memory Types + +### Observations + +- **What**: Direct experiences and perceptions +- **Examples**: "I spilled coffee", "Emma smiled at me", "It's raining outside" +- **Importance**: Usually 1-5 for mundane events, 8-10 for significant experiences +- **Purpose**: Raw building blocks of character experience + +### Reflections + +- **What**: Higher-level insights generated from observation patterns +- **Examples**: "I have romantic feelings for Emma", "I'm naturally shy in social situations" +- **Importance**: Usually 6-10 (insights are more valuable than raw observations) +- **Purpose**: Character self-understanding and behavioral consistency + +### Plans + +- **What**: Future intentions and goals +- **Examples**: "I want to ask Emma about her art", "I should finish my thesis chapter" +- **Importance**: 3-10 depending on goal significance +- **Purpose**: Drive future behavior and maintain character consistency + +## ๐Ÿ” Memory Retrieval + +### Smart Retrieval Algorithm + +Memories are scored using three factors: + +1. **Recency** - Recent memories are more accessible + ```python + recency = 0.995 ** hours_since_last_accessed + ``` + +2. **Importance** - Significant events stay memorable longer + ```python + importance = memory.importance_score / 10.0 + ``` + +3. **Relevance** - Contextually similar memories surface together + ```python + relevance = cosine_similarity(query_embedding, memory_embedding) + ``` + +### Final Score + +```python +score = recency + importance + relevance +``` + +## ๐ŸŽฏ Automatic Reflection Generation + +When accumulated importance of recent memories exceeds threshold (150): + +1. **Analyze Recent Experiences**: Get last 20 observations +2. **Generate Insights**: Use LLM to identify patterns and higher-level understanding +3. **Create Reflections**: Store insights as new reflection memories +4. **Link Evidence**: Connect reflections to supporting observations + +## ๐Ÿ’พ Memory Storage + +Each memory contains: + +- `description`: Natural language content +- `creation_time`: When the memory was formed +- `last_accessed`: When it was last retrieved (affects recency) +- `importance_score`: 1-10 significance rating +- `embedding`: Vector representation for similarity matching +- `memory_type`: observation/reflection/plan +- `related_memories`: Links to supporting evidence + +## ๐Ÿ”„ Memory Lifecycle + +1. **Creation**: New experience becomes observation +2. **Scoring**: LLM rates importance 1-10 +3. **Storage**: Added to memory stream with embedding +4. **Retrieval**: Accessed during relevant conversations +5. **Reflection**: Patterns trigger insight generation +6. **Evolution**: Older memories naturally fade unless repeatedly accessed + +This creates realistic, human-like memory behavior where important experiences remain accessible while mundane details +naturally fade over time. \ No newline at end of file diff --git a/docs/prompt-system.md b/docs/prompt-system.md new file mode 100644 index 0000000..30baa78 --- /dev/null +++ b/docs/prompt-system.md @@ -0,0 +1,191 @@ +# Prompt Management System + +A centralized system for managing LLM prompts with templating, structured outputs, and easy editing capabilities. + +## ๐ŸŽฏ System Goals + +- **Centralized Management**: All prompts in one organized location +- **Easy Editing**: Modify prompts without touching code +- **Template Support**: Dynamic variable substitution +- **Structured Outputs**: JSON schemas for reliable responses +- **Type Safety**: Validation of required variables + +## ๐Ÿ“ File Organization + +``` +living_agents/prompts/ +โ”œโ”€โ”€ react_to_situation.md # Character response generation +โ”œโ”€โ”€ score_observation_importance.md # Observation memory scoring +โ”œโ”€โ”€ score_reflection_importance.md # Reflection memory scoring +โ”œโ”€โ”€ score_plan_importance.md # Plan memory scoring +โ”œโ”€โ”€ extract_character_from_memories.md # Character data extraction +โ”œโ”€โ”€ extract_character_from_memories.json # Character data schema +โ”œโ”€โ”€ generate_reflection.md # Reflection generation prompt +โ”œโ”€โ”€ generate_reflection.json # Reflection schema +โ”œโ”€โ”€ assess_trait_impact.md # Trait analysis prompt +โ”œโ”€โ”€ assess_trait_impact.json # Trait update schema +โ””โ”€โ”€ character_summary.md # Character summary generation +``` + +## ๐Ÿ“ Prompt Templates + +### Template Syntax + +Use `{{variable_name}}` for dynamic substitution: + +```markdown +You are {{character_name}}. +Age: {{character_age}} +Personality: {{character_personality}} + +Relevant memories: +{{memory_context}} + +Current situation: {{situation}} + +Respond as {{character_name}} in first person past tense. +``` + +### Variable Extraction + +System automatically detects required variables: + +- Parses `{{variable}}` patterns +- Validates all variables are provided +- Warns about missing or extra variables +- Ensures template completeness + +## ๐Ÿ—๏ธ JSON Schemas + +### Structured Output Support + +Pair `.md` prompts with `.json` schemas for reliable structured responses: + +**Example Schema** (assess_trait_impact.json): + +```json +{ + "type": "object", + "properties": { + "trait_updates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "trait_name": { + "type": "string", + "pattern": "^[a-zA-Z]+$" + }, + "action": { + "type": "string", + "enum": ["create", "strengthen", "weaken"] + }, + "new_strength": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "description": { + "type": "string" + }, + "reasoning": { + "type": "string" + } + } + } + } + } +} +``` + +### Schema Benefits + +- **Guaranteed Structure**: Always get expected JSON format +- **Type Validation**: Ensures correct data types +- **Field Requirements**: Specify required vs optional fields +- **Value Constraints**: Set min/max values, string lengths +- **Consistent Parsing**: No more JSON parsing errors + +## ๐Ÿ”ง API Usage + +### Basic Prompt Retrieval + +```python +from living_agents.prompt_manager import PromptManager + +# Simple template substitution +prompt = PromptManager.get_prompt('react_to_situation', { + 'character_name': 'Alice', + 'character_age': 23, + 'situation': 'Someone asks how you are feeling' +}) +``` + +### Structured Output + +```python +from living_agents.prompt_manager import PromptManager + +# Get both prompt and schema +prompt, schema = PromptManager.get_prompt_with_schema('assess_trait_impact', { + 'observation': 'I felt nervous talking to Emma', + 'traits_summary': 'shy (8/10), romantic (7/10)' +}) + +# Use with LLM structured output +if schema: + response = await llm.get_structured_response(messages, schema) +else: + response = await llm.chat(messages) +``` + +### Development Helpers + +```python +# List all available prompts +prompts = PromptManager.list_prompts() +print(prompts) # Shows prompt names and required variables + +# Get prompt details +info = PromptManager.get_prompt_info('react_to_situation') +print(f"Variables needed: {info['variables']}") + +# Reload during development +PromptManager.reload_prompts() # Refresh from files +``` + +## ๐ŸŽฏ Prompt Design Guidelines + +### Template Best Practices + +- **Clear Instructions**: Specify exactly what you want +- **Consistent Formatting**: Use standard variable naming +- **Context Provision**: Give LLM necessary background +- **Output Specification**: Define expected response format +- **Example Inclusion**: Show desired output style + +### Schema Design + +- **Minimal Required Fields**: Only require truly essential data +- **Reasonable Constraints**: Set realistic min/max values +- **Clear Descriptions**: Help LLM understand field purposes +- **Flexible Structure**: Allow for natural language variation +- **Error Prevention**: Design to minimize parsing failures + +## ๐Ÿ”„ System Benefits + +### For Developers + +- **Easy Maintenance**: Edit prompts without code changes +- **Type Safety**: Automatic variable validation +- **Consistent Structure**: Standardized prompt format +- **Debugging Support**: Clear error messages for missing variables + +### For LLM Performance + +- **Structured Outputs**: Eliminates JSON parsing errors +- **Consistent Prompting**: Reduces response variance +- **Context Optimization**: Templates ensure complete context +- **Schema Guidance**: Helps LLM generate correct format + +This system makes prompt management scalable, maintainable, and reliable across the entire roleplay system. \ No newline at end of file diff --git a/docs/trait-system.md b/docs/trait-system.md new file mode 100644 index 0000000..31ed481 --- /dev/null +++ b/docs/trait-system.md @@ -0,0 +1,180 @@ +# Dynamic Trait Development System + +An incremental personality development system that builds character traits from experiences and observations. + +## ๐ŸŽฏ System Philosophy + +Characters develop personality traits naturally through their experiences, rather than having fixed, predefined +personalities. This creates more realistic, evolving characters that feel genuinely shaped by their interactions. + +## ๐Ÿงฌ Trait Structure + +### CharacterTrait Data Model + +```python +@dataclass +class CharacterTrait: + name: str # Single word (shy, romantic, studious) + strength: int # 1-10 intensity scale + description: str # How trait manifests behaviorally + updated: datetime # When last modified +``` + +### Integration with Character + +```python +@dataclass +class Character: + # ... other fields ... + traits: List[CharacterTrait] = field(default_factory=list) + + def has_trait(self, trait_name: str) -> bool + + def get_trait(self, trait_name: str) -> Optional[CharacterTrait] + + def get_trait_strength(self, trait_name: str) -> int +``` + +## ๐Ÿ”„ Incremental Development Process + +### 1. Observation Analysis + +When new memories are added, system analyzes trait impact: + +```python +# Every new observation is evaluated +memory = await agent.add_observation("I felt nervous talking to Emma") + +# System asks: Does this reveal or change personality traits? +# - Create new traits? +# - Strengthen existing traits? +# - Weaken contradicting traits? +# - No significant impact? +``` + +### 2. Trait Impact Assessment + +Uses structured LLM analysis to determine changes: + +**Input**: New observation + current trait list +**Output**: Specific trait updates with reasoning + +```json +{ + "trait_updates": [ + { + "trait_name": "shy", + "action": "strengthen", + "new_strength": 8, + "description": "gets nervous in social interactions", + "reasoning": "felt nervous talking shows social anxiety" + } + ] +} +``` + +### 3. Trait Updates Applied + +- **Create**: New trait discovered from behavior +- **Strengthen**: Evidence reinforces existing trait (+1 strength) +- **Weaken**: Contradicting evidence reduces trait (-1 strength) + +## ๐ŸŽฏ Design Principles + +### Conservative Analysis + +- **Avoid Over-Interpretation**: Single events rarely create major traits +- **Require Clear Evidence**: Traits must be obviously demonstrated +- **Skip Non-Behavioral**: Physical descriptions don't create personality traits +- **Focus on Patterns**: Look for consistent behavioral indicators + +### Single-Word Traits + +- **Simplicity**: Easy to understand and reference +- **Clarity**: Unambiguous personality descriptors +- **Consistency**: Standard vocabulary across characters +- **Examples**: shy, confident, romantic, studious, helpful, creative + +### Evidence-Based Development + +- **Every Change Justified**: All trait updates have clear reasoning +- **Observation-Driven**: Traits emerge from actual experiences +- **Gradual Evolution**: Strength changes incrementally over time +- **Realistic Growth**: Matches how real personality develops + +## ๐ŸŽช Trait Impact on Behavior + +### Behavioral Consistency + +Characters with established traits should act accordingly: + +- **High Shy (8/10)**: Avoids eye contact, speaks quietly, gets nervous +- **High Romantic (9/10)**: Focuses on attractive people, seeks connections +- **High Studious (7/10)**: Prioritizes learning, discusses academic topics + +### Dynamic Responses + +Traits influence how characters react to situations: + +```python +# Character with "shy" trait (strength 8) +response = "I looked down at my hands and mumbled quietly..." + +# Character with "confident" trait (strength 9) +response = "I smiled broadly and spoke up clearly..." +``` + +### Trait Interactions + +Multiple traits create complex, realistic personalities: + +- **Shy + Romantic**: Wants connection but too nervous to approach +- **Studious + Creative**: Academic pursuits with artistic expression +- **Helpful + Confident**: Takes charge to assist others + +## ๐Ÿ“Š Trait Analytics + +### Personality Summaries + +```python +# Get dominant traits +strong_traits = character.get_active_traits(min_strength=7) +# Returns: {shy: 8/10, romantic: 9/10, studious: 7/10} + +# Generate personality description +summary = character.get_personality_summary() +# Returns: "shy (8/10), romantic (9/10), studious (7/10)" +``` + +### Trait Evolution Tracking + +- **Strength Changes**: Monitor how traits develop over time +- **New Discoveries**: Track when traits first emerge +- **Behavioral Patterns**: Observe consistency between traits and actions +- **Character Growth**: See personality evolution through experiences + +## ๐ŸŽฏ Benefits + +### Realistic Development + +- **Gradual Change**: Personality evolves naturally over time +- **Experience-Driven**: Traits emerge from actual interactions +- **Individual Variation**: Each character develops uniquely +- **Authentic Growth**: Matches real psychological development + +### Improved Roleplay + +- **Consistent Characters**: Behavior matches established personality +- **Dynamic Evolution**: Characters grow and change realistically +- **Rich Personalities**: Complex trait combinations create depth +- **Believable Responses**: Actions align with developed traits + +### System Intelligence + +- **Automatic Development**: No manual trait assignment needed +- **Evidence-Based**: Every trait justified by specific experiences +- **Scalable Growth**: Works across unlimited characters and interactions +- **Self-Improving**: Characters become more defined over time + +This creates characters that feel genuinely alive and psychologically realistic, with personalities that develop +naturally from their experiences and relationships. \ No newline at end of file diff --git a/living_agents/character_agent.py b/living_agents/character_agent.py index a1bbb97..0653070 100644 --- a/living_agents/character_agent.py +++ b/living_agents/character_agent.py @@ -1,10 +1,12 @@ import logging +import random +from datetime import datetime from http.client import responses from pprint import pprint from tqdm.asyncio import tqdm from typing import List, Self from living_agents import MemoryStream, LLMAgent, Character, PromptManager, Memory -from living_agents.datatypes import CharacterTemplate +from living_agents.datatypes import CharacterTemplate, CharacterTrait from llm_connector import LLMMessage logger = logging.getLogger(__name__) @@ -37,9 +39,10 @@ class CharacterAgent: async def perceive(self, observation: str, skip_scoring=False) -> None: """Add new observation to memory stream""" if skip_scoring: - await self.memory_stream.add_observation(observation) + new_memory = await self.memory_stream.add_observation(observation) else: - await self.memory_stream.add_observation(observation, self._score_memory_importance) + new_memory = await self.memory_stream.add_observation(observation, self._score_memory_importance) + await self._analyze_trait_impact(new_memory) async def react_to_situation(self, situation: str) -> str: """Generate reaction based on memory and character""" @@ -118,37 +121,26 @@ Summary:""" except: return f"{self.character.name} is a {self.character.age}-year-old {self.character.occupation}." - async def _get_related_memories_for_scoring(self, memory_text: str, exclude_self=None, k=5) -> List: - """Get memories related to the one being scored""" - # Get embedding for the memory being scored - memory_embedding = await self.llm.get_embedding(memory_text) - - # Calculate similarity to other memories - similarities = [] - for mem in self.memory_stream.memories: - if mem == exclude_self: - continue - - if mem.embedding: - from sklearn.metrics.pairwise import cosine_similarity - similarity = cosine_similarity([memory_embedding], [mem.embedding])[0][0] - similarities.append((similarity, mem)) - - # Return top K most similar memories - similarities.sort(reverse=True, key=lambda x: x[0]) - return [mem for _, mem in similarities[:k]] - async def _score_memory_importance(self, memory: Memory) -> int: """Score importance with related memories as context""" - related_memories = await self._get_related_memories_for_scoring(memory.description, exclude_self=memory, k=5) + related_memories = await self.memory_stream.get_related_memories_for_scoring(memory.description, exclude_self=memory, k=5) - prompt_context = {'character': self._get_character_prompt(), + prompt_context = {'character_context': self._get_character_prompt(), + 'character_name': self.character.name, 'related_memories': "\n".join([m.description for m in related_memories]), - 'memory_text': memory.description, - 'memory_type': memory.memory_type} + 'memory_text': memory.description} + if memory.memory_type == 'observation': + prompt = PromptManager.get_prompt('score_observation_importance', prompt_context) + elif memory.memory_type == 'reflection': + prompt = PromptManager.get_prompt('score_reflection_importance', prompt_context) + elif memory.memory_type == 'plan': + prompt = PromptManager.get_prompt('score_plan_importance', prompt_context) - prompt = PromptManager.get_prompt('score_importance_with_context', prompt_context) + # if reflection or plan, add related memories. + if memory.memory_type == 'reflection' or memory.memory_type == 'plan': + for rel_memory in related_memories: + memory.related_memories.append(rel_memory) try: response = await self.llm.chat([{"role": "user", "content": prompt}], max_tokens=5) @@ -157,7 +149,38 @@ Summary:""" except: return 5 # Default - async def _extract_character_from_memories(self) -> Character: + async def _analyze_trait_impact(self, memory: Memory): + + traits_summary = "\n".join([f" - {trait.strength}/10 {trait.name} ({trait.description})" for trait in self.character.traits]) if self.character.traits else "No traits yet." + prompt_context = {'character_name': self.character.name, + 'current_traits': traits_summary, + 'new_observation': memory.description} + prompt, schema = PromptManager.get_prompt_with_schema('assess_trait_impact', prompt_context) + messages: List[LLMMessage] = [{'role': 'user', 'content': prompt}] + response = await self.llm.client.get_structured_response(messages, schema) + for trait_update in response['trait_updates']: + trait_to_update = self.character.get_trait(trait_update['trait_name'], trait_update['description']) + if trait_update['action'] == 'create' or trait_update['action'] == 'strengthen': + await self._strengthen_trait(trait_to_update) + else: + await self._weaken_trait(trait_to_update) + + @staticmethod + async def _strengthen_trait(trait: CharacterTrait, steepness: float = 1.0): + if trait.strength >= 10: + return + if random.random() < trait.change_by_probability(steepness): + trait.strength += 1 + trait.updated = datetime.now() + + async def _weaken_trait(self, trait: CharacterTrait, steepness: float = 1.0): + if random.random() < trait.change_by_probability(steepness): + trait.strength -= 1 + trait.updated = datetime.now() + if trait.strength <= 0: + self.character.traits.remove(trait) + + async def _generate_character_from_memories(self) -> Character: """Extract Character info from memories using JSON""" # Get different types of memories with targeted queries @@ -221,14 +244,16 @@ Summary:""" # create the character before we score to include the character in the prompts # Extract character info from memories to populate Character object logger.info(f"Creating Character...") - instance.character = await instance._extract_character_from_memories() + instance.character = await instance._generate_character_from_memories() logger.info(f"Added {len(instance.memory_stream.memories)} memories, now scoring importance...") - # Score all memories with full context - for memory in tqdm(instance.memory_stream.memories, desc="Scoring memory importance", unit="memory"): + # Score all observations with importance + observations = [memory for memory in instance.memory_stream.memories if memory.memory_type == 'observation'] + for memory in tqdm(observations, desc="Scoring memory importance", unit="memory"): # Score with related context memory.importance_score = await instance._score_memory_importance(memory) + await instance._analyze_trait_impact(memory) logger.info(f"Character {instance.character.name} created successfully") return instance diff --git a/living_agents/datatypes.py b/living_agents/datatypes.py index a87394a..85f794b 100644 --- a/living_agents/datatypes.py +++ b/living_agents/datatypes.py @@ -2,6 +2,7 @@ from dataclasses import dataclass, field from typing import Dict, List, Optional, Literal, TypedDict from datetime import datetime from uuid import uuid4 +import random class CharacterTemplate(TypedDict): @@ -21,24 +22,65 @@ class Memory: importance_score: int = 5 # 1-10 scale embedding: Optional[List[float]] = None memory_type: Literal["observation", "reflection", "plan"] = "observation" - related_memories: List[int] = field(default_factory=list) # IDs of supporting memories + related_memories: List['Memory'] = field(default_factory=list) # IDs of supporting memories def __post_init__(self): if self.last_accessed is None: self.last_accessed = self.creation_time +@dataclass +class CharacterTrait: + name: str + description: str + strength: int = 0 + updated: datetime = field(default_factory=datetime.now) + + def change_by_probability(self, steepness: float = 1.0) -> float: + """ + Returns probability of trait change (0.0 to 1.0) + + steepness: higher values = more resistance to change + steepness = 1.0 (moderate): + + Strength 1: 90% chance + Strength 5: 50% chance + Strength 9: 10% chance + + steepness = 2.0 (steep): + + Strength 1: 81% chance + Strength 5: 25% chance + Strength 9: 1% chance + + steepness = 0.5 (gradual): + + Strength 1: 95% chance + Strength 5: 71% chance + Strength 9: 32% chance + """ + return (10 - self.strength) / 10.0 ** steepness + + @dataclass class Character: - name: str # Still required + name: str age: Optional[int] = None personality: str = "" occupation: str = "" location: str = "" + traits: List[CharacterTrait] = field(default_factory=list) relationships: Dict[str, str] = field(default_factory=dict) goals: List[str] = field(default_factory=list) _id: str = field(default_factory=lambda: str(uuid4())[:8]) + def get_trait(self, trait_name, trait_description) -> CharacterTrait: + for trait in self.traits: + if trait.name.lower() == trait_name.lower(): + return trait + self.traits.append(CharacterTrait(name=trait_name.lower(), strength=0, description=trait_description)) + return self.traits[-1] + def __hash__(self): return hash(self._id) diff --git a/living_agents/memory_stream.py b/living_agents/memory_stream.py index d326ae1..3850e14 100644 --- a/living_agents/memory_stream.py +++ b/living_agents/memory_stream.py @@ -88,6 +88,26 @@ class MemoryStream: ) self.memories.append(reflection) + async def get_related_memories_for_scoring(self, memory_text: str, exclude_self=None, k=5) -> List: + """Get memories related to the one being scored""" + # Get embedding for the memory being scored + memory_embedding = await self.llm.get_embedding(memory_text) + + # Calculate similarity to other memories + similarities = [] + for mem in self.memories: + if mem == exclude_self: + continue + + if mem.embedding: + from sklearn.metrics.pairwise import cosine_similarity + similarity = cosine_similarity([memory_embedding], [mem.embedding])[0][0] + similarities.append((similarity, mem)) + + # Return top K most similar memories + similarities.sort(reverse=True, key=lambda x: x[0]) + return [mem for _, mem in similarities[:k]] + async def retrieve_related_memories(self, query: str, k: int = 10) -> List[Memory]: """Retrieve relevant memories using recency, importance, relevance""" if not self.memories: diff --git a/living_agents/prompts/assess_trait_impact.json b/living_agents/prompts/assess_trait_impact.json new file mode 100644 index 0000000..49e8175 --- /dev/null +++ b/living_agents/prompts/assess_trait_impact.json @@ -0,0 +1,52 @@ +{ + "type": "object", + "properties": { + "trait_updates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "trait_name": { + "type": "string", + "pattern": "^[a-zA-Z]+$", + "description": "Single word trait name (shy, confident, romantic, etc.)" + }, + "action": { + "type": "string", + "enum": [ + "create", + "strengthen", + "weaken" + ] + }, + "new_strength": { + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "description": { + "type": "string", + "maxLength": 60, + "description": "Very short description of the trait itself." + }, + "reasoning": { + "type": "string", + "maxLength": 80, + "description": "Brief explanation why this observation affects this trait (max 80 chars)" + } + }, + "required": [ + "trait_name", + "action", + "description", + "reasoning" + ] + }, + "maxItems": 2, + "description": "Max 2 trait updates per observation. Empty array if no trait impact." + } + }, + "required": [ + "trait_updates" + ] +} \ No newline at end of file diff --git a/living_agents/prompts/assess_trait_impact.md b/living_agents/prompts/assess_trait_impact.md new file mode 100644 index 0000000..30d4dbb --- /dev/null +++ b/living_agents/prompts/assess_trait_impact.md @@ -0,0 +1,25 @@ +A new observation has been added to {{character_name}}'s memories. Does this observation reveal or change any +personality traits? + +New observation: {{new_observation}} + +Current known traits: +{{current_traits}} + +**IMPORTANT RULES:** + +- Trait names must be single words (shy, confident, romantic, studious, etc.) +- Only analyze meaningful behavioral or emotional observations +- Physical descriptions and basic facts do NOT create personality traits +- If no clear trait impact, return empty array +- Be conservative - avoid over-interpreting single events +- Provide a short description of how the trait manifests + +Examples: + +- "I felt nervous talking to someone" โ†’ strengthen "shy" (gets anxious in social situations) +- "I helped a stranger" โ†’ strengthen "helpful" (assists others without being asked) +- "I have brown hair" โ†’ NO trait impact +- "My name is Alice" โ†’ NO trait impact + +Analyze ONLY if this observation shows personality, behavior, or emotional patterns. \ No newline at end of file diff --git a/living_agents/prompts/score_importance_with_context.md b/living_agents/prompts/score_importance_with_context.md deleted file mode 100644 index 3bea4f8..0000000 --- a/living_agents/prompts/score_importance_with_context.md +++ /dev/null @@ -1,37 +0,0 @@ -{{character}} - -Rate the importance of this memory on a scale 1-10. - -Related context from this character: -{{related_memories}} - -Memory to rate: {{memory_text}} -Memory type: {{memory_type}} - -Guidelines: - -**Observations:** - -- Core identity (name, age, physical traits): 8-9 (essential for character consistency) -- Personality traits and characteristics: 7-9 (fundamental to who they are) -- Significant relationships and emotional connections: 6-9 (defines social bonds) -- Major life events, achievements, failures: 8-10 (shapes character development) -- Skills, occupation, expertise: 6-8 (defines capabilities and role) -- Daily routines and mundane activities: 1-3 (low significance unless meaningful) -- Life-changing events, trauma, breakthroughs: 10 (transforms the character) - -**Reflections:** - -- Self-awareness and personality insights: 8-10 (core understanding of self) -- Understanding of relationships with others: 7-9 (social comprehension) -- Minor observations about preferences: 6-7 (useful but not critical) -- Life philosophy and values: 9-10 (guides all behavior) - -**Plans:** - -- Life-defining goals and dreams: 9-10 (drives major decisions) -- Important short-term objectives: 6-8 (affects immediate behavior) -- Casual wishes and minor wants: 3-5 (low priority desires) - -Given the context, how important is this memory for understanding and portraying this character? Respond with only a -number 1-10. \ No newline at end of file diff --git a/living_agents/prompts/score_memory_importance.md b/living_agents/prompts/score_memory_importance.md deleted file mode 100644 index bfdfc5e..0000000 --- a/living_agents/prompts/score_memory_importance.md +++ /dev/null @@ -1,25 +0,0 @@ -Rate how important this memory would be to this specific person (1-10): - -{{character_context}} - -Memory: {{description}} - -Consider: -- Does this relate to their personality traits? -- Does this connect to their occupation or goals? -- Would someone with this personality care deeply about this? -- Is this core identity information? (Always rate 8-9) - -Examples: -- "My name is Sarah and I'm 25" = 9 (fundamental identity) -- "My personality is shy and thoughtful" = 9 (core self-knowledge) -- Art student + "saw beautiful painting" = 8 -- Art student + "debugged code" = 3 -- Shy person + "gave public speech" = 9 -- Outgoing person + "gave public speech" = 5 -- "I brushed my teeth" = 1 -- "I had lunch" = 2 - -Return ONLY the number, no explanation. - -Rating: \ No newline at end of file diff --git a/living_agents/prompts/score_observation_importance.md b/living_agents/prompts/score_observation_importance.md new file mode 100644 index 0000000..44b683f --- /dev/null +++ b/living_agents/prompts/score_observation_importance.md @@ -0,0 +1,23 @@ +Rate the importance of this observation for understanding {{character_name}}. + +Character context: +{{character_context}} + +Related memories: +{{related_memories}} + +Observation to rate: {{memory_text}} + +**Observation Importance Guidelines:** + +- Core identity (name, age, physical traits): 8-9 (essential for character consistency) +- Personality-revealing behavior: 7-9 (shows who they really are) +- Significant emotional experiences: 6-9 (shapes their feelings and reactions) +- Relationship interactions and social moments: 6-8 (defines connections with others) +- Skills, talents, expertise demonstrations: 6-7 (shows capabilities) +- Daily routines and habits: 2-4 (unless they reveal personality) +- Mundane activities: 1-3 (low significance) +- Life-changing events: 9-10 (transforms the character) + +How important is this observation for understanding {{character_name}}'s personality and behavior? Respond with only a +number 1-10. \ No newline at end of file diff --git a/living_agents/prompts/score_plan_importance.md b/living_agents/prompts/score_plan_importance.md new file mode 100644 index 0000000..9b11428 --- /dev/null +++ b/living_agents/prompts/score_plan_importance.md @@ -0,0 +1,23 @@ +Rate the importance of this plan for understanding {{character_name}}'s motivations. + +Character context: +{{character_context}} + +Related memories that led to this plan: +{{related_memories}} + +Plan to rate: {{memory_text}} + +**Plan Importance Guidelines:** + +- Life-defining goals and dreams: 9-10 (shapes major life decisions) +- Important personal objectives: 7-9 (affects significant choices) +- Relationship goals: 6-8 ("ask X out", "reconnect with family") +- Career and achievement plans: 6-8 (defines professional direction) +- Short-term meaningful objectives: 5-7 (affects immediate behavior) +- Social plans and activities: 4-6 ("go to party", "meet friends") +- Casual wishes and minor wants: 2-4 (low priority desires) +- Routine plans: 1-3 ("buy groceries", "do laundry") + +Plans reveal what motivates the character and what they prioritize. How important is this plan for understanding +{{character_name}}'s drives and priorities? Respond with only a number 1-10. \ No newline at end of file diff --git a/living_agents/prompts/score_reflection_importance.md b/living_agents/prompts/score_reflection_importance.md new file mode 100644 index 0000000..1edb31f --- /dev/null +++ b/living_agents/prompts/score_reflection_importance.md @@ -0,0 +1,22 @@ +Rate the importance of this reflection for understanding {{character_name}}. + +Character context: +{{character_context}} + +Supporting evidence this reflection is based on: +{{related_memories}} + +Reflection to rate: {{memory_text}} + +**Reflection Importance Guidelines:** + +- Deep self-awareness insights: 9-10 (core understanding of self) +- Personality trait recognition: 8-9 ("I am shy", "I value kindness") +- Relationship understanding: 7-9 ("I have feelings for X", "X is trustworthy") +- Behavioral pattern recognition: 7-8 ("I avoid conflict", "I help others") +- Values and beliefs: 8-10 (guides all future behavior) +- Preferences and tastes: 5-7 ("I like coffee", "I prefer quiet places") +- Minor observations: 4-6 (small insights about habits) + +Reflections are generally more important than observations since they represent processed understanding. How important +is this reflection for {{character_name}}'s self-concept? Respond with only a number 1-10. \ No newline at end of file diff --git a/llm_connector/llm.py b/llm_connector/llm.py index a0a7642..151065b 100644 --- a/llm_connector/llm.py +++ b/llm_connector/llm.py @@ -30,7 +30,6 @@ class LLMClient: Args: text: Text to get embedding for - model: Optional embedding model to use (overrides backend model) Returns: List of float values representing the embedding vector