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

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

View File

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

View File

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

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.