This commit is contained in:
2025-09-02 04:41:06 +02:00
parent 45eb2b8bc5
commit 793213a834
19 changed files with 955 additions and 805 deletions

View File

@@ -4,16 +4,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## 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 ## Key Architecture Components
### Agent System (agents.py) ### Agent System (living_agents/)
- **Memory Stream**: Stanford's memory architecture with observations, reflections, and plans - **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) - **Smart Retrieval**: Combines recency (exponential decay), importance (1-10 scale), and relevance (cosine similarity)
- **Auto-Reflection**: Generates insights when importance threshold (150) is reached - **Auto-Reflection**: Generates insights when importance threshold (150) is reached
- **Character Components**: Character, CharacterAgent, MemoryStream, SceneManager - **Character Components**: Character, CharacterAgent, MemoryStream
- Currently uses OpenAI API directly but should be migrated to use llm_connector - **Trait Development**: Dynamic personality evolution based on experiences
- Uses llm_connector for flexible backend support
### LLM Connector Package ### LLM Connector Package
- **Custom LLM abstraction** that supports any OpenAI-compatible API - **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 - **Type definitions**: LLMBackend (base_url, api_token, model) and LLMMessage
- Environment variables: BACKEND_BASE_URL, BACKEND_API_TOKEN, BACKEND_MODEL - Environment variables: BACKEND_BASE_URL, BACKEND_API_TOKEN, BACKEND_MODEL
### UI Framework ### Character Explorer CLI
- **NiceGUI** for web interface (async components) - **CLI Testing Tool**: Interactive character development and testing interface
- **AsyncElement base class**: Simplified async UI component pattern - **Character Loading**: YAML template system for character initialization
- Constructor accepts element_type (default: ui.column) and element args/kwargs - **Real-time Development**: Direct testing of memory, traits, and personality evolution
- Implement build() method for async initialization logic - Located in `character_explorer.py` for easy development iteration
- 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
## Development Commands ## Development Commands
@@ -36,9 +34,8 @@ This is a multi-agent roleplay system implementing Stanford's "Generative Agents
# Install dependencies # Install dependencies
uv sync uv sync
# Run the application # Run the character explorer CLI
uv run python main.py uv run python character_explorer.py
# Application runs on http://localhost:8080
# Add new dependencies # Add new dependencies
uv add <package-name> uv add <package-name>
@@ -49,43 +46,14 @@ uv python pin 3.12 # Pin to Python 3.12
## Important Development Notes ## Important Development Notes
### AsyncElement Usage ### Character Development Focus
When creating UI components that extend AsyncElement: The current focus is on perfecting single-agent character development:
```python - Characters evolve through experiences and interactions
class MyComponent(AsyncElement): - Memory system creates realistic personality development
async def build(self, param1: str, param2: int, *args, **kwargs) -> None: - CLI tool allows rapid testing and iteration
# 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()
### LLM Integration ### LLM Integration
The project has two LLM integration approaches: The project uses a flexible LLM connector supporting any OpenAI-compatible API.
1. **Legacy** (in agents.py): Direct OpenAI client usage
2. **Current** (llm_connector): Flexible backend supporting any OpenAI-compatible API
When implementing new features, use the llm_connector package: When implementing new features, use the llm_connector package:
```python ```python
@@ -114,23 +82,23 @@ async for chunk in await get_response(backend, messages, stream=True):
``` ```
### Project Structure ### Project Structure
- `main.py`: Entry point, NiceGUI app configuration - `character_explorer.py`: CLI tool for character development and testing
- `agents.py`: Stanford memory architecture implementation (to be integrated) - `living_agents/`: Core agent system with memory, traits, and prompt management
- `llm_connector/`: Custom LLM integration package - `character_templates/`: YAML files defining character backgrounds
- `components/`: Reusable UI components with AsyncElement base - `llm_connector/`: Custom LLM integration package for flexible backend support
- `pages/`: UI pages (currently only MainPage)
### Environment Variables ### Environment Variables
Required in `.env`: Required in `.env`:
- `BACKEND_BASE_URL`: LLM API endpoint - `BACKEND_BASE_URL`: LLM API endpoint
- `BACKEND_API_TOKEN`: API authentication token - `BACKEND_API_TOKEN`: API authentication token
- `BACKEND_MODEL`: Model identifier - `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: The system currently focuses on single-agent character development:
1. Modified to use llm_connector instead of direct OpenAI client 1. Character agents with dynamic personality evolution
2. Integrated into the NiceGUI web interface 2. Stanford-inspired memory architecture
3. Create UI components for character interaction, memory viewing, scene management 3. CLI testing tool for rapid iteration
4. Implement real-time streaming of agent responses in the UI 4. Flexible LLM backend configuration
Future plans include multi-agent interactions and web interface integration.

