From 2c73979f46e05702739ae7c741cc91120db67977 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 09:04:01 +0200 Subject: [PATCH 01/15] Adds nfd Three Story simulation with embeddings --- .github/ISSUE_TEMPLATE/issue-.md | 1 + src/nfd_three_story_evolve.py | 339 +++++++++++++++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 src/nfd_three_story_evolve.py diff --git a/.github/ISSUE_TEMPLATE/issue-.md b/.github/ISSUE_TEMPLATE/issue-.md index e4f69a3..a7dacec 100644 --- a/.github/ISSUE_TEMPLATE/issue-.md +++ b/.github/ISSUE_TEMPLATE/issue-.md @@ -4,6 +4,7 @@ about: Describe an issue to help us tackle them title: '' labels: '' assignees: '' +project: 'friction-flow' --- diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py new file mode 100644 index 0000000..6859431 --- /dev/null +++ b/src/nfd_three_story_evolve.py @@ -0,0 +1,339 @@ +import sys +from pathlib import Path +import logging +from dataclasses import dataclass +import numpy as np +from typing import List, Dict, Optional +import torch +from torch import nn +import torch.nn.functional as F +from language_models import LanguageModel, OllamaInterface +import asyncio + + +@dataclass +class Story: + """Represents a single story in the narrative field""" + + id: str + content: str + embedding: np.ndarray # Core semantic embedding + memory_layer: List[Dict] # Past interactions and experiences + perspective_filter: np.ndarray # What this story is attuned to notice + position: np.ndarray # Current position in field space + velocity: np.ndarray # Current movement vector + themes: List[str] + resonance_history: List[Dict] + + +class NarrativeField: + def __init__(self, dimension: int = 1024): + self.dimension = dimension + self.stories: List[Story] = [] + self.field_memory = [] + self.collective_state = np.zeros(dimension) + self.time = 0.0 + + # Initialize field properties + self.field_potential = np.zeros(dimension) + self.resonance_threshold = 0.7 + + self.logger = logging.getLogger(__name__) + + def add_story(self, story: Story): + """Add a new story to the field""" + self.stories.append(story) + self._update_field_potential() + self.logger.info(f"Added new story: {story.id}") + + def _update_field_potential(self): + """Update the field's potential energy based on story positions""" + self.field_potential = np.zeros(self.dimension) + for story in self.stories: + # Each story contributes to the field potential + self.field_potential += story.embedding * np.exp( + -np.linalg.norm(story.position) / 10.0 + ) + + def detect_resonance(self, story1: Story, story2: Story) -> float: + """Calculate resonance between two stories""" + # Base resonance on embedding similarity + embedding_similarity = F.cosine_similarity( + torch.tensor(story1.embedding), torch.tensor(story2.embedding), dim=0 + ) + + # Consider theme overlap + theme_overlap = len(set(story1.themes) & set(story2.themes)) / len( + set(story1.themes) | set(story2.themes) + ) + + # Consider perspective filter alignment + filter_alignment = F.cosine_similarity( + torch.tensor(story1.perspective_filter), + torch.tensor(story2.perspective_filter), + dim=0, + ) + + return float( + 0.4 * embedding_similarity + 0.3 * theme_overlap + 0.3 * filter_alignment + ) + + +class StoryInteractionEngine: + """Handles interactions between stories in the field""" + + def __init__(self, field: NarrativeField): + self.field = field + self.logger = logging.getLogger(__name__) + + def process_interaction(self, story1: Story, story2: Story): + """Process an interaction between two stories""" + resonance = self.field.detect_resonance(story1, story2) + + if resonance > self.field.resonance_threshold: + self.logger.info( + f"Interaction between {story1.id} and {story2.id} with resonance {resonance:.2f}" + ) + + # Create memory imprints + memory1 = { + "time": self.field.time, + "interacted_with": story2.id, + "resonance": resonance, + "themes": story2.themes, + } + memory2 = { + "time": self.field.time, + "interacted_with": story1.id, + "resonance": resonance, + "themes": story1.themes, + } + + # Update memory layers + story1.memory_layer.append(memory1) + story2.memory_layer.append(memory2) + + # Update perspective filters + self._update_perspective_filter(story1, story2) + self._update_perspective_filter(story2, story1) + + def _update_perspective_filter(self, story1: Story, story2: Story): + """Update a story's perspective filter based on interaction""" + # Blend perspective filters with a decay factor + decay = 0.9 + story1.perspective_filter = ( + decay * story1.perspective_filter + (1 - decay) * story2.perspective_filter + ) + + +class CollectiveStoryEngine: + """Manages collective field effects and emergent patterns""" + + def __init__(self, field: NarrativeField): + self.field = field + self.collective_memories = [] + self.logger = logging.getLogger(__name__) + + def generate_field_pulse(self, theme: str, intensity: float): + """Generate a collective field pulse around a theme""" + self.logger.info( + f"Generating field pulse with theme '{theme}' and intensity {intensity}" + ) + + # Create pulse effect + pulse = { + "time": self.field.time, + "theme": theme, + "intensity": intensity, + "affected_stories": [], + } + + # Apply pulse to all stories in field + for story in self.field.stories: + if theme in story.themes: + # Temporarily enhance story's resonance sensitivity + story.perspective_filter *= 1 + intensity + pulse["affected_stories"].append(story.id) + + self.collective_memories.append(pulse) + + def detect_emergent_patterns(self) -> List[Dict]: + """Detect emergent patterns in the field""" + self.logger.info("Detecting emergent patterns in the field") + + # Analyze recent interactions and memory patterns + interaction_threshold = 0.5 + recent_interactions = [ + { + "story_id": story.id, + "interacted_with": memory["interacted_with"], + "themes": memory["themes"], + "resonance": memory["resonance"], + } + for story in self.field.stories + for memory in story.memory_layer[-10:] + if memory["resonance"] > interaction_threshold + ] + + # Count theme occurrences + theme_counts = {} + for interaction in recent_interactions: + for theme in interaction["themes"]: + theme_counts[theme] = theme_counts.get(theme, 0) + 1 + + # Identify most common themes + common_themes = sorted(theme_counts.items(), key=lambda x: x[1], reverse=True)[ + :3 + ] + + # Analyze resonance patterns + avg_resonance = ( + sum(inter["resonance"] for inter in recent_interactions) + / len(recent_interactions) + if recent_interactions + else 0 + ) + + self.logger.info( + f"Detected {len(recent_interactions)} significant recent interactions" + ) + self.logger.info(f"Most common themes: {common_themes}") + self.logger.info(f"Average resonance: {avg_resonance:.2f}") + + # Add detected patterns to the patterns list + return [ + { + "type": "recent_interactions", + "count": len(recent_interactions), + "common_themes": common_themes, + "avg_resonance": avg_resonance, + } + ] + + +async def create_lighthouse_story(llm: LanguageModel) -> Story: + """Create the lighthouse story with its properties""" + content = """ + The Lighthouse on the Cliff + This is the story of a lighthouse keeper who lights the beacon every night, + waiting for a ship that never comes. It contains themes of loneliness, duty, + and anticipation, with a constant longing for connection across the vast ocean. + """ + embedding = await llm.generate_embedding(content) + return Story( + id="lighthouse", + content=content, + embedding=np.array(embedding), + memory_layer=[], + perspective_filter=np.ones(len(embedding)), + position=np.zeros(3), + velocity=np.zeros(3), + themes=["loneliness", "duty", "hope", "guidance"], + resonance_history=[], + ) + + +async def create_path_story(llm: LanguageModel) -> Story: + content = """ + The Path Through the Forest + This story is about a traveler lost in a forest, searching for a way home. It holds elements of uncertainty, hope, and resilience as the traveler navigates unfamiliar terrain, feeling both wonder and isolation. + """ + embedding = await llm.generate_embedding(content) + return Story( + id="path", + content=content, + embedding=np.array(embedding), + memory_layer=[], + perspective_filter=np.ones(len(embedding)), + position=np.random.randn(3), + velocity=np.zeros(3), + themes=["journey", "discovery", "nature"], + resonance_history=[], + ) + + +async def create_dream_story(llm: LanguageModel) -> Story: + content = """ + The Child's Dream of Flight + This story follows a child who dreams each night of flying, soaring above villages, forests, and oceans. The dream is filled with freedom, innocence, and limitless possibility, untouched by fear or doubt. + """ + embedding = await llm.generate_embedding(content) + return Story( + id="dream", + content=content, + embedding=np.array(embedding), + memory_layer=[], + perspective_filter=np.ones(len(embedding)), + position=np.random.randn(3), + velocity=np.zeros(3), + themes=["imagination", "freedom", "subconscious"], + resonance_history=[], + ) + + +async def simulate_field(): + """Run a simulation of the narrative field""" + # Set up logging + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + logger = logging.getLogger(__name__) + + logger.info("Starting narrative field simulation") + + # Initialize language model + llm = OllamaInterface() # Using Ollama + + # Initialize field + field = NarrativeField() + interaction_engine = StoryInteractionEngine(field) + collective_engine = CollectiveStoryEngine(field) + + # Create stories + lighthouse = await create_lighthouse_story(llm) + logger.info(f"Created lighthouse story: {lighthouse.id}") + path = await create_path_story(llm) + logger.info(f"Created path story: {path.id}") + dream = await create_dream_story(llm) + logger.info(f"Created dream story: {dream.id}") + + # Add stories to field + field.add_story(lighthouse) + field.add_story(path) + field.add_story(dream) + + # Simulation loop + for t in range(1000): # 1000 timesteps + field.time = t + logger.debug(f"Simulation timestep: {t}") + + # Update story positions + for story in field.stories: + story.position += story.velocity + + # Check for interactions + for i, story1 in enumerate(field.stories): + for story2 in field.stories[i + 1 :]: + if np.linalg.norm(story1.position - story2.position) < 1.0: + interaction_engine.process_interaction(story1, story2) + + # Occasionally generate field pulses + if t % 100 == 0: + collective_engine.generate_field_pulse("seeking_connection", 0.5) + + # Update field state + field._update_field_potential() + + logger.info("Narrative field simulation completed") + + # Clean up + await llm.cleanup() + + +if __name__ == "__main__": + asyncio.run(simulate_field()) + + +if __name__ == "__main__": + asyncio.run(simulate_field()) From 85bcd90db02607fd5aa134b8c409c277168213d5 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 09:58:03 +0200 Subject: [PATCH 02/15] Interaction greatness and extensive logging --- src/nfd_three_story_evolve.py | 766 +++++++++++++++++++++++++++++----- 1 file changed, 657 insertions(+), 109 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 6859431..3da394b 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -1,7 +1,7 @@ import sys from pathlib import Path import logging -from dataclasses import dataclass +from dataclasses import dataclass, field import numpy as np from typing import List, Dict, Optional import torch @@ -9,6 +9,21 @@ import torch.nn.functional as F from language_models import LanguageModel, OllamaInterface import asyncio +import time + + +@dataclass +class StoryState: + """Captures the evolving state of a story over time""" + + resonance_level: float = 0.0 + active_themes: List[str] = field(default_factory=list) + interaction_count: int = 0 + + def update(self, resonance: float, new_themes: List[str]): + self.resonance_level = 0.8 * self.resonance_level + 0.2 * resonance + self.active_themes.extend(new_themes) + self.interaction_count += 1 @dataclass @@ -26,6 +41,40 @@ class Story: resonance_history: List[Dict] +class NarrativeFieldViz: + """Handles visualization of field state""" + + def __init__(self, field_size: int = 1024): + self.field_size = field_size + self.history: List[Dict] = [] + self.logger = logging.getLogger(__name__) + + async def capture_state(self, field, timestep: int): + """Capture current field state for visualization""" + state = { + "timestep": timestep, + "story_positions": { + story.id: story.position.copy() for story in field.stories + }, + "story_velocities": { + story.id: story.velocity.copy() for story in field.stories + }, + "resonance_map": self._compute_resonance_map(field), + "field_potential": field.field_potential.copy(), + } + self.history.append(state) + self.logger.debug(f"Captured field state at timestep {timestep}") + + def _compute_resonance_map(self, field) -> Dict: + """Compute resonance between all story pairs""" + resonance_map = {} + for i, story1 in enumerate(field.stories): + for story2 in field.stories[i + 1 :]: + resonance = field.detect_resonance(story1, story2) + resonance_map[f"{story1.id}-{story2.id}"] = resonance + return resonance_map + + class NarrativeField: def __init__(self, dimension: int = 1024): self.dimension = dimension @@ -34,12 +83,50 @@ def __init__(self, dimension: int = 1024): self.collective_state = np.zeros(dimension) self.time = 0.0 - # Initialize field properties + # Adjusted thresholds + self.resonance_threshold = 0.3 # Lower threshold + self.interaction_range = 3.0 # Increased range self.field_potential = np.zeros(dimension) - self.resonance_threshold = 0.7 - self.logger = logging.getLogger(__name__) + def detect_resonance(self, story1: Story, story2: Story) -> float: + """Calculate resonance with improved theme handling""" + # Get base embedding similarity + embedding_similarity = float( + F.cosine_similarity( + torch.tensor(story1.embedding), torch.tensor(story2.embedding), dim=0 + ) + ) + + # Enhanced theme handling + shared_themes = set(story1.themes) & set(story2.themes) + theme_overlap = len(shared_themes) / max( + len(set(story1.themes) | set(story2.themes)), 1 + ) + + # Weight shared themes more heavily + theme_bonus = 0.2 if shared_themes else 0.0 + + # Consider perspective filter alignment + filter_alignment = float( + F.cosine_similarity( + torch.tensor(story1.perspective_filter), + torch.tensor(story2.perspective_filter), + dim=0, + ) + ) + + # Scale by distance more gradually + distance = np.linalg.norm(story1.position - story2.position) + distance_factor = 1.0 / (1.0 + distance / self.interaction_range) + + # Combine factors with theme bonus + return ( + 0.4 * embedding_similarity + + 0.3 * (theme_overlap + theme_bonus) + + 0.3 * filter_alignment + ) * distance_factor + def add_story(self, story: Story): """Add a new story to the field""" self.stories.append(story) @@ -74,66 +161,117 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: dim=0, ) + # Scale resonance by distance + distance = np.linalg.norm(story1.position - story2.position) + distance_factor = np.exp(-distance / self.interaction_range) + return float( - 0.4 * embedding_similarity + 0.3 * theme_overlap + 0.3 * filter_alignment + (0.4 * embedding_similarity + 0.3 * theme_overlap + 0.3 * filter_alignment) + * distance_factor ) -class StoryInteractionEngine: - """Handles interactions between stories in the field""" - - def __init__(self, field: NarrativeField): - self.field = field +class StoryPhysics: + """Handles physical behavior of stories in the field""" + + def __init__( + self, + damping: float = 0.95, # Increased damping + attraction_strength: float = 0.1, # Reduced strength + max_force: float = 1.0, # Force limiting + max_velocity: float = 0.5, + ): # Velocity limiting + self.damping = damping + self.attraction_strength = attraction_strength + self.max_force = max_force + self.max_velocity = max_velocity self.logger = logging.getLogger(__name__) - def process_interaction(self, story1: Story, story2: Story): - """Process an interaction between two stories""" - resonance = self.field.detect_resonance(story1, story2) - - if resonance > self.field.resonance_threshold: - self.logger.info( - f"Interaction between {story1.id} and {story2.id} with resonance {resonance:.2f}" - ) - - # Create memory imprints - memory1 = { - "time": self.field.time, - "interacted_with": story2.id, - "resonance": resonance, - "themes": story2.themes, - } - memory2 = { - "time": self.field.time, - "interacted_with": story1.id, - "resonance": resonance, - "themes": story1.themes, - } - - # Update memory layers - story1.memory_layer.append(memory1) - story2.memory_layer.append(memory2) + def _normalize_force(self, force: np.ndarray) -> np.ndarray: + """Normalize force vector to prevent exponential growth""" + magnitude = np.linalg.norm(force) + if magnitude > self.max_force: + return (force / magnitude) * self.max_force + return force + + def _limit_velocity(self, velocity: np.ndarray) -> np.ndarray: + """Limit velocity magnitude""" + magnitude = np.linalg.norm(velocity) + if magnitude > self.max_velocity: + return (velocity / magnitude) * self.max_velocity + return velocity + + def update_story_motion(self, story: Story, field: NarrativeField, timestep: int): + """Update story position and velocity based on field forces""" + # Compute net force from other stories + net_force = np.zeros(3) + for other in field.stories: + if other.id != story.id: + # Compute force based on resonance + resonance = field.detect_resonance(story, other) + direction = other.position - story.position + distance = np.linalg.norm(direction) + 1e-6 # Prevent division by zero + + # Scale force by distance with a minimum threshold + force = self.attraction_strength * resonance * direction / distance + net_force += force + + # Normalize and limit forces + net_force = self._normalize_force(net_force) + + # Update velocity with damping + story.velocity = self._limit_velocity( + (1 - self.damping) * story.velocity + net_force + ) - # Update perspective filters - self._update_perspective_filter(story1, story2) - self._update_perspective_filter(story2, story1) + # Update position + story.position += story.velocity - def _update_perspective_filter(self, story1: Story, story2: Story): - """Update a story's perspective filter based on interaction""" - # Blend perspective filters with a decay factor - decay = 0.9 - story1.perspective_filter = ( - decay * story1.perspective_filter + (1 - decay) * story2.perspective_filter + self.logger.debug( + f"Story {story.id} - " + f"Position: {story.position}, " + f"Velocity: {np.linalg.norm(story.velocity):.3f}" ) -class CollectiveStoryEngine: - """Manages collective field effects and emergent patterns""" +class EnhancedCollectiveStoryEngine: + """Enhanced version with more sophisticated pattern detection""" def __init__(self, field: NarrativeField): self.field = field self.collective_memories = [] + self.story_states: Dict[str, StoryState] = {} self.logger = logging.getLogger(__name__) + def update_story_states(self): + """Update state tracking for all stories""" + for story in self.field.stories: + if story.id not in self.story_states: + self.story_states[story.id] = StoryState() + + # Update based on recent interactions + recent_memories = story.memory_layer[-5:] # Look at last 5 interactions + if recent_memories: + avg_resonance = np.mean([m["resonance"] for m in recent_memories]) + recent_themes = [ + theme for m in recent_memories for theme in m["themes"] + ] + self.story_states[story.id].update(avg_resonance, recent_themes) + + def detect_emergent_themes(self) -> List[str]: + """Detect themes that are becoming more prominent""" + theme_weights = {} + + for story_id, state in self.story_states.items(): + # Weight themes by story's resonance level + for theme in state.active_themes: + weight = state.resonance_level * (1 + state.interaction_count / 10) + theme_weights[theme] = theme_weights.get(theme, 0) + weight + + # Return top themes sorted by weight + sorted_themes = sorted(theme_weights.items(), key=lambda x: x[1], reverse=True) + return [theme for theme, _ in sorted_themes[:3]] + def generate_field_pulse(self, theme: str, intensity: float): """Generate a collective field pulse around a theme""" self.logger.info( @@ -157,58 +295,292 @@ def generate_field_pulse(self, theme: str, intensity: float): self.collective_memories.append(pulse) - def detect_emergent_patterns(self) -> List[Dict]: - """Detect emergent patterns in the field""" - self.logger.info("Detecting emergent patterns in the field") - # Analyze recent interactions and memory patterns - interaction_threshold = 0.5 - recent_interactions = [ - { - "story_id": story.id, - "interacted_with": memory["interacted_with"], - "themes": memory["themes"], - "resonance": memory["resonance"], - } - for story in self.field.stories - for memory in story.memory_layer[-10:] - if memory["resonance"] > interaction_threshold - ] - - # Count theme occurrences - theme_counts = {} - for interaction in recent_interactions: - for theme in interaction["themes"]: - theme_counts[theme] = theme_counts.get(theme, 0) + 1 - - # Identify most common themes - common_themes = sorted(theme_counts.items(), key=lambda x: x[1], reverse=True)[ - :3 - ] - - # Analyze resonance patterns - avg_resonance = ( - sum(inter["resonance"] for inter in recent_interactions) - / len(recent_interactions) - if recent_interactions - else 0 +class ThemeEvolutionEngine: + """Handles theme evolution and perspective shifts""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self.theme_resonance = {} # Track theme relationships + + def process_interaction(self, story1: Story, story2: Story, resonance: float): + """Process theme interactions and evolution""" + # Find theme relationships + direct_shared = set(story1.themes) & set(story2.themes) + story1_themes = set(story1.themes) + story2_themes = set(story2.themes) + + # Theme relationship analysis + relationships = { + ("hope", "journey"): 0.7, + ("loneliness", "discovery"): 0.6, + ("guidance", "nature"): 0.5, + ("duty", "freedom"): 0.4, + ("imagination", "guidance"): 0.6, + ("subconscious", "discovery"): 0.5, + } + + # Find indirect theme relationships + indirect_resonance = 0.0 + for t1 in story1_themes: + for t2 in story2_themes: + if (t1, t2) in relationships: + indirect_resonance += relationships[(t1, t2)] + elif (t2, t1) in relationships: + indirect_resonance += relationships[(t2, t1)] + + # Calculate thematic impact + theme_impact = len(direct_shared) * 0.3 + indirect_resonance * 0.2 + + return { + "direct_shared": direct_shared, + "indirect_resonance": indirect_resonance, + "theme_impact": theme_impact, + "related_themes": [ + (t1, t2) + for t1 in story1_themes + for t2 in story2_themes + if (t1, t2) in relationships or (t2, t1) in relationships + ], + } + + +class StoryInteractionEngine: + """Enhanced interaction engine with better memory formation""" + + def __init__(self, field: NarrativeField): + self.field = field + self.logger = logging.getLogger(__name__) + self.interaction_count = 0 + self.memory_threshold = 0.2 # Lower threshold for memory formation + self.theme_engine = ThemeEvolutionEngine() + + def process_interaction(self, story1: Story, story2: Story): + """Process interaction with theme evolution""" + resonance = self.field.detect_resonance(story1, story2) + + # Process theme relationships + theme_analysis = self.theme_engine.process_interaction( + story1, story2, resonance ) + + if resonance > self.memory_threshold: + self.interaction_count += 1 + + # Enhanced memory formation + memory1 = { + "time": self.field.time, + "interacted_with": story2.id, + "resonance": resonance, + "themes": list(story2.themes), + "shared_themes": list(theme_analysis['direct_shared']), + "related_themes": theme_analysis['related_themes'], + "interaction_id": self.interaction_count, + "emotional_impact": resonance * ( + len(theme_analysis['direct_shared']) + + theme_analysis['indirect_resonance'] + ) + } + memory2 = { + "time": self.field.time, + "interacted_with": story1.id, + "resonance": resonance, + "themes": list(story1.themes), + "shared_themes": list(theme_analysis['direct_shared']), + "interaction_id": self.interaction_count, + "emotional_impact": resonance * len(theme_analysis['direct_shared']), + } + + # Update memory layers + story1.memory_layer.append(memory1) + story2.memory_layer.append(memory2) + + # Log rich interaction details + self.logger.info( + f"\nRich Interaction #{self.interaction_count}:\n" + f" Stories: {story1.id} <-> {story2.id}\n" + f" Direct Shared Themes: {list(theme_analysis['direct_shared'])}\n" + f" Theme Relationships Found: {theme_analysis['related_themes']}\n" + f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" + f" Emotional Impact: {memory1['emotional_impact']:.2f}" + ) + + # Update perspectives with theme relationships + self._update_perspective_filter( + story1, story2, + theme_analysis['direct_shared'], + theme_analysis['indirect_resonance'] + ) + + def _update_perspective_filter( + self, story1: Story, story2: Story, + shared_themes: set, indirect_resonance: float + ): + """Update perspective with theme relationships""" + base_decay = 0.9 + direct_influence = len(shared_themes) * 0.15 + indirect_influence = indirect_resonance * 0.1 + + # Combined theme influence + decay = base_decay - (direct_influence + indirect_influence) + + # Update perspective + new_perspective = ( + decay * story1.perspective_filter + + (1 - decay) * story2.perspective_filter + ) + + # Record the shift + shift_magnitude = np.sum(np.abs(new_perspective - story1.perspective_filter)) + story1.perspective_filter = new_perspective + self.logger.info( - f"Detected {len(recent_interactions)} significant recent interactions" + f"Perspective Shift - {story1.id}:\n" + f" Magnitude: {shift_magnitude:.4f}\n" + f" Direct Theme Influence: {direct_influence:.2f}\n" + f" Indirect Theme Influence: {indirect_influence:.2f}" ) - self.logger.info(f"Most common themes: {common_themes}") - self.logger.info(f"Average resonance: {avg_resonance:.2f}") - # Add detected patterns to the patterns list - return [ - { - "type": "recent_interactions", - "count": len(recent_interactions), - "common_themes": common_themes, - "avg_resonance": avg_resonance, - } - ] + +class StoryPhysics: + """Handles physical behavior of stories in the field""" + + def __init__(self): + # Physics parameters + self.damping = 0.95 # Slightly reduced damping + self.attraction_strength = 0.2 # Stronger attraction + self.repulsion_strength = 0.1 # Add repulsion to prevent collapse + self.min_distance = 0.5 # Minimum distance between stories + self.interaction_range = 2.0 # Range for story interactions + self.random_force = 0.05 # Small random force for exploration + + # Movement limits + self.max_force = 0.3 + self.max_velocity = 0.2 + self.target_zone_radius = 10.0 # Desired story movement range + + self.logger = logging.getLogger(__name__) + + def update_story_motion(self, story: Story, field: NarrativeField, timestep: int): + """Update story position and velocity with balanced forces""" + net_force = np.zeros(3) + + # Forces from other stories + for other in field.stories: + if other.id != story.id: + direction = other.position - story.position + distance = np.linalg.norm(direction) + 1e-6 + direction_normalized = direction / distance + + # Resonance-based attraction + resonance = field.detect_resonance(story, other) + attraction = self.attraction_strength * resonance * direction_normalized + + # Distance-based repulsion + repulsion = ( + -self.repulsion_strength * direction_normalized / (distance**2) + ) + if distance < self.min_distance: + repulsion *= 2.0 # Stronger repulsion when too close + + net_force += attraction + repulsion + + # Containment force - quadratic increase with distance + displacement = story.position + distance_from_center = np.linalg.norm(displacement) + if distance_from_center > self.target_zone_radius: + containment = ( + -0.1 + * (distance_from_center / self.target_zone_radius) ** 2 + * displacement + ) + net_force += containment + + # Random exploration force - varies with time + random_direction = np.random.randn(3) + random_direction /= np.linalg.norm(random_direction) + exploration_force = ( + self.random_force * random_direction * np.sin(timestep / 100) + ) + net_force += exploration_force + + # Balance z-axis movement + net_force[2] *= 0.3 # Reduce but don't eliminate z-axis movement + + # Apply force limits + net_force = self._normalize_force(net_force) + + # Update velocity with damping + story.velocity = self._limit_velocity( + (1 - self.damping) * story.velocity + net_force + ) + + # Update position + story.position += story.velocity + + # Log significant movements + if timestep % 100 == 0: + self.logger.debug( + f"Story {story.id} at t={timestep}:\n" + f" Position: {story.position}\n" + f" Velocity: {np.linalg.norm(story.velocity):.3f}\n" + f" Force: {np.linalg.norm(net_force):.3f}" + ) + + def _normalize_force(self, force: np.ndarray) -> np.ndarray: + """Normalize force vector to prevent exponential growth""" + magnitude = np.linalg.norm(force) + if magnitude > self.max_force: + return (force / magnitude) * self.max_force + return force + + def _limit_velocity(self, velocity: np.ndarray) -> np.ndarray: + """Limit velocity magnitude""" + magnitude = np.linalg.norm(velocity) + if magnitude > self.max_velocity: + return (velocity / magnitude) * self.max_velocity + return velocity + + def apply_field_constraints(self, stories: List[Story]): + """Apply global constraints to all stories""" + # Find center of mass + com = np.mean([s.position for s in stories], axis=0) + + # If stories are drifting too far as a group, pull them back + if np.linalg.norm(com) > 5.0: + for story in stories: + # Apply centering force proportional to distance from origin + centering = -0.1 * story.position + story.velocity += self._normalize_force(centering) + story.velocity = self._limit_velocity(story.velocity) + + def _normalize_force(self, force: np.ndarray) -> np.ndarray: + """Normalize force vector to prevent exponential growth""" + magnitude = np.linalg.norm(force) + if magnitude > self.max_force: + return (force / magnitude) * self.max_force + return force + + def _limit_velocity(self, velocity: np.ndarray) -> np.ndarray: + """Limit velocity magnitude""" + magnitude = np.linalg.norm(velocity) + if magnitude > self.max_velocity: + return (velocity / magnitude) * self.max_velocity + return velocity + + def apply_field_constraints(self, stories: List[Story]): + """Apply global constraints to all stories""" + # Find center of mass + com = np.mean([s.position for s in stories], axis=0) + + # If stories are drifting too far as a group, pull them back + if np.linalg.norm(com) > 5.0: + for story in stories: + # Apply centering force proportional to distance from origin + centering = -0.1 * story.position + story.velocity += self._normalize_force(centering) + story.velocity = self._limit_velocity(story.velocity) async def create_lighthouse_story(llm: LanguageModel) -> Story: @@ -220,13 +592,14 @@ async def create_lighthouse_story(llm: LanguageModel) -> Story: and anticipation, with a constant longing for connection across the vast ocean. """ embedding = await llm.generate_embedding(content) + return Story( id="lighthouse", content=content, embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.zeros(3), + position=np.random.randn(3) * 2.0, # Scaled initial positions velocity=np.zeros(3), themes=["loneliness", "duty", "hope", "guidance"], resonance_history=[], @@ -236,7 +609,9 @@ async def create_lighthouse_story(llm: LanguageModel) -> Story: async def create_path_story(llm: LanguageModel) -> Story: content = """ The Path Through the Forest - This story is about a traveler lost in a forest, searching for a way home. It holds elements of uncertainty, hope, and resilience as the traveler navigates unfamiliar terrain, feeling both wonder and isolation. + This story is about a traveler lost in a forest, searching for a way home. + It holds elements of uncertainty, hope, and resilience as the traveler + navigates unfamiliar terrain, feeling both wonder and isolation. """ embedding = await llm.generate_embedding(content) return Story( @@ -245,7 +620,7 @@ async def create_path_story(llm: LanguageModel) -> Story: embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3), + position=np.random.randn(3) * 2.0, # Scaled initial positions velocity=np.zeros(3), themes=["journey", "discovery", "nature"], resonance_history=[], @@ -255,7 +630,9 @@ async def create_path_story(llm: LanguageModel) -> Story: async def create_dream_story(llm: LanguageModel) -> Story: content = """ The Child's Dream of Flight - This story follows a child who dreams each night of flying, soaring above villages, forests, and oceans. The dream is filled with freedom, innocence, and limitless possibility, untouched by fear or doubt. + This story follows a child who dreams each night of flying, soaring above + villages, forests, and oceans. The dream is filled with freedom, innocence, + and limitless possibility, untouched by fear or doubt. """ embedding = await llm.generate_embedding(content) return Story( @@ -264,13 +641,156 @@ async def create_dream_story(llm: LanguageModel) -> Story: embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3), + position=np.random.randn(3) * 2.0, # Scaled initial positions velocity=np.zeros(3), themes=["imagination", "freedom", "subconscious"], resonance_history=[], ) +class StoryJourneyLogger: + """Tracks and logs the journey of stories through the narrative field""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self.journey_log = {} + self.total_distances = {} # Track cumulative distance for each story + self.significant_events = [] # Track important moments + + def log_interaction(self, story1: Story, story2: Story, resonance: float): + """Log a meaningful interaction between stories""" + shared_themes = set(story1.themes) & set(story2.themes) + distance = np.linalg.norm(story1.position - story2.position) + + # Log immediate interaction details + self.significant_events.append( + { + "type": "interaction", + "time": time.time(), + "stories": (story1.id, story2.id), + "resonance": resonance, + "shared_themes": list(shared_themes), + "distance": distance, + "positions": { + story1.id: story1.position.copy(), + story2.id: story2.position.copy(), + }, + } + ) + + self.logger.info( + f"\nSignificant Interaction:\n" + f" {story1.id} <-> {story2.id}\n" + f" Resonance: {resonance:.2f}\n" + f" Shared Themes: {list(shared_themes)}\n" + f" Distance: {distance:.2f}\n" + f" Positions:\n" + f" {story1.id}: {story1.position}\n" + f" {story2.id}: {story2.position}\n" + ) + + # Track memory formation for both stories + for story in [story1, story2]: + if story.memory_layer: + latest_memory = story.memory_layer[-1] + self.logger.info( + f"\nMemory Formed - {story.id}:\n" + f" Interaction with: {latest_memory['interacted_with']}\n" + f" Resonance Level: {latest_memory['resonance']:.2f}\n" + f" Themes Gained: {latest_memory['themes']}\n" + f" Shared Themes: {latest_memory.get('shared_themes', [])}\n" + ) + + def log_story_state(self, story: Story, timestep: float): + """Log detailed story state and track journey metrics""" + if story.id not in self.journey_log: + self.journey_log[story.id] = [] + self.total_distances[story.id] = 0.0 + + # Calculate movement since last state + if self.journey_log[story.id]: + last_pos = self.journey_log[story.id][-1]["position"] + movement = np.linalg.norm(story.position - last_pos) + self.total_distances[story.id] += movement + + # Log significant movements + if movement > 0.5: # Threshold for significant movement + self.significant_events.append( + { + "type": "movement", + "time": timestep, + "story_id": story.id, + "distance": movement, + "direction": story.velocity + / (np.linalg.norm(story.velocity) + 1e-6), + } + ) + + # Store current state + state = { + "timestep": timestep, + "position": story.position.copy(), + "velocity": story.velocity.copy(), + "memory_count": len(story.memory_layer), + "perspective_sum": story.perspective_filter.sum(), + "total_distance": self.total_distances[story.id], + } + self.journey_log[story.id].append(state) + + def summarize_journey(self, story: Story): + """Provide comprehensive journey summary""" + journey = self.journey_log.get(story.id, []) + if not journey: + return + + start_state = journey[0] + end_state = journey[-1] + + # Calculate journey metrics + total_distance = self.total_distances[story.id] + direct_distance = np.linalg.norm( + end_state["position"] - start_state["position"] + ) + wandering_ratio = total_distance / (direct_distance + 1e-6) + + # Memory and interaction analysis + memories_formed = end_state["memory_count"] + unique_interactions = len(set(m["interacted_with"] for m in story.memory_layer)) + + # Perspective evolution + perspective_change = ( + end_state["perspective_sum"] - start_state["perspective_sum"] + ) + + # Log comprehensive summary + self.logger.info( + f"\n=== Journey Summary for {story.id} ===\n" + f"Movement Metrics:\n" + f" Total Distance Traveled: {total_distance:.2f}\n" + f" Direct Distance (start to end): {direct_distance:.2f}\n" + f" Wandering Ratio: {wandering_ratio:.2f}\n" + f"\nInteraction Metrics:\n" + f" Memories Formed: {memories_formed}\n" + f" Unique Interactions: {unique_interactions}\n" + f" Perspective Shift: {perspective_change:.2f}\n" + f"\nFinal State:\n" + f" Position: {end_state['position']}\n" + f" Velocity: {end_state['velocity']}\n" + f"\nSignificant Events: {len([e for e in self.significant_events if e['type'] == 'interaction' and story.id in e['stories']])}\n" + ) + + +async def create_story_cluster(): + """Create initial story positions in a balanced configuration""" + # Position stories in a triangle with some random offset + base_positions = np.array( + [[1.0, 0.0, 0.0], [-0.5, 0.866, 0.0], [-0.5, -0.866, 0.0]] + ) + + # Add random offset to make it interesting + return base_positions + np.random.randn(3, 3) * 0.2 + + async def simulate_field(): """Run a simulation of the narrative field""" # Set up logging @@ -285,10 +805,12 @@ async def simulate_field(): # Initialize language model llm = OllamaInterface() # Using Ollama - # Initialize field + # Initialize simulation components field = NarrativeField() + physics = StoryPhysics() + visualizer = NarrativeFieldViz() interaction_engine = StoryInteractionEngine(field) - collective_engine = CollectiveStoryEngine(field) + collective_engine = EnhancedCollectiveStoryEngine(field) # Create stories lighthouse = await create_lighthouse_story(llm) @@ -303,14 +825,17 @@ async def simulate_field(): field.add_story(path) field.add_story(dream) + # Add journey logger + journey_logger = StoryJourneyLogger() + # Simulation loop - for t in range(1000): # 1000 timesteps + for t in range(1000): field.time = t - logger.debug(f"Simulation timestep: {t}") - # Update story positions + # Update physics for story in field.stories: - story.position += story.velocity + physics.update_story_motion(story, field, t) + journey_logger.log_story_state(story, t) # Check for interactions for i, story1 in enumerate(field.stories): @@ -318,13 +843,40 @@ async def simulate_field(): if np.linalg.norm(story1.position - story2.position) < 1.0: interaction_engine.process_interaction(story1, story2) - # Occasionally generate field pulses + # Update story states + collective_engine.update_story_states() + + # Capture visualization data + await visualizer.capture_state(field, t) + + # Occasionally generate field pulses and detect patterns if t % 100 == 0: collective_engine.generate_field_pulse("seeking_connection", 0.5) + emergent_themes = collective_engine.detect_emergent_themes() + logger.info(f"Timestep {t} - Emergent themes: {emergent_themes}") # Update field state field._update_field_potential() + # Log detailed state for each story + for story in field.stories: + journey_logger.log_story_state(story, t) + + # Check for and log interactions + for i, story1 in enumerate(field.stories): + for story2 in field.stories[i + 1 :]: + distance = np.linalg.norm(story1.position - story2.position) + if distance < field.interaction_range: + interaction_engine.process_interaction(story1, story2) + journey_logger.log_interaction( + story1, story2, field.detect_resonance(story1, story2) + ) + + # Print final summaries + logger.info("\n=== Final Journey Summaries ===") + for story in field.stories: + journey_logger.summarize_journey(story) + logger.info("Narrative field simulation completed") # Clean up @@ -333,7 +885,3 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) - - -if __name__ == "__main__": - asyncio.run(simulate_field()) From 814ef176eb14ba405cb494ba2f7d056f178335d8 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 10:30:14 +0200 Subject: [PATCH 03/15] Summary of Alignment and Observations The simulation framework aligns well with the intended narrative dynamics: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resonance and Theme Influence: The system effectively uses resonance and thematic influence to determine the strength and direction of interactions between stories, as seen in the ThemeRelationshipMap and update_perspective methods. Perspective Shifts: The StoryPerspective and Story classes handle perspective shifts, allowing stories to evolve in response to interactions without losing their core identity. This mirrors the idea of stories subtly adapting with each experience. Memory Formation: Each story’s memory_layer and resonance_history capture cumulative experiences, which can impact future interactions and perspective adjustments. --- src/nfd_three_story_evolve.py | 393 ++++++++++++++++++++++++---------- 1 file changed, 275 insertions(+), 118 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 3da394b..b30c814 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -26,19 +26,168 @@ def update(self, resonance: float, new_themes: List[str]): self.interaction_count += 1 -@dataclass +class ThemeRelationshipMap: + """Manages theme relationships and their evolution""" + + def __init__(self): + # Primary theme relationships with resonance values + self.primary_relationships = { + ("hope", "journey"): 0.7, + ("loneliness", "discovery"): 0.6, + ("guidance", "nature"): 0.5, + ("duty", "freedom"): 0.4, + ("imagination", "guidance"): 0.6, + ("subconscious", "discovery"): 0.5, + # Add more primary relationships + ("hope", "freedom"): 0.6, + ("loneliness", "nature"): 0.5, + ("imagination", "discovery"): 0.8, + ("journey", "nature"): 0.7, + } + + # Secondary theme groups + self.theme_groups = { + "exploration": {"journey", "discovery", "nature"}, + "inner_world": {"imagination", "subconscious", "freedom"}, + "guidance": {"duty", "guidance", "hope"}, + "solitude": {"loneliness", "nature", "subconscious"}, + } + + def get_theme_resonance(self, theme1: str, theme2: str) -> float: + """Get resonance between two themes""" + # Check direct relationship + if (theme1, theme2) in self.primary_relationships: + return self.primary_relationships[(theme1, theme2)] + if (theme2, theme1) in self.primary_relationships: + return self.primary_relationships[(theme2, theme1)] + + # Check theme groups + shared_groups = 0 + for group in self.theme_groups.values(): + if theme1 in group and theme2 in group: + shared_groups += 1 + + return 0.3 * shared_groups if shared_groups > 0 else 0.1 + + +class StoryPerspective: + """Manages a story's evolving perspective""" + + def __init__(self, initial_filter: np.ndarray): + self.filter = initial_filter.copy() + self.shift_history = [] + self.theme_influences = {} + self.total_shift = 0.0 + + def update( + self, + other_filter: np.ndarray, + shared_themes: set, + indirect_resonance: float, + theme_relationships: List[tuple], + ): + """Update perspective with detailed tracking""" + # Calculate influence factors + direct_influence = len(shared_themes) * 0.15 + indirect_influence = indirect_resonance * 0.1 + relationship_influence = len(theme_relationships) * 0.05 + + total_influence = direct_influence + indirect_influence + relationship_influence + decay = max(0.5, 0.9 - total_influence) # Prevent complete override + + # Calculate new filter + old_filter = self.filter.copy() + self.filter = decay * self.filter + (1 - decay) * other_filter + + # Calculate shift magnitude + shift = np.sum(np.abs(self.filter - old_filter)) + self.total_shift += shift + + # Record shift details + self.shift_history.append( + { + "magnitude": shift, + "direct_influence": direct_influence, + "indirect_influence": indirect_influence, + "relationship_influence": relationship_influence, + "shared_themes": list(shared_themes), + "theme_relationships": theme_relationships, + } + ) + + # Update theme influences + for theme in shared_themes: + self.theme_influences[theme] = ( + self.theme_influences.get(theme, 0) + direct_influence + ) + for t1, t2 in theme_relationships: + self.theme_influences[t1] = ( + self.theme_influences.get(t1, 0) + relationship_influence + ) + self.theme_influences[t2] = ( + self.theme_influences.get(t2, 0) + relationship_influence + ) + + return shift + + class Story: - """Represents a single story in the narrative field""" + def __init__( + self, + id: str, + content: str, + embedding: np.ndarray, + perspective_filter: np.ndarray, + themes: List[str], + position: np.ndarray = None, + velocity: np.ndarray = None, + **kwargs, + ): + self.id = id + self.content = content + self.embedding = embedding + self.initial_perspective = perspective_filter.copy() + self.perspective_filter = perspective_filter.copy() # Change this line + self.themes = themes + self.memory_layer = [] + self.resonance_history = [] + self.total_perspective_shift = 0.0 + self.perspective_shifts = [] + + # Initialize position and velocity + self.position = position if position is not None else np.random.randn(3) + self.velocity = velocity if velocity is not None else np.zeros(3) + + def update_perspective(self, other: "Story", theme_impact: float, resonance: float): + """Update perspective with accumulation""" + # Calculate influence factors + theme_weight = theme_impact * 0.2 + resonance_weight = resonance * 0.3 + total_weight = min(0.5, theme_weight + resonance_weight) # Cap maximum shift + + # Calculate new perspective + new_perspective = ( + 1 - total_weight + ) * self.perspective_filter + total_weight * other.perspective_filter - id: str - content: str - embedding: np.ndarray # Core semantic embedding - memory_layer: List[Dict] # Past interactions and experiences - perspective_filter: np.ndarray # What this story is attuned to notice - position: np.ndarray # Current position in field space - velocity: np.ndarray # Current movement vector - themes: List[str] - resonance_history: List[Dict] + # Calculate shift magnitude + shift = float(np.sum(np.abs(new_perspective - self.perspective_filter))) + + # Update accumulators + self.total_perspective_shift += shift + self.perspective_shifts.append( + { + "interaction_with": other.id, + "magnitude": shift, + "theme_impact": theme_impact, + "resonance": resonance, + } + ) + + # Store new perspective + self.perspective_filter = new_perspective + + return shift class NarrativeFieldViz: @@ -89,59 +238,6 @@ def __init__(self, dimension: int = 1024): self.field_potential = np.zeros(dimension) self.logger = logging.getLogger(__name__) - def detect_resonance(self, story1: Story, story2: Story) -> float: - """Calculate resonance with improved theme handling""" - # Get base embedding similarity - embedding_similarity = float( - F.cosine_similarity( - torch.tensor(story1.embedding), torch.tensor(story2.embedding), dim=0 - ) - ) - - # Enhanced theme handling - shared_themes = set(story1.themes) & set(story2.themes) - theme_overlap = len(shared_themes) / max( - len(set(story1.themes) | set(story2.themes)), 1 - ) - - # Weight shared themes more heavily - theme_bonus = 0.2 if shared_themes else 0.0 - - # Consider perspective filter alignment - filter_alignment = float( - F.cosine_similarity( - torch.tensor(story1.perspective_filter), - torch.tensor(story2.perspective_filter), - dim=0, - ) - ) - - # Scale by distance more gradually - distance = np.linalg.norm(story1.position - story2.position) - distance_factor = 1.0 / (1.0 + distance / self.interaction_range) - - # Combine factors with theme bonus - return ( - 0.4 * embedding_similarity - + 0.3 * (theme_overlap + theme_bonus) - + 0.3 * filter_alignment - ) * distance_factor - - def add_story(self, story: Story): - """Add a new story to the field""" - self.stories.append(story) - self._update_field_potential() - self.logger.info(f"Added new story: {story.id}") - - def _update_field_potential(self): - """Update the field's potential energy based on story positions""" - self.field_potential = np.zeros(self.dimension) - for story in self.stories: - # Each story contributes to the field potential - self.field_potential += story.embedding * np.exp( - -np.linalg.norm(story.position) / 10.0 - ) - def detect_resonance(self, story1: Story, story2: Story) -> float: """Calculate resonance between two stories""" # Base resonance on embedding similarity @@ -158,7 +254,7 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: filter_alignment = F.cosine_similarity( torch.tensor(story1.perspective_filter), torch.tensor(story2.perspective_filter), - dim=0, + dim=0 ) # Scale resonance by distance @@ -170,6 +266,21 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: * distance_factor ) + def add_story(self, story: Story): + """Add a new story to the field""" + self.stories.append(story) + self._update_field_potential() + self.logger.info(f"Added new story: {story.id}") + + def _update_field_potential(self): + """Update the field's potential energy based on story positions""" + self.field_potential = np.zeros(self.dimension) + for story in self.stories: + # Each story contributes to the field potential + self.field_potential += story.embedding * np.exp( + -np.linalg.norm(story.position) / 10.0 + ) + class StoryPhysics: """Handles physical behavior of stories in the field""" @@ -356,30 +467,39 @@ def __init__(self, field: NarrativeField): self.theme_engine = ThemeEvolutionEngine() def process_interaction(self, story1: Story, story2: Story): - """Process interaction with theme evolution""" resonance = self.field.detect_resonance(story1, story2) - - # Process theme relationships theme_analysis = self.theme_engine.process_interaction( story1, story2, resonance ) - + if resonance > self.memory_threshold: - self.interaction_count += 1 - - # Enhanced memory formation + # Update perspectives + shift1 = story1.update_perspective( + story2, theme_analysis["theme_impact"], resonance + ) + shift2 = story2.update_perspective( + story1, theme_analysis["theme_impact"], resonance + ) + + self.logger.info( + f"\nPerspective Updates:\n" + f"{story1.id}: shift={shift1:.4f}, total={story1.total_perspective_shift:.4f}\n" + f"{story2.id}: shift={shift2:.4f}, total={story2.total_perspective_shift:.4f}" + ) + + # Create memories with perspective shift information memory1 = { "time": self.field.time, "interacted_with": story2.id, "resonance": resonance, "themes": list(story2.themes), - "shared_themes": list(theme_analysis['direct_shared']), - "related_themes": theme_analysis['related_themes'], - "interaction_id": self.interaction_count, - "emotional_impact": resonance * ( - len(theme_analysis['direct_shared']) + - theme_analysis['indirect_resonance'] - ) + "shared_themes": list(theme_analysis["direct_shared"]), + "theme_impact": theme_analysis["theme_impact"], + "perspective_shift": shift1, + "total_shift": story1.total_perspective_shift, + "interaction_id": self.interaction_count, # TODO LBO? + "emotional_impact": resonance + * len(theme_analysis["direct_shared"]), # TODO LBO? } memory2 = { @@ -387,9 +507,12 @@ def process_interaction(self, story1: Story, story2: Story): "interacted_with": story1.id, "resonance": resonance, "themes": list(story1.themes), - "shared_themes": list(theme_analysis['direct_shared']), + "shared_themes": list(theme_analysis["direct_shared"]), + "theme_impact": theme_analysis["theme_impact"], + "perspective_shift": shift2, + "total_shift": story2.total_perspective_shift, "interaction_id": self.interaction_count, - "emotional_impact": resonance * len(theme_analysis['direct_shared']), + "emotional_impact": resonance * len(theme_analysis["direct_shared"]), } # Update memory layers @@ -405,36 +528,39 @@ def process_interaction(self, story1: Story, story2: Story): f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" f" Emotional Impact: {memory1['emotional_impact']:.2f}" ) - + # Update perspectives with theme relationships self._update_perspective_filter( - story1, story2, - theme_analysis['direct_shared'], - theme_analysis['indirect_resonance'] + story1, + story2, + theme_analysis["direct_shared"], + theme_analysis["indirect_resonance"], ) def _update_perspective_filter( - self, story1: Story, story2: Story, - shared_themes: set, indirect_resonance: float + self, + story1: Story, + story2: Story, + shared_themes: set, + indirect_resonance: float, ): """Update perspective with theme relationships""" base_decay = 0.9 direct_influence = len(shared_themes) * 0.15 indirect_influence = indirect_resonance * 0.1 - + # Combined theme influence decay = base_decay - (direct_influence + indirect_influence) - + # Update perspective new_perspective = ( - decay * story1.perspective_filter + - (1 - decay) * story2.perspective_filter + decay * story1.perspective_filter + (1 - decay) * story2.perspective_filter ) - + # Record the shift shift_magnitude = np.sum(np.abs(new_perspective - story1.perspective_filter)) story1.perspective_filter = new_perspective - + self.logger.info( f"Perspective Shift - {story1.id}:\n" f" Magnitude: {shift_magnitude:.4f}\n" @@ -583,7 +709,7 @@ def apply_field_constraints(self, stories: List[Story]): story.velocity = self._limit_velocity(story.velocity) -async def create_lighthouse_story(llm: LanguageModel) -> Story: +async def create_lighthouse_story(llm: LanguageModel, position: np.ndarray) -> Story: """Create the lighthouse story with its properties""" content = """ The Lighthouse on the Cliff @@ -599,14 +725,14 @@ async def create_lighthouse_story(llm: LanguageModel) -> Story: embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3) * 2.0, # Scaled initial positions + position=position, velocity=np.zeros(3), themes=["loneliness", "duty", "hope", "guidance"], resonance_history=[], ) -async def create_path_story(llm: LanguageModel) -> Story: +async def create_path_story(llm: LanguageModel, position: np.ndarray) -> Story: content = """ The Path Through the Forest This story is about a traveler lost in a forest, searching for a way home. @@ -620,14 +746,14 @@ async def create_path_story(llm: LanguageModel) -> Story: embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3) * 2.0, # Scaled initial positions + position=position, velocity=np.zeros(3), themes=["journey", "discovery", "nature"], resonance_history=[], ) -async def create_dream_story(llm: LanguageModel) -> Story: +async def create_dream_story(llm: LanguageModel, position: np.ndarray) -> Story: content = """ The Child's Dream of Flight This story follows a child who dreams each night of flying, soaring above @@ -641,7 +767,7 @@ async def create_dream_story(llm: LanguageModel) -> Story: embedding=np.array(embedding), memory_layer=[], perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3) * 2.0, # Scaled initial positions + position=position, velocity=np.zeros(3), themes=["imagination", "freedom", "subconscious"], resonance_history=[], @@ -738,7 +864,7 @@ def log_story_state(self, story: Story, timestep: float): self.journey_log[story.id].append(state) def summarize_journey(self, story: Story): - """Provide comprehensive journey summary""" + """Enhanced journey summary with accumulated perspective shifts""" journey = self.journey_log.get(story.id, []) if not journey: return @@ -746,23 +872,23 @@ def summarize_journey(self, story: Story): start_state = journey[0] end_state = journey[-1] - # Calculate journey metrics + # Calculate metrics total_distance = self.total_distances[story.id] direct_distance = np.linalg.norm( end_state["position"] - start_state["position"] ) wandering_ratio = total_distance / (direct_distance + 1e-6) - # Memory and interaction analysis - memories_formed = end_state["memory_count"] - unique_interactions = len(set(m["interacted_with"] for m in story.memory_layer)) - - # Perspective evolution - perspective_change = ( - end_state["perspective_sum"] - start_state["perspective_sum"] + # Perspective analysis + significant_shifts = [ + s for s in story.perspective_shifts if s["magnitude"] > 0.01 + ] + avg_shift = ( + np.mean([s["magnitude"] for s in significant_shifts]) + if significant_shifts + else 0 ) - # Log comprehensive summary self.logger.info( f"\n=== Journey Summary for {story.id} ===\n" f"Movement Metrics:\n" @@ -770,13 +896,15 @@ def summarize_journey(self, story: Story): f" Direct Distance (start to end): {direct_distance:.2f}\n" f" Wandering Ratio: {wandering_ratio:.2f}\n" f"\nInteraction Metrics:\n" - f" Memories Formed: {memories_formed}\n" - f" Unique Interactions: {unique_interactions}\n" - f" Perspective Shift: {perspective_change:.2f}\n" + f" Memories Formed: {len(story.memory_layer)}\n" + f" Unique Interactions: {len(set(m['interacted_with'] for m in story.memory_layer))}\n" + f" Total Perspective Shift: {story.total_perspective_shift:.4f}\n" + f" Average Shift Magnitude: {avg_shift:.4f}\n" + f" Significant Perspective Changes: {len(significant_shifts)}\n" f"\nFinal State:\n" f" Position: {end_state['position']}\n" f" Velocity: {end_state['velocity']}\n" - f"\nSignificant Events: {len([e for e in self.significant_events if e['type'] == 'interaction' and story.id in e['stories']])}\n" + f"\nSignificant Events: {len(story.memory_layer)}" ) @@ -791,6 +919,33 @@ async def create_story_cluster(): return base_positions + np.random.randn(3, 3) * 0.2 +def summarize_story_journey(story: Story): + """Enhanced journey summary with perspective analysis""" + theme_counts = {} + for memory in story.memory_layer: + for theme in memory["themes"]: + theme_counts[theme] = theme_counts.get(theme, 0) + 1 + + most_influential_themes = sorted( + story.perspective.theme_influences.items(), key=lambda x: x[1], reverse=True + )[:3] + + total_perspective_shift = story.perspective.total_shift + + return { + "total_memories": len(story.memory_layer), + "unique_interactions": len( + set(m["interacted_with"] for m in story.memory_layer) + ), + "theme_exposure": theme_counts, + "total_perspective_shift": total_perspective_shift, + "most_influential_themes": most_influential_themes, + "perspective_shifts": len( + [s for s in story.perspective.shift_history if s["magnitude"] > 0.01] + ), + } + + async def simulate_field(): """Run a simulation of the narrative field""" # Set up logging @@ -812,12 +967,13 @@ async def simulate_field(): interaction_engine = StoryInteractionEngine(field) collective_engine = EnhancedCollectiveStoryEngine(field) - # Create stories - lighthouse = await create_lighthouse_story(llm) + # Create stories with initial positions + initial_positions = await create_story_cluster() + lighthouse = await create_lighthouse_story(llm, position=initial_positions[0]) logger.info(f"Created lighthouse story: {lighthouse.id}") - path = await create_path_story(llm) + path = await create_path_story(llm, position=initial_positions[1]) logger.info(f"Created path story: {path.id}") - dream = await create_dream_story(llm) + dream = await create_dream_story(llm, position=initial_positions[2]) logger.info(f"Created dream story: {dream.id}") # Add stories to field @@ -829,7 +985,7 @@ async def simulate_field(): journey_logger = StoryJourneyLogger() # Simulation loop - for t in range(1000): + for t in range(10000): field.time = t # Update physics @@ -885,3 +1041,4 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) + From eb59d0cf7258f2b2227fefc370fe681ac9c913b6 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 10:49:37 +0200 Subject: [PATCH 04/15] Adds origin story an emotional state tracking --- src/nfd_three_story_evolve.md | 69 +++++++++++++++ src/nfd_three_story_evolve.py | 159 ++++++++++++++++++++++++++++++---- 2 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 src/nfd_three_story_evolve.md diff --git a/src/nfd_three_story_evolve.md b/src/nfd_three_story_evolve.md new file mode 100644 index 0000000..cdef5c7 --- /dev/null +++ b/src/nfd_three_story_evolve.md @@ -0,0 +1,69 @@ +# Three Stories in the Field** + +In the quiet hum of the narrative field, three stories drift, each with its own essence, its own silent history. They move, not in straight lines but in subtle, winding trajectories, always pulled by something unseen. This is their story. + +**The First Story: “The Waiting Lighthouse”** + +The first story has a lighthouse as its core. Its themes are **loneliness** and **enduring duty**. It waits, steady and vigilant, with a soft glow in the field—its core narrative like a beam of light, reaching out. This light, encoded in its **core embedding**, represents its themes of solitude and patience. A quiet sense of **hope** lingers in its memory layer, an echo of past encounters, though that hope has started to fade, losing brightness with time. + +The lighthouse moves slowly, unhurried. Its **perspective filter** tunes it to notice other stories that carry tones of seeking or loss—stories that might, even if briefly, cross its light. It moves like the light it casts: careful, steady, not actively seeking but ever open to something beyond itself. + +**The Second Story: “The Wandering Forest Path”** + +The second story is the tale of a traveler lost in a forest, its core theme an **exploration tinged with fear and curiosity**. Its embedding is close to the lighthouse, drawn by a kind of gravitational pull, for the themes of seeking and being found resonate between them. This path has traces of memories from past interactions in the forest—glimpses of fleeting encounters, echoes of other travelers. Its perspective filter is tuned to notice stability and guidance, a longing for direction in its wandering path. + +The path moves more restlessly, pulled by hints of guidance in the field. It propels itself through the field as if following shadows, creating ripples in the nearby stories. Each ripple shifts its own embedding slightly, pushing it forward with every new encounter. + +**The Third Story: “The Dream of Flight”** + +The third story is light and free—a child’s dream of flying, untethered by gravity. Its embedding holds themes of **freedom, innocence, and boundless wonder**. It hovers nearby, drifting like a leaf on the wind, pulled by currents of curiosity. But in its memories are faint imprints of past encounters with stories grounded and heavy; a slight trace of yearning tints its core. + +Its movement is sporadic, darting and circling, unconcerned with direction but deeply responsive to hints of change and connection. Its perspective filter is wide, noticing joy, possibility, and discovery in other stories, even if faint. + +--- + +## First Encounter: The Lighthouse and the Path** + +The field, attuned to the proximity of these stories, senses a resonance between the lighthouse and the path, softly adjusting their trajectories. Slowly, they drift closer. The lighthouse, aware of a nearby story with themes of seeking, extends its light, casting a gentle sense of comfort toward the path. The path feels this and pauses, sensing direction in the lighthouse’s presence. + +In the technical model, this interaction prompts the **Memory and Resonance Update**: + +- The path receives a new **imprint**—a memory of the lighthouse’s guidance, stored in its memory layer as a faint but growing sense of comfort and clarity. +- The lighthouse, in turn, holds a subtle trace of the path’s exploration, giving its memory a gentle spark of possibility, a reminder that its light has reached someone. + +The encounter shifts their **embeddings** slightly: + +- The path’s position in semantic space shifts a bit closer to stability, while the lighthouse gains a touch of motion in its otherwise steadfast embedding. + +## Second Encounter: The Path and the Dream** + +The path, now feeling a faint pull of comfort, continues forward in the field until it senses something playful nearby: the dream of flight. The path is drawn in by the lightness of the dream, and the two stories circle each other, curious but hesitant. The dream of flight feels the path’s echo of hope and seeking, which awakens a new sensation—a hint of groundedness in its own memory layer. + +The **Interaction Engine** registers a resonance, storing fragments in their memories: + +- The path carries forward a memory of the dream’s freedom, imprinted as a lightness in its core—a reminder that even in seeking, there’s room for play. +- The dream of flight absorbs the path’s yearning, a subtle grounding that reminds it of all the things that exist below. + +These memories subtly alter their **perspective filters**: + +- The path begins to notice stories with a sense of lightness and play, softening its need for guidance. +- The dream, meanwhile, gains a slight “weight,” now attuned to stories carrying themes of longing and groundedness. + +## Third Encounter: The Collective Shockwave** + +Time passes, and a **pulse** moves through the field. This is a collective event generated by the **Collective Story Engine**—a moment where the narrative field takes on a dominant theme of **seeking connection**. This pulse temporarily heightens each story’s awareness, expanding their perception to even distant stories in the field. + +The dream, the lighthouse, and the path feel this pulse together, drawn toward each other in a moment of unity. Their embeddings, sensing each other fully for the first time, draw closer, almost as if overlapping. The **Collective Story** emerges in this moment—a shared tale of guidance, seeking, and boundless possibility. + +As the pulse fades, the collective story remains like a **memory imprint** in each story: + +- The lighthouse remembers the dream of flight and the path, carrying forward the memory of their presence as a sense of connectedness. +- The path feels a renewed sense of direction, enriched by the lighthouse and the dream. +- The dream remembers the light and the path, grounding its flight with a sense of shared purpose. + +Each story, now moving in its own direction, carries a **piece of the collective memory** with it—a new understanding shaped by the others, a reminder of what it means to be part of something larger than itself. + +## In the Narrative Field** + +- The lighthouse, the path, and the dream continue to move, influenced by new memories, pulling each in ways that subtly shift their trajectories. Their memory layers grow, resonant with each other, even as they drift apart. +- In this evolving field, the stories are no longer quite the same as they were. The field itself feels their change—a movement toward something, and yet toward nothing fixed, only the endless, shared becoming of all the stories it holds. diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index b30c814..237fcdb 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -131,6 +131,14 @@ def update( return shift +@dataclass +class EmotionalState: + joy: float = 0.0 + sadness: float = 0.0 + fear: float = 0.0 + hope: float = 0.0 + curiosity: float = 0.0 + class Story: def __init__( self, @@ -139,6 +147,7 @@ def __init__( embedding: np.ndarray, perspective_filter: np.ndarray, themes: List[str], + field: 'NarrativeField', # Add this line position: np.ndarray = None, velocity: np.ndarray = None, **kwargs, @@ -146,9 +155,13 @@ def __init__( self.id = id self.content = content self.embedding = embedding - self.initial_perspective = perspective_filter.copy() - self.perspective_filter = perspective_filter.copy() # Change this line + self.perspective_filter = perspective_filter self.themes = themes + self.field = field # Add this line + self.position = position if position is not None else np.random.randn(3) + self.velocity = velocity if velocity is not None else np.zeros(3) + self.emotional_state = EmotionalState() + self.previous_emotional_state = EmotionalState() self.memory_layer = [] self.resonance_history = [] self.total_perspective_shift = 0.0 @@ -163,7 +176,10 @@ def update_perspective(self, other: "Story", theme_impact: float, resonance: flo # Calculate influence factors theme_weight = theme_impact * 0.2 resonance_weight = resonance * 0.3 - total_weight = min(0.5, theme_weight + resonance_weight) # Cap maximum shift + + # Factor in emotional impact + emotional_influence = self._calculate_emotional_influence(other) + total_weight = min(0.6, theme_weight + resonance_weight + emotional_influence) # Calculate new perspective new_perspective = ( @@ -189,6 +205,19 @@ def update_perspective(self, other: "Story", theme_impact: float, resonance: flo return shift + def _calculate_emotional_influence(self, other: "Story") -> float: + similarity = self.field._calculate_emotional_similarity(self, other) + intensity = np.mean([getattr(self.emotional_state, e) for e in vars(self.emotional_state)]) + return similarity * intensity * 0.1 + + def update_emotional_state(self, interaction_impact: float, themes: List[str]): + self.previous_emotional_state = EmotionalState(**vars(self.emotional_state)) + if "hope" in themes: + self.emotional_state.hope += interaction_impact * 0.2 + if "journey" in themes: + self.emotional_state.curiosity += interaction_impact * 0.15 + # ... other emotion updates based on themes + class NarrativeFieldViz: """Handles visualization of field state""" @@ -210,6 +239,9 @@ async def capture_state(self, field, timestep: int): }, "resonance_map": self._compute_resonance_map(field), "field_potential": field.field_potential.copy(), + "emotional_states": { + story.id: vars(story.emotional_state) for story in field.stories + }, } self.history.append(state) self.logger.debug(f"Captured field state at timestep {timestep}") @@ -257,17 +289,38 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: dim=0 ) + # Add emotional resonance + emotional_similarity = self._calculate_emotional_similarity(story1, story2) + # Scale resonance by distance distance = np.linalg.norm(story1.position - story2.position) distance_factor = np.exp(-distance / self.interaction_range) return float( - (0.4 * embedding_similarity + 0.3 * theme_overlap + 0.3 * filter_alignment) + (0.3 * embedding_similarity + 0.3 * theme_overlap + 0.2 * filter_alignment + 0.2 * emotional_similarity) * distance_factor ) + def _calculate_emotional_similarity(self, story1: Story, story2: Story) -> float: + emotions1 = np.array([ + story1.emotional_state.joy, + story1.emotional_state.sadness, + story1.emotional_state.fear, + story1.emotional_state.hope, + story1.emotional_state.curiosity + ]) + emotions2 = np.array([ + story2.emotional_state.joy, + story2.emotional_state.sadness, + story2.emotional_state.fear, + story2.emotional_state.hope, + story2.emotional_state.curiosity + ]) + return float(F.cosine_similarity(torch.tensor(emotions1).float(), torch.tensor(emotions2).float(), dim=0)) + def add_story(self, story: Story): """Add a new story to the field""" + story.field = self # Add this line self.stories.append(story) self._update_field_potential() self.logger.info(f"Added new story: {story.id}") @@ -487,7 +540,12 @@ def process_interaction(self, story1: Story, story2: Story): f"{story2.id}: shift={shift2:.4f}, total={story2.total_perspective_shift:.4f}" ) - # Create memories with perspective shift information + # Update emotional states + emotional_impact = resonance * len(theme_analysis["direct_shared"]) * 0.1 + story1.update_emotional_state(emotional_impact, story2.themes) + story2.update_emotional_state(emotional_impact, story1.themes) + + # Create memories with emotional impact memory1 = { "time": self.field.time, "interacted_with": story2.id, @@ -497,9 +555,9 @@ def process_interaction(self, story1: Story, story2: Story): "theme_impact": theme_analysis["theme_impact"], "perspective_shift": shift1, "total_shift": story1.total_perspective_shift, - "interaction_id": self.interaction_count, # TODO LBO? - "emotional_impact": resonance - * len(theme_analysis["direct_shared"]), # TODO LBO? + "interaction_id": self.interaction_count, + "emotional_impact": emotional_impact, + "emotional_state_change": self._calculate_emotional_change(story1), } memory2 = { @@ -512,7 +570,8 @@ def process_interaction(self, story1: Story, story2: Story): "perspective_shift": shift2, "total_shift": story2.total_perspective_shift, "interaction_id": self.interaction_count, - "emotional_impact": resonance * len(theme_analysis["direct_shared"]), + "emotional_impact": emotional_impact, + "emotional_state_change": self._calculate_emotional_change(story2), } # Update memory layers @@ -526,7 +585,7 @@ def process_interaction(self, story1: Story, story2: Story): f" Direct Shared Themes: {list(theme_analysis['direct_shared'])}\n" f" Theme Relationships Found: {theme_analysis['related_themes']}\n" f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" - f" Emotional Impact: {memory1['emotional_impact']:.2f}" + f" Emotional Impact: {emotional_impact:.2f}" ) # Update perspectives with theme relationships @@ -537,6 +596,16 @@ def process_interaction(self, story1: Story, story2: Story): theme_analysis["indirect_resonance"], ) + def _calculate_emotional_change(self, story: Story) -> Dict[str, float]: + # Calculate the change in emotional state + return { + "joy_change": story.emotional_state.joy - story.previous_emotional_state.joy, + "sadness_change": story.emotional_state.sadness - story.previous_emotional_state.sadness, + "fear_change": story.emotional_state.fear - story.previous_emotional_state.fear, + "hope_change": story.emotional_state.hope - story.previous_emotional_state.hope, + "curiosity_change": story.emotional_state.curiosity - story.previous_emotional_state.curiosity, + } + def _update_perspective_filter( self, story1: Story, @@ -709,7 +778,7 @@ def apply_field_constraints(self, stories: List[Story]): story.velocity = self._limit_velocity(story.velocity) -async def create_lighthouse_story(llm: LanguageModel, position: np.ndarray) -> Story: +async def create_lighthouse_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: """Create the lighthouse story with its properties""" content = """ The Lighthouse on the Cliff @@ -729,10 +798,11 @@ async def create_lighthouse_story(llm: LanguageModel, position: np.ndarray) -> S velocity=np.zeros(3), themes=["loneliness", "duty", "hope", "guidance"], resonance_history=[], + field=field # Add this line ) -async def create_path_story(llm: LanguageModel, position: np.ndarray) -> Story: +async def create_path_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: content = """ The Path Through the Forest This story is about a traveler lost in a forest, searching for a way home. @@ -750,10 +820,53 @@ async def create_path_story(llm: LanguageModel, position: np.ndarray) -> Story: velocity=np.zeros(3), themes=["journey", "discovery", "nature"], resonance_history=[], + field=field + ) + +async def create_dream_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: + content = """ + The Child's Dream of Flight + This story follows a child who dreams each night of flying, soaring above + villages, forests, and oceans. The dream is filled with freedom, innocence, + and limitless possibility, untouched by fear or doubt. + """ + embedding = await llm.generate_embedding(content) + return Story( + id="dream", + content=content, + embedding=np.array(embedding), + memory_layer=[], + perspective_filter=np.ones(len(embedding)), + position=position, + velocity=np.zeros(3), + themes=["imagination", "freedom", "subconscious"], + resonance_history=[], + field=field ) -async def create_dream_story(llm: LanguageModel, position: np.ndarray) -> Story: +async def create_path_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: + content = """ + The Path Through the Forest + This story is about a traveler lost in a forest, searching for a way home. + It holds elements of uncertainty, hope, and resilience as the traveler + navigates unfamiliar terrain, feeling both wonder and isolation. + """ + embedding = await llm.generate_embedding(content) + return Story( + id="path", + content=content, + embedding=np.array(embedding), + memory_layer=[], + perspective_filter=np.ones(len(embedding)), + position=position, + velocity=np.zeros(3), + themes=["journey", "discovery", "nature"], + resonance_history=[], + field=field + ) + +async def create_dream_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: content = """ The Child's Dream of Flight This story follows a child who dreams each night of flying, soaring above @@ -771,6 +884,7 @@ async def create_dream_story(llm: LanguageModel, position: np.ndarray) -> Story: velocity=np.zeros(3), themes=["imagination", "freedom", "subconscious"], resonance_history=[], + field=field ) @@ -827,6 +941,15 @@ def log_interaction(self, story1: Story, story2: Story, resonance: float): f" Shared Themes: {latest_memory.get('shared_themes', [])}\n" ) + self.logger.info( + f"\nEmotional Impact:\n" + f" {story1.id} Emotional Change: {self._format_emotional_change(story1)}\n" + f" {story2.id} Emotional Change: {self._format_emotional_change(story2)}\n" + ) + + def _format_emotional_change(self, story: Story) -> str: + return ", ".join([f"{e}: {getattr(story.emotional_state, e):.2f}" for e in vars(story.emotional_state)]) + def log_story_state(self, story: Story, timestep: float): """Log detailed story state and track journey metrics""" if story.id not in self.journey_log: @@ -967,13 +1090,13 @@ async def simulate_field(): interaction_engine = StoryInteractionEngine(field) collective_engine = EnhancedCollectiveStoryEngine(field) - # Create stories with initial positions + # Create stories with initial positions and pass the field reference initial_positions = await create_story_cluster() - lighthouse = await create_lighthouse_story(llm, position=initial_positions[0]) + lighthouse = await create_lighthouse_story(llm, field, position=initial_positions[0]) logger.info(f"Created lighthouse story: {lighthouse.id}") - path = await create_path_story(llm, position=initial_positions[1]) + path = await create_path_story(llm, field, position=initial_positions[1]) logger.info(f"Created path story: {path.id}") - dream = await create_dream_story(llm, position=initial_positions[2]) + dream = await create_dream_story(llm, field, position=initial_positions[2]) logger.info(f"Created dream story: {dream.id}") # Add stories to field @@ -1042,3 +1165,5 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) + + From c19b58294bd6486f914af82c68ae6e28f20827e2 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 10:57:52 +0200 Subject: [PATCH 05/15] Emotional Updates: The emotional changes are now visible and vary between interactions. For example: 00 This shows that the stories are experiencing emotional changes during interactions. Varied Emotional Impacts: Different interactions result in different emotional impacts. For instance: 04 This indicates that stories are responding differently to interactions based on their themes and resonance. Resonance Levels: The resonance levels between stories vary (e.g., 0.36, 0.39, 0.38), which affects the strength of their interactions and emotional impacts. 4. Theme Relationships: The simulation is identifying theme relationships between stories, such as: ] These relationships contribute to the emotional and thematic interactions between stories. 5. Memory Formation: Stories are forming memories of their interactions, including the themes they gain from other stories. --- src/nfd_three_story_evolve.py | 84 ++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 237fcdb..396b316 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -210,13 +210,57 @@ def _calculate_emotional_influence(self, other: "Story") -> float: intensity = np.mean([getattr(self.emotional_state, e) for e in vars(self.emotional_state)]) return similarity * intensity * 0.1 - def update_emotional_state(self, interaction_impact: float, themes: List[str]): + def update_emotional_state(self, interaction_impact: float, other_story: 'Story'): + """Update emotional state based on interaction impact and themes""" self.previous_emotional_state = EmotionalState(**vars(self.emotional_state)) - if "hope" in themes: - self.emotional_state.hope += interaction_impact * 0.2 - if "journey" in themes: - self.emotional_state.curiosity += interaction_impact * 0.15 - # ... other emotion updates based on themes + + # Calculate theme-based emotional changes + joy_change = 0.0 + sadness_change = 0.0 + fear_change = 0.0 + hope_change = 0.0 + curiosity_change = 0.0 + + shared_themes = set(self.themes) & set(other_story.themes) + all_themes = set(self.themes) | set(other_story.themes) + + for theme in all_themes: + if theme in ["hope", "freedom", "imagination"]: + joy_change += 0.05 + hope_change += 0.1 + elif theme in ["loneliness", "duty"]: + sadness_change += 0.05 + elif theme in ["journey", "discovery"]: + curiosity_change += 0.1 + fear_change += 0.02 # A little fear in the unknown + elif theme in ["nature", "guidance"]: + hope_change += 0.05 + fear_change -= 0.02 # Nature and guidance reduce fear slightly + + # Apply resonance-based amplification + resonance = self.field.detect_resonance(self, other_story) + amplification = 1 + resonance + + # Introduce a small random factor for variability + random_factor = 0.01 * (2 * np.random.random() - 1) + + # Update emotional state with amplified changes + self.emotional_state.joy = min(1.0, max(0.0, self.emotional_state.joy + (joy_change + random_factor) * amplification)) + self.emotional_state.sadness = min(1.0, max(0.0, self.emotional_state.sadness + (sadness_change + random_factor) * amplification)) + self.emotional_state.fear = min(1.0, max(0.0, self.emotional_state.fear + (fear_change + random_factor) * amplification)) + self.emotional_state.hope = min(1.0, max(0.0, self.emotional_state.hope + (hope_change + random_factor) * amplification)) + self.emotional_state.curiosity = min(1.0, max(0.0, self.emotional_state.curiosity + (curiosity_change + random_factor) * amplification)) + + # Calculate overall emotional change + total_change = ( + abs(self.emotional_state.joy - self.previous_emotional_state.joy) + + abs(self.emotional_state.sadness - self.previous_emotional_state.sadness) + + abs(self.emotional_state.fear - self.previous_emotional_state.fear) + + abs(self.emotional_state.hope - self.previous_emotional_state.hope) + + abs(self.emotional_state.curiosity - self.previous_emotional_state.curiosity) + ) + + return total_change class NarrativeFieldViz: @@ -541,9 +585,16 @@ def process_interaction(self, story1: Story, story2: Story): ) # Update emotional states - emotional_impact = resonance * len(theme_analysis["direct_shared"]) * 0.1 - story1.update_emotional_state(emotional_impact, story2.themes) - story2.update_emotional_state(emotional_impact, story1.themes) + emotional_change1 = story1.update_emotional_state(resonance, story2) + emotional_change2 = story2.update_emotional_state(resonance, story1) + + # Log emotional updates with a lower threshold + if emotional_change1 > 0.001 or emotional_change2 > 0.001: + self.logger.info( + f"\nEmotional Updates:\n" + f"{story1.id}: change={emotional_change1:.4f}\n" + f"{story2.id}: change={emotional_change2:.4f}" + ) # Create memories with emotional impact memory1 = { @@ -556,7 +607,7 @@ def process_interaction(self, story1: Story, story2: Story): "perspective_shift": shift1, "total_shift": story1.total_perspective_shift, "interaction_id": self.interaction_count, - "emotional_impact": emotional_impact, + "emotional_impact": emotional_change1, "emotional_state_change": self._calculate_emotional_change(story1), } @@ -570,7 +621,7 @@ def process_interaction(self, story1: Story, story2: Story): "perspective_shift": shift2, "total_shift": story2.total_perspective_shift, "interaction_id": self.interaction_count, - "emotional_impact": emotional_impact, + "emotional_impact": emotional_change2, "emotional_state_change": self._calculate_emotional_change(story2), } @@ -585,19 +636,12 @@ def process_interaction(self, story1: Story, story2: Story): f" Direct Shared Themes: {list(theme_analysis['direct_shared'])}\n" f" Theme Relationships Found: {theme_analysis['related_themes']}\n" f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" - f" Emotional Impact: {emotional_impact:.2f}" + f" Emotional Impact: {emotional_change1:.2f}, {emotional_change2:.2f}" ) - # Update perspectives with theme relationships - self._update_perspective_filter( - story1, - story2, - theme_analysis["direct_shared"], - theme_analysis["indirect_resonance"], - ) + self.interaction_count += 1 def _calculate_emotional_change(self, story: Story) -> Dict[str, float]: - # Calculate the change in emotional state return { "joy_change": story.emotional_state.joy - story.previous_emotional_state.joy, "sadness_change": story.emotional_state.sadness - story.previous_emotional_state.sadness, From 8fc5d94e48c6c20e3c3bd50181d40653e2143e4e Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 11:03:16 +0200 Subject: [PATCH 06/15] Interaction Dynamics: The stories are consistently interacting with each other, as evidenced by the numerous memory formations and emotional updates. Resonance levels between stories range from about 0.26 to 0.28, indicating moderate levels of connection. There are no directly shared themes between the stories, but they influence each other through related themes (e.g., "loneliness" and "discovery", "guidance" and "nature"). Emotional Impact: Each interaction causes subtle changes in the emotional states of the stories. All stories seem to maintain high levels of joy, hope, and curiosity throughout the interactions. Fear and sadness fluctuate more, sometimes increasing slightly after interactions. 3. Perspective Shifts: The stories experience small but consistent perspective shifts with each interaction. The dream story seems to have undergone the largest total perspective shift (22.7162), followed by the lighthouse (11.8148), and then the path (6.7595). Movement Patterns: All three stories have traveled significant distances (260+ units) but ended up relatively close to their starting positions (direct distances of 1.77 to 2.35 units). This results in high "wandering ratios" (111 to 148), indicating complex, non-linear paths through the narrative space. Memory Formation: Each story formed 20,000 memories, suggesting frequent and consistent interactions. Interestingly, they each only record 2 unique interactions, which might indicate a focus on deeper, repeated engagements with the same stories. 6. Theme Dynamics: While there are no directly shared themes, the stories are influencing each other's thematic content through related concepts. The theme relationships (e.g., "duty" and "freedom", "guidance" and "imagination") suggest a rich interplay of ideas. 7. Collective Behavior: The consistent interaction patterns and similar movement metrics suggest a kind of collective behavior emerging from the individual story dynamics. These results demonstrate a complex, interconnected narrative ecosystem where stories influence each other in subtle but significant ways. The high wandering ratios and large total distances traveled suggest a rich exploration of the narrative space, while the emotional and thematic interactions show how the stories are constantly evolving in response to each other. --- src/nfd_three_story_evolve.py | 69 ++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 396b316..763624e 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -171,38 +171,10 @@ def __init__( self.position = position if position is not None else np.random.randn(3) self.velocity = velocity if velocity is not None else np.zeros(3) - def update_perspective(self, other: "Story", theme_impact: float, resonance: float): - """Update perspective with accumulation""" - # Calculate influence factors - theme_weight = theme_impact * 0.2 - resonance_weight = resonance * 0.3 - - # Factor in emotional impact - emotional_influence = self._calculate_emotional_influence(other) - total_weight = min(0.6, theme_weight + resonance_weight + emotional_influence) - - # Calculate new perspective - new_perspective = ( - 1 - total_weight - ) * self.perspective_filter + total_weight * other.perspective_filter - - # Calculate shift magnitude - shift = float(np.sum(np.abs(new_perspective - self.perspective_filter))) - - # Update accumulators - self.total_perspective_shift += shift - self.perspective_shifts.append( - { - "interaction_with": other.id, - "magnitude": shift, - "theme_impact": theme_impact, - "resonance": resonance, - } - ) - - # Store new perspective - self.perspective_filter = new_perspective - + def update_perspective(self, other: "Story", theme_impact: float, resonance: float, emotional_change): + shift = theme_impact * resonance * sum(emotional_change.values()) + self.perspective_filter += shift * (other.embedding - self.embedding) + self.total_perspective_shift += abs(shift) return shift def _calculate_emotional_influence(self, other: "Story") -> float: @@ -262,6 +234,31 @@ def update_emotional_state(self, interaction_impact: float, other_story: 'Story' return total_change + def decay_emotions(self, decay_rate=0.01): + for emotion in vars(self.emotional_state): + current_value = getattr(self.emotional_state, emotion) + decayed_value = max(0, current_value - decay_rate) + setattr(self.emotional_state, emotion, decayed_value) + + def check_emotional_thresholds(self): + if self.emotional_state.joy > 0.9: + self.trigger_joyful_event() + elif self.emotional_state.sadness > 0.8: + self.trigger_melancholy_event() + + def evolve_themes(self, interaction_history): + new_themes = set() + for interaction in interaction_history[-10:]: # Consider last 10 interactions + if interaction['resonance'] > 0.5: + new_themes.update(interaction['themes_gained']) + self.themes = list(set(self.themes) | new_themes)[:5] # Keep top 5 themes + + def calculate_interaction_strength(self, other_story): + base_strength = self.field.detect_resonance(self, other_story) + interaction_history = self.get_interaction_history(other_story) + familiarity_bonus = min(0.2, len(interaction_history) * 0.02) + return base_strength + familiarity_bonus + class NarrativeFieldViz: """Handles visualization of field state""" @@ -378,6 +375,10 @@ def _update_field_potential(self): -np.linalg.norm(story.position) / 10.0 ) + def apply_environmental_event(self, event_type, intensity): + for story in self.stories: + story.respond_to_environmental_event(event_type, intensity) + class StoryPhysics: """Handles physical behavior of stories in the field""" @@ -572,10 +573,10 @@ def process_interaction(self, story1: Story, story2: Story): if resonance > self.memory_threshold: # Update perspectives shift1 = story1.update_perspective( - story2, theme_analysis["theme_impact"], resonance + story2, theme_analysis["theme_impact"], resonance, self._calculate_emotional_change(story1) ) shift2 = story2.update_perspective( - story1, theme_analysis["theme_impact"], resonance + story1, theme_analysis["theme_impact"], resonance, self._calculate_emotional_change(story2) ) self.logger.info( From 1ff014cc80eb91167f826d60ebb5633724570619 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 11:09:07 +0200 Subject: [PATCH 07/15] Enhance story interactions and field dynamics: - Implement more nuanced interaction types (collaboration, conflict, inspiration, reflection) - Add emotional state tracking and updates for stories - Improve theme evolution and perspective shift mechanics - Enhance StoryPhysics with balanced forces and exploration - Implement StoryJourneyLogger for detailed interaction and movement tracking - Add EnhancedCollectiveStoryEngine for emergent theme detection - Refine resonance calculation with emotional similarity - Implement memory formation with emotional impact - Add periodic field pulses to simulate collective events - Enhance visualization capabilities for field state capture --- src/nfd_three_story_evolve.py | 165 ++++++++++++++++++---------------- 1 file changed, 88 insertions(+), 77 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 763624e..c113f41 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -171,8 +171,20 @@ def __init__( self.position = position if position is not None else np.random.randn(3) self.velocity = velocity if velocity is not None else np.zeros(3) - def update_perspective(self, other: "Story", theme_impact: float, resonance: float, emotional_change): - shift = theme_impact * resonance * sum(emotional_change.values()) + def update_perspective(self, other: "Story", theme_impact: float, resonance: float, + emotional_change: Dict[str, float], interaction_type: str) -> float: + base_shift = theme_impact * resonance * sum(emotional_change.values()) + + # Adjust shift based on interaction type + if interaction_type == "collaboration": + shift = base_shift * 1.2 # Enhance shift for collaborative interactions + elif interaction_type == "conflict": + shift = base_shift * 0.8 # Reduce shift for conflicting interactions + elif interaction_type == "inspiration": + shift = base_shift * 1.5 # Significantly enhance shift for inspirational interactions + else: # reflection + shift = base_shift # No change for reflective interactions + self.perspective_filter += shift * (other.embedding - self.embedding) self.total_perspective_shift += abs(shift) return shift @@ -182,8 +194,7 @@ def _calculate_emotional_influence(self, other: "Story") -> float: intensity = np.mean([getattr(self.emotional_state, e) for e in vars(self.emotional_state)]) return similarity * intensity * 0.1 - def update_emotional_state(self, interaction_impact: float, other_story: 'Story'): - """Update emotional state based on interaction impact and themes""" + def update_emotional_state(self, interaction_impact: float, other_story: 'Story', interaction_type: str) -> float: self.previous_emotional_state = EmotionalState(**vars(self.emotional_state)) # Calculate theme-based emotional changes @@ -209,6 +220,18 @@ def update_emotional_state(self, interaction_impact: float, other_story: 'Story' hope_change += 0.05 fear_change -= 0.02 # Nature and guidance reduce fear slightly + # Adjust emotional changes based on interaction type + if interaction_type == "collaboration": + joy_change *= 1.2 + hope_change *= 1.2 + elif interaction_type == "conflict": + sadness_change *= 1.2 + fear_change *= 1.2 + elif interaction_type == "inspiration": + curiosity_change *= 1.5 + hope_change *= 1.3 + # No changes for "reflection" type + # Apply resonance-based amplification resonance = self.field.detect_resonance(self, other_story) amplification = 1 + resonance @@ -555,41 +578,46 @@ def process_interaction(self, story1: Story, story2: Story, resonance: float): class StoryInteractionEngine: - """Enhanced interaction engine with better memory formation""" + """Enhanced interaction engine with more nuanced and impactful interactions""" def __init__(self, field: NarrativeField): self.field = field self.logger = logging.getLogger(__name__) self.interaction_count = 0 - self.memory_threshold = 0.2 # Lower threshold for memory formation + self.memory_threshold = 0.2 self.theme_engine = ThemeEvolutionEngine() + self.interaction_types = [ + "collaboration", "conflict", "inspiration", "reflection" + ] def process_interaction(self, story1: Story, story2: Story): resonance = self.field.detect_resonance(story1, story2) - theme_analysis = self.theme_engine.process_interaction( - story1, story2, resonance - ) - + theme_analysis = self.theme_engine.process_interaction(story1, story2, resonance) + if resonance > self.memory_threshold: - # Update perspectives + interaction_type = self._determine_interaction_type(story1, story2) + + # Update perspectives with interaction type influence shift1 = story1.update_perspective( - story2, theme_analysis["theme_impact"], resonance, self._calculate_emotional_change(story1) + story2, theme_analysis["theme_impact"], resonance, + self._calculate_emotional_change(story1), interaction_type ) shift2 = story2.update_perspective( - story1, theme_analysis["theme_impact"], resonance, self._calculate_emotional_change(story2) + story2, theme_analysis["theme_impact"], resonance, + self._calculate_emotional_change(story2), interaction_type ) self.logger.info( - f"\nPerspective Updates:\n" + f"\nInteraction Type: {interaction_type}\n" + f"Perspective Updates:\n" f"{story1.id}: shift={shift1:.4f}, total={story1.total_perspective_shift:.4f}\n" f"{story2.id}: shift={shift2:.4f}, total={story2.total_perspective_shift:.4f}" ) - # Update emotional states - emotional_change1 = story1.update_emotional_state(resonance, story2) - emotional_change2 = story2.update_emotional_state(resonance, story1) + # Update emotional states with interaction type influence + emotional_change1 = story1.update_emotional_state(resonance, story2, interaction_type) + emotional_change2 = story2.update_emotional_state(resonance, story1, interaction_type) - # Log emotional updates with a lower threshold if emotional_change1 > 0.001 or emotional_change2 > 0.001: self.logger.info( f"\nEmotional Updates:\n" @@ -597,34 +625,11 @@ def process_interaction(self, story1: Story, story2: Story): f"{story2.id}: change={emotional_change2:.4f}" ) - # Create memories with emotional impact - memory1 = { - "time": self.field.time, - "interacted_with": story2.id, - "resonance": resonance, - "themes": list(story2.themes), - "shared_themes": list(theme_analysis["direct_shared"]), - "theme_impact": theme_analysis["theme_impact"], - "perspective_shift": shift1, - "total_shift": story1.total_perspective_shift, - "interaction_id": self.interaction_count, - "emotional_impact": emotional_change1, - "emotional_state_change": self._calculate_emotional_change(story1), - } - - memory2 = { - "time": self.field.time, - "interacted_with": story1.id, - "resonance": resonance, - "themes": list(story1.themes), - "shared_themes": list(theme_analysis["direct_shared"]), - "theme_impact": theme_analysis["theme_impact"], - "perspective_shift": shift2, - "total_shift": story2.total_perspective_shift, - "interaction_id": self.interaction_count, - "emotional_impact": emotional_change2, - "emotional_state_change": self._calculate_emotional_change(story2), - } + # Create memories with emotional impact and interaction type + memory1 = self._create_memory(story1, story2, resonance, theme_analysis, + shift1, emotional_change1, interaction_type) + memory2 = self._create_memory(story2, story1, resonance, theme_analysis, + shift2, emotional_change2, interaction_type) # Update memory layers story1.memory_layer.append(memory1) @@ -634,6 +639,7 @@ def process_interaction(self, story1: Story, story2: Story): self.logger.info( f"\nRich Interaction #{self.interaction_count}:\n" f" Stories: {story1.id} <-> {story2.id}\n" + f" Interaction Type: {interaction_type}\n" f" Direct Shared Themes: {list(theme_analysis['direct_shared'])}\n" f" Theme Relationships Found: {theme_analysis['related_themes']}\n" f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" @@ -642,6 +648,39 @@ def process_interaction(self, story1: Story, story2: Story): self.interaction_count += 1 + def _determine_interaction_type(self, story1: Story, story2: Story) -> str: + """Determine the type of interaction based on story properties""" + shared_themes = set(story1.themes) & set(story2.themes) + emotional_similarity = self.field._calculate_emotional_similarity(story1, story2) + + if len(shared_themes) > 2 and emotional_similarity > 0.7: + return "collaboration" + elif len(shared_themes) < 1 and emotional_similarity < 0.3: + return "conflict" + elif story1.total_perspective_shift > story2.total_perspective_shift * 1.5: + return "inspiration" + else: + return "reflection" + + def _create_memory(self, story: Story, other: Story, resonance: float, + theme_analysis: dict, shift: float, emotional_change: float, + interaction_type: str) -> dict: + """Create a detailed memory of the interaction""" + return { + "time": self.field.time, + "interacted_with": other.id, + "resonance": resonance, + "themes": list(other.themes), + "shared_themes": list(theme_analysis["direct_shared"]), + "theme_impact": theme_analysis["theme_impact"], + "perspective_shift": shift, + "total_shift": story.total_perspective_shift, + "interaction_id": self.interaction_count, + "emotional_impact": emotional_change, + "emotional_state_change": self._calculate_emotional_change(story), + "interaction_type": interaction_type + } + def _calculate_emotional_change(self, story: Story) -> Dict[str, float]: return { "joy_change": story.emotional_state.joy - story.previous_emotional_state.joy, @@ -651,37 +690,6 @@ def _calculate_emotional_change(self, story: Story) -> Dict[str, float]: "curiosity_change": story.emotional_state.curiosity - story.previous_emotional_state.curiosity, } - def _update_perspective_filter( - self, - story1: Story, - story2: Story, - shared_themes: set, - indirect_resonance: float, - ): - """Update perspective with theme relationships""" - base_decay = 0.9 - direct_influence = len(shared_themes) * 0.15 - indirect_influence = indirect_resonance * 0.1 - - # Combined theme influence - decay = base_decay - (direct_influence + indirect_influence) - - # Update perspective - new_perspective = ( - decay * story1.perspective_filter + (1 - decay) * story2.perspective_filter - ) - - # Record the shift - shift_magnitude = np.sum(np.abs(new_perspective - story1.perspective_filter)) - story1.perspective_filter = new_perspective - - self.logger.info( - f"Perspective Shift - {story1.id}:\n" - f" Magnitude: {shift_magnitude:.4f}\n" - f" Direct Theme Influence: {direct_influence:.2f}\n" - f" Indirect Theme Influence: {indirect_influence:.2f}" - ) - class StoryPhysics: """Handles physical behavior of stories in the field""" @@ -1212,3 +1220,6 @@ async def simulate_field(): + + + From 43616c2949c5aad2f3d036d06d236ab1ec678a63 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 12:43:29 +0200 Subject: [PATCH 08/15] Story and state generation --- src/config.py | 7 +- src/nfd_three_story_evolve.py | 515 ++++++++++++++++++++++------------ 2 files changed, 339 insertions(+), 183 deletions(-) diff --git a/src/config.py b/src/config.py index 52fb3bf..73c8678 100644 --- a/src/config.py +++ b/src/config.py @@ -21,7 +21,7 @@ "Mistral-Nemo-Instruct-2407-GGUF/" "Mistral-Nemo-Instruct-2407-Q4_K_M.gguf" ).expanduser(), - "model_name": "mistral-nemo:latest", + "model_name": "llama3.2:latest", # "mistral-nemo:latest", }, "embedding": { "path": Path( @@ -34,15 +34,16 @@ "optimal_config": { "n_gpu_layers": -1, "n_batch": 512, - "n_ctx": 4096, + "n_ctx": 16384, "metal_device": "mps", "main_gpu": 0, "use_metal": True, - "n_threads": 4, + "n_threads": 8, }, }, } + def get_model_config(config_name: str = "balanced") -> Dict[str, Any]: """ Retrieve the model configuration for a given configuration name. diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index c113f41..1bb787c 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -3,13 +3,24 @@ import logging from dataclasses import dataclass, field import numpy as np -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Any import torch from torch import nn import torch.nn.functional as F from language_models import LanguageModel, OllamaInterface import asyncio import time +import random +import json + +# Import the logging setup function +from logging_config import setup_logging + +# Set up logging at the beginning of the script +setup_logging() + +# Get a logger for this module +logger = logging.getLogger(__name__) @dataclass @@ -52,6 +63,7 @@ def __init__(self): "guidance": {"duty", "guidance", "hope"}, "solitude": {"loneliness", "nature", "subconscious"}, } + self.logger = logging.getLogger(__name__) def get_theme_resonance(self, theme1: str, theme2: str) -> float: """Get resonance between two themes""" @@ -78,6 +90,7 @@ def __init__(self, initial_filter: np.ndarray): self.shift_history = [] self.theme_influences = {} self.total_shift = 0.0 + self.logger = logging.getLogger(__name__) def update( self, @@ -139,6 +152,7 @@ class EmotionalState: hope: float = 0.0 curiosity: float = 0.0 + class Story: def __init__( self, @@ -147,7 +161,7 @@ def __init__( embedding: np.ndarray, perspective_filter: np.ndarray, themes: List[str], - field: 'NarrativeField', # Add this line + field: "NarrativeField", # Add this line position: np.ndarray = None, velocity: np.ndarray = None, **kwargs, @@ -166,37 +180,50 @@ def __init__( self.resonance_history = [] self.total_perspective_shift = 0.0 self.perspective_shifts = [] - + # Initialize position and velocity self.position = position if position is not None else np.random.randn(3) self.velocity = velocity if velocity is not None else np.zeros(3) + self.logger = logging.getLogger(__name__) - def update_perspective(self, other: "Story", theme_impact: float, resonance: float, - emotional_change: Dict[str, float], interaction_type: str) -> float: + def update_perspective( + self, + other: "Story", + theme_impact: float, + resonance: float, + emotional_change: Dict[str, float], + interaction_type: str, + ) -> float: base_shift = theme_impact * resonance * sum(emotional_change.values()) - + # Adjust shift based on interaction type if interaction_type == "collaboration": shift = base_shift * 1.2 # Enhance shift for collaborative interactions elif interaction_type == "conflict": shift = base_shift * 0.8 # Reduce shift for conflicting interactions elif interaction_type == "inspiration": - shift = base_shift * 1.5 # Significantly enhance shift for inspirational interactions + shift = ( + base_shift * 1.5 + ) # Significantly enhance shift for inspirational interactions else: # reflection shift = base_shift # No change for reflective interactions - + self.perspective_filter += shift * (other.embedding - self.embedding) self.total_perspective_shift += abs(shift) return shift def _calculate_emotional_influence(self, other: "Story") -> float: similarity = self.field._calculate_emotional_similarity(self, other) - intensity = np.mean([getattr(self.emotional_state, e) for e in vars(self.emotional_state)]) + intensity = np.mean( + [getattr(self.emotional_state, e) for e in vars(self.emotional_state)] + ) return similarity * intensity * 0.1 - def update_emotional_state(self, interaction_impact: float, other_story: 'Story', interaction_type: str) -> float: + def update_emotional_state( + self, interaction_impact: float, other_story: "Story", interaction_type: str + ) -> float: self.previous_emotional_state = EmotionalState(**vars(self.emotional_state)) - + # Calculate theme-based emotional changes joy_change = 0.0 sadness_change = 0.0 @@ -206,7 +233,7 @@ def update_emotional_state(self, interaction_impact: float, other_story: 'Story' shared_themes = set(self.themes) & set(other_story.themes) all_themes = set(self.themes) | set(other_story.themes) - + for theme in all_themes: if theme in ["hope", "freedom", "imagination"]: joy_change += 0.05 @@ -240,21 +267,57 @@ def update_emotional_state(self, interaction_impact: float, other_story: 'Story' random_factor = 0.01 * (2 * np.random.random() - 1) # Update emotional state with amplified changes - self.emotional_state.joy = min(1.0, max(0.0, self.emotional_state.joy + (joy_change + random_factor) * amplification)) - self.emotional_state.sadness = min(1.0, max(0.0, self.emotional_state.sadness + (sadness_change + random_factor) * amplification)) - self.emotional_state.fear = min(1.0, max(0.0, self.emotional_state.fear + (fear_change + random_factor) * amplification)) - self.emotional_state.hope = min(1.0, max(0.0, self.emotional_state.hope + (hope_change + random_factor) * amplification)) - self.emotional_state.curiosity = min(1.0, max(0.0, self.emotional_state.curiosity + (curiosity_change + random_factor) * amplification)) + self.emotional_state.joy = min( + 1.0, + max( + 0.0, + self.emotional_state.joy + (joy_change + random_factor) * amplification, + ), + ) + self.emotional_state.sadness = min( + 1.0, + max( + 0.0, + self.emotional_state.sadness + + (sadness_change + random_factor) * amplification, + ), + ) + self.emotional_state.fear = min( + 1.0, + max( + 0.0, + self.emotional_state.fear + + (fear_change + random_factor) * amplification, + ), + ) + self.emotional_state.hope = min( + 1.0, + max( + 0.0, + self.emotional_state.hope + + (hope_change + random_factor) * amplification, + ), + ) + self.emotional_state.curiosity = min( + 1.0, + max( + 0.0, + self.emotional_state.curiosity + + (curiosity_change + random_factor) * amplification, + ), + ) # Calculate overall emotional change total_change = ( - abs(self.emotional_state.joy - self.previous_emotional_state.joy) + - abs(self.emotional_state.sadness - self.previous_emotional_state.sadness) + - abs(self.emotional_state.fear - self.previous_emotional_state.fear) + - abs(self.emotional_state.hope - self.previous_emotional_state.hope) + - abs(self.emotional_state.curiosity - self.previous_emotional_state.curiosity) + abs(self.emotional_state.joy - self.previous_emotional_state.joy) + + abs(self.emotional_state.sadness - self.previous_emotional_state.sadness) + + abs(self.emotional_state.fear - self.previous_emotional_state.fear) + + abs(self.emotional_state.hope - self.previous_emotional_state.hope) + + abs( + self.emotional_state.curiosity - self.previous_emotional_state.curiosity + ) ) - + return total_change def decay_emotions(self, decay_rate=0.01): @@ -272,8 +335,8 @@ def check_emotional_thresholds(self): def evolve_themes(self, interaction_history): new_themes = set() for interaction in interaction_history[-10:]: # Consider last 10 interactions - if interaction['resonance'] > 0.5: - new_themes.update(interaction['themes_gained']) + if interaction["resonance"] > 0.5: + new_themes.update(interaction["themes_gained"]) self.themes = list(set(self.themes) | new_themes)[:5] # Keep top 5 themes def calculate_interaction_strength(self, other_story): @@ -350,7 +413,7 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: filter_alignment = F.cosine_similarity( torch.tensor(story1.perspective_filter), torch.tensor(story2.perspective_filter), - dim=0 + dim=0, ) # Add emotional resonance @@ -361,26 +424,39 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: distance_factor = np.exp(-distance / self.interaction_range) return float( - (0.3 * embedding_similarity + 0.3 * theme_overlap + 0.2 * filter_alignment + 0.2 * emotional_similarity) + ( + 0.3 * embedding_similarity + + 0.3 * theme_overlap + + 0.2 * filter_alignment + + 0.2 * emotional_similarity + ) * distance_factor ) def _calculate_emotional_similarity(self, story1: Story, story2: Story) -> float: - emotions1 = np.array([ - story1.emotional_state.joy, - story1.emotional_state.sadness, - story1.emotional_state.fear, - story1.emotional_state.hope, - story1.emotional_state.curiosity - ]) - emotions2 = np.array([ - story2.emotional_state.joy, - story2.emotional_state.sadness, - story2.emotional_state.fear, - story2.emotional_state.hope, - story2.emotional_state.curiosity - ]) - return float(F.cosine_similarity(torch.tensor(emotions1).float(), torch.tensor(emotions2).float(), dim=0)) + emotions1 = np.array( + [ + story1.emotional_state.joy, + story1.emotional_state.sadness, + story1.emotional_state.fear, + story1.emotional_state.hope, + story1.emotional_state.curiosity, + ] + ) + emotions2 = np.array( + [ + story2.emotional_state.joy, + story2.emotional_state.sadness, + story2.emotional_state.fear, + story2.emotional_state.hope, + story2.emotional_state.curiosity, + ] + ) + return float( + F.cosine_similarity( + torch.tensor(emotions1).float(), torch.tensor(emotions2).float(), dim=0 + ) + ) def add_story(self, story: Story): """Add a new story to the field""" @@ -534,6 +610,7 @@ class ThemeEvolutionEngine: def __init__(self): self.logger = logging.getLogger(__name__) self.theme_resonance = {} # Track theme relationships + self.logger = logging.getLogger(__name__) def process_interaction(self, story1: Story, story2: Story, resonance: float): """Process theme interactions and evolution""" @@ -578,117 +655,51 @@ def process_interaction(self, story1: Story, story2: Story, resonance: float): class StoryInteractionEngine: - """Enhanced interaction engine with more nuanced and impactful interactions""" - def __init__(self, field: NarrativeField): self.field = field self.logger = logging.getLogger(__name__) - self.interaction_count = 0 - self.memory_threshold = 0.2 - self.theme_engine = ThemeEvolutionEngine() - self.interaction_types = [ - "collaboration", "conflict", "inspiration", "reflection" - ] - def process_interaction(self, story1: Story, story2: Story): + async def process_interaction(self, story1: Story, story2: Story): + # Basic interaction processing resonance = self.field.detect_resonance(story1, story2) - theme_analysis = self.theme_engine.process_interaction(story1, story2, resonance) - - if resonance > self.memory_threshold: - interaction_type = self._determine_interaction_type(story1, story2) - - # Update perspectives with interaction type influence - shift1 = story1.update_perspective( - story2, theme_analysis["theme_impact"], resonance, - self._calculate_emotional_change(story1), interaction_type - ) - shift2 = story2.update_perspective( - story2, theme_analysis["theme_impact"], resonance, - self._calculate_emotional_change(story2), interaction_type - ) - - self.logger.info( - f"\nInteraction Type: {interaction_type}\n" - f"Perspective Updates:\n" - f"{story1.id}: shift={shift1:.4f}, total={story1.total_perspective_shift:.4f}\n" - f"{story2.id}: shift={shift2:.4f}, total={story2.total_perspective_shift:.4f}" - ) - - # Update emotional states with interaction type influence - emotional_change1 = story1.update_emotional_state(resonance, story2, interaction_type) - emotional_change2 = story2.update_emotional_state(resonance, story1, interaction_type) - - if emotional_change1 > 0.001 or emotional_change2 > 0.001: - self.logger.info( - f"\nEmotional Updates:\n" - f"{story1.id}: change={emotional_change1:.4f}\n" - f"{story2.id}: change={emotional_change2:.4f}" - ) - - # Create memories with emotional impact and interaction type - memory1 = self._create_memory(story1, story2, resonance, theme_analysis, - shift1, emotional_change1, interaction_type) - memory2 = self._create_memory(story2, story1, resonance, theme_analysis, - shift2, emotional_change2, interaction_type) + if resonance > self.field.resonance_threshold: + # Perform basic interaction logic here + pass - # Update memory layers - story1.memory_layer.append(memory1) - story2.memory_layer.append(memory2) - - # Log rich interaction details - self.logger.info( - f"\nRich Interaction #{self.interaction_count}:\n" - f" Stories: {story1.id} <-> {story2.id}\n" - f" Interaction Type: {interaction_type}\n" - f" Direct Shared Themes: {list(theme_analysis['direct_shared'])}\n" - f" Theme Relationships Found: {theme_analysis['related_themes']}\n" - f" Theme Impact: {theme_analysis['theme_impact']:.2f}\n" - f" Emotional Impact: {emotional_change1:.2f}, {emotional_change2:.2f}" - ) - self.interaction_count += 1 +class EnhancedInteractionEngine(StoryInteractionEngine): + def __init__(self, field: NarrativeField, llm: LanguageModel): + super().__init__(field) + self.llm = llm + self.logger = logging.getLogger(__name__) - def _determine_interaction_type(self, story1: Story, story2: Story) -> str: - """Determine the type of interaction based on story properties""" - shared_themes = set(story1.themes) & set(story2.themes) - emotional_similarity = self.field._calculate_emotional_similarity(story1, story2) + async def determine_interaction_type(self, story1: Story, story2: Story) -> str: + prompt = f""" + Story 1 Themes: {', '.join(story1.themes)} + Story 1 Emotional State: {vars(story1.emotional_state)} - if len(shared_themes) > 2 and emotional_similarity > 0.7: - return "collaboration" - elif len(shared_themes) < 1 and emotional_similarity < 0.3: - return "conflict" - elif story1.total_perspective_shift > story2.total_perspective_shift * 1.5: - return "inspiration" - else: - return "reflection" - - def _create_memory(self, story: Story, other: Story, resonance: float, - theme_analysis: dict, shift: float, emotional_change: float, - interaction_type: str) -> dict: - """Create a detailed memory of the interaction""" - return { - "time": self.field.time, - "interacted_with": other.id, - "resonance": resonance, - "themes": list(other.themes), - "shared_themes": list(theme_analysis["direct_shared"]), - "theme_impact": theme_analysis["theme_impact"], - "perspective_shift": shift, - "total_shift": story.total_perspective_shift, - "interaction_id": self.interaction_count, - "emotional_impact": emotional_change, - "emotional_state_change": self._calculate_emotional_change(story), - "interaction_type": interaction_type - } + Story 2 Themes: {', '.join(story2.themes)} + Story 2 Emotional State: {vars(story2.emotional_state)} + + Based on the themes and emotional states of these two stories, what type of interaction might occur between them? + Choose from: collaboration, conflict, inspiration, reflection, transformation, challenge, or synthesis. + Provide only the interaction type as a single word response. + """ + return await self.llm.generate(prompt) + + async def process_interaction(self, story1: Story, story2: Story): + interaction_type = await self.determine_interaction_type(story1, story2) + resonance = self.field.detect_resonance(story1, story2) - def _calculate_emotional_change(self, story: Story) -> Dict[str, float]: - return { - "joy_change": story.emotional_state.joy - story.previous_emotional_state.joy, - "sadness_change": story.emotional_state.sadness - story.previous_emotional_state.sadness, - "fear_change": story.emotional_state.fear - story.previous_emotional_state.fear, - "hope_change": story.emotional_state.hope - story.previous_emotional_state.hope, - "curiosity_change": story.emotional_state.curiosity - story.previous_emotional_state.curiosity, - } + # Process the interaction based on the determined type + if resonance > self.field.resonance_threshold: + # Implement the interaction logic here + # This could include updating story states, creating memories, etc. + pass + + self.logger.info( + f"Interaction between {story1.id} and {story2.id}: {interaction_type} (Resonance: {resonance:.2f})" + ) class StoryPhysics: @@ -831,7 +842,9 @@ def apply_field_constraints(self, stories: List[Story]): story.velocity = self._limit_velocity(story.velocity) -async def create_lighthouse_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: +async def create_lighthouse_story( + llm: LanguageModel, field: NarrativeField, position: np.ndarray +) -> Story: """Create the lighthouse story with its properties""" content = """ The Lighthouse on the Cliff @@ -851,11 +864,13 @@ async def create_lighthouse_story(llm: LanguageModel, field: NarrativeField, pos velocity=np.zeros(3), themes=["loneliness", "duty", "hope", "guidance"], resonance_history=[], - field=field # Add this line + field=field, # Add this line ) -async def create_path_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: +async def create_path_story( + llm: LanguageModel, field: NarrativeField, position: np.ndarray +) -> Story: content = """ The Path Through the Forest This story is about a traveler lost in a forest, searching for a way home. @@ -873,10 +888,13 @@ async def create_path_story(llm: LanguageModel, field: NarrativeField, position: velocity=np.zeros(3), themes=["journey", "discovery", "nature"], resonance_history=[], - field=field + field=field, ) -async def create_dream_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: + +async def create_dream_story( + llm: LanguageModel, field: NarrativeField, position: np.ndarray +) -> Story: content = """ The Child's Dream of Flight This story follows a child who dreams each night of flying, soaring above @@ -894,11 +912,13 @@ async def create_dream_story(llm: LanguageModel, field: NarrativeField, position velocity=np.zeros(3), themes=["imagination", "freedom", "subconscious"], resonance_history=[], - field=field + field=field, ) -async def create_path_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: +async def create_path_story( + llm: LanguageModel, field: NarrativeField, position: np.ndarray +) -> Story: content = """ The Path Through the Forest This story is about a traveler lost in a forest, searching for a way home. @@ -916,10 +936,13 @@ async def create_path_story(llm: LanguageModel, field: NarrativeField, position: velocity=np.zeros(3), themes=["journey", "discovery", "nature"], resonance_history=[], - field=field + field=field, ) -async def create_dream_story(llm: LanguageModel, field: NarrativeField, position: np.ndarray) -> Story: + +async def create_dream_story( + llm: LanguageModel, field: NarrativeField, position: np.ndarray +) -> Story: content = """ The Child's Dream of Flight This story follows a child who dreams each night of flying, soaring above @@ -937,7 +960,7 @@ async def create_dream_story(llm: LanguageModel, field: NarrativeField, position velocity=np.zeros(3), themes=["imagination", "freedom", "subconscious"], resonance_history=[], - field=field + field=field, ) @@ -949,6 +972,7 @@ def __init__(self): self.journey_log = {} self.total_distances = {} # Track cumulative distance for each story self.significant_events = [] # Track important moments + self.logger = logging.getLogger(__name__) def log_interaction(self, story1: Story, story2: Story, resonance: float): """Log a meaningful interaction between stories""" @@ -1001,7 +1025,12 @@ def log_interaction(self, story1: Story, story2: Story, resonance: float): ) def _format_emotional_change(self, story: Story) -> str: - return ", ".join([f"{e}: {getattr(story.emotional_state, e):.2f}" for e in vars(story.emotional_state)]) + return ", ".join( + [ + f"{e}: {getattr(story.emotional_state, e):.2f}" + for e in vars(story.emotional_state) + ] + ) def log_story_state(self, story: Story, timestep: float): """Log detailed story state and track journey metrics""" @@ -1122,6 +1151,131 @@ def summarize_story_journey(story: Story): } +class DynamicThemeGenerator: + def __init__(self, llm: LanguageModel): + self.llm = llm + self.theme_cache = set() + self.logger = logging.getLogger(__name__) + + async def generate_themes(self, context: str, num_themes: int = 3) -> List[str]: + prompt = f"Given the context '{context}', generate {num_themes} unique, single-word themes that could be present in a story. Separate the themes with commas." + response = await self.llm.generate(prompt) + new_themes = [theme.strip() for theme in response.split(",")] + self.theme_cache.update(new_themes) + return new_themes + + def get_random_themes(self, num_themes: int = 3) -> List[str]: + return random.sample( + list(self.theme_cache), min(num_themes, len(self.theme_cache)) + ) + + +class DynamicStoryGenerator: + def __init__(self, llm: LanguageModel, theme_generator: DynamicThemeGenerator): + self.llm = llm + self.theme_generator = theme_generator + self.logger = logging.getLogger(__name__) + + async def generate_story(self, field: NarrativeField) -> Story: + themes = await self.theme_generator.generate_themes("Create a new story") + + prompt = f"Write a short story (2-3 sentences) incorporating the themes: {', '.join(themes)}." + content = await self.llm.generate(prompt) + + embedding = await self.llm.generate_embedding(content) + + emotional_state = await self.generate_emotional_state(content) + + return Story( + id=f"story_{len(field.stories)}", + content=content, + embedding=np.array(embedding), + perspective_filter=np.ones(len(embedding)), + position=np.random.randn(3), + velocity=np.zeros(3), + themes=themes, + emotional_state=emotional_state, + field=field, + ) + + async def generate_emotional_state(self, content: str) -> EmotionalState: + + prompt = f"""Given the story: '{content}', provide numerical values (0.0 to 1.0) for the following emotions: joy, sadness, fear, hope, curiosity. Output format as JSON object without comments. Example: + {{ + "joy": 0.1, + "sadness": 0.2, + "fear": 0.3, + "hope": 0.4, + "curiosity": 0.5 + }}""" + response = await self.llm.generate(prompt) + + try: + emotions = json.loads(response) + except json.JSONDecodeError: + self.logger.error(f"Failed to parse emotional state: {response}") + emotions = {} + + # Ensure all required emotions are present, default to 0.0 if missing + for emotion, value in emotions.items(): + if emotion in ["joy", "sadness", "fear", "hope", "curiosity"]: + if value is None: + emotions[emotion] = 0.0 + else: + self.logger.error(f"Invalid emotion: {emotion}") + del emotions[emotion] + + self.logger.info(f"Emotional state: {emotions}") + + return EmotionalState(**emotions) + + +class EnvironmentalEventGenerator: + def __init__(self, llm: LanguageModel): + self.llm = llm + self.logger = logging.getLogger(__name__) + + async def generate_event(self) -> Dict[str, Any]: + prompt = "Generate a random environmental event for a narrative field. Include an event name, description, and intensity (0.0 to 1.0)." + response = await self.llm.generate(prompt) + + event_data = { + "name": "Unknown Event", + "description": "An unexpected event occurred.", + "intensity": 0.5, + } + + for line in response.split("\n"): + if ":" in line: + key, value = line.split(":", 1) + key = key.strip().lower() + value = value.strip() + if key in ["name", "description"]: + event_data[key] = value + elif key == "intensity": + try: + event_data[key] = float(value) + except ValueError: + self.logger.warning( + f"Invalid intensity value: {value}. Using default." + ) + + return event_data + + async def apply_event(self, field: NarrativeField): + try: + event = await self.generate_event() + self.logger.info( + f"Environmental Event: {event['name']} (Intensity: {event['intensity']})" + ) + self.logger.info(f"Description: {event['description']}") + + for story in field.stories: + story.respond_to_environmental_event(event) + except Exception as e: + self.logger.error(f"Failed to generate or apply environmental event: {e}") + + async def simulate_field(): """Run a simulation of the narrative field""" # Set up logging @@ -1134,34 +1288,28 @@ async def simulate_field(): logger.info("Starting narrative field simulation") # Initialize language model - llm = OllamaInterface() # Using Ollama + llm = OllamaInterface() + theme_generator = DynamicThemeGenerator(llm) + story_generator = DynamicStoryGenerator(llm, theme_generator) + event_generator = EnvironmentalEventGenerator(llm) # Initialize simulation components field = NarrativeField() physics = StoryPhysics() visualizer = NarrativeFieldViz() - interaction_engine = StoryInteractionEngine(field) collective_engine = EnhancedCollectiveStoryEngine(field) + interaction_engine = EnhancedInteractionEngine(field, llm) - # Create stories with initial positions and pass the field reference - initial_positions = await create_story_cluster() - lighthouse = await create_lighthouse_story(llm, field, position=initial_positions[0]) - logger.info(f"Created lighthouse story: {lighthouse.id}") - path = await create_path_story(llm, field, position=initial_positions[1]) - logger.info(f"Created path story: {path.id}") - dream = await create_dream_story(llm, field, position=initial_positions[2]) - logger.info(f"Created dream story: {dream.id}") - - # Add stories to field - field.add_story(lighthouse) - field.add_story(path) - field.add_story(dream) + # Generate initial stories + for _ in range(5): + story = await story_generator.generate_story(field) + field.add_story(story) # Add journey logger journey_logger = StoryJourneyLogger() # Simulation loop - for t in range(10000): + for t in range(100): field.time = t # Update physics @@ -1169,11 +1317,24 @@ async def simulate_field(): physics.update_story_motion(story, field, t) journey_logger.log_story_state(story, t) - # Check for interactions + # Occasionally generate new stories + if t % 500 == 0 and len(field.stories) < 10: + new_story = await story_generator.generate_story(field) + field.add_story(new_story) + logger.info(f"New story added: {new_story.id}") + + # Occasionally generate environmental events + if t % 200 == 0: + await event_generator.apply_event(field) + + # Check for interactions with enhanced engine for i, story1 in enumerate(field.stories): for story2 in field.stories[i + 1 :]: - if np.linalg.norm(story1.position - story2.position) < 1.0: - interaction_engine.process_interaction(story1, story2) + if ( + np.linalg.norm(story1.position - story2.position) + < field.interaction_range + ): + await interaction_engine.process_interaction(story1, story2) # Update story states collective_engine.update_story_states() @@ -1199,7 +1360,7 @@ async def simulate_field(): for story2 in field.stories[i + 1 :]: distance = np.linalg.norm(story1.position - story2.position) if distance < field.interaction_range: - interaction_engine.process_interaction(story1, story2) + await interaction_engine.process_interaction(story1, story2) journey_logger.log_interaction( story1, story2, field.detect_resonance(story1, story2) ) @@ -1217,9 +1378,3 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) - - - - - - From 9384d6acae92cb55a2a333f9718769a61b19c029 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 13:34:29 +0200 Subject: [PATCH 09/15] analysis of the narrative field simulation run: 1. Story Generation and Interaction: The simulation created 4 stories (story_0 to story_3). Each story interacted with the other 3 stories, as evidenced by the "Unique Interactions: 3" metric for all stories. Movement and Trajectories: All stories showed significant movement, with total distances traveled ranging from 1.48 to 2.27 units. The stories exhibited complex trajectories, as indicated by the high "Wandering Ratio" values (ranging from 2.41 to 7.30). This suggests that the stories took indirect paths rather than moving in straight lines. Story_0 had the highest wandering ratio (7.30), indicating it had the most complex trajectory. Interactions and Memories: The stories formed a varying number of memories, ranging from 120 (story_1) to 301 (story_3). The high number of memories compared to unique interactions suggests that stories had multiple interactions with each other. Perspective Shifts: All stories underwent significant total perspective shifts, ranging from 15.4215 (story_1) to 79.0313 (story_3). Interestingly, the average shift magnitude and significant perspective changes are reported as 0 for all stories. This could indicate that while many small shifts occurred, they didn't meet the threshold for "significant" changes. Final Positions and Velocities: The stories ended up in different quadrants of the 3D space, suggesting they diverged over time. Final velocities are relatively small compared to the total distances traveled, indicating that the stories' movements slowed down towards the end of the simulation. 6. Story-Specific Observations: Story_0: Traveled the second-longest distance but ended up closest to its starting point, hence its high wandering ratio. Story_1: Had the fewest memories but traveled the longest distance. Story_2: Showed balanced behavior in terms of distance traveled and memories formed. Story_3: Formed the most memories and underwent the largest perspective shift, despite traveling the shortest distance. 7. Simulation Dynamics: The varying number of memories and perspective shifts for each story suggests that the stories had different levels of susceptibility to interactions and environmental influences. The high number of significant events (equal to the number of memories for each story) indicates that the simulation captured many noteworthy moments in the stories' journeys. Overall, the simulation demonstrates complex dynamics with stories showing individual behaviors while still influencing each other. The high wandering ratios and the discrepancy between total distance traveled and direct distance suggest that the narrative field effectively created intricate paths for the stories, mimicking the complex nature of narrative evolution. --- src/nfd_three_story_evolve.py | 863 ++++++++++++++++++++-------------- 1 file changed, 518 insertions(+), 345 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 1bb787c..93f9f08 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -3,7 +3,7 @@ import logging from dataclasses import dataclass, field import numpy as np -from typing import List, Dict, Optional, Any +from typing import List, Dict, Optional, Any, Tuple import torch from torch import nn import torch.nn.functional as F @@ -12,6 +12,7 @@ import time import random import json +import re # Import the logging setup function from logging_config import setup_logging @@ -144,13 +145,20 @@ def update( return shift -@dataclass class EmotionalState: - joy: float = 0.0 - sadness: float = 0.0 - fear: float = 0.0 - hope: float = 0.0 - curiosity: float = 0.0 + def __init__(self, emotions: Dict[str, float] = None): + self.emotions = emotions or {"neutral": 0.5} + + def update(self, change: float): + # Adjust all emotions based on the change + for emotion in self.emotions: + self.emotions[emotion] = max(0, min(1, self.emotions[emotion] + change)) + + def get_dominant_emotions(self, top_n: int = 3) -> List[Tuple[str, float]]: + return sorted(self.emotions.items(), key=lambda x: x[1], reverse=True)[:top_n] + + def __str__(self): + return ", ".join([f"{e}: {v:.2f}" for e, v in self.get_dominant_emotions()]) class Story: @@ -164,6 +172,7 @@ def __init__( field: "NarrativeField", # Add this line position: np.ndarray = None, velocity: np.ndarray = None, + emotional_state: EmotionalState = field(default_factory=EmotionalState), **kwargs, ): self.id = id @@ -174,8 +183,10 @@ def __init__( self.field = field # Add this line self.position = position if position is not None else np.random.randn(3) self.velocity = velocity if velocity is not None else np.zeros(3) - self.emotional_state = EmotionalState() - self.previous_emotional_state = EmotionalState() + self.emotional_state = emotional_state + self.previous_emotional_state = EmotionalState( + emotions=self.emotional_state.emotions.copy() + ) self.memory_layer = [] self.resonance_history = [] self.total_perspective_shift = 0.0 @@ -191,25 +202,28 @@ def update_perspective( other: "Story", theme_impact: float, resonance: float, - emotional_change: Dict[str, float], + emotional_change: float, interaction_type: str, ) -> float: - base_shift = theme_impact * resonance * sum(emotional_change.values()) + base_shift = ( + theme_impact + resonance + emotional_change + ) / 3 # Average instead of product # Adjust shift based on interaction type - if interaction_type == "collaboration": - shift = base_shift * 1.2 # Enhance shift for collaborative interactions - elif interaction_type == "conflict": - shift = base_shift * 0.8 # Reduce shift for conflicting interactions - elif interaction_type == "inspiration": - shift = ( - base_shift * 1.5 - ) # Significantly enhance shift for inspirational interactions - else: # reflection - shift = base_shift # No change for reflective interactions + shift_multipliers = { + "collaboration": 1.2, + "conflict": 0.8, + "inspiration": 1.5, + "reflection": 1.0, + } + shift = base_shift * shift_multipliers.get(interaction_type, 1.0) + + # Ensure a minimum shift + min_shift = 0.01 + shift = max(shift, min_shift) self.perspective_filter += shift * (other.embedding - self.embedding) - self.total_perspective_shift += abs(shift) + self.total_perspective_shift += shift return shift def _calculate_emotional_influence(self, other: "Story") -> float: @@ -220,105 +234,24 @@ def _calculate_emotional_influence(self, other: "Story") -> float: return similarity * intensity * 0.1 def update_emotional_state( - self, interaction_impact: float, other_story: "Story", interaction_type: str + self, other: "Story", interaction_type: str, resonance: float ) -> float: - self.previous_emotional_state = EmotionalState(**vars(self.emotional_state)) - - # Calculate theme-based emotional changes - joy_change = 0.0 - sadness_change = 0.0 - fear_change = 0.0 - hope_change = 0.0 - curiosity_change = 0.0 - - shared_themes = set(self.themes) & set(other_story.themes) - all_themes = set(self.themes) | set(other_story.themes) - - for theme in all_themes: - if theme in ["hope", "freedom", "imagination"]: - joy_change += 0.05 - hope_change += 0.1 - elif theme in ["loneliness", "duty"]: - sadness_change += 0.05 - elif theme in ["journey", "discovery"]: - curiosity_change += 0.1 - fear_change += 0.02 # A little fear in the unknown - elif theme in ["nature", "guidance"]: - hope_change += 0.05 - fear_change -= 0.02 # Nature and guidance reduce fear slightly - - # Adjust emotional changes based on interaction type - if interaction_type == "collaboration": - joy_change *= 1.2 - hope_change *= 1.2 - elif interaction_type == "conflict": - sadness_change *= 1.2 - fear_change *= 1.2 - elif interaction_type == "inspiration": - curiosity_change *= 1.5 - hope_change *= 1.3 - # No changes for "reflection" type - - # Apply resonance-based amplification - resonance = self.field.detect_resonance(self, other_story) - amplification = 1 + resonance - - # Introduce a small random factor for variability - random_factor = 0.01 * (2 * np.random.random() - 1) - - # Update emotional state with amplified changes - self.emotional_state.joy = min( - 1.0, - max( - 0.0, - self.emotional_state.joy + (joy_change + random_factor) * amplification, - ), - ) - self.emotional_state.sadness = min( - 1.0, - max( - 0.0, - self.emotional_state.sadness - + (sadness_change + random_factor) * amplification, - ), - ) - self.emotional_state.fear = min( - 1.0, - max( - 0.0, - self.emotional_state.fear - + (fear_change + random_factor) * amplification, - ), - ) - self.emotional_state.hope = min( - 1.0, - max( - 0.0, - self.emotional_state.hope - + (hope_change + random_factor) * amplification, - ), - ) - self.emotional_state.curiosity = min( - 1.0, - max( - 0.0, - self.emotional_state.curiosity - + (curiosity_change + random_factor) * amplification, - ), - ) + # Simple emotional change calculation + base_change = resonance * 0.5 # Scale the change based on resonance + + # Adjust change based on interaction type + change_multipliers = { + "collaboration": 1.2, + "conflict": 0.8, + "inspiration": 1.5, + "reflection": 1.0, + } + emotional_change = base_change * change_multipliers.get(interaction_type, 1.0) - # Calculate overall emotional change - total_change = ( - abs(self.emotional_state.joy - self.previous_emotional_state.joy) - + abs(self.emotional_state.sadness - self.previous_emotional_state.sadness) - + abs(self.emotional_state.fear - self.previous_emotional_state.fear) - + abs(self.emotional_state.hope - self.previous_emotional_state.hope) - + abs( - self.emotional_state.curiosity - self.previous_emotional_state.curiosity - ) - ) + # Update emotional state + self.emotional_state.update(emotional_change) - return total_change + return emotional_change def decay_emotions(self, decay_rate=0.01): for emotion in vars(self.emotional_state): @@ -345,6 +278,31 @@ def calculate_interaction_strength(self, other_story): familiarity_bonus = min(0.2, len(interaction_history) * 0.02) return base_strength + familiarity_bonus + def respond_to_environmental_event(self, event: Dict[str, Any]): + # Simple response to environmental events + intensity = event.get("intensity", 0.5) + + # Update emotional state based on event intensity + self.emotional_state.update(intensity * 0.2) + + # Add a memory of the event + memory = { + "type": "environmental_event", + "event_name": event.get("name", "Unknown Event"), + "intensity": intensity, + "timestamp": self.field.time, + } + self.memory_layer.append(memory) + + def update_state(self, avg_resonance: float, avg_shift: float): + # Update the story's state based on recent interactions + self.emotional_state.update( + avg_resonance * 0.1 + ) # Small emotional update based on average resonance + self.perspective_filter += ( + avg_shift * 0.1 + ) # Small perspective update based on average shift + class NarrativeFieldViz: """Handles visualization of field state""" @@ -392,7 +350,7 @@ def __init__(self, dimension: int = 1024): self.time = 0.0 # Adjusted thresholds - self.resonance_threshold = 0.3 # Lower threshold + self.resonance_threshold = 0.2 # Lower threshold to allow more interactions self.interaction_range = 3.0 # Increased range self.field_potential = np.zeros(dimension) self.logger = logging.getLogger(__name__) @@ -434,29 +392,19 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: ) def _calculate_emotional_similarity(self, story1: Story, story2: Story) -> float: - emotions1 = np.array( - [ - story1.emotional_state.joy, - story1.emotional_state.sadness, - story1.emotional_state.fear, - story1.emotional_state.hope, - story1.emotional_state.curiosity, - ] - ) - emotions2 = np.array( - [ - story2.emotional_state.joy, - story2.emotional_state.sadness, - story2.emotional_state.fear, - story2.emotional_state.hope, - story2.emotional_state.curiosity, - ] - ) - return float( - F.cosine_similarity( - torch.tensor(emotions1).float(), torch.tensor(emotions2).float(), dim=0 - ) - ) + emotions1 = set(story1.emotional_state.emotions.keys()) + emotions2 = set(story2.emotional_state.emotions.keys()) + shared_emotions = emotions1 & emotions2 + + if not shared_emotions: + return 0.0 + + similarity = sum( + min(story1.emotional_state.emotions[e], story2.emotional_state.emotions[e]) + for e in shared_emotions + ) / len(shared_emotions) + + return similarity def add_story(self, story: Story): """Add a new story to the field""" @@ -552,19 +500,18 @@ def __init__(self, field: NarrativeField): self.logger = logging.getLogger(__name__) def update_story_states(self): - """Update state tracking for all stories""" for story in self.field.stories: - if story.id not in self.story_states: - self.story_states[story.id] = StoryState() - - # Update based on recent interactions - recent_memories = story.memory_layer[-5:] # Look at last 5 interactions + recent_memories = story.memory_layer[-5:] # Get the 5 most recent memories if recent_memories: - avg_resonance = np.mean([m["resonance"] for m in recent_memories]) - recent_themes = [ - theme for m in recent_memories for theme in m["themes"] - ] - self.story_states[story.id].update(avg_resonance, recent_themes) + avg_resonance = np.mean( + [m.get("resonance", 0) for m in recent_memories] + ) + avg_shift = np.mean( + [m.get("perspective_shift", 0) for m in recent_memories] + ) + + # Update story state based on recent interactions + story.update_state(avg_resonance, avg_shift) def detect_emergent_themes(self) -> List[str]: """Detect themes that are becoming more prominent""" @@ -683,7 +630,8 @@ async def determine_interaction_type(self, story1: Story, story2: Story) -> str: Based on the themes and emotional states of these two stories, what type of interaction might occur between them? Choose from: collaboration, conflict, inspiration, reflection, transformation, challenge, or synthesis. - Provide only the interaction type as a single word response. + + Respond only with one chosen interaction type as a SINGLE WORD response. """ return await self.llm.generate(prompt) @@ -691,15 +639,51 @@ async def process_interaction(self, story1: Story, story2: Story): interaction_type = await self.determine_interaction_type(story1, story2) resonance = self.field.detect_resonance(story1, story2) - # Process the interaction based on the determined type if resonance > self.field.resonance_threshold: - # Implement the interaction logic here - # This could include updating story states, creating memories, etc. - pass + # Calculate theme impact and emotional change + theme_impact = self.calculate_theme_impact(story1, story2) + emotional_change1 = story1.update_emotional_state( + story2, interaction_type, resonance + ) + emotional_change2 = story2.update_emotional_state( + story1, interaction_type, resonance + ) - self.logger.info( - f"Interaction between {story1.id} and {story2.id}: {interaction_type} (Resonance: {resonance:.2f})" - ) + # Update perspectives + shift1 = story1.update_perspective( + story2, theme_impact, resonance, emotional_change1, interaction_type + ) + shift2 = story2.update_perspective( + story1, theme_impact, resonance, emotional_change2, interaction_type + ) + + # Create memories for both stories + self.create_memory(story1, story2, resonance, shift1, interaction_type) + self.create_memory(story2, story1, resonance, shift2, interaction_type) + + return resonance, interaction_type + return 0, "no_interaction" + + def calculate_theme_impact(self, story1: Story, story2: Story) -> float: + shared_themes = set(story1.themes) & set(story2.themes) + return len(shared_themes) / max(len(story1.themes), len(story2.themes)) + + def create_memory( + self, + story: Story, + other: Story, + resonance: float, + shift: float, + interaction_type: str, + ): + memory = { + "interacted_with": other.id, + "resonance": resonance, + "perspective_shift": shift, + "interaction_type": interaction_type, + "timestamp": self.field.time, + } + story.memory_layer.append(memory) class StoryPhysics: @@ -842,127 +826,313 @@ def apply_field_constraints(self, stories: List[Story]): story.velocity = self._limit_velocity(story.velocity) -async def create_lighthouse_story( - llm: LanguageModel, field: NarrativeField, position: np.ndarray -) -> Story: - """Create the lighthouse story with its properties""" - content = """ - The Lighthouse on the Cliff - This is the story of a lighthouse keeper who lights the beacon every night, - waiting for a ship that never comes. It contains themes of loneliness, duty, - and anticipation, with a constant longing for connection across the vast ocean. - """ - embedding = await llm.generate_embedding(content) - - return Story( - id="lighthouse", - content=content, - embedding=np.array(embedding), - memory_layer=[], - perspective_filter=np.ones(len(embedding)), - position=position, - velocity=np.zeros(3), - themes=["loneliness", "duty", "hope", "guidance"], - resonance_history=[], - field=field, # Add this line - ) +class StoryJourneyLogger: + """Tracks and logs the journey of stories through the narrative field""" + def __init__(self): + self.logger = logging.getLogger(__name__) + self.journey_log = {} + self.total_distances = {} # Track cumulative distance for each story + self.significant_events = [] # Track important moments + self.logger = logging.getLogger(__name__) -async def create_path_story( - llm: LanguageModel, field: NarrativeField, position: np.ndarray -) -> Story: - content = """ - The Path Through the Forest - This story is about a traveler lost in a forest, searching for a way home. - It holds elements of uncertainty, hope, and resilience as the traveler - navigates unfamiliar terrain, feeling both wonder and isolation. - """ - embedding = await llm.generate_embedding(content) - return Story( - id="path", - content=content, - embedding=np.array(embedding), - memory_layer=[], - perspective_filter=np.ones(len(embedding)), - position=position, - velocity=np.zeros(3), - themes=["journey", "discovery", "nature"], - resonance_history=[], - field=field, - ) + def log_interaction(self, story1: Story, story2: Story, resonance: float): + latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} + self.logger.info( + f"\nSignificant Interaction:\n" + f" {story1.id} <-> {story2.id}\n" + f" Resonance: {resonance:.2f}\n" + f" Shared Themes: {set(story1.themes) & set(story2.themes)}\n" + f" Distance: {np.linalg.norm(story1.position - story2.position):.2f}\n" + f" Positions:\n" + f" {story1.id}: {story1.position}\n" + f" {story2.id}: {story2.position}\n" + ) -async def create_dream_story( - llm: LanguageModel, field: NarrativeField, position: np.ndarray -) -> Story: - content = """ - The Child's Dream of Flight - This story follows a child who dreams each night of flying, soaring above - villages, forests, and oceans. The dream is filled with freedom, innocence, - and limitless possibility, untouched by fear or doubt. - """ - embedding = await llm.generate_embedding(content) - return Story( - id="dream", - content=content, - embedding=np.array(embedding), - memory_layer=[], - perspective_filter=np.ones(len(embedding)), - position=position, - velocity=np.zeros(3), - themes=["imagination", "freedom", "subconscious"], - resonance_history=[], - field=field, - ) + if latest_memory: + self.logger.info( + f"\nInteraction Details:\n" + f" Interaction Type: {latest_memory.get('interaction_type', 'Unknown')}\n" + f" Perspective Shift: {latest_memory.get('perspective_shift', 0):.4f}\n" + ) + self.logger.info( + f"\nEmotional Impact:\n" + f" {story1.id} Emotional State: {story1.emotional_state}\n" + f" {story2.id} Emotional State: {story2.emotional_state}\n" + ) -async def create_path_story( - llm: LanguageModel, field: NarrativeField, position: np.ndarray -) -> Story: - content = """ - The Path Through the Forest - This story is about a traveler lost in a forest, searching for a way home. - It holds elements of uncertainty, hope, and resilience as the traveler - navigates unfamiliar terrain, feeling both wonder and isolation. - """ - embedding = await llm.generate_embedding(content) - return Story( - id="path", - content=content, - embedding=np.array(embedding), - memory_layer=[], - perspective_filter=np.ones(len(embedding)), - position=position, - velocity=np.zeros(3), - themes=["journey", "discovery", "nature"], - resonance_history=[], - field=field, - ) + def _format_emotional_change(self, story: Story) -> str: + return ", ".join( + [f"{e}: {v:.2f}" for e, v in story.emotional_state.get_dominant_emotions()] + ) + def log_story_state(self, story: Story, timestep: float): + """Log detailed story state and track journey metrics""" + if story.id not in self.journey_log: + self.journey_log[story.id] = [] + self.total_distances[story.id] = 0.0 + + # Calculate movement since last state + if self.journey_log[story.id]: + last_pos = self.journey_log[story.id][-1]["position"] + movement = np.linalg.norm(story.position - last_pos) + self.total_distances[story.id] += movement + + # Log significant movements + if movement > 0.5: # Threshold for significant movement + self.significant_events.append( + { + "type": "movement", + "time": timestep, + "story_id": story.id, + "distance": movement, + "direction": story.velocity + / (np.linalg.norm(story.velocity) + 1e-6), + } + ) + + # Store current state + state = { + "timestep": timestep, + "position": story.position.copy(), + "velocity": story.velocity.copy(), + "memory_count": len(story.memory_layer), + "perspective_sum": story.perspective_filter.sum(), + "total_distance": self.total_distances[story.id], + } + self.journey_log[story.id].append(state) + + def summarize_journey(self, story: Story): + """Enhanced journey summary with accumulated perspective shifts""" + journey = self.journey_log.get(story.id, []) + if not journey: + return + + start_state = journey[0] + end_state = journey[-1] -async def create_dream_story( - llm: LanguageModel, field: NarrativeField, position: np.ndarray -) -> Story: - content = """ - The Child's Dream of Flight - This story follows a child who dreams each night of flying, soaring above - villages, forests, and oceans. The dream is filled with freedom, innocence, - and limitless possibility, untouched by fear or doubt. - """ - embedding = await llm.generate_embedding(content) - return Story( - id="dream", - content=content, - embedding=np.array(embedding), - memory_layer=[], - perspective_filter=np.ones(len(embedding)), - position=position, - velocity=np.zeros(3), - themes=["imagination", "freedom", "subconscious"], - resonance_history=[], - field=field, + # Calculate metrics + total_distance = self.total_distances[story.id] + direct_distance = np.linalg.norm( + end_state["position"] - start_state["position"] + ) + wandering_ratio = total_distance / (direct_distance + 1e-6) + + # Perspective analysis + significant_shifts = [ + s for s in story.perspective_shifts if s["magnitude"] > 0.01 + ] + avg_shift = ( + np.mean([s["magnitude"] for s in significant_shifts]) + if significant_shifts + else 0 + ) + + self.logger.info( + f"\n=== Journey Summary for {story.id} ===\n" + f"Movement Metrics:\n" + f" Total Distance Traveled: {total_distance:.2f}\n" + f" Direct Distance (start to end): {direct_distance:.2f}\n" + f" Wandering Ratio: {wandering_ratio:.2f}\n" + f"\nInteraction Metrics:\n" + f" Memories Formed: {len(story.memory_layer)}\n" + f" Unique Interactions: {len(set(m['interacted_with'] for m in story.memory_layer))}\n" + f" Total Perspective Shift: {story.total_perspective_shift:.4f}\n" + f" Average Shift Magnitude: {avg_shift:.4f}\n" + f" Significant Perspective Changes: {len(significant_shifts)}\n" + f"\nFinal State:\n" + f" Position: {end_state['position']}\n" + f" Velocity: {end_state['velocity']}\n" + f"\nSignificant Events: {len(story.memory_layer)}" + ) + + +async def create_story_cluster(): + """Create initial story positions in a balanced configuration""" + # Position stories in a triangle with some random offset + base_positions = np.array( + [[1.0, 0.0, 0.0], [-0.5, 0.866, 0.0], [-0.5, -0.866, 0.0]] ) + # Add random offset to make it interesting + return base_positions + np.random.randn(3, 3) * 0.2 + + +def summarize_story_journey(story: Story): + """Enhanced journey summary with perspective analysis""" + theme_counts = {} + for memory in story.memory_layer: + for theme in memory["themes"]: + theme_counts[theme] = theme_counts.get(theme, 0) + 1 + + most_influential_themes = sorted( + story.perspective.theme_influences.items(), key=lambda x: x[1], reverse=True + )[:3] + + total_perspective_shift = story.perspective.total_shift + + return { + "total_memories": len(story.memory_layer), + "unique_interactions": len( + set(m["interacted_with"] for m in story.memory_layer) + ), + "theme_exposure": theme_counts, + "total_perspective_shift": total_perspective_shift, + "most_influential_themes": most_influential_themes, + "perspective_shifts": len( + [s for s in story.perspective.shift_history if s["magnitude"] > 0.01] + ), + } + + +class DynamicThemeGenerator: + def __init__(self, llm: LanguageModel): + self.llm = llm + self.theme_cache = set() + self.logger = logging.getLogger(__name__) + + async def generate_themes(self, context: str, num_themes: int = 3) -> List[str]: + prompt = f"""Given the context '{context}', generate {num_themes} unique, single-word themes that could be present in a story. Output ONLY a valid JSON array of strings, nothing else. Example: + ["hope", "journey", "transformation"] + """ + response = await self.llm.generate(prompt) + + try: + # Extract JSON array from the response + json_match = re.search(r"\[.*\]", response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + new_themes = json.loads(json_str) + else: + raise ValueError("No JSON array found in response") + + # Ensure we have the correct number of themes + new_themes = [theme.lower() for theme in new_themes[:num_themes]] + while len(new_themes) < num_themes: + new_themes.append(f"theme_{len(new_themes) + 1}") + + self.theme_cache.update(new_themes) + return new_themes + except json.JSONDecodeError: + self.logger.error(f"Failed to parse themes JSON: {response}") + return [f"theme_{i+1}" for i in range(num_themes)] + except Exception as e: + self.logger.error(f"Error generating themes: {e}") + return [f"theme_{i+1}" for i in range(num_themes)] + + def get_random_themes(self, num_themes: int = 3) -> List[str]: + return random.sample( + list(self.theme_cache), min(num_themes, len(self.theme_cache)) + ) + + +class DynamicStoryGenerator: + def __init__(self, llm: LanguageModel, theme_generator: DynamicThemeGenerator): + self.llm = llm + self.theme_generator = theme_generator + self.logger = logging.getLogger(__name__) + + async def generate_story(self, field: NarrativeField) -> Story: + themes = await self.theme_generator.generate_themes("Create a new story") + + prompt = f"Write a short story (2-3 sentences) incorporating the themes: {', '.join(themes)}." + content = await self.llm.generate(prompt) + + embedding = await self.llm.generate_embedding(content) + + emotional_state = await self.generate_emotional_state(content) + + return Story( + id=f"story_{len(field.stories)}", + content=content, + embedding=np.array(embedding), + perspective_filter=np.ones(len(embedding)), + position=np.random.randn(3), + velocity=np.zeros(3), + themes=themes, + emotional_state=emotional_state, + field=field, + ) + + async def generate_emotional_state(self, content: str) -> EmotionalState: + prompt = f"""Given the story: '{content}', provide 3-5 emotions that best describe the overall mood or atmosphere. For each emotion, assign a numerical value (0.0 to 1.0) indicating its intensity. Output ONLY a valid JSON object, nothing else. Example: + {{ + "curiosity": 0.8, + "melancholy": 0.5, + "hope": 0.3 + }} + """ + response = await self.llm.generate(prompt) + + # Extract JSON object from the response and ensure it's properly closed + json_match = re.search(r"\{[^{]*", response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + # Add closing brace if missing + if not json_str.strip().endswith("}"): + json_str += "}" + try: + emotions = json.loads(json_str) + except json.JSONDecodeError: + self.logger.error(f"Failed to parse extracted JSON: {json_str}") + emotions = {"neutral": 0.5} # Fallback to a default emotion + else: + self.logger.error(f"No JSON object found in response: {response}") + emotions = {"neutral": 0.5} # Fallback to a default emotion + + # Ensure all values are floats between 0 and 1 + emotions = {k: min(max(float(v), 0.0), 1.0) for k, v in emotions.items()} + + return EmotionalState(emotions=emotions) + + +class EnvironmentalEventGenerator: + def __init__(self, llm: LanguageModel): + self.llm = llm + self.logger = logging.getLogger(__name__) + + async def generate_event(self) -> Dict[str, Any]: + prompt = "Generate a random environmental event for a narrative field. Include an event name, description, and intensity (0.0 to 1.0)." + response = await self.llm.generate(prompt) + + event_data = { + "name": "Unknown Event", + "description": "An unexpected event occurred.", + "intensity": 0.5, + } + + for line in response.split("\n"): + if ":" in line: + key, value = line.split(":", 1) + key = key.strip().lower() + value = value.strip() + if key in ["name", "description"]: + event_data[key] = value + elif key == "intensity": + try: + event_data[key] = float(value) + except ValueError: + self.logger.warning( + f"Invalid intensity value: {value}. Using default." + ) + + return event_data + + async def apply_event(self, field: NarrativeField): + try: + event = await self.generate_event() + self.logger.info( + f"Environmental Event: {event['name']} (Intensity: {event['intensity']})" + ) + self.logger.info(f"Description: {event['description']}") + + for story in field.stories: + story.respond_to_environmental_event(event) + except Exception as e: + self.logger.error(f"Failed to generate or apply environmental event: {e}") + class StoryJourneyLogger: """Tracks and logs the journey of stories through the narrative field""" @@ -975,61 +1145,35 @@ def __init__(self): self.logger = logging.getLogger(__name__) def log_interaction(self, story1: Story, story2: Story, resonance: float): - """Log a meaningful interaction between stories""" - shared_themes = set(story1.themes) & set(story2.themes) - distance = np.linalg.norm(story1.position - story2.position) - - # Log immediate interaction details - self.significant_events.append( - { - "type": "interaction", - "time": time.time(), - "stories": (story1.id, story2.id), - "resonance": resonance, - "shared_themes": list(shared_themes), - "distance": distance, - "positions": { - story1.id: story1.position.copy(), - story2.id: story2.position.copy(), - }, - } - ) + latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} self.logger.info( f"\nSignificant Interaction:\n" f" {story1.id} <-> {story2.id}\n" f" Resonance: {resonance:.2f}\n" - f" Shared Themes: {list(shared_themes)}\n" - f" Distance: {distance:.2f}\n" + f" Shared Themes: {set(story1.themes) & set(story2.themes)}\n" + f" Distance: {np.linalg.norm(story1.position - story2.position):.2f}\n" f" Positions:\n" f" {story1.id}: {story1.position}\n" f" {story2.id}: {story2.position}\n" ) - # Track memory formation for both stories - for story in [story1, story2]: - if story.memory_layer: - latest_memory = story.memory_layer[-1] - self.logger.info( - f"\nMemory Formed - {story.id}:\n" - f" Interaction with: {latest_memory['interacted_with']}\n" - f" Resonance Level: {latest_memory['resonance']:.2f}\n" - f" Themes Gained: {latest_memory['themes']}\n" - f" Shared Themes: {latest_memory.get('shared_themes', [])}\n" - ) + if latest_memory: + self.logger.info( + f"\nInteraction Details:\n" + f" Interaction Type: {latest_memory.get('interaction_type', 'Unknown')}\n" + f" Perspective Shift: {latest_memory.get('perspective_shift', 0):.4f}\n" + ) self.logger.info( f"\nEmotional Impact:\n" - f" {story1.id} Emotional Change: {self._format_emotional_change(story1)}\n" - f" {story2.id} Emotional Change: {self._format_emotional_change(story2)}\n" + f" {story1.id} Emotional State: {story1.emotional_state}\n" + f" {story2.id} Emotional State: {story2.emotional_state}\n" ) def _format_emotional_change(self, story: Story) -> str: return ", ".join( - [ - f"{e}: {getattr(story.emotional_state, e):.2f}" - for e in vars(story.emotional_state) - ] + [f"{e}: {v:.2f}" for e, v in story.emotional_state.get_dominant_emotions()] ) def log_story_state(self, story: Story, timestep: float): @@ -1094,6 +1238,12 @@ def summarize_journey(self, story: Story): else 0 ) + # Safely get unique interactions + unique_interactions = set( + m["interacted_with"] for m in story.memory_layer if "interacted_with" in m + ) + num_unique_interactions = len(unique_interactions) + self.logger.info( f"\n=== Journey Summary for {story.id} ===\n" f"Movement Metrics:\n" @@ -1102,7 +1252,7 @@ def summarize_journey(self, story: Story): f" Wandering Ratio: {wandering_ratio:.2f}\n" f"\nInteraction Metrics:\n" f" Memories Formed: {len(story.memory_layer)}\n" - f" Unique Interactions: {len(set(m['interacted_with'] for m in story.memory_layer))}\n" + f" Unique Interactions: {num_unique_interactions}\n" f" Total Perspective Shift: {story.total_perspective_shift:.4f}\n" f" Average Shift Magnitude: {avg_shift:.4f}\n" f" Significant Perspective Changes: {len(significant_shifts)}\n" @@ -1158,11 +1308,33 @@ def __init__(self, llm: LanguageModel): self.logger = logging.getLogger(__name__) async def generate_themes(self, context: str, num_themes: int = 3) -> List[str]: - prompt = f"Given the context '{context}', generate {num_themes} unique, single-word themes that could be present in a story. Separate the themes with commas." + prompt = f"""Given the context '{context}', generate {num_themes} unique, single-word themes that could be present in a story. Output ONLY a valid JSON array of strings, nothing else. Example: + ["hope", "journey", "transformation"] + """ response = await self.llm.generate(prompt) - new_themes = [theme.strip() for theme in response.split(",")] - self.theme_cache.update(new_themes) - return new_themes + + try: + # Extract JSON array from the response + json_match = re.search(r"\[.*\]", response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + new_themes = json.loads(json_str) + else: + raise ValueError("No JSON array found in response") + + # Ensure we have the correct number of themes + new_themes = [theme.lower() for theme in new_themes[:num_themes]] + while len(new_themes) < num_themes: + new_themes.append(f"theme_{len(new_themes) + 1}") + + self.theme_cache.update(new_themes) + return new_themes + except json.JSONDecodeError: + self.logger.error(f"Failed to parse themes JSON: {response}") + return [f"theme_{i+1}" for i in range(num_themes)] + except Exception as e: + self.logger.error(f"Error generating themes: {e}") + return [f"theme_{i+1}" for i in range(num_themes)] def get_random_themes(self, num_themes: int = 3) -> List[str]: return random.sample( @@ -1199,35 +1371,36 @@ async def generate_story(self, field: NarrativeField) -> Story: ) async def generate_emotional_state(self, content: str) -> EmotionalState: - - prompt = f"""Given the story: '{content}', provide numerical values (0.0 to 1.0) for the following emotions: joy, sadness, fear, hope, curiosity. Output format as JSON object without comments. Example: + prompt = f"""Given the story: '{content}', provide 3-5 emotions that best describe the overall mood or atmosphere. For each emotion, assign a numerical value (0.0 to 1.0) indicating its intensity. Output ONLY a valid JSON object, nothing else. Example: {{ - "joy": 0.1, - "sadness": 0.2, - "fear": 0.3, - "hope": 0.4, - "curiosity": 0.5 - }}""" + "curiosity": 0.8, + "melancholy": 0.5, + "hope": 0.3 + }} + """ response = await self.llm.generate(prompt) - - try: - emotions = json.loads(response) - except json.JSONDecodeError: - self.logger.error(f"Failed to parse emotional state: {response}") - emotions = {} - - # Ensure all required emotions are present, default to 0.0 if missing - for emotion, value in emotions.items(): - if emotion in ["joy", "sadness", "fear", "hope", "curiosity"]: - if value is None: - emotions[emotion] = 0.0 - else: - self.logger.error(f"Invalid emotion: {emotion}") - del emotions[emotion] - - self.logger.info(f"Emotional state: {emotions}") - - return EmotionalState(**emotions) + response = response.strip().lower() + + # Extract JSON object from the response and ensure it's properly closed + json_match = re.search(r"\{[^{]*", response, re.DOTALL) + if json_match: + json_str = json_match.group(0) + # Add closing brace if missing + if not json_str.strip().endswith("}"): + json_str += "}" + try: + emotions = json.loads(json_str) + except json.JSONDecodeError: + self.logger.error(f"Failed to parse extracted JSON: {json_str}") + emotions = {"neutral": 0.5} # Fallback to a default emotion + else: + self.logger.error(f"No JSON object found in response: {response}") + emotions = {"neutral": 0.5} # Fallback to a default emotion + + # Ensure all values are floats between 0 and 1 + emotions = {k: min(max(float(v), 0.0), 1.0) for k, v in emotions.items()} + + return EmotionalState(emotions=emotions) class EnvironmentalEventGenerator: @@ -1301,7 +1474,7 @@ async def simulate_field(): interaction_engine = EnhancedInteractionEngine(field, llm) # Generate initial stories - for _ in range(5): + for _ in range(2): story = await story_generator.generate_story(field) field.add_story(story) @@ -1318,13 +1491,13 @@ async def simulate_field(): journey_logger.log_story_state(story, t) # Occasionally generate new stories - if t % 500 == 0 and len(field.stories) < 10: + if t % 50 == 0 and len(field.stories) < 10: new_story = await story_generator.generate_story(field) field.add_story(new_story) logger.info(f"New story added: {new_story.id}") # Occasionally generate environmental events - if t % 200 == 0: + if t % 40 == 0: await event_generator.apply_event(field) # Check for interactions with enhanced engine From d9878e1cb2a0a9eab3123127afecada503d64d0f Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 14:49:34 +0200 Subject: [PATCH 10/15] Refactor story interactions and emotional states - Update EmotionalState to use embeddings instead of discrete emotions - Modify Story class to handle asynchronous updates and interactions - Enhance EnhancedInteractionEngine for more nuanced story interactions - Update DynamicStoryGenerator to create more detailed stories with protagonists - Improve EnvironmentalEventGenerator for positive/negative events - Refactor StoryJourneyLogger and add new JourneyLogger for better logging - Update simulate_field function to handle asynchronous operations and improved logging - Various code cleanup and optimization throughout the simulation --- src/config.py | 2 +- src/nfd_three_story_evolve.py | 698 ++++++++++++---------------------- 2 files changed, 240 insertions(+), 460 deletions(-) diff --git a/src/config.py b/src/config.py index 73c8678..f1a9b16 100644 --- a/src/config.py +++ b/src/config.py @@ -21,7 +21,7 @@ "Mistral-Nemo-Instruct-2407-GGUF/" "Mistral-Nemo-Instruct-2407-Q4_K_M.gguf" ).expanduser(), - "model_name": "llama3.2:latest", # "mistral-nemo:latest", + "model_name": "mistral-nemo:latest", }, "embedding": { "path": Path( diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 93f9f08..0833d62 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -146,19 +146,45 @@ def update( class EmotionalState: - def __init__(self, emotions: Dict[str, float] = None): - self.emotions = emotions or {"neutral": 0.5} + def __init__(self, description: str, embedding: np.ndarray): + self.description = description + self.embedding = embedding + + async def update( + self, + other: Optional["EmotionalState"] = None, + interaction_strength: float = 0.1, + llm: Optional[LanguageModel] = None, + ): + if other is None or llm is None: + # Simple update without interaction + self.embedding += np.random.normal( + 0, interaction_strength, self.embedding.shape + ) + return + + # Calculate the distance between emotional states + distance = np.linalg.norm(self.embedding - other.embedding) + + # Generate a new emotional state description + prompt = f""" + Current emotional state: {self.description} + Interacting emotional state: {other.description} + Interaction strength: {interaction_strength} + Distance between states: {distance} + + Based on this interaction, describe the new positive, neutral, or negative emotional state in 1 sentence: + """ + new_description = await llm.generate(prompt) - def update(self, change: float): - # Adjust all emotions based on the change - for emotion in self.emotions: - self.emotions[emotion] = max(0, min(1, self.emotions[emotion] + change)) + # Generate a new embedding for the updated emotional state + new_embedding = await llm.generate_embedding(new_description) - def get_dominant_emotions(self, top_n: int = 3) -> List[Tuple[str, float]]: - return sorted(self.emotions.items(), key=lambda x: x[1], reverse=True)[:top_n] + self.description = new_description + self.embedding = np.array(new_embedding) def __str__(self): - return ", ".join([f"{e}: {v:.2f}" for e, v in self.get_dominant_emotions()]) + return self.description class Story: @@ -169,10 +195,11 @@ def __init__( embedding: np.ndarray, perspective_filter: np.ndarray, themes: List[str], - field: "NarrativeField", # Add this line + field: "NarrativeField", position: np.ndarray = None, velocity: np.ndarray = None, - emotional_state: EmotionalState = field(default_factory=EmotionalState), + emotional_state: EmotionalState = None, + protagonist_name: str = None, **kwargs, ): self.id = id @@ -180,51 +207,43 @@ def __init__( self.embedding = embedding self.perspective_filter = perspective_filter self.themes = themes - self.field = field # Add this line + self.field = field self.position = position if position is not None else np.random.randn(3) self.velocity = velocity if velocity is not None else np.zeros(3) self.emotional_state = emotional_state - self.previous_emotional_state = EmotionalState( - emotions=self.emotional_state.emotions.copy() - ) + self.previous_emotional_state = None self.memory_layer = [] self.resonance_history = [] self.total_perspective_shift = 0.0 self.perspective_shifts = [] + self.protagonist_name = protagonist_name - # Initialize position and velocity - self.position = position if position is not None else np.random.randn(3) - self.velocity = velocity if velocity is not None else np.zeros(3) self.logger = logging.getLogger(__name__) - def update_perspective( + def __str__(self): + return f"Story {self.id} ({self.protagonist_name}): {self.content[:50]}..." + + async def update_perspective( self, - other: "Story", + other_story: 'Story', theme_impact: float, resonance: float, emotional_change: float, - interaction_type: str, + interaction_type: str ) -> float: - base_shift = ( - theme_impact + resonance + emotional_change - ) / 3 # Average instead of product + # Calculate perspective shift based on interaction factors + shift = theme_impact * resonance * emotional_change * 0.1 # Adjust shift based on interaction type - shift_multipliers = { - "collaboration": 1.2, - "conflict": 0.8, - "inspiration": 1.5, - "reflection": 1.0, - } - shift = base_shift * shift_multipliers.get(interaction_type, 1.0) + if interaction_type == "conflict": + shift *= -1 # Negative shift for conflicting interactions + elif interaction_type == "collaboration": + shift *= 1.5 # Larger positive shift for collaborative interactions - # Ensure a minimum shift - min_shift = 0.01 - shift = max(shift, min_shift) + # Update perspective filter + self.perspective_filter += shift * np.random.randn(len(self.perspective_filter)) - self.perspective_filter += shift * (other.embedding - self.embedding) - self.total_perspective_shift += shift - return shift + return float(shift) # Ensure we return a float def _calculate_emotional_influence(self, other: "Story") -> float: similarity = self.field._calculate_emotional_similarity(self, other) @@ -233,25 +252,17 @@ def _calculate_emotional_influence(self, other: "Story") -> float: ) return similarity * intensity * 0.1 - def update_emotional_state( - self, other: "Story", interaction_type: str, resonance: float - ) -> float: - # Simple emotional change calculation - base_change = resonance * 0.5 # Scale the change based on resonance - - # Adjust change based on interaction type - change_multipliers = { - "collaboration": 1.2, - "conflict": 0.8, - "inspiration": 1.5, - "reflection": 1.0, - } - emotional_change = base_change * change_multipliers.get(interaction_type, 1.0) - - # Update emotional state - self.emotional_state.update(emotional_change) - - return emotional_change + async def update_emotional_state( + self, + other: "Story", + interaction_type: str, + resonance: float, + llm: LanguageModel, + ): + self.previous_emotional_state = EmotionalState( + self.emotional_state.description, self.emotional_state.embedding.copy() + ) + await self.emotional_state.update(other.emotional_state, resonance, llm) def decay_emotions(self, decay_rate=0.01): for emotion in vars(self.emotional_state): @@ -278,12 +289,12 @@ def calculate_interaction_strength(self, other_story): familiarity_bonus = min(0.2, len(interaction_history) * 0.02) return base_strength + familiarity_bonus - def respond_to_environmental_event(self, event: Dict[str, Any]): + async def respond_to_environmental_event(self, event: Dict[str, Any]) -> float: # Simple response to environmental events intensity = event.get("intensity", 0.5) # Update emotional state based on event intensity - self.emotional_state.update(intensity * 0.2) + await self.emotional_state.update(interaction_strength=intensity * 0.2) # Add a memory of the event memory = { @@ -294,14 +305,16 @@ def respond_to_environmental_event(self, event: Dict[str, Any]): } self.memory_layer.append(memory) - def update_state(self, avg_resonance: float, avg_shift: float): + return intensity + + async def update_state(self, avg_resonance: float, avg_shift: float) -> float: # Update the story's state based on recent interactions - self.emotional_state.update( - avg_resonance * 0.1 - ) # Small emotional update based on average resonance - self.perspective_filter += ( - avg_shift * 0.1 - ) # Small perspective update based on average shift + await self.emotional_state.update( + interaction_strength=avg_resonance * 0.1 + ) + shift = avg_shift * 0.1 + self.perspective_filter += shift # Small perspective update based on average shift + return shift class NarrativeFieldViz: @@ -392,19 +405,13 @@ def detect_resonance(self, story1: Story, story2: Story) -> float: ) def _calculate_emotional_similarity(self, story1: Story, story2: Story) -> float: - emotions1 = set(story1.emotional_state.emotions.keys()) - emotions2 = set(story2.emotional_state.emotions.keys()) - shared_emotions = emotions1 & emotions2 - - if not shared_emotions: - return 0.0 - - similarity = sum( - min(story1.emotional_state.emotions[e], story2.emotional_state.emotions[e]) - for e in shared_emotions - ) / len(shared_emotions) - - return similarity + # Calculate cosine similarity between emotional state embeddings + similarity = F.cosine_similarity( + torch.tensor(story1.emotional_state.embedding), + torch.tensor(story2.emotional_state.embedding), + dim=0, + ) + return float(similarity) def add_story(self, story: Story): """Add a new story to the field""" @@ -422,9 +429,11 @@ def _update_field_potential(self): -np.linalg.norm(story.position) / 10.0 ) - def apply_environmental_event(self, event_type, intensity): + async def apply_environmental_event(self, event: Dict[str, Any]): + """Apply an environmental event to all stories in the field""" for story in self.stories: - story.respond_to_environmental_event(event_type, intensity) + await story.respond_to_environmental_event(event) + self.field_memory.append(event) class StoryPhysics: @@ -499,19 +508,25 @@ def __init__(self, field: NarrativeField): self.story_states: Dict[str, StoryState] = {} self.logger = logging.getLogger(__name__) - def update_story_states(self): + async def update_story_states(self): for story in self.field.stories: recent_memories = story.memory_layer[-5:] # Get the 5 most recent memories if recent_memories: avg_resonance = np.mean( [m.get("resonance", 0) for m in recent_memories] ) - avg_shift = np.mean( - [m.get("perspective_shift", 0) for m in recent_memories] - ) + perspective_shifts = [] + for m in recent_memories: + shift = m.get("perspective_shift", 0) + if asyncio.iscoroutine(shift): + shift = await shift + perspective_shifts.append(shift) + + avg_shift = np.mean(perspective_shifts) # Update story state based on recent interactions - story.update_state(avg_resonance, avg_shift) + shift = await story.update_state(avg_resonance, avg_shift) + story.total_perspective_shift += shift def detect_emergent_themes(self) -> List[str]: """Detect themes that are becoming more prominent""" @@ -559,46 +574,41 @@ def __init__(self): self.theme_resonance = {} # Track theme relationships self.logger = logging.getLogger(__name__) - def process_interaction(self, story1: Story, story2: Story, resonance: float): - """Process theme interactions and evolution""" - # Find theme relationships - direct_shared = set(story1.themes) & set(story2.themes) - story1_themes = set(story1.themes) - story2_themes = set(story2.themes) +class EnhancedInteractionEngine: + # ... (other methods remain the same) - # Theme relationship analysis - relationships = { - ("hope", "journey"): 0.7, - ("loneliness", "discovery"): 0.6, - ("guidance", "nature"): 0.5, - ("duty", "freedom"): 0.4, - ("imagination", "guidance"): 0.6, - ("subconscious", "discovery"): 0.5, - } + async def process_interaction(self, story1: Story, story2: Story): + if story1.id == story2.id: + return # Prevent a story from interacting with itself - # Find indirect theme relationships - indirect_resonance = 0.0 - for t1 in story1_themes: - for t2 in story2_themes: - if (t1, t2) in relationships: - indirect_resonance += relationships[(t1, t2)] - elif (t2, t1) in relationships: - indirect_resonance += relationships[(t2, t1)] - - # Calculate thematic impact - theme_impact = len(direct_shared) * 0.3 + indirect_resonance * 0.2 - - return { - "direct_shared": direct_shared, - "indirect_resonance": indirect_resonance, - "theme_impact": theme_impact, - "related_themes": [ - (t1, t2) - for t1 in story1_themes - for t2 in story2_themes - if (t1, t2) in relationships or (t2, t1) in relationships - ], - } + interaction_type = await self.determine_interaction_type(story1, story2) + resonance = self.field.detect_resonance(story1, story2) + + if resonance > self.field.resonance_threshold: + theme_impact = self.calculate_theme_impact(story1, story2) + emotional_change = self._calculate_emotional_influence(story1, story2) + + perspective_shift = await story1.update_perspective( + story2, theme_impact, resonance, emotional_change, interaction_type + ) + + await story1.update_emotional_state(story2, interaction_type, resonance, self.llm) + + # Add memory of interaction + memory = { + "type": "interaction", + "partner_id": story2.id, + "resonance": resonance, + "interaction_type": interaction_type, + "perspective_shift": perspective_shift, + "timestamp": self.field.time, + } + story1.memory_layer.append(memory) + + return resonance, interaction_type + return 0, None + + # ... (rest of the class remains the same) class StoryInteractionEngine: @@ -623,10 +633,10 @@ def __init__(self, field: NarrativeField, llm: LanguageModel): async def determine_interaction_type(self, story1: Story, story2: Story) -> str: prompt = f""" Story 1 Themes: {', '.join(story1.themes)} - Story 1 Emotional State: {vars(story1.emotional_state)} + Story 1 Emotional State: {story1.emotional_state} Story 2 Themes: {', '.join(story2.themes)} - Story 2 Emotional State: {vars(story2.emotional_state)} + Story 2 Emotional State: {story2.emotional_state} Based on the themes and emotional states of these two stories, what type of interaction might occur between them? Choose from: collaboration, conflict, inspiration, reflection, transformation, challenge, or synthesis. @@ -636,54 +646,44 @@ async def determine_interaction_type(self, story1: Story, story2: Story) -> str: return await self.llm.generate(prompt) async def process_interaction(self, story1: Story, story2: Story): + if story1.id == story2.id: + return # Prevent a story from interacting with itself + interaction_type = await self.determine_interaction_type(story1, story2) resonance = self.field.detect_resonance(story1, story2) if resonance > self.field.resonance_threshold: - # Calculate theme impact and emotional change theme_impact = self.calculate_theme_impact(story1, story2) - emotional_change1 = story1.update_emotional_state( - story2, interaction_type, resonance - ) - emotional_change2 = story2.update_emotional_state( - story1, interaction_type, resonance - ) + emotional_change = self._calculate_emotional_influence(story1, story2) - # Update perspectives - shift1 = story1.update_perspective( - story2, theme_impact, resonance, emotional_change1, interaction_type - ) - shift2 = story2.update_perspective( - story1, theme_impact, resonance, emotional_change2, interaction_type + perspective_shift = await story1.update_perspective( + story2, theme_impact, resonance, emotional_change, interaction_type ) - # Create memories for both stories - self.create_memory(story1, story2, resonance, shift1, interaction_type) - self.create_memory(story2, story1, resonance, shift2, interaction_type) + await story1.update_emotional_state(story2, interaction_type, resonance, self.llm) + + # Add memory of interaction + memory = { + "type": "interaction", + "partner_id": story2.id, + "resonance": resonance, + "interaction_type": interaction_type, + "perspective_shift": perspective_shift, # This is now awaited + "timestamp": self.field.time, + } + story1.memory_layer.append(memory) return resonance, interaction_type - return 0, "no_interaction" + return 0, None def calculate_theme_impact(self, story1: Story, story2: Story) -> float: shared_themes = set(story1.themes) & set(story2.themes) return len(shared_themes) / max(len(story1.themes), len(story2.themes)) - def create_memory( - self, - story: Story, - other: Story, - resonance: float, - shift: float, - interaction_type: str, - ): - memory = { - "interacted_with": other.id, - "resonance": resonance, - "perspective_shift": shift, - "interaction_type": interaction_type, - "timestamp": self.field.time, - } - story.memory_layer.append(memory) + def _calculate_emotional_influence(self, story1: Story, story2: Story) -> float: + # Implement the emotional influence calculation here + # This is a placeholder implementation + return 0.5 # Return a value between 0 and 1 class StoryPhysics: @@ -834,39 +834,27 @@ def __init__(self): self.journey_log = {} self.total_distances = {} # Track cumulative distance for each story self.significant_events = [] # Track important moments - self.logger = logging.getLogger(__name__) - def log_interaction(self, story1: Story, story2: Story, resonance: float): + def log_interaction(self, story1: Story, story2: Story, resonance: float, interaction_type: str): latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} + perspective_shift = latest_memory.get('perspective_shift', 0) + + # If perspective_shift is a coroutine, we need to run it in an event loop + if asyncio.iscoroutine(perspective_shift): + perspective_shift = asyncio.get_event_loop().run_until_complete(perspective_shift) - self.logger.info( - f"\nSignificant Interaction:\n" - f" {story1.id} <-> {story2.id}\n" + log_entry = ( + f"Interaction between {story1.id} and {story2.id}:\n" f" Resonance: {resonance:.2f}\n" + f" Interaction Type: {interaction_type}\n" + f" Perspective Shift: {perspective_shift:.4f}\n" f" Shared Themes: {set(story1.themes) & set(story2.themes)}\n" f" Distance: {np.linalg.norm(story1.position - story2.position):.2f}\n" f" Positions:\n" f" {story1.id}: {story1.position}\n" f" {story2.id}: {story2.position}\n" ) - - if latest_memory: - self.logger.info( - f"\nInteraction Details:\n" - f" Interaction Type: {latest_memory.get('interaction_type', 'Unknown')}\n" - f" Perspective Shift: {latest_memory.get('perspective_shift', 0):.4f}\n" - ) - - self.logger.info( - f"\nEmotional Impact:\n" - f" {story1.id} Emotional State: {story1.emotional_state}\n" - f" {story2.id} Emotional State: {story2.emotional_state}\n" - ) - - def _format_emotional_change(self, story: Story) -> str: - return ", ".join( - [f"{e}: {v:.2f}" for e, v in story.emotional_state.get_dominant_emotions()] - ) + self.logger.info(log_entry) def log_story_state(self, story: Story, timestep: float): """Log detailed story state and track journey metrics""" @@ -888,8 +876,7 @@ def log_story_state(self, story: Story, timestep: float): "time": timestep, "story_id": story.id, "distance": movement, - "direction": story.velocity - / (np.linalg.norm(story.velocity) + 1e-6), + "direction": story.velocity / (np.linalg.norm(story.velocity) + 1e-6), } ) @@ -915,20 +902,16 @@ def summarize_journey(self, story: Story): # Calculate metrics total_distance = self.total_distances[story.id] - direct_distance = np.linalg.norm( - end_state["position"] - start_state["position"] - ) + direct_distance = np.linalg.norm(end_state["position"] - start_state["position"]) wandering_ratio = total_distance / (direct_distance + 1e-6) # Perspective analysis - significant_shifts = [ - s for s in story.perspective_shifts if s["magnitude"] > 0.01 - ] - avg_shift = ( - np.mean([s["magnitude"] for s in significant_shifts]) - if significant_shifts - else 0 - ) + significant_shifts = [s for s in story.perspective_shifts if s["magnitude"] > 0.01] + avg_shift = np.mean([s["magnitude"] for s in significant_shifts]) if significant_shifts else 0 + + # Safely get unique interactions + unique_interactions = set(m["interacted_with"] for m in story.memory_layer if "interacted_with" in m) + num_unique_interactions = len(unique_interactions) self.logger.info( f"\n=== Journey Summary for {story.id} ===\n" @@ -938,7 +921,7 @@ def summarize_journey(self, story: Story): f" Wandering Ratio: {wandering_ratio:.2f}\n" f"\nInteraction Metrics:\n" f" Memories Formed: {len(story.memory_layer)}\n" - f" Unique Interactions: {len(set(m['interacted_with'] for m in story.memory_layer))}\n" + f" Unique Interactions: {num_unique_interactions}\n" f" Total Perspective Shift: {story.total_perspective_shift:.4f}\n" f" Average Shift Magnitude: {avg_shift:.4f}\n" f" Significant Perspective Changes: {len(significant_shifts)}\n" @@ -948,228 +931,27 @@ def summarize_journey(self, story: Story): f"\nSignificant Events: {len(story.memory_layer)}" ) - -async def create_story_cluster(): - """Create initial story positions in a balanced configuration""" - # Position stories in a triangle with some random offset - base_positions = np.array( - [[1.0, 0.0, 0.0], [-0.5, 0.866, 0.0], [-0.5, -0.866, 0.0]] - ) - - # Add random offset to make it interesting - return base_positions + np.random.randn(3, 3) * 0.2 - - -def summarize_story_journey(story: Story): - """Enhanced journey summary with perspective analysis""" - theme_counts = {} - for memory in story.memory_layer: - for theme in memory["themes"]: - theme_counts[theme] = theme_counts.get(theme, 0) + 1 - - most_influential_themes = sorted( - story.perspective.theme_influences.items(), key=lambda x: x[1], reverse=True - )[:3] - - total_perspective_shift = story.perspective.total_shift - - return { - "total_memories": len(story.memory_layer), - "unique_interactions": len( - set(m["interacted_with"] for m in story.memory_layer) - ), - "theme_exposure": theme_counts, - "total_perspective_shift": total_perspective_shift, - "most_influential_themes": most_influential_themes, - "perspective_shifts": len( - [s for s in story.perspective.shift_history if s["magnitude"] > 0.01] - ), - } - - -class DynamicThemeGenerator: - def __init__(self, llm: LanguageModel): - self.llm = llm - self.theme_cache = set() - self.logger = logging.getLogger(__name__) - - async def generate_themes(self, context: str, num_themes: int = 3) -> List[str]: - prompt = f"""Given the context '{context}', generate {num_themes} unique, single-word themes that could be present in a story. Output ONLY a valid JSON array of strings, nothing else. Example: - ["hope", "journey", "transformation"] - """ - response = await self.llm.generate(prompt) - - try: - # Extract JSON array from the response - json_match = re.search(r"\[.*\]", response, re.DOTALL) - if json_match: - json_str = json_match.group(0) - new_themes = json.loads(json_str) - else: - raise ValueError("No JSON array found in response") - - # Ensure we have the correct number of themes - new_themes = [theme.lower() for theme in new_themes[:num_themes]] - while len(new_themes) < num_themes: - new_themes.append(f"theme_{len(new_themes) + 1}") - - self.theme_cache.update(new_themes) - return new_themes - except json.JSONDecodeError: - self.logger.error(f"Failed to parse themes JSON: {response}") - return [f"theme_{i+1}" for i in range(num_themes)] - except Exception as e: - self.logger.error(f"Error generating themes: {e}") - return [f"theme_{i+1}" for i in range(num_themes)] - - def get_random_themes(self, num_themes: int = 3) -> List[str]: - return random.sample( - list(self.theme_cache), min(num_themes, len(self.theme_cache)) - ) - - -class DynamicStoryGenerator: - def __init__(self, llm: LanguageModel, theme_generator: DynamicThemeGenerator): - self.llm = llm - self.theme_generator = theme_generator - self.logger = logging.getLogger(__name__) - - async def generate_story(self, field: NarrativeField) -> Story: - themes = await self.theme_generator.generate_themes("Create a new story") - - prompt = f"Write a short story (2-3 sentences) incorporating the themes: {', '.join(themes)}." - content = await self.llm.generate(prompt) - - embedding = await self.llm.generate_embedding(content) - - emotional_state = await self.generate_emotional_state(content) - - return Story( - id=f"story_{len(field.stories)}", - content=content, - embedding=np.array(embedding), - perspective_filter=np.ones(len(embedding)), - position=np.random.randn(3), - velocity=np.zeros(3), - themes=themes, - emotional_state=emotional_state, - field=field, - ) - - async def generate_emotional_state(self, content: str) -> EmotionalState: - prompt = f"""Given the story: '{content}', provide 3-5 emotions that best describe the overall mood or atmosphere. For each emotion, assign a numerical value (0.0 to 1.0) indicating its intensity. Output ONLY a valid JSON object, nothing else. Example: - {{ - "curiosity": 0.8, - "melancholy": 0.5, - "hope": 0.3 - }} - """ - response = await self.llm.generate(prompt) - - # Extract JSON object from the response and ensure it's properly closed - json_match = re.search(r"\{[^{]*", response, re.DOTALL) - if json_match: - json_str = json_match.group(0) - # Add closing brace if missing - if not json_str.strip().endswith("}"): - json_str += "}" - try: - emotions = json.loads(json_str) - except json.JSONDecodeError: - self.logger.error(f"Failed to parse extracted JSON: {json_str}") - emotions = {"neutral": 0.5} # Fallback to a default emotion - else: - self.logger.error(f"No JSON object found in response: {response}") - emotions = {"neutral": 0.5} # Fallback to a default emotion - - # Ensure all values are floats between 0 and 1 - emotions = {k: min(max(float(v), 0.0), 1.0) for k, v in emotions.items()} - - return EmotionalState(emotions=emotions) - - -class EnvironmentalEventGenerator: - def __init__(self, llm: LanguageModel): - self.llm = llm - self.logger = logging.getLogger(__name__) - - async def generate_event(self) -> Dict[str, Any]: - prompt = "Generate a random environmental event for a narrative field. Include an event name, description, and intensity (0.0 to 1.0)." - response = await self.llm.generate(prompt) - - event_data = { - "name": "Unknown Event", - "description": "An unexpected event occurred.", - "intensity": 0.5, - } - - for line in response.split("\n"): - if ":" in line: - key, value = line.split(":", 1) - key = key.strip().lower() - value = value.strip() - if key in ["name", "description"]: - event_data[key] = value - elif key == "intensity": - try: - event_data[key] = float(value) - except ValueError: - self.logger.warning( - f"Invalid intensity value: {value}. Using default." - ) - - return event_data - - async def apply_event(self, field: NarrativeField): - try: - event = await self.generate_event() - self.logger.info( - f"Environmental Event: {event['name']} (Intensity: {event['intensity']})" - ) - self.logger.info(f"Description: {event['description']}") - - for story in field.stories: - story.respond_to_environmental_event(event) - except Exception as e: - self.logger.error(f"Failed to generate or apply environmental event: {e}") - - -class StoryJourneyLogger: - """Tracks and logs the journey of stories through the narrative field""" - - def __init__(self): - self.logger = logging.getLogger(__name__) - self.journey_log = {} - self.total_distances = {} # Track cumulative distance for each story - self.significant_events = [] # Track important moments - self.logger = logging.getLogger(__name__) - - def log_interaction(self, story1: Story, story2: Story, resonance: float): +class JourneyLogger: + def log_interaction(self, story1: Story, story2: Story, resonance: float, interaction_type: str): latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} + perspective_shift = latest_memory.get('perspective_shift', 0) + + # If perspective_shift is a coroutine, we need to run it in an event loop + if asyncio.iscoroutine(perspective_shift): + perspective_shift = asyncio.get_event_loop().run_until_complete(perspective_shift) - self.logger.info( - f"\nSignificant Interaction:\n" - f" {story1.id} <-> {story2.id}\n" + log_entry = ( + f"Interaction between {story1.id} and {story2.id}:\n" f" Resonance: {resonance:.2f}\n" + f" Interaction Type: {interaction_type}\n" + f" Perspective Shift: {perspective_shift:.4f}\n" f" Shared Themes: {set(story1.themes) & set(story2.themes)}\n" f" Distance: {np.linalg.norm(story1.position - story2.position):.2f}\n" f" Positions:\n" f" {story1.id}: {story1.position}\n" f" {story2.id}: {story2.position}\n" ) - - if latest_memory: - self.logger.info( - f"\nInteraction Details:\n" - f" Interaction Type: {latest_memory.get('interaction_type', 'Unknown')}\n" - f" Perspective Shift: {latest_memory.get('perspective_shift', 0):.4f}\n" - ) - - self.logger.info( - f"\nEmotional Impact:\n" - f" {story1.id} Emotional State: {story1.emotional_state}\n" - f" {story2.id} Emotional State: {story2.emotional_state}\n" - ) + self.logger.info(log_entry) def _format_emotional_change(self, story: Story) -> str: return ", ".join( @@ -1312,6 +1094,7 @@ async def generate_themes(self, context: str, num_themes: int = 3) -> List[str]: ["hope", "journey", "transformation"] """ response = await self.llm.generate(prompt) + response = response.strip().lower() try: # Extract JSON array from the response @@ -1351,9 +1134,13 @@ def __init__(self, llm: LanguageModel, theme_generator: DynamicThemeGenerator): async def generate_story(self, field: NarrativeField) -> Story: themes = await self.theme_generator.generate_themes("Create a new story") - prompt = f"Write a short story (2-3 sentences) incorporating the themes: {', '.join(themes)}." + prompt = f"Write a short positive, neutral, or negative story (5-8 sentences) incorporating the themes: {', '.join(themes)}. Make it interesting and engaging. Make it personal and emotional. Make it unique and memorable, with one clearly defined protagonist. Start the story by introducing the protagonist's name." content = await self.llm.generate(prompt) + # Extract the protagonist's name from the first sentence + first_sentence = content.split('.')[0] + protagonist_name = first_sentence.split()[0] # Assume the first word is the name + embedding = await self.llm.generate_embedding(content) emotional_state = await self.generate_emotional_state(content) @@ -1368,39 +1155,14 @@ async def generate_story(self, field: NarrativeField) -> Story: themes=themes, emotional_state=emotional_state, field=field, + protagonist_name=protagonist_name ) async def generate_emotional_state(self, content: str) -> EmotionalState: - prompt = f"""Given the story: '{content}', provide 3-5 emotions that best describe the overall mood or atmosphere. For each emotion, assign a numerical value (0.0 to 1.0) indicating its intensity. Output ONLY a valid JSON object, nothing else. Example: - {{ - "curiosity": 0.8, - "melancholy": 0.5, - "hope": 0.3 - }} - """ - response = await self.llm.generate(prompt) - response = response.strip().lower() - - # Extract JSON object from the response and ensure it's properly closed - json_match = re.search(r"\{[^{]*", response, re.DOTALL) - if json_match: - json_str = json_match.group(0) - # Add closing brace if missing - if not json_str.strip().endswith("}"): - json_str += "}" - try: - emotions = json.loads(json_str) - except json.JSONDecodeError: - self.logger.error(f"Failed to parse extracted JSON: {json_str}") - emotions = {"neutral": 0.5} # Fallback to a default emotion - else: - self.logger.error(f"No JSON object found in response: {response}") - emotions = {"neutral": 0.5} # Fallback to a default emotion - - # Ensure all values are floats between 0 and 1 - emotions = {k: min(max(float(v), 0.0), 1.0) for k, v in emotions.items()} - - return EmotionalState(emotions=emotions) + prompt = f"""Given the story: '{content}', describe its positive, neutral, or negative emotional state in 2-3 sentences: """ + description = await self.llm.generate(prompt) + embedding = await self.llm.generate_embedding(description) + return EmotionalState(description, np.array(embedding)) class EnvironmentalEventGenerator: @@ -1409,7 +1171,7 @@ def __init__(self, llm: LanguageModel): self.logger = logging.getLogger(__name__) async def generate_event(self) -> Dict[str, Any]: - prompt = "Generate a random environmental event for a narrative field. Include an event name, description, and intensity (0.0 to 1.0)." + prompt = "Generate a random positive or negative environmental event for a narrative field. Include an event name, description, and intensity (0.0 to 1.0)." response = await self.llm.generate(prompt) event_data = { @@ -1452,14 +1214,8 @@ async def apply_event(self, field: NarrativeField): async def simulate_field(): """Run a simulation of the narrative field""" # Set up logging - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) logger = logging.getLogger(__name__) - logger.info("Starting narrative field simulation") - # Initialize language model llm = OllamaInterface() theme_generator = DynamicThemeGenerator(llm) @@ -1496,21 +1252,44 @@ async def simulate_field(): field.add_story(new_story) logger.info(f"New story added: {new_story.id}") - # Occasionally generate environmental events - if t % 40 == 0: - await event_generator.apply_event(field) + # Generate and apply environmental events + if random.random() < 0.025: # Assuming environmental_event_probability is 0.025 + event = await event_generator.generate_event() + try: + await field.apply_environmental_event(event) + logger.info( + f"Environmental Event: {event['name']} (Intensity: {event['intensity']})" + ) + logger.info(f"Description: {event['description']}") + except Exception as e: + logger.error( + f"Failed to generate or apply environmental event: {str(e)}" + ) - # Check for interactions with enhanced engine + # Check for interactions for i, story1 in enumerate(field.stories): - for story2 in field.stories[i + 1 :]: - if ( - np.linalg.norm(story1.position - story2.position) - < field.interaction_range - ): - await interaction_engine.process_interaction(story1, story2) + for story2 in field.stories[i + 1:]: + distance = np.linalg.norm(story1.position - story2.position) + if distance < field.interaction_range: + resonance, interaction_type = await interaction_engine.process_interaction(story1, story2) + if resonance > 0: + logger.info(f"\nSignificant Interaction:") + logger.info(f" {story1.id} <-> {story2.id}") + logger.info(f" Resonance: {resonance:.2f}") + logger.info(f" Shared Themes: {set(story1.themes) & set(story2.themes)}") + logger.info(f" Distance: {distance:.2f}") + logger.info(f" Positions:") + logger.info(f" {story1.id}: {story1.position}") + logger.info(f" {story2.id}: {story2.position}") + logger.info(f"\nInteraction Details:") + logger.info(f" Interaction Type: {interaction_type}") + logger.info(f" Perspective Shift: {story1.total_perspective_shift:.4f}") + logger.info(f"\nEmotional Impact:") + logger.info(f" {story1.id} Emotional State: {story1.emotional_state}") + logger.info(f" {story2.id} Emotional State: {story2.emotional_state}") # Update story states - collective_engine.update_story_states() + await collective_engine.update_story_states() # Capture visualization data await visualizer.capture_state(field, t) @@ -1533,9 +1312,9 @@ async def simulate_field(): for story2 in field.stories[i + 1 :]: distance = np.linalg.norm(story1.position - story2.position) if distance < field.interaction_range: - await interaction_engine.process_interaction(story1, story2) + resonance, interaction_type = await interaction_engine.process_interaction(story1, story2) journey_logger.log_interaction( - story1, story2, field.detect_resonance(story1, story2) + story1, story2, resonance, interaction_type ) # Print final summaries @@ -1551,3 +1330,4 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) + From f4aa29650ceb7ba195afddb2c9e278bb55cb0b21 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 15:03:43 +0200 Subject: [PATCH 11/15] Adds periodical collective story --- src/nfd_three_story_evolve.py | 138 +++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 36 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 0833d62..49aa522 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -225,11 +225,11 @@ def __str__(self): async def update_perspective( self, - other_story: 'Story', + other_story: "Story", theme_impact: float, resonance: float, emotional_change: float, - interaction_type: str + interaction_type: str, ) -> float: # Calculate perspective shift based on interaction factors shift = theme_impact * resonance * emotional_change * 0.1 @@ -309,11 +309,11 @@ async def respond_to_environmental_event(self, event: Dict[str, Any]) -> float: async def update_state(self, avg_resonance: float, avg_shift: float) -> float: # Update the story's state based on recent interactions - await self.emotional_state.update( - interaction_strength=avg_resonance * 0.1 - ) + await self.emotional_state.update(interaction_strength=avg_resonance * 0.1) shift = avg_shift * 0.1 - self.perspective_filter += shift # Small perspective update based on average shift + self.perspective_filter += ( + shift # Small perspective update based on average shift + ) return shift @@ -507,6 +507,7 @@ def __init__(self, field: NarrativeField): self.collective_memories = [] self.story_states: Dict[str, StoryState] = {} self.logger = logging.getLogger(__name__) + self.collective_story = "" # Add this line to initialize the collective story async def update_story_states(self): for story in self.field.stories: @@ -521,7 +522,7 @@ async def update_story_states(self): if asyncio.iscoroutine(shift): shift = await shift perspective_shifts.append(shift) - + avg_shift = np.mean(perspective_shifts) # Update story state based on recent interactions @@ -565,6 +566,26 @@ def generate_field_pulse(self, theme: str, intensity: float): self.collective_memories.append(pulse) + def write_collective_story(self): + """Generate and update the collective story based on current field state""" + emergent_themes = self.detect_emergent_themes() + story_summaries = [self.summarize_story(story) for story in self.field.stories] + + collective_narrative = ( + f"The narrative field pulses with {len(self.field.stories)} stories. " + ) + collective_narrative += f"Emergent themes of {', '.join(emergent_themes)} weave through the collective consciousness. " + + for summary in story_summaries: + collective_narrative += f"{summary} " + + self.collective_story = collective_narrative + self.logger.info(f"Updated collective story: {self.collective_story[:100]}...") + + def summarize_story(self, story: Story) -> str: + """Generate a brief summary of a story's current state""" + return f"Story {story.id} resonates with {', '.join(story.themes[:3])}, its journey marked by {len(story.memory_layer)} memories." + class ThemeEvolutionEngine: """Handles theme evolution and perspective shifts""" @@ -574,6 +595,7 @@ def __init__(self): self.theme_resonance = {} # Track theme relationships self.logger = logging.getLogger(__name__) + class EnhancedInteractionEngine: # ... (other methods remain the same) @@ -592,7 +614,9 @@ async def process_interaction(self, story1: Story, story2: Story): story2, theme_impact, resonance, emotional_change, interaction_type ) - await story1.update_emotional_state(story2, interaction_type, resonance, self.llm) + await story1.update_emotional_state( + story2, interaction_type, resonance, self.llm + ) # Add memory of interaction memory = { @@ -660,7 +684,9 @@ async def process_interaction(self, story1: Story, story2: Story): story2, theme_impact, resonance, emotional_change, interaction_type ) - await story1.update_emotional_state(story2, interaction_type, resonance, self.llm) + await story1.update_emotional_state( + story2, interaction_type, resonance, self.llm + ) # Add memory of interaction memory = { @@ -835,13 +861,17 @@ def __init__(self): self.total_distances = {} # Track cumulative distance for each story self.significant_events = [] # Track important moments - def log_interaction(self, story1: Story, story2: Story, resonance: float, interaction_type: str): + def log_interaction( + self, story1: Story, story2: Story, resonance: float, interaction_type: str + ): latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} - perspective_shift = latest_memory.get('perspective_shift', 0) - + perspective_shift = latest_memory.get("perspective_shift", 0) + # If perspective_shift is a coroutine, we need to run it in an event loop if asyncio.iscoroutine(perspective_shift): - perspective_shift = asyncio.get_event_loop().run_until_complete(perspective_shift) + perspective_shift = asyncio.get_event_loop().run_until_complete( + perspective_shift + ) log_entry = ( f"Interaction between {story1.id} and {story2.id}:\n" @@ -876,7 +906,8 @@ def log_story_state(self, story: Story, timestep: float): "time": timestep, "story_id": story.id, "distance": movement, - "direction": story.velocity / (np.linalg.norm(story.velocity) + 1e-6), + "direction": story.velocity + / (np.linalg.norm(story.velocity) + 1e-6), } ) @@ -902,15 +933,25 @@ def summarize_journey(self, story: Story): # Calculate metrics total_distance = self.total_distances[story.id] - direct_distance = np.linalg.norm(end_state["position"] - start_state["position"]) + direct_distance = np.linalg.norm( + end_state["position"] - start_state["position"] + ) wandering_ratio = total_distance / (direct_distance + 1e-6) # Perspective analysis - significant_shifts = [s for s in story.perspective_shifts if s["magnitude"] > 0.01] - avg_shift = np.mean([s["magnitude"] for s in significant_shifts]) if significant_shifts else 0 + significant_shifts = [ + s for s in story.perspective_shifts if s["magnitude"] > 0.01 + ] + avg_shift = ( + np.mean([s["magnitude"] for s in significant_shifts]) + if significant_shifts + else 0 + ) # Safely get unique interactions - unique_interactions = set(m["interacted_with"] for m in story.memory_layer if "interacted_with" in m) + unique_interactions = set( + m["interacted_with"] for m in story.memory_layer if "interacted_with" in m + ) num_unique_interactions = len(unique_interactions) self.logger.info( @@ -931,14 +972,19 @@ def summarize_journey(self, story: Story): f"\nSignificant Events: {len(story.memory_layer)}" ) + class JourneyLogger: - def log_interaction(self, story1: Story, story2: Story, resonance: float, interaction_type: str): + def log_interaction( + self, story1: Story, story2: Story, resonance: float, interaction_type: str + ): latest_memory = story1.memory_layer[-1] if story1.memory_layer else {} - perspective_shift = latest_memory.get('perspective_shift', 0) - + perspective_shift = latest_memory.get("perspective_shift", 0) + # If perspective_shift is a coroutine, we need to run it in an event loop if asyncio.iscoroutine(perspective_shift): - perspective_shift = asyncio.get_event_loop().run_until_complete(perspective_shift) + perspective_shift = asyncio.get_event_loop().run_until_complete( + perspective_shift + ) log_entry = ( f"Interaction between {story1.id} and {story2.id}:\n" @@ -1138,8 +1184,10 @@ async def generate_story(self, field: NarrativeField) -> Story: content = await self.llm.generate(prompt) # Extract the protagonist's name from the first sentence - first_sentence = content.split('.')[0] - protagonist_name = first_sentence.split()[0] # Assume the first word is the name + first_sentence = content.split(".")[0] + protagonist_name = first_sentence.split()[ + 0 + ] # Assume the first word is the name embedding = await self.llm.generate_embedding(content) @@ -1155,7 +1203,7 @@ async def generate_story(self, field: NarrativeField) -> Story: themes=themes, emotional_state=emotional_state, field=field, - protagonist_name=protagonist_name + protagonist_name=protagonist_name, ) async def generate_emotional_state(self, content: str) -> EmotionalState: @@ -1230,7 +1278,7 @@ async def simulate_field(): interaction_engine = EnhancedInteractionEngine(field, llm) # Generate initial stories - for _ in range(2): + for _ in range(1): story = await story_generator.generate_story(field) field.add_story(story) @@ -1247,7 +1295,7 @@ async def simulate_field(): journey_logger.log_story_state(story, t) # Occasionally generate new stories - if t % 50 == 0 and len(field.stories) < 10: + if t % 17 == 0 and len(field.stories) < 10: new_story = await story_generator.generate_story(field) field.add_story(new_story) logger.info(f"New story added: {new_story.id}") @@ -1268,25 +1316,35 @@ async def simulate_field(): # Check for interactions for i, story1 in enumerate(field.stories): - for story2 in field.stories[i + 1:]: + for story2 in field.stories[i + 1 :]: distance = np.linalg.norm(story1.position - story2.position) if distance < field.interaction_range: - resonance, interaction_type = await interaction_engine.process_interaction(story1, story2) + resonance, interaction_type = ( + await interaction_engine.process_interaction(story1, story2) + ) if resonance > 0: logger.info(f"\nSignificant Interaction:") logger.info(f" {story1.id} <-> {story2.id}") logger.info(f" Resonance: {resonance:.2f}") - logger.info(f" Shared Themes: {set(story1.themes) & set(story2.themes)}") + logger.info( + f" Shared Themes: {set(story1.themes) & set(story2.themes)}" + ) logger.info(f" Distance: {distance:.2f}") logger.info(f" Positions:") logger.info(f" {story1.id}: {story1.position}") logger.info(f" {story2.id}: {story2.position}") logger.info(f"\nInteraction Details:") logger.info(f" Interaction Type: {interaction_type}") - logger.info(f" Perspective Shift: {story1.total_perspective_shift:.4f}") + logger.info( + f" Perspective Shift: {story1.total_perspective_shift:.4f}" + ) logger.info(f"\nEmotional Impact:") - logger.info(f" {story1.id} Emotional State: {story1.emotional_state}") - logger.info(f" {story2.id} Emotional State: {story2.emotional_state}") + logger.info( + f" {story1.id} Emotional State: {story1.emotional_state}" + ) + logger.info( + f" {story2.id} Emotional State: {story2.emotional_state}" + ) # Update story states await collective_engine.update_story_states() @@ -1295,7 +1353,14 @@ async def simulate_field(): await visualizer.capture_state(field, t) # Occasionally generate field pulses and detect patterns - if t % 100 == 0: + if t % 30 == 0: + collective_engine.write_collective_story() + logger.info( + f"Collective story at timestep {t}: {collective_engine.collective_story[:500]}..." + ) + logger.debug(f"Collective story written at timestep {t}") + logger.debug(f"Collective story: {collective_engine.collective_story}") + collective_engine.generate_field_pulse("seeking_connection", 0.5) emergent_themes = collective_engine.detect_emergent_themes() logger.info(f"Timestep {t} - Emergent themes: {emergent_themes}") @@ -1312,7 +1377,9 @@ async def simulate_field(): for story2 in field.stories[i + 1 :]: distance = np.linalg.norm(story1.position - story2.position) if distance < field.interaction_range: - resonance, interaction_type = await interaction_engine.process_interaction(story1, story2) + resonance, interaction_type = ( + await interaction_engine.process_interaction(story1, story2) + ) journey_logger.log_interaction( story1, story2, resonance, interaction_type ) @@ -1330,4 +1397,3 @@ async def simulate_field(): if __name__ == "__main__": asyncio.run(simulate_field()) - From 60de95dc31688e23e3a19399d034cfe7bc728835 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 15:06:35 +0200 Subject: [PATCH 12/15] Update src/nfd_three_story_evolve.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/nfd_three_story_evolve.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index 49aa522..ebf7a1c 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -74,12 +74,11 @@ def get_theme_resonance(self, theme1: str, theme2: str) -> float: if (theme2, theme1) in self.primary_relationships: return self.primary_relationships[(theme2, theme1)] - # Check theme groups - shared_groups = 0 - for group in self.theme_groups.values(): - if theme1 in group and theme2 in group: - shared_groups += 1 - + shared_groups = sum( + 1 + for group in self.theme_groups.values() + if theme1 in group and theme2 in group + ) return 0.3 * shared_groups if shared_groups > 0 else 0.1 From ce098a2f484832e7655a15aeacc985b3c1362281 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 15:06:51 +0200 Subject: [PATCH 13/15] Update src/nfd_three_story_evolve.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/nfd_three_story_evolve.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index ebf7a1c..a12d423 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -948,9 +948,11 @@ def summarize_journey(self, story: Story): ) # Safely get unique interactions - unique_interactions = set( - m["interacted_with"] for m in story.memory_layer if "interacted_with" in m - ) + unique_interactions = { + m["interacted_with"] + for m in story.memory_layer + if "interacted_with" in m + } num_unique_interactions = len(unique_interactions) self.logger.info( From 302ae66756032cda0255808109b13344bed4d377 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 15:07:16 +0200 Subject: [PATCH 14/15] Update src/nfd_three_story_evolve.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/nfd_three_story_evolve.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index a12d423..ff12e88 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -1068,9 +1068,11 @@ def summarize_journey(self, story: Story): ) # Safely get unique interactions - unique_interactions = set( - m["interacted_with"] for m in story.memory_layer if "interacted_with" in m - ) + unique_interactions = { + m["interacted_with"] + for m in story.memory_layer + if "interacted_with" in m + } num_unique_interactions = len(unique_interactions) self.logger.info( From c5e43eff9d4f28fd7dddd4b73c267651948542e3 Mon Sep 17 00:00:00 2001 From: Leon van Bokhorst Date: Sat, 26 Oct 2024 15:07:37 +0200 Subject: [PATCH 15/15] Update src/nfd_three_story_evolve.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- src/nfd_three_story_evolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nfd_three_story_evolve.py b/src/nfd_three_story_evolve.py index ff12e88..4b743e2 100644 --- a/src/nfd_three_story_evolve.py +++ b/src/nfd_three_story_evolve.py @@ -1333,7 +1333,7 @@ async def simulate_field(): f" Shared Themes: {set(story1.themes) & set(story2.themes)}" ) logger.info(f" Distance: {distance:.2f}") - logger.info(f" Positions:") + logger.info(" Positions:") logger.info(f" {story1.id}: {story1.position}") logger.info(f" {story2.id}: {story2.position}") logger.info(f"\nInteraction Details:")