too much
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
52
living_agents/prompts/assess_trait_impact.json
Normal file
52
living_agents/prompts/assess_trait_impact.json
Normal 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"
|
||||
]
|
||||
}
|
||||
25
living_agents/prompts/assess_trait_impact.md
Normal file
25
living_agents/prompts/assess_trait_impact.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
23
living_agents/prompts/score_observation_importance.md
Normal file
23
living_agents/prompts/score_observation_importance.md
Normal 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.
|
||||
23
living_agents/prompts/score_plan_importance.md
Normal file
23
living_agents/prompts/score_plan_importance.md
Normal 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.
|
||||
22
living_agents/prompts/score_reflection_importance.md
Normal file
22
living_agents/prompts/score_reflection_importance.md
Normal 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.
|
||||
Reference in New Issue
Block a user