604
agents.py
View File

@@ -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> <message> - Character responds using their memories")
print(" chat <char1> <char2> <context> - Two characters interact")
print(" time <hours> - Advance time, triggers planning")
print(" memories <character> [type] - Show character's memories")
print(" summary <character> - 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 <character> <message>")
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 <character1> <character2> <context>")
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 <hours>")
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 <character> [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 <character>")
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()

View File

@@ -94,31 +94,32 @@ class CharacterExplorer:
filter_choice = input("Choose filter (1-6): ").strip() 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": 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" title = "Observations"
elif filter_choice == "3": 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" title = "Reflections"
elif filter_choice == "4": 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" title = "Plans"
elif filter_choice == "5": 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)" title = "All Memories (by importance)"
elif filter_choice == "6": 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)" title = "All Memories (by recency)"
else: else:
memories = self.agent.memory_stream.memories
title = "All Memories" title = "All Memories"
print(f"\n📋 {title} ({len(memories)} total):") 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 age_hours = (memory.last_accessed - memory.creation_time).total_seconds() / 3600
print( 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: if len(memories) > 20:
print(f"\n... showing first 20 of {len(memories)} memories") print(f"\n... showing first 20 of {len(memories)} memories")
@@ -126,43 +127,28 @@ class CharacterExplorer:
async def _handle_view_memory(self): async def _handle_view_memory(self):
"""View a specific memory with its related memories""" """View a specific memory with its related memories"""
try: memory_num = int(input(f"\nEnter memory number (1-{len(self.agent.memory_stream.memories) - 1}): ").strip())
memory_num = int(input(f"\nEnter memory number (1-{len(self.agent.memory_stream.memories)}): ").strip())
if 1 <= memory_num <= len(self.agent.memory_stream.memories): if 0 <= memory_num <= len(self.agent.memory_stream.memories) - 1:
memory = self.agent.memory_stream.memories[memory_num - 1] memory = self.agent.memory_stream.memories[memory_num]
print(f"\n🔍 Memory #{memory_num} Details:") print(f"\n🔍 Memory #{memory_num} Details:")
print(f" Type: {memory.memory_type}") print(f" Type: {memory.memory_type}")
print(f" Importance: {memory.importance_score}/10") print(f" Importance: {memory.importance_score}/10")
print(f" Created: {memory.creation_time.strftime('%Y-%m-%d %H:%M:%S')}") 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" Last accessed: {memory.last_accessed.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" Description: {memory.description}") print(f" Description: {memory.description}")
# Show related memories using embeddings # Show related memories using embeddings
print(f"\n🔗 Related memories (by similarity):") if memory.memory_type == 'observation' or memory.memory_type == 'plan':
try: print(f"\n🔗 Related memories:")
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 ''}")
if not related: for rel_mem in memory.related_memories:
print(" (No related memories found)") rel_index = self.agent.memory_stream.memories.index(rel_mem)
print(
except Exception as e: f" [#{rel_index:3d}] [{rel_mem.memory_type}] {rel_mem.description}")
print(f" ❌ Error finding related memories: {e}") else:
print(f"❌ Invalid memory number. Range: 1-{len(self.agent.memory_stream.memories)}")
else:
print(f"❌ Invalid memory number. Range: 1-{len(self.agent.memory_stream.memories)}")
except ValueError:
print("❌ Please enter a valid number")
async def _handle_memory_stats(self): async def _handle_memory_stats(self):
"""Show detailed memory statistics""" """Show detailed memory statistics"""
@@ -217,7 +203,10 @@ class CharacterExplorer:
print(f" Personality: {self.agent.character.personality}") print(f" Personality: {self.agent.character.personality}")
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(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: if self.agent.character.relationships:
print(f" Relationships:") print(f" Relationships:")
for person, relationship in self.agent.character.relationships.items(): for person, relationship in self.agent.character.relationships.items():

53
docs/README.md Normal file
View File

@@ -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

117
docs/character-agents.md Normal file
View File

@@ -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.

87
docs/memory-system.md Normal file
View File

@@ -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.

191
docs/prompt-system.md Normal file
View File

@@ -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.

180
docs/trait-system.md Normal file
View File

@@ -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.

View File

@@ -1,10 +1,12 @@
import logging import logging
import random
from datetime import datetime
from http.client import responses from http.client import responses
from pprint import pprint from pprint import pprint
from tqdm.asyncio import tqdm from tqdm.asyncio import tqdm
from typing import List, Self from typing import List, Self
from living_agents import MemoryStream, LLMAgent, Character, PromptManager, Memory 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 from llm_connector import LLMMessage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -37,9 +39,10 @@ class CharacterAgent:
async def perceive(self, observation: str, skip_scoring=False) -> None: async def perceive(self, observation: str, skip_scoring=False) -> None:
"""Add new observation to memory stream""" """Add new observation to memory stream"""
if skip_scoring: if skip_scoring:
await self.memory_stream.add_observation(observation) new_memory = await self.memory_stream.add_observation(observation)
else: 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: async def react_to_situation(self, situation: str) -> str:
"""Generate reaction based on memory and character""" """Generate reaction based on memory and character"""
@@ -118,37 +121,26 @@ Summary:"""
except: except:
return f"{self.character.name} is a {self.character.age}-year-old {self.character.occupation}." 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: async def _score_memory_importance(self, memory: Memory) -> int:
"""Score importance with related memories as context""" """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]), 'related_memories': "\n".join([m.description for m in related_memories]),
'memory_text': memory.description, 'memory_text': memory.description}
'memory_type': memory.memory_type} 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: try:
response = await self.llm.chat([{"role": "user", "content": prompt}], max_tokens=5) response = await self.llm.chat([{"role": "user", "content": prompt}], max_tokens=5)
@@ -157,7 +149,38 @@ Summary:"""
except: except:
return 5 # Default 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""" """Extract Character info from memories using JSON"""
# Get different types of memories with targeted queries # 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 # create the character before we score to include the character in the prompts
# Extract character info from memories to populate Character object # Extract character info from memories to populate Character object
logger.info(f"Creating Character...") 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...") logger.info(f"Added {len(instance.memory_stream.memories)} memories, now scoring importance...")
# Score all memories with full context # Score all observations with importance
for memory in tqdm(instance.memory_stream.memories, desc="Scoring memory importance", unit="memory"): 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 # Score with related context
memory.importance_score = await instance._score_memory_importance(memory) memory.importance_score = await instance._score_memory_importance(memory)
await instance._analyze_trait_impact(memory)
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,6 +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
class CharacterTemplate(TypedDict): class CharacterTemplate(TypedDict):
@@ -21,24 +22,65 @@ class Memory:
importance_score: int = 5 # 1-10 scale importance_score: int = 5 # 1-10 scale
embedding: Optional[List[float]] = None embedding: Optional[List[float]] = None
memory_type: Literal["observation", "reflection", "plan"] = "observation" 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): def __post_init__(self):
if self.last_accessed is None: if self.last_accessed is None:
self.last_accessed = self.creation_time 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 @dataclass
class Character: class Character:
name: str # Still required name: str
age: Optional[int] = None age: Optional[int] = None
personality: str = "" personality: str = ""
occupation: str = "" occupation: str = ""
location: str = "" location: str = ""
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)
_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:
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): def __hash__(self):
return hash(self._id) return hash(self._id)

View File

@@ -88,6 +88,26 @@ class MemoryStream:
) )
self.memories.append(reflection) 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]: async def retrieve_related_memories(self, query: str, k: int = 10) -> List[Memory]:
"""Retrieve relevant memories using recency, importance, relevance""" """Retrieve relevant memories using recency, importance, relevance"""
if not self.memories: if not self.memories:

View File

@@ -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"
]
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -30,7 +30,6 @@ class LLMClient:
Args: Args:
text: Text to get embedding for text: Text to get embedding for
model: Optional embedding model to use (overrides backend model)
Returns: Returns:
List of float values representing the embedding vector List of float values representing the embedding vector