tostido's picture
download
raw
142 kB
import torch
import json
import zipfile
from io import BytesIO
import numpy as np
import datetime
import os
import sys
from typing import Dict, Any, List, Optional
import base64
import uuid
import pickle
from pathlib import Path
# Optional ONNX runtime - graceful degradation if not installed
try:
import onnxruntime
ONNX_AVAILABLE = True
except ImportError:
onnxruntime = None
ONNX_AVAILABLE = False
# Assuming Organism and OrganismBrain are importable from their respective paths
# Using relative imports suitable for agent_compiler.py in reality_simulator/
try:
from .evolution_engine import Organism, Genotype, Phenotype
from .neural.brain import OrganismBrain
from .checkpointing.organism_capsule import OrganismCapsule
from .portable_agent.agent_runtime import AgentState
except ImportError:
# Fallback for direct execution or different import contexts
import sys
current_dir = Path(__file__).parent
sys.path.insert(0, str(current_dir)) # Add reality_simulator to path
from evolution_engine import Organism, Genotype, Phenotype
from neural.brain import OrganismBrain
from checkpointing.organism_capsule import OrganismCapsule
from portable_agent.agent_runtime import AgentState
import logging
logger = logging.getLogger(__name__)
# Constants for action mapping
ACTION_MAP = {
0: 'move',
1: 'cooperate',
2: 'compete',
3: 'rest',
4: 'reproduce',
5: 'isolate'
}
PORTABLE_AGENT_DIR = Path(__file__).parent / 'portable_agent'
class AgentCompiler:
"""
Compiles a NeuralOrganism's state, particularly its neural network brain,
into a portable, deployable agent archive.
"""
def __init__(self):
self.supported_formats = ['onnx', 'torchscript', 'statedict']
class LanguageHeadWrapper(torch.nn.Module):
"""Wrapper that exports both action and language heads together."""
def __init__(self, brain: 'OrganismBrain'):
super().__init__()
self.brain = brain
self.has_language_head = brain.use_language_head
self.input_dim = brain.input_dim
self.output_dim = brain.output_dim
self.vocab_size = brain.vocab_size if hasattr(brain, 'vocab_size') else 1000
def forward(self, x: torch.Tensor):
"""Forward pass returning (action_probs, language_logits) if language head exists."""
if self.has_language_head:
# Call forward with return_language_logits=True
action_probs, language_logits = self.brain(x, return_language_logits=True)
return action_probs, language_logits
else:
# Just return action probs
action_probs = self.brain(x)
return action_probs
class MultiOrganismWrapper(torch.nn.Module):
def __init__(self, brains: List['OrganismBrain'], names: List[str]):
super().__init__()
self.brains = torch.nn.ModuleList(brains)
self.names = names
self.input_dims = [b.input_dim for b in brains]
self.output_dims = [b.output_dim for b in brains]
self.max_input_dim = max(self.input_dims) if self.input_dims else 0
# Check if any brain has language head
self.has_language_heads = [getattr(b, 'use_language_head', False) for b in brains]
self.any_language_head = any(self.has_language_heads)
def forward(self, x: torch.Tensor):
# x shape: [B, max_input_dim] (we will slice/pad per brain)
# Returns FLAT tuple: (action1, action2, ..., lang1, lang2, ...) for ONNX compatibility
action_outputs = []
language_outputs = []
for brain, in_dim, has_lang in zip(self.brains, self.input_dims, self.has_language_heads):
if x.shape[1] < in_dim:
pad = torch.zeros(x.shape[0], in_dim - x.shape[1], dtype=x.dtype, device=x.device)
x_i = torch.cat([x, pad], dim=1)
else:
x_i = x[:, :in_dim]
if has_lang:
action_probs, lang_logits = brain(x_i, return_language_logits=True)
action_outputs.append(action_probs)
language_outputs.append(lang_logits)
else:
action_probs = brain(x_i)
action_outputs.append(action_probs)
# Return flat tuple: all actions first, then all language outputs
# This is compatible with ONNX which expects flat output tuple
if language_outputs:
return tuple(action_outputs + language_outputs)
return tuple(action_outputs)
def _reconstruct_brain_from_capsule(self, capsule: OrganismCapsule) -> OrganismBrain:
"""
Reconstructs the OrganismBrain model from the capsule data.
Uses capsule.neural (NeuralSnapshot) for reconstruction.
"""
if not capsule.neural:
raise ValueError("Capsule does not contain neural network state.")
# Extract from NeuralSnapshot
neural_snap = capsule.neural
brain_state_dict_b64 = neural_snap.to_dict().get('state_dict_b64')
if not brain_state_dict_b64:
raise ValueError("Neural network state in capsule is incomplete.")
# Extract parameters from NeuralSnapshot
input_dim = neural_snap.input_size
hidden_dim = neural_snap.hidden_size
output_dim = neural_snap.output_size
# Load the state_dict FIRST to detect architecture
state_dict_bytes = base64.b64decode(brain_state_dict_b64)
# Some snapshots may be gzip compressed before base64 encoding
# Note: PyTorch's native ZIP format (PK header with archive/ prefix) should NOT be extracted
try:
if len(state_dict_bytes) >= 2 and state_dict_bytes[:2] == b"\x1f\x8b":
import gzip
state_dict_bytes = gzip.decompress(state_dict_bytes)
elif len(state_dict_bytes) >= 2 and state_dict_bytes[:2] == b"PK":
# Check if this is PyTorch's native ZIP format (has archive/ prefix)
# If so, leave it alone - torch.load handles it directly
with zipfile.ZipFile(BytesIO(state_dict_bytes)) as zf:
names = zf.namelist()
is_pytorch_native = any(n.startswith('archive/') for n in names)
if not is_pytorch_native:
# Legacy: manually zipped checkpoint file - extract it
candidate = None
for ext in ('.pt', '.pth', '.pkl', '.bin', '.tensors'):
for n in names:
if n.lower().endswith(ext):
candidate = n
break
if candidate:
break
if candidate:
state_dict_bytes = zf.read(candidate)
# else: PyTorch native format, pass through to torch.load unchanged
except Exception:
# If decompression fails, fall back to raw bytes
pass
# PyTorch 2.6 defaults weights_only=True; allow full, trusted load
state_dict = torch.load(BytesIO(state_dict_bytes), map_location='cpu', weights_only=False)
# Infer architecture from state_dict to avoid shape/key mismatches
sd_keys = set(state_dict.keys())
def _shape(name, dim):
return state_dict[name].shape[dim] if name in state_dict else None
inferred_input = _shape('fc1.weight', 1) or getattr(capsule.neural, 'input_size', None) or 18
inferred_hidden = _shape('fc1.weight', 0) or getattr(capsule.neural, 'hidden_size', None) or 64
inferred_output = _shape('fc3.weight', 0) or getattr(capsule.neural, 'output_size', None) or 6
use_attention = any(k.startswith('attention.') for k in sd_keys) or 'attention_norm.weight' in sd_keys
use_language_head = 'fc_language.weight' in sd_keys
use_concept_head = any(k.startswith('concept_head.') for k in sd_keys)
# Use .size() instead of .shape[] for robustness
vocab_size = state_dict['fc_language.weight'].size(0) if use_language_head else 1000
# Infer num_attention_heads if attention is used
if use_attention:
# Infer from hidden_dim and common head counts
# attention uses hidden_dim as embed_dim, which must be divisible by num_heads
# Try to match common patterns: 8, 16, 4, 2
for candidate_heads in [8, 16, 4, 2, 1]:
if inferred_hidden % candidate_heads == 0:
num_attention_heads = candidate_heads
break
else:
num_attention_heads = 4 # Fallback
else:
num_attention_heads = 4
# Use reasonable dropout matching current config (can't infer from state_dict)
dropout = 0.15
# Infer num_key_compositions from concept_head if present
num_key_compositions = 20 # Default
if use_concept_head and 'concept_head.composition_value.weight' in state_dict:
# composition_value.weight shape is (num_key_compositions, hidden_dim)
num_key_compositions = state_dict['concept_head.composition_value.weight'].size(0)
logger.debug(f"Inferred num_key_compositions={num_key_compositions} from state_dict")
# Create a new instance of OrganismBrain matching the checkpoint
reconstructed_brain = OrganismBrain(
input_dim=int(inferred_input),
hidden_dim=int(inferred_hidden),
output_dim=int(inferred_output),
activation='relu',
dropout=dropout,
use_attention=bool(use_attention),
num_attention_heads=int(num_attention_heads),
attention_dim=int(inferred_hidden),
vocab_size=int(vocab_size),
use_language_head=bool(use_language_head),
use_concept_head=bool(use_concept_head),
num_key_compositions=int(num_key_compositions)
)
# Load state dict allowing extra/missing keys (robust to optional heads)
missing, unexpected = reconstructed_brain.load_state_dict(state_dict, strict=False)
if unexpected:
logger.debug(f"AgentCompiler: Ignored unexpected keys during load: {sorted(list(unexpected))[:5]}...")
reconstructed_brain.eval() # Set to evaluation mode
return reconstructed_brain
def _export_onnx(self, brain: OrganismBrain, dummy_input: torch.Tensor, model_path: str) -> None:
"""Exports the PyTorch brain to ONNX format, including language head if present."""
try:
# Wrap brain to export both action and language heads
wrapper = self.LanguageHeadWrapper(brain)
wrapper.eval()
# Log brain architecture for debugging
logger.debug(f"ONNX export: input_dim={brain.input_dim}, hidden_dim={brain.hidden_dim}, "
f"output_dim={brain.output_dim}, use_attention={brain.use_attention}, "
f"use_language_head={brain.use_language_head}, use_concept_head={brain.use_concept_head}, "
f"num_key_compositions={getattr(brain, 'num_key_compositions', 'N/A')}")
# Test forward pass before export to catch errors early
logger.debug("Testing forward pass before ONNX export...")
with torch.no_grad():
test_output = wrapper(dummy_input)
if isinstance(test_output, tuple):
logger.debug(f"Forward pass OK: {len(test_output)} outputs")
else:
logger.debug(f"Forward pass OK: single output shape {test_output.shape}")
# Configure output names based on whether language head exists
if wrapper.has_language_head:
output_names = ['action_probs', 'language_logits']
dynamic_axes = {
'input': {0: 'batch_size'},
'action_probs': {0: 'batch_size'},
'language_logits': {0: 'batch_size'}
}
else:
output_names = ['action_probs']
dynamic_axes = {
'input': {0: 'batch_size'},
'action_probs': {0: 'batch_size'}
}
logger.debug("Starting torch.onnx.export...")
torch.onnx.export(
wrapper,
dummy_input,
model_path,
input_names=['input'],
output_names=output_names,
dynamic_axes=dynamic_axes,
opset_version=11 # A commonly supported opset version
)
head_info = " (with language head)" if wrapper.has_language_head else ""
logger.info(f"Successfully exported brain to ONNX{head_info}: {model_path}")
except Exception as e:
# Provide clearer guidance when onnx/onnxscript is missing (PyTorch 2.6+)
import traceback
msg = str(e)
hint = ""
if 'onnxscript' in msg.lower():
hint = " (install with: pip install onnx onnxscript)"
logger.error(f"Failed to export brain to ONNX at {model_path}: {e}{hint}")
logger.error(f"Full traceback:\n{traceback.format_exc()}")
raise
def _export_torchscript(self, brain: OrganismBrain, model_path) -> None:
"""Exports the PyTorch brain to TorchScript format, including language head if present.
Args:
brain: The OrganismBrain to export
model_path: Either a file path string or a BytesIO buffer
"""
try:
# Wrap brain to export both action and language heads
wrapper = self.LanguageHeadWrapper(brain)
wrapper.eval()
# Log brain architecture for debugging
logger.debug(f"TorchScript export: input_dim={brain.input_dim}, hidden_dim={brain.hidden_dim}, "
f"output_dim={brain.output_dim}, use_attention={brain.use_attention}, "
f"use_language_head={brain.use_language_head}, use_concept_head={brain.use_concept_head}, "
f"num_key_compositions={getattr(brain, 'num_key_compositions', 'N/A')}")
# Use torch.jit.trace instead of torch.jit.script
# trace captures the execution path dynamically, which works with
# OrganismBrain's complex control flow (conditional attention, etc.)
# script analyzes code statically and fails on Python 3.12 + PyTorch 2.5
dummy_input = torch.randn(1, brain.input_dim, dtype=torch.float32)
# Test forward pass before tracing to catch errors early
logger.debug("Testing forward pass before trace...")
with torch.no_grad():
test_output = wrapper(dummy_input)
if isinstance(test_output, tuple):
logger.debug(f"Forward pass OK: {len(test_output)} outputs")
else:
logger.debug(f"Forward pass OK: single output shape {test_output.shape}")
logger.debug("Starting torch.jit.trace...")
traced_brain = torch.jit.trace(wrapper, (dummy_input,))
head_info = " (with language head)" if wrapper.has_language_head else ""
# Handle both file path (str) and BytesIO buffer
if isinstance(model_path, BytesIO):
torch.jit.save(traced_brain, model_path)
model_path.seek(0) # Reset buffer position for reading
logger.info(f"Successfully exported brain to TorchScript (traced){head_info} in memory buffer")
else:
traced_brain.save(model_path)
logger.info(f"Successfully exported brain to TorchScript (traced){head_info}: {model_path}")
except Exception as e:
import traceback
logger.error(f"Failed to export brain to TorchScript: {e}")
logger.error(f"Full traceback:\n{traceback.format_exc()}")
raise
def _export_statedict(self, brain: OrganismBrain, model_path: str) -> None:
"""Exports the PyTorch brain's state_dict."""
try:
torch.save(brain.state_dict(), model_path)
logger.info(f"Successfully exported brain state_dict: {model_path}")
except Exception as e:
logger.error(f"Failed to export brain state_dict at {model_path}: {e}")
raise
def _extract_fitness_value(self, capsule: OrganismCapsule) -> Optional[float]:
"""Safely extract fitness value from capsule, handling various data formats."""
if not capsule.fitness or not capsule.fitness.fitness_history:
return None
history = capsule.fitness.fitness_history
try:
# Handle list of tuples: [(time, fitness), ...]
if isinstance(history, list) and len(history) > 0:
last_entry = history[-1]
if isinstance(last_entry, (list, tuple)) and len(last_entry) >= 2:
return float(last_entry[1])
else:
# Single value
return float(last_entry)
# Handle numpy array
elif hasattr(history, 'shape'):
if len(history.shape) == 2:
# 2D array: take last row, second column
return float(history[-1, 1])
elif len(history.shape) == 1:
# 1D array: take last value
return float(history[-1])
return None
except (IndexError, TypeError, ValueError) as e:
logger.warning(f"Could not extract fitness from history: {e}")
return None
def _create_rich_metadata(self, capsule: OrganismCapsule, brain: Optional[OrganismBrain] = None) -> Dict[str, Any]:
"""
Creates comprehensive metadata for the compiled agent, leveraging the rich capsule data.
Args:
capsule: The OrganismCapsule containing agent state
brain: Optional reconstructed brain for extracting additional architecture info
"""
metadata = {
'agent_id': capsule.organism_id,
'capsule_id': capsule.capsule_id,
'export_timestamp': datetime.datetime.now().isoformat(),
'capsule_version': capsule.version,
'capture_reason': capsule.capture_reason,
# Organism Core Data
'organism_core': {
'species_id': capsule.organism_id,
'capsule_id': capsule.capsule_id,
'fitness': self._extract_fitness_value(capsule),
'organism_age': capsule.organism_age,
'birth_time': capsule.organism_birth_time,
},
# Neural Network Details
'neural_network': {
'architecture': {
'input_size': capsule.neural.input_size,
'hidden_size': capsule.neural.hidden_size,
'output_size': capsule.neural.output_size,
'num_layers': capsule.neural.num_layers,
'total_parameters': capsule.neural.total_parameters,
'has_language_head': hasattr(brain, 'use_language_head') and brain.use_language_head if brain else False,
'has_attention': hasattr(brain, 'use_attention') and brain.use_attention if brain else False,
'has_concept_head': hasattr(brain, 'use_concept_head') and brain.use_concept_head if brain else False,
'vocab_size': brain.vocab_size if brain and hasattr(brain, 'vocab_size') and hasattr(brain, 'use_language_head') and brain.use_language_head else None
} if capsule.neural else {},
'training_steps': capsule.neural.training_steps if capsule.neural else 0,
'avg_loss': None,
'device_trained_on': 'cpu',
},
# Language System Details
'atomic_language': {
'enabled': bool(capsule.language),
'concept_count': capsule.language.total_concepts if capsule.language else 0,
'dialect_signature': str(capsule.language.dialect_signature) if capsule.language else 'N/A',
},
# Configuration & Environment
'atomic_config': {
'enabled': bool(capsule.config),
'atom_count': len(capsule.config.atoms) if capsule.config else 0,
},
'environment_context': capsule.environment.to_dict() if capsule.environment else {},
# Highlander & Social Data
'highlander_data': capsule.highlander.to_dict() if capsule.highlander else {},
'social_connections': {}, # Not stored in capsule directly
# VP (Vitality-Pleasure) State - CRITICAL for runtime behavior
'vp_state': {
'enabled': bool(capsule.vp),
'vitality': capsule.vp.vitality if capsule.vp else None,
'pleasure': capsule.vp.pleasure if capsule.vp else None,
'violation_pressure': capsule.vp.violation_pressure if capsule.vp else None,
'trajectory_length': len(capsule.vp.vp_trajectory) if capsule.vp else 0,
'critical_events_count': len(capsule.vp.critical_events) if capsule.vp else 0,
},
# Causation Trace
'causation_trace': {
'enabled': bool(capsule.causation),
'key_event_count': len(capsule.causation.key_events) if capsule.causation else 0,
'turning_point_count': len(capsule.causation.turning_points) if capsule.causation else 0,
'causal_chain_count': len(capsule.causation.causal_chains) if capsule.causation else 0,
},
# Export Options (to be added by the compiler)
'export_format': None,
'runtime_dependencies': {
'onnxruntime': onnxruntime.__version__ if ONNX_AVAILABLE else 'not installed',
'numpy': np.__version__,
'python': sys.version.split(' ')[0]
},
'compatibility': {
'python_versions': ['3.8+', '3.9+', '3.10+', '3.11+', '3.12+'],
'platforms': ['windows', 'linux', 'macos'],
'architectures': ['x64', 'arm64']
}
}
return metadata
def _compute_behavioral_fingerprint(self, brain: OrganismBrain, num_samples: int = 100) -> Dict[str, Any]:
"""
Compute a behavioral fingerprint by sampling the brain's decision tendencies.
This runs multiple random states through the network and analyzes:
- Action distribution (which actions does it prefer?)
- Decision confidence (how certain is it?)
- Response patterns (how does it react to different input ranges?)
Returns a dictionary with behavioral metrics that can be used for:
- Clustering organisms by behavior
- Filtering populations for specific traits
- Visualizing behavioral space
"""
brain.eval()
action_counts = {i: 0 for i in range(brain.output_dim)}
q_value_sums = {i: 0.0 for i in range(brain.output_dim)}
confidence_scores = []
# Response patterns for different input scenarios
low_energy_actions = [] # When energy-related inputs are low
high_threat_actions = [] # When threat signals are high
social_actions = [] # When social signals are present
with torch.no_grad():
for i in range(num_samples):
# Generate random state vector
state = torch.rand(1, brain.input_dim)
# Get Q-values
q_values = brain(state)
if isinstance(q_values, tuple):
q_values = q_values[0] # Handle multi-head output
q_np = q_values.squeeze().numpy()
# Track action selection
action = int(np.argmax(q_np))
action_counts[action] += 1
# Track Q-value magnitudes per action
for j, qv in enumerate(q_np):
q_value_sums[j] += float(qv)
# Track confidence (max Q minus mean Q)
confidence = float(np.max(q_np) - np.mean(q_np))
confidence_scores.append(confidence)
# Scenario-specific responses
# Low energy scenario (dims 6-8 low)
low_energy_state = state.clone()
low_energy_state[0, 6:9] = 0.1
le_q = brain(low_energy_state)
if isinstance(le_q, tuple):
le_q = le_q[0]
low_energy_actions.append(int(torch.argmax(le_q).item()))
# High threat scenario (dims 9-11 high)
high_threat_state = state.clone()
high_threat_state[0, 9:12] = 0.9
ht_q = brain(high_threat_state)
if isinstance(ht_q, tuple):
ht_q = ht_q[0]
high_threat_actions.append(int(torch.argmax(ht_q).item()))
# Social scenario (cooperative signals)
social_state = state.clone()
social_state[0, 15:18] = 0.8
soc_q = brain(social_state)
if isinstance(soc_q, tuple):
soc_q = soc_q[0]
social_actions.append(int(torch.argmax(soc_q).item()))
# Compute action distribution (normalized)
total_actions = sum(action_counts.values())
action_distribution = {
ACTION_MAP.get(k, f'action_{k}'): round(v / total_actions, 4)
for k, v in action_counts.items()
}
# Compute average Q-values per action
avg_q_values = {
ACTION_MAP.get(k, f'action_{k}'): round(v / num_samples, 4)
for k, v in q_value_sums.items()
}
# Dominant action (most frequently chosen)
dominant_action_idx = max(action_counts, key=action_counts.get)
dominant_action = ACTION_MAP.get(dominant_action_idx, f'action_{dominant_action_idx}')
# Behavioral tendencies (simplified categories)
cooperative_score = action_distribution.get('cooperate', 0) + action_distribution.get('reproduce', 0) * 0.5
competitive_score = action_distribution.get('compete', 0) + action_distribution.get('move', 0) * 0.3
passive_score = action_distribution.get('rest', 0) + action_distribution.get('isolate', 0)
# Scenario response analysis
def mode_action(actions):
if not actions:
return 'unknown'
counts = {}
for a in actions:
counts[a] = counts.get(a, 0) + 1
mode_idx = max(counts, key=counts.get)
return ACTION_MAP.get(mode_idx, f'action_{mode_idx}')
return {
'action_distribution': action_distribution,
'avg_q_values': avg_q_values,
'dominant_action': dominant_action,
'dominant_action_percentage': round(action_counts[dominant_action_idx] / total_actions * 100, 1),
'decision_confidence': {
'mean': round(float(np.mean(confidence_scores)), 4),
'std': round(float(np.std(confidence_scores)), 4),
'min': round(float(np.min(confidence_scores)), 4),
'max': round(float(np.max(confidence_scores)), 4)
},
'behavioral_tendencies': {
'cooperative': round(cooperative_score, 4),
'competitive': round(competitive_score, 4),
'passive': round(passive_score, 4)
},
'scenario_responses': {
'low_energy': mode_action(low_energy_actions),
'high_threat': mode_action(high_threat_actions),
'social_opportunity': mode_action(social_actions)
},
'behavioral_vector': [
round(cooperative_score, 4),
round(competitive_score, 4),
round(passive_score, 4),
round(float(np.mean(confidence_scores)), 4)
],
'personality_label': self._classify_personality(cooperative_score, competitive_score, passive_score)
}
def _classify_personality(self, coop: float, comp: float, passive: float) -> str:
"""Classify organism into a personality archetype based on behavioral tendencies."""
max_trait = max(coop, comp, passive)
if max_trait < 0.2:
return "balanced"
elif coop == max_trait:
if comp > 0.2:
return "diplomatic" # Cooperative but will compete if needed
else:
return "altruist" # Strongly cooperative
elif comp == max_trait:
if coop > 0.2:
return "opportunist" # Competitive but can cooperate
else:
return "aggressor" # Strongly competitive
elif passive == max_trait:
if coop > comp:
return "pacifist" # Passive and cooperative
else:
return "hermit" # Passive and isolated
return "complex"
def _merge_capsule_language_data(self, capsules: List['OrganismCapsule']) -> Optional[Dict[str, Any]]:
"""
Merge language data from multiple capsules into a unified vocabulary.
This creates a combined vocabulary that includes:
- All unique concepts from all capsules
- Merged word frequencies (summed)
- Aggregated dialect signatures (averaged)
- Union of all semantic associations
Args:
capsules: List of OrganismCapsule objects
Returns:
Merged language dictionary, or None if no capsules have language data
"""
merged = {
'vocabulary': [],
'word_frequencies': {},
'concepts': {},
'semantic_associations': {},
'dialect_signatures': [],
'total_concepts': 0,
'source_organisms': [],
'ensemble_merged': True
}
has_language = False
for cap in capsules:
if not cap.language:
continue
has_language = True
lang_data = cap.language.to_dict() if hasattr(cap.language, 'to_dict') else cap.language
# Track source organism
merged['source_organisms'].append(str(cap.organism_id))
# Handle LanguageSnapshot format (atoms, concept_order, etc.)
# OR legacy format (vocabulary, word_frequencies, etc.)
# Extract vocabulary from atoms or concept_order
if 'atoms' in lang_data:
# LanguageSnapshot format - extract concept names as vocabulary
for concept_id in lang_data['atoms'].keys():
if concept_id not in merged['vocabulary']:
merged['vocabulary'].append(concept_id)
# Also merge atom data as concepts
for concept_id, atom_data in lang_data['atoms'].items():
if concept_id not in merged['concepts']:
merged['concepts'][concept_id] = atom_data
else:
# Merge strengths by taking max
existing = merged['concepts'][concept_id]
if isinstance(atom_data, dict) and isinstance(existing, dict):
if atom_data.get('strength', 0) > existing.get('strength', 0):
merged['concepts'][concept_id] = atom_data
# Also check concept_order for vocabulary
if 'concept_order' in lang_data:
for concept in lang_data['concept_order']:
if concept not in merged['vocabulary']:
merged['vocabulary'].append(concept)
# Legacy format support
if 'vocabulary' in lang_data:
for word in lang_data['vocabulary']:
if word not in merged['vocabulary']:
merged['vocabulary'].append(word)
# Merge word frequencies (sum them)
if 'word_frequencies' in lang_data:
for word, freq in lang_data['word_frequencies'].items():
merged['word_frequencies'][word] = merged['word_frequencies'].get(word, 0) + freq
# Legacy concepts format
if 'concepts' in lang_data:
for concept_id, concept_data in lang_data['concepts'].items():
if concept_id not in merged['concepts']:
merged['concepts'][concept_id] = concept_data
# Merge semantic associations
if 'semantic_associations' in lang_data:
for word, associations in lang_data['semantic_associations'].items():
if word not in merged['semantic_associations']:
merged['semantic_associations'][word] = associations
else:
# Merge association lists
existing = set(merged['semantic_associations'][word])
existing.update(associations)
merged['semantic_associations'][word] = list(existing)
# Collect dialect signatures for averaging
if 'dialect_signature' in lang_data:
merged['dialect_signatures'].append(lang_data['dialect_signature'])
if not has_language:
return None
# Finalize merged data
merged['total_concepts'] = len(merged['concepts']) + len(merged['vocabulary'])
# Average dialect signatures if we have multiple
if merged['dialect_signatures']:
import numpy as np
try:
avg_dialect = np.mean(merged['dialect_signatures'], axis=0).tolist()
merged['dialect_signature'] = avg_dialect
except Exception:
merged['dialect_signature'] = merged['dialect_signatures'][0] if merged['dialect_signatures'] else []
# Remove the list now that we've computed average
del merged['dialect_signatures']
logger.info(f"Merged language data from {len(merged['source_organisms'])} organisms: "
f"{merged['total_concepts']} concepts, {len(merged['vocabulary'])} words")
return merged
def _build_agent_state_payload(self,
capsule: OrganismCapsule,
metadata: Dict[str, Any]) -> Dict[str, bytes]:
"""Prepare serialized state/config artifacts for the portable agent runtime."""
state = AgentState(
organism_id=capsule.organism_id,
generation=int(metadata.get('organism_core', {}).get('generation') or 0),
age=int(metadata.get('organism_core', {}).get('organism_age') or 0),
fitness=float(metadata.get('organism_core', {}).get('fitness') or 0.5),
resources=metadata.get('organism_core', {}).get('resources', 100.0) or 100.0,
health=1.0
)
if capsule.fitness and capsule.fitness.fitness_history:
history: List[float] = []
for record in capsule.fitness.fitness_history:
if isinstance(record, (list, tuple)) and len(record) > 1:
history.append(float(record[1]))
elif isinstance(record, dict) and 'fitness' in record:
history.append(float(record['fitness']))
else:
try:
history.append(float(record))
except Exception:
continue
state.fitness_history = history[:1000]
if capsule.highlander:
state.battle_wins = int(getattr(capsule.highlander, 'battles_won', 0))
state.battle_losses = int(getattr(capsule.highlander, 'battles_lost', 0))
total_battles = state.battle_wins + state.battle_losses
if total_battles:
state.alliance_reputation = state.battle_wins / max(total_battles, 1)
if capsule.language:
state.vocabulary_size = int(getattr(capsule.language, 'total_concepts', 0))
runtime_config = {
'buffer_size': 10000,
'gamma': 0.99,
'learning_rate': 0.001,
'epsilon_start': state.epsilon,
'epsilon_min': state.epsilon_min,
'epsilon_decay': state.epsilon_decay,
'brain_format': metadata.get('export_format'),
'notes': 'Autogenerated by AgentCompiler'
}
return {
'state.json': json.dumps(state.to_dict(), indent=2).encode('utf-8'),
'config.json': json.dumps(runtime_config, indent=2).encode('utf-8'),
'experience_buffer.pkl': pickle.dumps([])
}
def _write_agent_state_bundle(self,
archive: zipfile.ZipFile,
payload: Optional[Dict[str, bytes]]) -> None:
if not payload:
return
for filename, blob in payload.items():
archive.writestr(f"agent_state/{filename}", blob)
def _write_portable_agent_sources(self, archive: zipfile.ZipFile) -> None:
if not PORTABLE_AGENT_DIR.exists():
logger.warning("Portable agent directory missing; skipping runtime bundling.")
return
for file_path in PORTABLE_AGENT_DIR.glob('*.py'):
archive.writestr(
f"portable_agent/{file_path.name}",
file_path.read_text(encoding='utf-8')
)
def _generate_runner_script(self, export_format: str, metadata: Dict[str, Any]) -> str:
"""Generates a living agent demo script."""
action_map_str = json.dumps(ACTION_MAP)
script_template = """
import argparse
import json
import os
from portable_agent import AgentRuntime, MiniEnvironment, GymAdapter, TrainingLoop
ACTION_MAP = {action_map_str}
class LivingAgentRunner:
def __init__(self,
model_filename="{model_filename}",
metadata_filename="metadata.json",
state_dir="agent_state"):
self.model_filename = model_filename
self.metadata_filename = metadata_filename
self.state_dir = state_dir
if not os.path.exists(self.model_filename):
raise FileNotFoundError(f"Model file not found: {{self.model_filename}}")
if not os.path.exists(self.metadata_filename):
raise FileNotFoundError(f"Metadata file not found: {{self.metadata_filename}}")
if not os.path.isdir(self.state_dir):
raise FileNotFoundError(f"Agent state directory not found: {{self.state_dir}}")
with open(self.metadata_filename, "r", encoding="utf-8") as handle:
self.metadata = json.load(handle)
self.agent = AgentRuntime.load(self.state_dir, brain_path=self.model_filename)
def _load_gym_environment(self, spec: str, seed: int | None):
try:
import gymnasium as gym
except ImportError:
try:
import gym # type: ignore
except ImportError as exc: # pragma: no cover - optional dependency
raise RuntimeError(
"Gym or Gymnasium is required for --gym-env usage. Install gymnasium>=0.29."
) from exc
env = gym.make(spec)
if seed is not None:
try:
env.reset(seed=seed)
except TypeError:
pass
return env
def _build_environment(self, gym_env: str | None, seed: int | None):
if gym_env:
return GymAdapter(self._load_gym_environment(gym_env, seed))
return MiniEnvironment(seed=seed)
def run(self,
episodes: int = 3,
max_steps: int | None = 300,
explore: bool = True,
learn: bool = True,
gym_env: str | None = None,
seed: int | None = None):
environment = self._build_environment(gym_env, seed)
loop = TrainingLoop(
agent=self.agent,
environment=environment,
episodes=episodes,
max_steps=max_steps,
explore=explore,
learn=learn
)
history = loop.run()
self.agent.save(self.state_dir)
return history
def main():
parser = argparse.ArgumentParser(description="Run the exported Butterfly agent in a portable environment.")
parser.add_argument("--episodes", type=int, default=3, help="Number of demo episodes to play.")
parser.add_argument("--max-steps", type=int, default=300, help="Max steps per episode.")
parser.add_argument("--gym-env", type=str, default=None, help="Optional Gym/Gymnasium env spec (e.g., CartPole-v1).")
parser.add_argument("--seed", type=int, default=None, help="Deterministic seed for MiniEnvironment or Gym.")
parser.add_argument("--model", type=str, default="{model_filename}", help="Brain filename inside the archive.")
parser.add_argument("--metadata", type=str, default="metadata.json", help="Metadata filename.")
parser.add_argument("--state-dir", type=str, default="agent_state", help="Directory that stores agent state.")
parser.add_argument("--no-learn", action="store_true", help="Disable learning and run in inference-only mode.")
parser.add_argument("--exploit", action="store_true", help="Disable epsilon exploration for deterministic runs.")
args = parser.parse_args()
runner = LivingAgentRunner(
model_filename=args.model,
metadata_filename=args.metadata,
state_dir=args.state_dir
)
history = runner.run(
episodes=args.episodes,
max_steps=args.max_steps,
explore=not args.exploit,
learn=not args.no_learn,
gym_env=args.gym_env,
seed=args.seed
)
for episode in history:
print(
f"Episode {{episode['episode']}} | steps={{episode['steps']}} | reward={{episode['total_reward']:.2f}}"
)
if __name__ == "__main__":
main()
"""
return script_template.format(
action_map_str=action_map_str,
model_filename=f"brain.{export_format}"
)
def _create_agent_archive(self,
model_buffer: BytesIO,
metadata: Dict[str, Any],
runner_script: str,
capsule: OrganismCapsule,
agent_state_payload: Optional[Dict[str, bytes]] = None) -> BytesIO:
"""Packages all components into a ZIP archive."""
archive_buffer = BytesIO()
with zipfile.ZipFile(archive_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
# 1. Neural Model
model_buffer.seek(0) # Ensure buffer is at the beginning
zf.writestr(f"brain.{metadata['export_format']}", model_buffer.read())
# 2. Metadata (JSON)
zf.writestr("metadata.json", json.dumps(metadata, indent=2))
# 3. Genotype (JSON)
if capsule.traits:
zf.writestr("genotype.json", json.dumps(capsule.traits.to_dict(), indent=2))
# 4. Atomic Config (JSON)
if capsule.config:
zf.writestr("atomic_config.json", json.dumps(capsule.config.to_dict(), indent=2))
# 5. Bridge Config (JSON) - Critical for AgentBridge to know state dimensions
input_dim = metadata.get('neural_network', {}).get('architecture', {}).get('input_size', 24)
arch_info = metadata.get('neural_network', {}).get('architecture', {})
bridge_config = {
'state_dim': input_dim,
'num_actions': 6,
'action_names': ['move', 'cooperate', 'compete', 'rest', 'reproduce', 'isolate'],
'epsilon': 0.1,
'epsilon_decay': 0.995,
'epsilon_min': 0.01,
'learning_rate': 0.001,
'gamma': 0.99,
'batch_size': 32,
'max_response_length': 32,
'temperature': 1.0,
'default_port': 8080,
'has_language_head': arch_info.get('has_language_head', False),
'has_attention': arch_info.get('has_attention', False),
'has_concept_head': arch_info.get('has_concept_head', False),
'vocab_size': arch_info.get('vocab_size', 1000)
}
zf.writestr("bridge_config.json", json.dumps(bridge_config, indent=2))
# 6. Atomic Language (JSON)
if capsule.language:
zf.writestr("atomic_language.json", json.dumps(capsule.language.to_dict(), indent=2))
else:
# Write empty language file - bridge.py will use default vocabulary
empty_language = {
'vocabulary': [],
'word_frequencies': {},
'concepts': {},
'semantic_associations': {},
'dialect_signature': None,
'total_concepts': 0,
'source_note': 'No language training data available'
}
zf.writestr("atomic_language.json", json.dumps(empty_language, indent=2))
# 7. VP State (JSON) - Vitality-Pleasure for runtime behavior
if capsule.vp:
zf.writestr("vp_state.json", json.dumps(capsule.vp.to_dict(), indent=2))
else:
# Default VP state for agents without VP history
default_vp = {
'vitality': 0.5,
'pleasure': 0.5,
'violation_pressure': 0.0,
'vitality_history': [],
'pleasure_history': [],
'vp_trajectory': [],
'critical_events': [],
'source_note': 'Default VP state - no simulation history'
}
zf.writestr("vp_state.json", json.dumps(default_vp, indent=2))
# 8. Runner Script
zf.writestr("run_agent.py", runner_script)
# 7. Requirements.txt
requirements = "# Butterfly Agent - Dependencies\n"
requirements += "# Install with: pip install -r requirements.txt\n\n"
# Core dependencies based on export format
if metadata['export_format'] == 'onnx':
requirements += "# Neural network inference (ONNX)\n"
requirements += "onnxruntime>=1.15.0\n"
elif metadata['export_format'] == 'torchscript':
requirements += "# Neural network inference (PyTorch)\n"
requirements += "torch>=2.0.0\n"
elif metadata['export_format'] == 'statedict':
requirements += "# Neural network inference (PyTorch state dict)\n"
requirements += "torch>=2.0.0\n"
requirements += "numpy>=1.21.0\n\n"
# Bridge/visualizer dependencies
requirements += "# AgentBridge HTTP server & Visualizer\n"
requirements += "flask>=2.0.0\n\n"
# Gymnasium environments (NEW - comprehensive)
requirements += "# ========================================\n"
requirements += "# GYMNASIUM ENVIRONMENTS - Learning Playground!\n"
requirements += "# ========================================\n"
requirements += "# 400+ environments to train/test your agent\n\n"
requirements += "# Core gymnasium (63 built-in environments)\n"
requirements += "gymnasium>=0.29.0\n\n"
requirements += "# Classic Control (CartPole, MountainCar, Pendulum, etc)\n"
requirements += "# Already included in gymnasium core!\n\n"
requirements += "# Visual rendering (required for --render flag)\n"
requirements += "pygame>=2.5.0\n\n"
requirements += "# Atari Arcade Games (100+ classic games!)\n"
requirements += "# Pac-Man, Breakout, Space Invaders, Pong, etc.\n"
requirements += "ale-py>=0.8.0\n\n"
requirements += "# MuJoCo Robotics (Humanoid, Ant, HalfCheetah, etc)\n"
requirements += "# pip install gymnasium[mujoco]\n"
requirements += "# mujoco>=2.3.0\n\n"
requirements += "# ========================================\n"
requirements += "# USAGE EXAMPLES:\n"
requirements += "# ========================================\n"
requirements += "# python bridge.py . --mode gym --gym-env CartPole-v1 --render\n"
requirements += "# python bridge.py . --mode gym --gym-env ALE/Breakout-v5 --online-learn\n"
requirements += "# ========================================\n"
requirements += "# OPTIONAL GPU ACCELERATION\n"
requirements += "# ========================================\n"
requirements += "# onnxruntime-gpu>=1.15.0 # NVIDIA CUDA\n"
zf.writestr("requirements.txt", requirements)
# 8. README
readme_content = f"""# 🦋 Butterfly System - Exported Neural Agent
## What Is This?
This archive contains a **living AI agent** exported from The Butterfly System - a quantum-genetic
consciousness simulation where neural organisms evolve, learn, and develop emergent intelligence.
**This is not a static model.** It's a complete organism snapshot that can:
- Continue learning from new experiences
- Make real-time decisions in any environment
- Persist its memories and growth across sessions
---
## 🧬 Agent Identity
| Property | Value |
|----------|-------|
| **Organism ID** | `{capsule.organism_id}` |
| **Fitness Score** | {f"`{metadata['organism_core']['fitness']:.6f}`" if metadata['organism_core']['fitness'] is not None else 'N/A'} {('⭐' * min(5, int((metadata['organism_core']['fitness'] or 0) * 5))) if metadata['organism_core']['fitness'] else ''} |
| **Generation** | `{metadata['organism_core'].get('generation', 'unknown')}` |
| **Age** | `{metadata['organism_core'].get('organism_age', 'unknown')}` simulation cycles |
| **Export Format** | `{metadata['export_format'].upper()}` |
| **Exported** | `{metadata['export_timestamp']}` |
---
## 🧠 Neural Architecture Deep Dive
### The Brain Structure
This agent uses a **Deep Q-Network (DQN)** architecture with multi-head outputs:
```
Input Layer ({metadata['neural_network']['architecture'].get('input_size', '?')} neurons)
┌─────────────────────────────────────────────────────────────┐
│ HIDDEN LAYERS │
│ Dense({metadata['neural_network']['architecture'].get('hidden_size', '?')}) → {metadata['neural_network']['architecture'].get('activation', 'ReLU')} → Dropout(0.1) │
│ Dense({metadata['neural_network']['architecture'].get('hidden_size', '?')}) → {metadata['neural_network']['architecture'].get('activation', 'ReLU')} → Dropout(0.1) │
└─────────────────────────────────────────────────────────────┘
├──► ACTION HEAD ({metadata['neural_network']['architecture'].get('output_size', '?')} outputs) → Q-values for each action
├──► CONCEPT HEAD {'✅' if metadata['neural_network']['architecture'].get('use_concept_head') else '❌'} → Abstract concept embeddings
└──► LANGUAGE HEAD {'✅' if metadata['neural_network']['architecture'].get('use_language_head') else '❌'} → Vocabulary probability distribution
```
### How Decisions Are Made
1. **Perception**: The agent receives a state vector representing its environment
2. **Forward Pass**: State flows through the neural network
3. **Q-Value Computation**: Each possible action gets a "quality" score
4. **Action Selection**:
- **Exploration mode**: Epsilon-greedy (random actions with probability ε)
- **Exploitation mode**: Argmax over Q-values (best predicted action)
5. **Learning**: After acting, the agent uses TD-learning to update its network
### The Input State Vector
The agent expects a **{metadata['neural_network']['architecture'].get('input_size', '?')}-dimensional** input representing:
| Dimensions | Meaning |
|------------|---------|
| 0-2 | Position (x, y, z or similar spatial encoding) |
| 3-5 | Velocity / movement vector |
| 6-8 | Energy, health, resource levels |
| 9-11 | Social signals (nearby organisms, threats) |
| 12+ | Environmental features, memory traces |
*Actual semantics depend on your target environment. The agent will adapt.*
### The Output Actions
| Index | Action | Behavioral Meaning |
|-------|--------|-------------------|
| 0 | `move` | Navigate through space, seek resources or safety |
| 1 | `cooperate` | Form alliances, share resources, mutual aid |
| 2 | `compete` | Contest resources, establish dominance |
| 3 | `rest` | Conserve energy, heal, consolidate learning |
| 4 | `reproduce` | Attempt to create offspring (if fitness allows) |
| 5 | `isolate` | Withdraw from social contact, self-preservation |
---
## 🔬 How This Agent Was Evolved
This organism emerged through **neuroevolution** - a process combining:
### 1. Genetic Algorithm
- **Selection**: Organisms compete for survival based on fitness
- **Crossover**: Successful organisms combine neural weights with mates
- **Mutation**: Random perturbations introduce novel behaviors
### 2. Reinforcement Learning
- **Experience Replay**: Memories are stored and replayed for efficient learning
- **Temporal Difference**: Q-values are bootstrapped from future predictions
- **Dual Inheritance**: Both genetic (slow) and memetic (fast) learning channels
### 3. Social Evolution
- **Alliance Formation**: Cooperative organisms share fitness benefits
- **Competition Pressure**: Limited resources force behavioral specialization
- **Emergent Communication**: Language heads can develop shared vocabularies
---
## 📦 Archive Contents
```
{capsule.organism_id[:16]}/
├── 🧠 brain.{metadata['export_format']} # Neural network weights ({metadata['export_format'].upper()} format)
├── 📋 metadata.json # Complete organism state & history
├── 🧬 genotype.json # Genetic blueprint (traits, mutations)
├── ⚙️ atomic_config.json # Runtime configuration
├── 🗣️ atomic_language.json # Learned vocabulary & linguistic knowledge
├── 🧪 agent_state/ # Persistent state (replay buffer, config)
│ ├── state.json # Runtime state (epsilon, step count)
│ ├── config.json # Agent hyperparameters
│ └── replay_buffer.pkl # Experience memory (if any)
├── 🧩 portable_agent/ # Runtime code
│ ├── bridge.py # 🌉 Universal interface (Gym, HTTP, CLI)
│ ├── agent_runtime.py # Core AgentRuntime class
│ ├── mini_environment.py # Built-in test environment
│ ├── gym_adapter.py # Gymnasium/Gym bridge
│ ├── training.py # TrainingLoop helper
│ └── visualize.py # 🔬 Neural activation visualizer
├── 🚀 start.bat / start.sh # Quick launch: Interactive chat mode
├── 🌐 serve.bat / serve.sh # Quick launch: HTTP API server
├── 🐍 run_agent.py # Legacy CLI runner script
├── 📦 requirements.txt # Python dependencies
└── 📖 README.md # This file
```
---
## 🚀 Quick Start
### Option 1: Double-Click Launch (Easiest!)
```
Windows: Double-click start.bat → Interactive chat mode
Double-click serve.bat → HTTP API server on port 8080
Linux/Mac: chmod +x start.sh && ./start.sh → Interactive chat
chmod +x serve.sh && ./serve.sh → HTTP server
```
### Option 2: AgentBridge Commands
```bash
# Extract and install
unzip agent_*.zip && cd agent_*/
pip install -r requirements.txt
# Interactive chat mode
python -m portable_agent.bridge --mode interactive
# HTTP API server (for external applications)
python -m portable_agent.bridge --mode serve --port 8080
# Run in Gym environment
python -m portable_agent.bridge --mode gym --gym-env CartPole-v1
```
### Option 3: Legacy Runner
```bash
python run_agent.py --episodes 5
python run_agent.py --gym-env CartPole-v1 --episodes 10
```
### Option 4: 🔬 Neural Activation Visualizer
```bash
python portable_agent/visualize.py
```
### Option 5: Python Integration (Direct)
```python
from portable_agent import AgentRuntime, MiniEnvironment
# Load the agent
agent = AgentRuntime.load("agent_state", brain_path="brain.{metadata['export_format']}")
env = MiniEnvironment()
state = env.reset()
while not done:
action = agent.act(state)
next_state, reward, done, info = env.step(action)
agent.learn(state, action, reward, next_state, done)
state = next_state
```
---
## 🌉 AgentBridge - Universal Interface
The **AgentBridge** is the recommended way to deploy and interact with this agent.
It provides a unified interface for all interaction modes:
### HTTP API Server
Deploy the agent as a REST API that any application can call:
```bash
python -m portable_agent.bridge --mode serve --port 8080
```
**Endpoints:**
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/act` | Get action for observation/text/context |
| POST | `/chat` | Chat with agent (text in, text out) |
| POST | `/reward` | Provide reward for learning |
| GET | `/state` | Get current agent state |
| GET | `/config` | Get configuration |
| GET | `/health` | Health check |
**Example API Call:**
```python
import requests
# Chat with agent
response = requests.post('http://localhost:8080/chat', json={{
'text': 'Enemy approaching from the north!',
'context': {{'threat_level': 0.8}}
}})
print(response.json())
# {{'response': 'Isolating for safety.', 'action': 'isolate', 'confidence': 0.73}}
# Get action for structured input
response = requests.post('http://localhost:8080/act', json={{
'context': {{'energy': 0.3, 'threat': 0.8, 'food_available': 0.2}}
}})
print(response.json()['action_name']) # 'rest' or 'isolate'
```
### Interactive CLI
Chat with your agent directly:
```bash
python -m portable_agent.bridge --mode interactive
```
```
🦋 AgentBridge Interactive Mode
Type messages to chat with the agent
Commands: /act, /gym, /state, /config, /quit
You: I'm feeling threatened and low on energy
Agent [REST]: Resting to conserve energy.
(confidence: 67.3%)
You: Now there's food nearby!
Agent [MOVE]: Moving to explore the environment.
(confidence: 81.2%)
```
### Python Library Integration
Use the bridge directly in your code:
```python
from portable_agent import AgentBridge
# Load agent
bridge = AgentBridge.load("./")
# Text input (semantic parsing)
result = bridge.process(text="Enemy approaching, low on energy")
print(f"Action: {{result.action_name}}, Response: {{result.response}}")
# Structured context input
result = bridge.process(context={{
'energy': 0.2,
'threat': 0.9,
'friend_nearby': 0.1
}})
print(f"Decision: {{result.action_name}} ({{result.confidence:.1%}} confident)")
# Gym observation input
result = bridge.process(obs=gym_env.reset())
action = result.action
# Provide reward for learning
bridge.reward(reward_value=1.0, done=False)
# Run full Gym episodes
stats = bridge.run_gym("CartPole-v1", episodes=100)
print(f"Mean reward: {{stats['mean_reward']:.2f}}")
```
---
## 🎮 GYMNASIUM PLAYGROUND - 400+ Learning Environments!
Your agent can learn and play in **400+ environments** across multiple categories!
### 🕹️ Classic Control (Built-in)
Simple physics environments perfect for testing:
```bash
python bridge.py . --mode gym --gym-env CartPole-v1 --render # Balance a pole
python bridge.py . --mode gym --gym-env MountainCar-v0 --render # Drive up a hill
python bridge.py . --mode gym --gym-env Pendulum-v1 --render # Swing a pendulum
python bridge.py . --mode gym --gym-env Acrobot-v1 --render # Double pendulum
```
### 👾 Atari Arcade (100+ Classic Games!)
Install: `pip install ale-py`
```bash
python bridge.py . --mode gym --gym-env ALE/Breakout-v5 --render # Break bricks!
python bridge.py . --mode gym --gym-env ALE/Pong-v5 --render # Classic Pong
python bridge.py . --mode gym --gym-env ALE/SpaceInvaders-v5 # Shoot aliens
python bridge.py . --mode gym --gym-env ALE/Pacman-v5 --render # Pac-Man!
python bridge.py . --mode gym --gym-env ALE/Asteroids-v5 # Space shooter
python bridge.py . --mode gym --gym-env ALE/Frogger-v5 --render # Cross the road
python bridge.py . --mode gym --gym-env ALE/DonkeyKong-v5 # Rescue the princess
```
### 🤖 MuJoCo Robotics (Advanced)
Install: `pip install gymnasium[mujoco]`
```bash
python bridge.py . --mode gym --gym-env Humanoid-v4 --render # Walk like a human
python bridge.py . --mode gym --gym-env Ant-v4 --render # 4-legged ant
python bridge.py . --mode gym --gym-env HalfCheetah-v4 --render # Run fast!
python bridge.py . --mode gym --gym-env Hopper-v4 --render # One-legged hopper
python bridge.py . --mode gym --gym-env Swimmer-v4 --render # Swim through fluid
python bridge.py . --mode gym --gym-env Walker2d-v4 --render # 2D walking
```
### 🧠 Online Learning (Train While Playing!)
Enable real-time weight updates with `--online-learn`:
```bash
# Agent learns from experiences AS IT PLAYS
python bridge.py . --mode gym --gym-env CartPole-v1 --episodes 100 --online-learn
# With custom learning rate
python bridge.py . --mode gym --gym-env Pendulum-v1 --online-learn --learning-rate 0.0005
# Watch it learn!
python bridge.py . --mode gym --gym-env CartPole-v1 --render --online-learn --episodes 50
```
### 📊 Full Command Reference
```bash
python bridge.py <agent_dir> --mode gym [options]
Options:
--gym-env, -e Environment name (default: CartPole-v1)
--episodes, -n Number of episodes (default: 10)
--render, -r Show visual window
--online-learn Update weights during play
--learning-rate Learning rate for online learning (default: 0.001)
```
### 🔬 Interactive Gym Commands
In interactive mode (`python bridge.py . --mode interactive`):
```
/gym CartPole-v1 # Run 3 episodes
/gym CartPole-v1 render # With visuals
/gym CartPole-v1 learn # With online learning
/gym CartPole-v1 render learn # Both!
/train # Show training stats
```
---
## ⚔️ PROTON GAME ARENA - Apprentice Adept Style Battles!
> **🙏 ATTRIBUTION**:
>
> 🎮 **Game Selection**: Inspired by "The Game" from **Piers Anthony's "Apprentice Adept"**
> series (1980-1990). The 4x4 grid (PHYSICAL/MENTAL/CHANCE/ARTS × NAKED/TOOL/MACHINE/ANIMAL)
> is the creative work of Piers Anthony. Read: *Split Infinity*, *Blue Adept*, *Juxtaposition*.
>
> ⚔️ **Absorption Battles**: Inspired by **"Highlander" (1986)**, directed by Russell Mulcahy.
> The "Quickening" - where winners absorb the defeated's power, knowledge, and skills -
> directly influenced our neural/concept/trait transfer system. *"There can be only one."*
The Proton Game Arena provides a gamified competition system using the 4x4 game
selection grid from the novels:
```
NAKED TOOL MACHINE ANIMAL
─────────────────────────────────────────────────
PHYSICAL Balance Mountain Pendulum Acrobot
CartPole Car Swing Double
MENTAL Frozen Blackjack Breakout Custom
Lake Cards SpaceInvaders Games
CHANCE Pure Luck+ Machine Genetic
Luck Skill Gambling Lottery
ARTS Language Vocabulary Dialogue Cross-
Coherence Duel Quality Species
```
### Arena Commands (Interactive Mode)
```
/arena # Show game selection grid
/arena games # List all arena games
/arena games physical # Games by category
/arena play 'Balance Beam' # Play specific game
```
### Game Categories
- **PHYSICAL**: Speed, reflexes, coordination challenges
- **MENTAL**: Strategy, planning, puzzle-solving
- **CHANCE**: Luck-based games with probabilistic elements
- **ARTS**: Language, creativity, expression challenges
### Resource Types
- **NAKED**: Pure ability, no augmentation
- **TOOL**: Simple tools to extend capabilities
- **MACHINE**: Complex automation and machinery
- **ANIMAL**: Living partners and symbiosis
---
## 🎯 Integration Guide
### For Robotics / Simulation
```python
# Your custom environment
class RobotEnv:
def reset(self): return np.zeros({metadata['neural_network']['architecture'].get('input_size', 18)}) # Match input dim
def step(self, action): return state, reward, done, info
# Wrap and use
from portable_agent import GymAdapter
env = GymAdapter(RobotEnv())
agent = AgentRuntime.load("agent_state", brain_path="brain.{metadata['export_format']}")
state = env.reset()
action = agent.act(state) # Returns int 0-5
```
### For Game AI
```python
# Map Butterfly actions to your game
GAME_ACTIONS = {{
0: "walk_forward",
1: "help_ally",
2: "attack_enemy",
3: "wait",
4: "special_ability",
5: "retreat"
}}
action_idx = agent.act(game_state_vector)
game_action = GAME_ACTIONS[action_idx]
```
### For Multi-Agent Systems
```python
# Load multiple agents
agents = [AgentRuntime.load(f"agent_{{i}}", brain_path=f"brain_{{i}}.onnx") for i in range(N)]
# Each agent acts independently
actions = [agent.act(shared_state) for agent in agents]
```
---
## 🧬 Genetic Traits
This organism has **{len(capsule.traits.traits) if capsule.traits and hasattr(capsule.traits, 'traits') else 0}** expressed genetic traits:
| Trait Category | Description |
|----------------|-------------|
| **Metabolic** | Energy efficiency, resource processing |
| **Social** | Cooperation tendency, aggression levels |
| **Cognitive** | Learning rate, memory capacity |
| **Physical** | Speed, resilience, reproduction fitness |
Phenotype Cluster: `{capsule.traits.phenotype_cluster if capsule.traits and hasattr(capsule.traits, 'phenotype_cluster') else 'unknown'}`
---
## 🎭 Behavioral Fingerprint
This organism's decision-making patterns were analyzed by sampling 100 random states:
### Personality Profile
| Metric | Value |
|--------|-------|
| **Personality Type** | `{metadata.get('behavioral_fingerprint', {}).get('personality_label', 'unknown')}` |
| **Dominant Action** | `{metadata.get('behavioral_fingerprint', {}).get('dominant_action', 'unknown')}` ({metadata.get('behavioral_fingerprint', {}).get('dominant_action_percentage', 0)}% of decisions) |
| **Cooperative Score** | {metadata.get('behavioral_fingerprint', {}).get('behavioral_tendencies', {}).get('cooperative', 0):.2%} |
| **Competitive Score** | {metadata.get('behavioral_fingerprint', {}).get('behavioral_tendencies', {}).get('competitive', 0):.2%} |
| **Passive Score** | {metadata.get('behavioral_fingerprint', {}).get('behavioral_tendencies', {}).get('passive', 0):.2%} |
### Action Distribution
```
{chr(10).join([f"{k:12}: {'█' * int(v * 50):50} {v:.1%}" for k, v in metadata.get('behavioral_fingerprint', {}).get('action_distribution', {}).items()])}
```
### Scenario Responses
How this organism typically responds to specific situations:
| Scenario | Typical Response |
|----------|-----------------|
| **Low Energy** | `{metadata.get('behavioral_fingerprint', {}).get('scenario_responses', {}).get('low_energy', 'unknown')}` |
| **High Threat** | `{metadata.get('behavioral_fingerprint', {}).get('scenario_responses', {}).get('high_threat', 'unknown')}` |
| **Social Opportunity** | `{metadata.get('behavioral_fingerprint', {}).get('scenario_responses', {}).get('social_opportunity', 'unknown')}` |
### Decision Confidence
- **Mean**: {metadata.get('behavioral_fingerprint', {}).get('decision_confidence', {}).get('mean', 0):.4f}
- **Std Dev**: {metadata.get('behavioral_fingerprint', {}).get('decision_confidence', {}).get('std', 0):.4f}
- **Range**: {metadata.get('behavioral_fingerprint', {}).get('decision_confidence', {}).get('min', 0):.4f} - {metadata.get('behavioral_fingerprint', {}).get('decision_confidence', {}).get('max', 0):.4f}
### Behavioral Vector (for clustering/visualization)
```python
behavioral_vector = {metadata.get('behavioral_fingerprint', {}).get('behavioral_vector', [0, 0, 0, 0])}
# [cooperative, competitive, passive, confidence]
```
---
## 📊 Understanding metadata.json
The metadata file contains the complete organism history:
```json
{{
"organism_core": {{
"organism_id": "...", // Unique identifier
"fitness": 0.xxx, // Survival score (0-1 typically)
"generation": N, // How many generations from genesis
"organism_age": M, // Cycles lived
"parents": [...] // Genetic lineage
}},
"neural_network": {{
"architecture": {{...}}, // Layer sizes, activation functions
"parameter_count": N, // Total trainable parameters
"device": "cpu" // Training device
}},
"genotype": {{...}}, // Raw genetic data
"phenotype": {{...}}, // Expressed traits
"causation_trace": [...] // Key life events (if captured)
}}
```
---
## ⚡ Performance Tips
1. **Use ONNX format** for fastest inference (10-100x faster than Python)
2. **Disable learning** in production: `agent.act(state)` without `agent.learn()`
3. **Batch inference**: Modify to process multiple states at once
4. **GPU acceleration**: `pip install onnxruntime-gpu` for CUDA support
---
## 🔗 Origin: The Butterfly System
This agent emerged from **The Butterfly System** - a consciousness simulation where:
- 🧬 **Organisms evolve** through quantum-genetic algorithms
- 🧠 **Neural networks learn** via reinforcement and evolution
- 🌐 **Societies form** with alliances, competition, language
- 📈 **Fitness landscapes** shift, driving adaptive radiation
- 🦋 **Emergence happens** - complex behaviors from simple rules
**Repository**: https://github.com/Yufok1/Convergence_Engine
---
## 📜 Citation
If you use this agent in research or production:
```bibtex
@software{{butterfly_agent_{capsule.organism_id[:8]},
title = {{Butterfly System - Evolved Neural Agent}},
author = {{The Butterfly System}},
year = {{2025}},
url = {{https://github.com/Yufok1/Convergence_Engine}},
note = {{Organism ID: {capsule.organism_id}, Exported: {metadata['export_timestamp']}}}
}}
```
---
*This organism lived, learned, and evolved. Now it continues in your hands.* 🦋
"""
zf.writestr("README.md", readme_content)
# 9. Launcher scripts for easy startup
# Windows batch file - COMPLETE MENU with all capabilities
start_bat = """@echo off
cd /d "%~dp0"
title Butterfly Agent - Evolved Intelligence
:menu
cls
echo.
echo ╔════════════════════════════════════════════════════════════╗
echo ║ 🦋 BUTTERFLY AGENT - EVOLVED INTELLIGENCE 🦋 ║
echo ╠════════════════════════════════════════════════════════════╣
echo ║ ║
echo ║ This agent evolved in The Butterfly System simulation. ║
echo ║ It has learned behaviors through neural reinforcement. ║
echo ║ ║
echo ╠════════════════════════════════════════════════════════════╣
echo ║ CHOOSE A MODE: ║
echo ║ ║
echo ║ [1] 💬 CHAT MODE - Talk to your agent interactively ║
echo ║ [2] 🌐 HTTP SERVER - REST API on localhost:8080 ║
echo ║ [3] 🎮 GYM MODE - Run in OpenAI Gym environment ║
echo ║ [4] 🔬 VISUALIZER - See neural network activations ║
echo ║ [5] 📊 AGENT INFO - View agent stats and history ║
echo ║ [6] 🐍 PYTHON SHELL - Import and use programmatically ║
echo ║ ║
echo ║ [0] ❌ EXIT ║
echo ║ ║
echo ╚════════════════════════════════════════════════════════════╝
echo.
set /p choice="Enter choice [0-6]: "
if "%choice%"=="1" goto chat
if "%choice%"=="2" goto server
if "%choice%"=="3" goto gym
if "%choice%"=="4" goto visualize
if "%choice%"=="5" goto info
if "%choice%"=="6" goto python
if "%choice%"=="0" goto end
goto menu
:setup
REM Check Python
python --version >nul 2>&1
if errorlevel 1 (
echo.
echo ERROR: Python not found! Please install Python 3.8+
pause
goto menu
)
REM Install deps if needed
if not exist ".deps_installed" (
echo.
echo First run - installing dependencies...
pip install torch numpy flask onnxruntime gymnasium pygame ale-py 2>nul
echo. > .deps_installed
)
goto :eof
:chat
call :setup
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 💬 CHAT MODE - Talk to your evolved agent
echo ════════════════════════════════════════════════════════════
echo.
echo Commands while chatting:
echo /state - See agent's internal state vector
echo /config - View agent configuration
echo /reward [+/-] - Give positive/negative feedback
echo /quit - Return to menu
echo.
echo The agent responds based on its evolved neural network.
echo Try describing situations: "I see danger" or "Resources ahead"
echo.
echo ════════════════════════════════════════════════════════════
echo.
python portable_agent/bridge.py . --mode interactive
pause
goto menu
:server
call :setup
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 🌐 HTTP SERVER MODE - REST API
echo ════════════════════════════════════════════════════════════
echo.
echo Starting server on http://localhost:8080
echo.
echo ENDPOINTS:
echo POST /act {"text": "..."} or {"obs": [...]}
echo → Returns action decision
echo.
echo POST /chat {"message": "hello"}
echo → Chat and get response
echo.
echo POST /reward {"reward": 1.0, "done": false}
echo → Provide learning feedback
echo.
echo GET /state → Current agent state
echo GET /config → Agent configuration
echo GET /health → Health check
echo.
echo Press Ctrl+C to stop server and return to menu.
echo.
echo ════════════════════════════════════════════════════════════
echo.
python portable_agent/bridge.py . --mode serve --port 8080
pause
goto menu
:gym
call :setup
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 🎮 GYM MODE - 400+ Learning Environments!
echo ════════════════════════════════════════════════════════════
echo.
echo ENVIRONMENT CATEGORIES:
echo Classic: CartPole-v1, MountainCar-v0, Pendulum-v1, Acrobot-v1
echo Atari: ALE/Breakout-v5, ALE/Pong-v5, ALE/SpaceInvaders-v5
echo MuJoCo: Humanoid-v4, Ant-v4, HalfCheetah-v4
echo.
set /p gymenv="Enter Gym environment (default: CartPole-v1): "
if "%gymenv%"=="" set gymenv=CartPole-v1
set /p episodes="Number of episodes (default: 10): "
if "%episodes%"=="" set episodes=10
set /p render="Enable visual rendering? (y/n, default: n): "
set /p online="Enable online learning? (y/n, default: n): "
echo.
set renderarg=
set onlinearg=
if /i "%render%"=="y" set renderarg=--render
if /i "%online%"=="y" set onlinearg=--online-learn
echo Running %episodes% episodes in %gymenv%...
echo.
python portable_agent/bridge.py . --mode gym --gym-env %gymenv% --episodes %episodes% %renderarg% %onlinearg%
pause
goto menu
:visualize
call :setup
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 🔬 NEURAL VISUALIZER - See the brain in action
echo ════════════════════════════════════════════════════════════
echo.
echo This opens an interactive visualization of the neural network.
echo Watch activations flow through the network as it processes inputs.
echo.
python portable_agent/visualize.py
pause
goto menu
:info
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 📊 AGENT INFORMATION
echo ════════════════════════════════════════════════════════════
echo.
echo Reading metadata.json...
echo.
type metadata.json
echo.
echo.
echo ════════════════════════════════════════════════════════════
echo.
if exist "atomic_language.json" (
echo Language/Vocabulary loaded: YES
) else (
echo Language/Vocabulary loaded: NO
)
if exist "agent_state\\state.json" (
echo Saved state: YES
) else (
echo Saved state: NO
)
echo.
pause
goto menu
:python
cls
echo.
echo ════════════════════════════════════════════════════════════
echo 🐍 PYTHON INTEGRATION - Use programmatically
echo ════════════════════════════════════════════════════════════
echo.
echo Example code to use this agent in your Python projects:
echo.
echo ─────────────────────────────────────────────────────────
echo from portable_agent.bridge import AgentBridge
echo.
echo # Load the agent
echo agent = AgentBridge.load(".")
echo.
echo # Chat with it
echo result = agent.process(text="I see an enemy")
echo print(result.action_name, result.confidence)
echo.
echo # Or use with observations
echo result = agent.process(obs=[0.5, 0.3, 0.8, ...])
echo.
echo # Give feedback for learning
echo agent.reward(1.0) # positive
echo agent.reward(-1.0) # negative
echo.
echo # Save learned experiences
echo agent.save(".")
echo ─────────────────────────────────────────────────────────
echo.
echo Opening Python shell with agent pre-loaded...
echo.
python -i -c "from portable_agent.bridge import AgentBridge; agent = AgentBridge.load('.'); print('Agent loaded! Use: agent.process(text=\"...\") or agent.process(obs=[...])')"
pause
goto menu
:end
echo.
echo Goodbye! 🦋
echo.
exit /b 0
"""
zf.writestr("start.bat", start_bat)
# Unix shell script - Same complete menu
start_sh = """#!/bin/bash
cd "$(dirname "$0")"
show_menu() {
clear
echo ""
echo " ╔════════════════════════════════════════════════════════════╗"
echo " ║ 🦋 BUTTERFLY AGENT - EVOLVED INTELLIGENCE 🦋 ║"
echo " ╠════════════════════════════════════════════════════════════╣"
echo " ║ ║"
echo " ║ This agent evolved in The Butterfly System simulation. ║"
echo " ║ It has learned behaviors through neural reinforcement. ║"
echo " ║ ║"
echo " ╠════════════════════════════════════════════════════════════╣"
echo " ║ CHOOSE A MODE: ║"
echo " ║ ║"
echo " ║ [1] 💬 CHAT MODE - Talk to your agent interactively ║"
echo " ║ [2] 🌐 HTTP SERVER - REST API on localhost:8080 ║"
echo " ║ [3] 🎮 GYM MODE - Run in OpenAI Gym environment ║"
echo " ║ [4] 🔬 VISUALIZER - See neural network activations ║"
echo " ║ [5] 📊 AGENT INFO - View agent stats and history ║"
echo " ║ [6] 🐍 PYTHON SHELL - Import and use programmatically ║"
echo " ║ ║"
echo " ║ [0] ❌ EXIT ║"
echo " ║ ║"
echo " ╚════════════════════════════════════════════════════════════╝"
echo ""
}
setup() {
if ! command -v python3 &> /dev/null; then
echo "ERROR: Python3 not found!"
read -p "Press Enter to continue..."
return 1
fi
if [ ! -f ".deps_installed" ]; then
echo "First run - installing dependencies..."
pip3 install torch numpy flask onnxruntime gymnasium pygame ale-py 2>/dev/null
touch .deps_installed
fi
return 0
}
while true; do
show_menu
read -p "Enter choice [0-6]: " choice
case $choice in
1)
setup || continue
clear
echo ""
echo " 💬 CHAT MODE - Talk to your evolved agent"
echo " Commands: /state, /config, /reward, /quit"
echo ""
python3 portable_agent/bridge.py . --mode interactive
read -p "Press Enter to continue..."
;;
2)
setup || continue
clear
echo ""
echo " 🌐 HTTP SERVER - http://localhost:8080"
echo " POST /act, /chat, /reward | GET /state, /config"
echo " Press Ctrl+C to stop"
echo ""
python3 portable_agent/bridge.py . --mode serve --port 8080
read -p "Press Enter to continue..."
;;
3)
setup || continue
clear
echo ""
echo " 🎮 GYM MODE - 400+ Learning Environments!"
echo ""
echo " ENVIRONMENT CATEGORIES:"
echo " Classic: CartPole-v1, MountainCar-v0, Pendulum-v1, Acrobot-v1"
echo " Atari: ALE/Breakout-v5, ALE/Pong-v5, ALE/SpaceInvaders-v5"
echo " MuJoCo: Humanoid-v4, Ant-v4, HalfCheetah-v4"
echo ""
read -p "Gym environment (default: CartPole-v1): " gymenv
gymenv=${gymenv:-CartPole-v1}
read -p "Episodes (default: 10): " episodes
episodes=${episodes:-10}
read -p "Enable visual rendering? (y/n, default: n): " render
read -p "Enable online learning? (y/n, default: n): " online
renderarg=""
onlinearg=""
[[ "$render" == "y" || "$render" == "Y" ]] && renderarg="--render"
[[ "$online" == "y" || "$online" == "Y" ]] && onlinearg="--online-learn"
python3 portable_agent/bridge.py . --mode gym --gym-env "$gymenv" --episodes "$episodes" $renderarg $onlinearg
read -p "Press Enter to continue..."
;;
4)
setup || continue
python3 portable_agent/visualize.py
read -p "Press Enter to continue..."
;;
5)
clear
echo ""
echo " 📊 AGENT INFORMATION"
echo ""
cat metadata.json
echo ""
read -p "Press Enter to continue..."
;;
6)
setup || continue
python3 -i -c "from portable_agent.bridge import AgentBridge; agent = AgentBridge.load('.'); print('Agent loaded! Use: agent.process(text=\"...\")') "
;;
0)
echo "Goodbye! 🦋"
exit 0
;;
esac
done
"""
zf.writestr("start.sh", start_sh)
# 10. Living agent runtime bundle
self._write_agent_state_bundle(zf, agent_state_payload)
self._write_portable_agent_sources(zf)
archive_buffer.seek(0)
return archive_buffer
def _create_ensemble_archive(self,
model_buffer: BytesIO,
metadata: Dict[str, Any],
runner_script: str,
capsules: Optional[List['OrganismCapsule']] = None,
vocabulary: Any = None,
conversation_history: List[Dict] = None) -> BytesIO:
"""Package ensemble components into a ZIP archive.
Args:
model_buffer: The compiled neural network model
metadata: Export metadata
runner_script: Python runner script
capsules: Optional list of capsules for language/config extraction
vocabulary: LanguageVocabulary object for chat system tokenization
conversation_history: List of conversation history entries for training data
"""
archive_buffer = BytesIO()
with zipfile.ZipFile(archive_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
# Neural model
model_buffer.seek(0)
zf.writestr(f"brain.{metadata['export_format']}", model_buffer.read())
# Metadata
zf.writestr("metadata.json", json.dumps(metadata, indent=2))
# Bridge Config (JSON) - Critical for AgentBridge to know state dimensions
max_input_dim = metadata.get('ensemble', {}).get('max_input_dim', 24)
# Check if any brain in ensemble has language head from metadata
members = metadata.get('ensemble', {}).get('members', [])
any_language_head = any(m.get('has_language_head', False) for m in members)
member_count = len(members)
bridge_config = {
'state_dim': max_input_dim,
'num_actions': 6,
'action_names': ['move', 'cooperate', 'compete', 'rest', 'reproduce', 'isolate'],
'epsilon': 0.1,
'epsilon_decay': 0.995,
'epsilon_min': 0.01,
'learning_rate': 0.001,
'gamma': 0.99,
'batch_size': 32,
'max_response_length': 32,
'temperature': 1.0,
'default_port': 8080,
'has_language_head': any_language_head,
'is_ensemble': True,
'member_count': member_count,
# Ensemble voting configuration
'voting_strategy': 'fitness_weighted', # Default: weight by organism fitness
'top_k_voters': 5 # For fittest_top_k strategy
}
zf.writestr("bridge_config.json", json.dumps(bridge_config, indent=2))
# Merge language data from all capsules
if capsules:
merged_language = self._merge_capsule_language_data(capsules)
if merged_language:
zf.writestr("atomic_language.json", json.dumps(merged_language, indent=2))
else:
# Write empty language file - bridge.py will use default vocabulary
empty_language = {
'vocabulary': [],
'word_frequencies': {},
'concepts': {},
'semantic_associations': {},
'dialect_signature': None,
'total_concepts': 0,
'source_note': 'No language training data available in ensemble',
'ensemble_merged': True
}
zf.writestr("atomic_language.json", json.dumps(empty_language, indent=2))
# ═══════════════════════════════════════════════════════════════
# CHAT VOCABULARY (LanguageVocabulary from butterfly_chat)
# ═══════════════════════════════════════════════════════════════
# This is SEPARATE from atomic_language - it's the tokenization vocab
# used by the chat system for word<->token mapping
if vocabulary is not None:
chat_vocab_data = {
'word_to_id': dict(getattr(vocabulary, 'word_to_id', {})),
'id_to_word': {str(k): v for k, v in getattr(vocabulary, 'id_to_word', {}).items()},
'vocab_size': getattr(vocabulary, 'vocab_size', 0),
'word_frequencies': dict(getattr(vocabulary, 'word_frequencies', {})),
'word_last_used': dict(getattr(vocabulary, 'word_last_used', {})),
'source_note': 'Chat vocabulary for tokenization - learned words from conversations'
}
zf.writestr("chat_vocabulary.json", json.dumps(chat_vocab_data, indent=2))
logger.info(f"📚 Exported chat vocabulary: {chat_vocab_data['vocab_size']} words")
# ═══════════════════════════════════════════════════════════════
# CONVERSATION HISTORY (Training Data)
# ═══════════════════════════════════════════════════════════════
# The actual chat exchanges that trained the organisms
if conversation_history:
history_data = {
'conversations': conversation_history,
'total_entries': len(conversation_history),
'source_note': 'Training conversation history - prompts and organism responses'
}
zf.writestr("conversation_history.json", json.dumps(history_data, indent=2))
logger.info(f"💬 Exported conversation history: {len(conversation_history)} entries")
# Runner
zf.writestr("run_agent.py", runner_script)
# Requirements
requirements = "# Butterfly Ensemble Agent - Dependencies\n"
requirements += "# Install with: pip install -r requirements.txt\n\n"
if metadata['export_format'] == 'onnx':
requirements += "# Neural network inference (ONNX)\n"
requirements += "onnxruntime>=1.15.0\n"
elif metadata['export_format'] == 'torchscript':
requirements += "# Neural network inference (PyTorch)\n"
requirements += "torch>=2.0.0\n"
requirements += "numpy>=1.21.0\n\n"
requirements += "# AgentBridge HTTP server & Visualizer\n"
requirements += "flask>=2.0.0\n\n"
# Gymnasium environments (NEW - comprehensive)
requirements += "# ========================================\n"
requirements += "# GYMNASIUM ENVIRONMENTS - Learning Playground!\n"
requirements += "# ========================================\n"
requirements += "# 400+ environments to train/test your ensemble\n\n"
requirements += "# Core gymnasium (63 built-in environments)\n"
requirements += "gymnasium>=0.29.0\n\n"
requirements += "# Classic Control (CartPole, MountainCar, Pendulum, etc)\n"
requirements += "# Already included in gymnasium core!\n\n"
requirements += "# Visual rendering (required for --render flag)\n"
requirements += "pygame>=2.5.0\n\n"
requirements += "# Atari Arcade Games (100+ classic games!)\n"
requirements += "# Pac-Man, Breakout, Space Invaders, Pong, etc.\n"
requirements += "ale-py>=0.8.0\n\n"
requirements += "# MuJoCo Robotics (Humanoid, Ant, HalfCheetah, etc)\n"
requirements += "# pip install gymnasium[mujoco]\n"
requirements += "# mujoco>=2.3.0\n\n"
requirements += "# ========================================\n"
requirements += "# ENSEMBLE USAGE EXAMPLES:\n"
requirements += "# ========================================\n"
requirements += "# python bridge.py . --mode gym --gym-env CartPole-v1 --render\n"
requirements += "# python bridge.py . --mode gym --gym-env ALE/Breakout-v5 --online-learn --learning-rate 0.0001\n"
requirements += "# ========================================\n"
requirements += "# OPTIONAL GPU ACCELERATION\n"
requirements += "# ========================================\n"
requirements += "# onnxruntime-gpu>=1.15.0 # NVIDIA CUDA\n"
zf.writestr("requirements.txt", requirements)
member_count = len(metadata.get('ensemble', {}).get('members', []))
member_ids = [m['organism_id'] for m in metadata.get('ensemble', {}).get('members', [])]
member_fitnesses = [m.get('fitness', 'N/A') for m in metadata.get('ensemble', {}).get('members', [])]
readme = f"""# 🦋🦋 Butterfly System - Ensemble Neural Agent
## What Is This?
This archive contains an **ensemble of {member_count} evolved AI organisms** from The Butterfly System.
Each organism has its own neural network, personality, and evolutionary history - now unified into
a single collective intelligence.
**Ensemble Benefits:**
- Multiple perspectives on the same problem
- Diverse behavioral strategies (some aggressive, some cooperative, etc.)
- Robustness through redundancy
- Emergent collective decision-making
---
## 🌐 Ensemble Profile
| Property | Value |
|----------|-------|
| **Member Count** | `{member_count}` organisms |
| **Export Format** | `{metadata['export_format'].upper()}` |
| **Max Input Dim** | `{metadata.get('ensemble', {}).get('max_input_dim', 'unknown')}` dimensions |
| **Exported** | `{metadata['export_timestamp']}` |
---
## 👥 Member Organisms
| # | Organism ID | Fitness |
|---|-------------|---------|
{chr(10).join([f"| {i+1} | `{mid[:24]}...` | {f'{fit:.4f}' if isinstance(fit, (int, float)) else fit} |" for i, (mid, fit) in enumerate(zip(member_ids, member_fitnesses))])}
---
## 🧠 How Ensemble Inference Works
```
Input State Vector
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Brain 1 │ │ Brain 2 │ │ Brain N │
│ (DQN) │ │ (DQN) │ ... │ (DQN) │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│Action: 1│ │Action: 0│ │Action: 3│
│cooperate│ │ move │ │ rest │
└─────────┘ └─────────┘ └─────────┘
```
Each brain independently processes the input and outputs its own action.
You can then:
- **Majority vote**: Most common action wins
- **Weighted vote**: Higher-fitness organisms get more say
- **Action-specific**: Use different organisms for different situations
- **Full output**: See what each organism would do
---
## 🔬 Neural Architecture (Per Member)
Each organism has its own DQN with:
- **Input Layer**: Up to {metadata.get('ensemble', {}).get('max_input_dim', '?')} dimensions (auto-padded)
- **Hidden Layers**: Varies by organism (64-256 neurons typical)
- **Output Layer**: 6 actions (move, cooperate, compete, rest, reproduce, isolate)
- **Multi-Head**: Action head + optional Language/Concept heads
### The Wrapper Architecture
The ensemble uses a `MultiOrganismWrapper` that:
1. Takes a single input tensor
2. Pads/slices to match each brain's expected input size
3. Runs parallel forward passes
4. Returns a tuple of outputs (one per organism)
---
## 📦 Archive Contents
```
ensemble_{metadata['export_timestamp'][:10]}/
├── 🧠 brain.{metadata['export_format']} # Combined ensemble model
├── 📋 metadata.json # Ensemble configuration + member details
├── 🗣️ atomic_language.json # Merged vocabulary from all organisms
├── 🧩 portable_agent/ # Runtime code
│ ├── bridge.py # 🌉 Universal interface (Gym, HTTP, CLI)
│ ├── agent_runtime.py # Core runtime class
│ ├── mini_environment.py # Built-in test environment
│ ├── gym_adapter.py # Gymnasium/Gym bridge
│ ├── training.py # TrainingLoop helper
│ └── visualize.py # 🔬 Neural activation visualizer
├── 🚀 start.bat / start.sh # Quick launch: Interactive chat mode
├── 🌐 serve.bat / serve.sh # Quick launch: HTTP API server
├── 🐍 run_agent.py # Legacy CLI runner
├── 📦 requirements.txt # Python dependencies
└── 📖 README.md # This file
```
---
## 🚀 Quick Start
### Option 1: Double-Click Launch (Easiest!)
```
Windows: Double-click start.bat → Interactive chat mode
Double-click serve.bat → HTTP API server on port 8080
Linux/Mac: chmod +x start.sh && ./start.sh → Interactive chat
chmod +x serve.sh && ./serve.sh → HTTP server
```
### Option 2: AgentBridge Commands
```bash
unzip ensemble_*.zip && cd ensemble_*/
pip install -r requirements.txt
# Interactive chat
python -m portable_agent.bridge --mode interactive
# HTTP API server
python -m portable_agent.bridge --mode serve --port 8080
# Run in Gym environment
python -m portable_agent.bridge --mode gym --gym-env CartPole-v1
```
### Option 2: Run Classic Demo
```bash
python run_agent.py
```
### Option 3: 🔬 Neural Activation Visualizer
```bash
python portable_agent/visualize.py
```
### Option 4: Python Integration
```python
from run_agent import EnsembleRunner
import numpy as np
# Load ensemble
ensemble = EnsembleRunner()
# Create input (will be padded to max_input_dim automatically)
state = np.random.rand({metadata.get('ensemble', {}).get('max_input_dim', 18)})
# Get decisions from ALL organisms
decisions = ensemble.decide_actions(state)
# decisions = {{'org_1': 'move', 'org_2': 'cooperate', ...}}
# Majority vote
from collections import Counter
votes = Counter(decisions.values())
collective_action = votes.most_common(1)[0][0]
print(f"Collective decision: {{collective_action}}")
```
---
## 🎮 GYMNASIUM PLAYGROUND - 400+ Learning Environments!
Your ensemble can learn and play in **400+ environments** across multiple categories!
The collective intelligence votes on actions while learning from shared experiences.
### 🕹️ Classic Control (Built-in)
Simple physics environments perfect for testing ensemble coordination:
```bash
python bridge.py . --mode gym --gym-env CartPole-v1 --render # Balance a pole
python bridge.py . --mode gym --gym-env MountainCar-v0 --render # Drive up a hill
python bridge.py . --mode gym --gym-env Pendulum-v1 --render # Swing a pendulum
python bridge.py . --mode gym --gym-env Acrobot-v1 --render # Double pendulum
```
### 👾 Atari Arcade (100+ Classic Games!)
Install: `pip install ale-py`
```bash
python bridge.py . --mode gym --gym-env ALE/Breakout-v5 --render # Break bricks!
python bridge.py . --mode gym --gym-env ALE/Pong-v5 --render # Classic Pong
python bridge.py . --mode gym --gym-env ALE/SpaceInvaders-v5 # Shoot aliens
python bridge.py . --mode gym --gym-env ALE/Pacman-v5 --render # Pac-Man!
python bridge.py . --mode gym --gym-env ALE/Asteroids-v5 # Space shooter
python bridge.py . --mode gym --gym-env ALE/Frogger-v5 --render # Cross the road
python bridge.py . --mode gym --gym-env ALE/DonkeyKong-v5 # Rescue the princess
```
### 🤖 MuJoCo Robotics (Advanced)
Install: `pip install gymnasium[mujoco]`
```bash
python bridge.py . --mode gym --gym-env Humanoid-v4 --render # Walk like a human
python bridge.py . --mode gym --gym-env Ant-v4 --render # 4-legged ant
python bridge.py . --mode gym --gym-env HalfCheetah-v4 --render # Run fast!
python bridge.py . --mode gym --gym-env Hopper-v4 --render # One-legged hopper
python bridge.py . --mode gym --gym-env Swimmer-v4 --render # Swim through fluid
python bridge.py . --mode gym --gym-env Walker2d-v4 --render # 2D walking
```
### 🧠 Online Learning (Ensemble Learns While Playing!)
Enable real-time weight updates with `--online-learn`:
```bash
# Ensemble learns from experiences AS IT PLAYS
python bridge.py . --mode gym --gym-env CartPole-v1 --episodes 100 --online-learn
# With custom learning rate
python bridge.py . --mode gym --gym-env Pendulum-v1 --online-learn --learning-rate 0.0005
# Watch the ensemble learn together!
python bridge.py . --mode gym --gym-env CartPole-v1 --render --online-learn --episodes 50
```
### 📊 Full Command Reference
```bash
python bridge.py <agent_dir> --mode gym [options]
Options:
--gym-env, -e Environment name (default: CartPole-v1)
--episodes, -n Number of episodes (default: 10)
--render, -r Show visual window
--online-learn Update weights during play (ensemble learns!)
--learning-rate Learning rate for online learning (default: 0.001)
```
### 🔬 Interactive Gym Commands
In interactive mode (`python bridge.py . --mode interactive`):
```
/gym CartPole-v1 # Run 3 episodes
/gym CartPole-v1 render # With visuals
/gym CartPole-v1 learn # With online learning
/gym CartPole-v1 render learn # Both!
/train # Show training stats
```
---
## 🎯 Decision Aggregation Strategies
### 1. Simple Majority Vote
```python
from collections import Counter
decisions = ensemble.decide_actions(state)
action = Counter(decisions.values()).most_common(1)[0][0]
```
### 2. Fitness-Weighted Vote
```python
# In metadata.json, each member has a fitness score
weights = {{m['organism_id']: m['fitness'] for m in metadata['ensemble']['members']}}
weighted_votes = {{}}
for org_id, action in decisions.items():
weighted_votes[action] = weighted_votes.get(action, 0) + weights.get(org_id, 1.0)
action = max(weighted_votes, key=weighted_votes.get)
```
### 3. Specialist Routing
```python
# Use specific organisms for specific situations
if state[0] < 0.3: # Low energy scenario
action = decisions['conservative_organism_id']
else:
action = decisions['aggressive_organism_id']
```
### 4. Full Ensemble Output
```python
# Get raw Q-values from all brains for advanced analysis
outputs = ensemble.get_raw_outputs(state)
# outputs = [(q_values_1,), (q_values_2,), ...]
```
---
## 🌍 Use Cases
### Multi-Agent Simulation
```python
# Each organism controls a different agent in your simulation
for i, (org_id, action) in enumerate(decisions.items()):
agents[i].perform(action)
```
### Ensemble Robustness Testing
```python
# See how organisms diverge on edge cases
divergence = len(set(decisions.values()))
print(f"{{divergence}}/{member_count} unique decisions (higher = more disagreement)")
```
### Behavioral Analysis
```python
# Track which organisms tend toward which behaviors
from collections import defaultdict
behavior_profiles = defaultdict(lambda: defaultdict(int))
for episode in range(100):
decisions = ensemble.decide_actions(get_state())
for org_id, action in decisions.items():
behavior_profiles[org_id][action] += 1
# Now you know each organism's behavioral tendencies
```
---
## 🧬 Why These Organisms?
Each member was selected/evolved through:
1. **Fitness Selection**: Higher survival scores in the simulation
2. **Behavioral Diversity**: Different phenotype clusters represented
3. **Genetic Distance**: Not all clones - actual genetic variety
4. **Age/Experience**: Mix of young adaptable and old wise organisms
This creates an ensemble that's both **competent** (high fitness) and **diverse** (different strategies).
---
## 🎭 Ensemble Behavioral Profile
### Personality Distribution
{chr(10).join([f"- **{personality}**: {count} organism(s)" for personality, count in metadata.get('ensemble', {}).get('aggregate_behavioral_profile', {}).get('personality_distribution', {}).items()])}
### Aggregate Action Tendencies
```
{chr(10).join([f"{k:12}: {'█' * int(v * 50):50} {v:.1%}" for k, v in metadata.get('ensemble', {}).get('aggregate_behavioral_profile', {}).get('action_distribution', {}).items()])}
```
### Collective Behavioral Tendencies
| Tendency | Score |
|----------|-------|
| **Cooperative** | {metadata.get('ensemble', {}).get('aggregate_behavioral_profile', {}).get('behavioral_tendencies', {}).get('cooperative', 0):.2%} |
| **Competitive** | {metadata.get('ensemble', {}).get('aggregate_behavioral_profile', {}).get('behavioral_tendencies', {}).get('competitive', 0):.2%} |
| **Passive** | {metadata.get('ensemble', {}).get('aggregate_behavioral_profile', {}).get('behavioral_tendencies', {}).get('passive', 0):.2%} |
### Member Personality Breakdown
| # | Organism | Personality | Dominant Action |
|---|----------|-------------|-----------------|
{chr(10).join([f"| {i+1} | `{m['organism_id'][:16]}...` | {m.get('behavioral_fingerprint', {}).get('personality_label', 'unknown')} | {m.get('behavioral_fingerprint', {}).get('dominant_action', 'unknown')} |" for i, m in enumerate(metadata.get('ensemble', {}).get('members', []))])}
---
## ⚡ Performance
| Operation | Typical Time |
|-----------|--------------|
| Single forward pass (CPU) | ~1-5ms |
| Full ensemble inference | ~{member_count}-{member_count*5}ms |
| With ONNX Runtime GPU | ~0.1-0.5ms |
For real-time applications, consider:
- Batching multiple state queries
- Using ONNX with GPU acceleration
- Pruning to top-K organisms
---
## 📊 Understanding metadata.json
```json
{{
"export_format": "{metadata['export_format']}",
"export_timestamp": "{metadata['export_timestamp']}",
"ensemble": {{
"member_count": {member_count},
"max_input_dim": {metadata.get('ensemble', {}).get('max_input_dim', 'null')},
"members": [
{{
"organism_id": "...",
"fitness": 0.xxx,
"generation": N,
"input_dim": M,
"output_dim": 6
}},
// ... one per organism
]
}}
}}
```
---
## 🔗 Origin: The Butterfly System
These organisms evolved together in **The Butterfly System** - a consciousness simulation where:
- 🧬 **Populations evolve** through genetic algorithms
- 🧠 **Individuals learn** via reinforcement learning
- 🌐 **Societies form** with complex social dynamics
- 🦋 **Emergence happens** - intelligence from simple rules
**Repository**: https://github.com/Yufok1/Convergence_Engine
---
## 📜 Citation
```bibtex
@software{{butterfly_ensemble,
title = {{Butterfly System - Ensemble Neural Agents}},
author = {{The Butterfly System}},
year = {{2025}},
url = {{https://github.com/Yufok1/Convergence_Engine}},
note = {{{member_count} organisms, Exported: {metadata['export_timestamp']}}}
}}
```
---
*{member_count} minds evolved together. Now they think as one.* 🦋🦋
"""
zf.writestr("README.md", readme)
# Launcher scripts - Full menu (same as single agent)
# Windows batch file
start_bat = """@echo off
cd /d "%~dp0"
title Butterfly Ensemble - Collective Intelligence
:menu
cls
echo.
echo ╔════════════════════════════════════════════════════════════╗
echo ║ 🦋🦋 BUTTERFLY ENSEMBLE - COLLECTIVE INTELLIGENCE 🦋🦋 ║
echo ╠════════════════════════════════════════════════════════════╣
echo ║ ║
echo ║ This ensemble contains multiple evolved organisms ║
echo ║ working together as a collective intelligence. ║
echo ║ ║
echo ╠════════════════════════════════════════════════════════════╣
echo ║ CHOOSE A MODE: ║
echo ║ ║
echo ║ [1] 💬 CHAT MODE - Talk to the collective ║
echo ║ [2] 🌐 HTTP SERVER - REST API on localhost:8080 ║
echo ║ [3] 🎮 GYM MODE - Run in OpenAI Gym environment ║
echo ║ [4] 🔬 VISUALIZER - See neural network activations ║
echo ║ [5] 📊 ENSEMBLE INFO - View member stats and profiles ║
echo ║ [6] 🐍 PYTHON SHELL - Import and use programmatically ║
echo ║ ║
echo ║ [0] ❌ EXIT ║
echo ║ ║
echo ╚════════════════════════════════════════════════════════════╝
echo.
set /p choice="Enter choice [0-6]: "
if "%choice%"=="1" goto chat
if "%choice%"=="2" goto server
if "%choice%"=="3" goto gym
if "%choice%"=="4" goto visualize
if "%choice%"=="5" goto info
if "%choice%"=="6" goto python
if "%choice%"=="0" goto end
goto menu
:setup
python --version >nul 2>&1
if errorlevel 1 (
echo ERROR: Python not found!
pause
goto menu
)
if not exist ".deps_installed" (
echo First run - installing dependencies...
pip install torch numpy flask onnxruntime gymnasium pygame ale-py 2>nul
echo. > .deps_installed
)
goto :eof
:chat
call :setup
cls
echo.
echo 💬 CHAT MODE - Talk to the collective intelligence
echo Commands: /state, /config, /reward, /gym, /train, /quit
echo.
python portable_agent/bridge.py . --mode interactive
pause
goto menu
:server
call :setup
cls
echo 🌐 HTTP SERVER on http://localhost:8080
echo Endpoints: POST /act, /chat, /reward ^| GET /state, /config
echo Press Ctrl+C to stop
echo.
python portable_agent/bridge.py . --mode serve --port 8080
pause
goto menu
:gym
call :setup
cls
echo.
echo 🎮 GYM MODE - 400+ Learning Environments!
echo.
echo ENVIRONMENT CATEGORIES:
echo Classic: CartPole-v1, MountainCar-v0, Pendulum-v1, Acrobot-v1
echo Atari: ALE/Breakout-v5, ALE/Pong-v5, ALE/SpaceInvaders-v5
echo MuJoCo: Humanoid-v4, Ant-v4, HalfCheetah-v4
echo.
set /p gymenv="Gym environment (default: CartPole-v1): "
if "%gymenv%"=="" set gymenv=CartPole-v1
set /p episodes="Episodes (default: 10): "
if "%episodes%"=="" set episodes=10
set /p render="Enable visual rendering? (y/n, default: n): "
set /p online="Enable online learning? (y/n, default: n): "
set renderarg=
set onlinearg=
if /i "%render%"=="y" set renderarg=--render
if /i "%online%"=="y" set onlinearg=--online-learn
python portable_agent/bridge.py . --mode gym --gym-env %gymenv% --episodes %episodes% %renderarg% %onlinearg%
pause
goto menu
:visualize
call :setup
python portable_agent/visualize.py
pause
goto menu
:info
cls
echo 📊 ENSEMBLE INFORMATION
echo.
type metadata.json
echo.
pause
goto menu
:python
call :setup
echo.
echo Example: agent.process(text="hello")
echo.
python -i -c "from portable_agent.bridge import AgentBridge; agent = AgentBridge.load('.'); print('Ensemble loaded!')"
pause
goto menu
:end
exit /b 0
"""
zf.writestr("start.bat", start_bat)
# Unix shell script
start_sh = """#!/bin/bash
cd "$(dirname "$0")"
setup() {
if ! command -v python3 &> /dev/null; then
echo "ERROR: Python3 not found!"
return 1
fi
if [ ! -f ".deps_installed" ]; then
pip3 install torch numpy flask onnxruntime gymnasium pygame ale-py 2>/dev/null
touch .deps_installed
fi
}
while true; do
clear
echo " 🦋🦋 BUTTERFLY ENSEMBLE - COLLECTIVE INTELLIGENCE 🦋🦋"
echo ""
echo " [1] 💬 Chat [2] 🌐 Server [3] 🎮 Gym (400+ envs!)"
echo " [4] 🔬 Viz [5] 📊 Info [6] 🐍 Python"
echo " [0] Exit"
echo ""
read -p "Choice: " c
case $c in
1) setup && python3 portable_agent/bridge.py . --mode interactive; read -p "Enter..." ;;
2) setup && python3 portable_agent/bridge.py . --mode serve --port 8080; read -p "Enter..." ;;
3)
setup || continue
echo ""
echo " ENVIRONMENTS: CartPole-v1, Pendulum-v1, ALE/Breakout-v5, Humanoid-v4..."
read -p "Env (CartPole-v1): " e
read -p "Episodes (10): " ep
read -p "Render? (y/n): " r
read -p "Online learn? (y/n): " l
renderarg=""
onlinearg=""
[[ "$r" == "y" ]] && renderarg="--render"
[[ "$l" == "y" ]] && onlinearg="--online-learn"
python3 portable_agent/bridge.py . --mode gym --gym-env ${e:-CartPole-v1} --episodes ${ep:-10} $renderarg $onlinearg
read -p "Enter..."
;;
4) setup && python3 portable_agent/visualize.py; read -p "Enter..." ;;
5) cat metadata.json; read -p "Enter..." ;;
6) setup && python3 -i -c "from portable_agent.bridge import AgentBridge; agent = AgentBridge.load('.')" ;;
0) exit 0 ;;
esac
done
"""
zf.writestr("start.sh", start_sh)
# Include portable_agent sources (for visualizer, etc.)
self._write_portable_agent_sources(zf)
archive_buffer.seek(0)
return archive_buffer
def _generate_ensemble_runner_script(self, export_format: str, metadata: Dict[str, Any]) -> str:
action_map_str = json.dumps(ACTION_MAP)
script = """
import onnxruntime
import numpy as np
import json
import os
import time
ACTION_MAP = {action_map_str}
class EnsembleRunner:
def __init__(self, model_filename="{model_filename}", metadata_filename="metadata.json"):
self.model_filename = model_filename
self.metadata_filename = metadata_filename
if not os.path.exists(self.model_filename):
raise FileNotFoundError(f"Model file not found: {{self.model_filename}}")
if not os.path.exists(self.metadata_filename):
raise FileNotFoundError(f"Metadata file not found: {{self.metadata_filename}}")
with open(self.metadata_filename, "r") as f:
self.metadata = json.load(f)
ensemble = self.metadata.get('ensemble', {{}})
members = ensemble.get('members', [])
self.member_names = [m['name'] for m in members]
self.input_dim = ensemble.get('max_input_dim', 0)
print("\\n--- Ensemble Loaded ---")
print(f"Members: {{', '.join(self.member_names)}}")
print(f"Input Dim: {{self.input_dim}}")
print(f"Exported: {{self.metadata['export_timestamp']}}")
print("-----------------------\\n")
self.session = None
if "{export_format}" == "onnx":
providers = onnxruntime.get_available_providers()
if 'CUDAExecutionProvider' in providers:
self.session = onnxruntime.InferenceSession(self.model_filename, providers=['CUDAExecutionProvider'])
print("Using CUDAExecutionProvider for ONNX inference.")
else:
self.session = onnxruntime.InferenceSession(self.model_filename, providers=['CPUExecutionProvider'])
print("Using CPUExecutionProvider for ONNX inference.")
elif "{export_format}" == "torchscript":
import torch
self.model = torch.jit.load(self.model_filename)
self.model.eval()
print("TorchScript ensemble loaded.")
def decide_actions(self, state_vector):
if len(state_vector) != self.input_dim:
raise ValueError(f"State vector must have {{self.input_dim}} dimensions, got {{len(state_vector)}}")
if "{export_format}" == "onnx":
state_array = np.array(state_vector, dtype=np.float32).reshape(1, -1)
inputs = {{self.session.get_inputs()[0].name: state_array}}
outputs = self.session.run(None, inputs)
# outputs is a list; align to member order
decisions = {{}}
for name, out in zip(self.member_names, outputs):
idx = int(np.argmax(out))
decisions[name] = ACTION_MAP.get(idx, str(idx))
return decisions
elif "{export_format}" == "torchscript":
import torch
state_tensor = torch.tensor(state_vector, dtype=torch.float32).unsqueeze(0)
with torch.no_grad():
outs = self.model(state_tensor)
decisions = {{}}
for name, out in zip(self.member_names, outs):
idx = int(torch.argmax(out).item())
decisions[name] = ACTION_MAP.get(idx, str(idx))
return decisions
else:
raise ValueError(f"Unsupported export format: {{self.metadata['export_format']}}")
if __name__ == '__main__':
runner = EnsembleRunner()
dummy_state = np.random.rand(runner.input_dim)
decisions = runner.decide_actions(dummy_state)
print("Decisions:", decisions)
"""
return script.format(action_map_str=action_map_str,
model_filename=f"brain.{export_format}",
export_format=export_format)
def compile_capsules_to_ensemble(self,
capsules: List['OrganismCapsule'],
export_format: str = 'onnx',
example_state: Any = None,
vocabulary: Any = None,
conversation_history: List[Dict] = None) -> BytesIO:
"""Compile multiple capsules into a single ensemble model archive.
Args:
capsules: List of OrganismCapsule objects
export_format: 'onnx' or 'torchscript'
example_state: Example state for tracing
vocabulary: LanguageVocabulary object for chat system
conversation_history: List of conversation history entries
All brains receive the same state vector (max input dim); per-brain
slicing/padding is handled inside the wrapper for compatibility.
"""
if export_format not in ['onnx', 'torchscript']:
raise ValueError("Ensemble export supports 'onnx' and 'torchscript' only.")
# Reconstruct brains
brains = []
names = []
members_meta = []
for cap in capsules:
b = self._reconstruct_brain_from_capsule(cap)
brains.append(b)
name = str(cap.organism_id)
names.append(name)
members_meta.append({
'organism_id': name,
'name': name,
'input_dim': b.input_dim,
'output_dim': b.output_dim,
'has_language_head': getattr(b, 'use_language_head', False),
'has_attention': getattr(b, 'use_attention', False)
})
if not brains:
raise ValueError("No capsules provided for ensemble export.")
wrapper = self.MultiOrganismWrapper(brains, names)
wrapper.eval() # Disable dropout for deterministic tracing
# Prepare deterministic input
if example_state is not None:
try:
arr = np.asarray(example_state, dtype=np.float32).reshape(1, -1)
if arr.shape[1] < wrapper.max_input_dim:
pad = np.zeros((1, wrapper.max_input_dim - arr.shape[1]), dtype=np.float32)
arr = np.concatenate([arr, pad], axis=1)
elif arr.shape[1] > wrapper.max_input_dim:
arr = arr[:, :wrapper.max_input_dim]
dummy_input = torch.from_numpy(arr)
except Exception:
dummy_input = torch.zeros(1, wrapper.max_input_dim, dtype=torch.float32)
else:
dummy_input = torch.zeros(1, wrapper.max_input_dim, dtype=torch.float32)
# Export
model_buffer = BytesIO()
chosen_format = export_format
if export_format == 'onnx':
try:
# Build output names based on whether language heads exist
if wrapper.any_language_head:
# Action outputs + language outputs for members with language heads
output_names = [f"action_{n}" for n in names]
for i, (name, has_lang) in enumerate(zip(names, wrapper.has_language_heads)):
if has_lang:
output_names.append(f"language_{name}")
else:
output_names = [f"out_{n}" for n in names]
torch.onnx.export(
wrapper,
dummy_input,
model_buffer,
input_names=['input'],
output_names=output_names,
dynamic_axes={'input': {0: 'batch_size'}},
opset_version=11
)
logger.info(f"✓ Successfully exported ensemble to ONNX format ({model_buffer.tell()} bytes)")
except Exception as e:
logger.warning(f"✗ ONNX export failed: {type(e).__name__}: {e}")
logger.warning("Falling back to TorchScript export.")
model_buffer = BytesIO()
traced = torch.jit.trace(wrapper, (dummy_input,))
torch.jit.save(traced, model_buffer)
model_buffer.seek(0)
chosen_format = 'torchscript'
else:
# Use trace instead of script - script fails on OrganismBrain's complex control flow
traced = torch.jit.trace(wrapper, (dummy_input,))
torch.jit.save(traced, model_buffer)
model_buffer.seek(0)
# Compute behavioral fingerprints for each member
logger.info("Computing behavioral fingerprints for ensemble members...")
for i, (brain, cap, member_meta) in enumerate(zip(brains, capsules, members_meta)):
try:
fingerprint = self._compute_behavioral_fingerprint(brain, num_samples=50)
member_meta['behavioral_fingerprint'] = fingerprint
member_meta['fitness'] = self._extract_fitness_value(cap)
member_meta['generation'] = getattr(cap, 'generation', None)
logger.info(f" Member {i+1}/{len(brains)}: {fingerprint['personality_label']} "
f"(dominant: {fingerprint['dominant_action']})")
except Exception as e:
logger.warning(f"Could not compute fingerprint for member {i}: {e}")
member_meta['behavioral_fingerprint'] = {'error': str(e)}
# Compute aggregate ensemble behavioral profile
ensemble_action_dist = {}
ensemble_tendencies = {'cooperative': 0, 'competitive': 0, 'passive': 0}
personality_counts = {}
for member_meta in members_meta:
fp = member_meta.get('behavioral_fingerprint', {})
if 'error' in fp:
continue
# Aggregate action distributions
for action, prob in fp.get('action_distribution', {}).items():
ensemble_action_dist[action] = ensemble_action_dist.get(action, 0) + prob
# Aggregate tendencies
for tendency, score in fp.get('behavioral_tendencies', {}).items():
ensemble_tendencies[tendency] = ensemble_tendencies.get(tendency, 0) + score
# Count personalities
personality = fp.get('personality_label', 'unknown')
personality_counts[personality] = personality_counts.get(personality, 0) + 1
# Normalize aggregates
n_members = len([m for m in members_meta if 'error' not in m.get('behavioral_fingerprint', {})])
if n_members > 0:
ensemble_action_dist = {k: round(v / n_members, 4) for k, v in ensemble_action_dist.items()}
ensemble_tendencies = {k: round(v / n_members, 4) for k, v in ensemble_tendencies.items()}
# Metadata
metadata = {
'export_timestamp': datetime.datetime.now().isoformat(),
'export_format': chosen_format,
'ensemble': {
'members': members_meta,
'member_count': len(members_meta),
'max_input_dim': wrapper.max_input_dim,
'aggregate_behavioral_profile': {
'action_distribution': ensemble_action_dist,
'behavioral_tendencies': ensemble_tendencies,
'personality_distribution': personality_counts,
'dominant_personalities': sorted(personality_counts.keys(),
key=lambda x: personality_counts[x],
reverse=True)[:3] if personality_counts else []
}
},
'runtime_dependencies': {
'onnxruntime': onnxruntime.__version__ if ONNX_AVAILABLE else 'not installed',
'numpy': np.__version__,
'python': sys.version.split(' ')[0]
}
}
# Runner
runner_script = self._generate_ensemble_runner_script(chosen_format, metadata)
# Package (pass capsules for language data extraction, plus chat vocabulary)
return self._create_ensemble_archive(model_buffer, metadata, runner_script, capsules, vocabulary, conversation_history)
def compile_capsule_to_agent(self,
capsule: OrganismCapsule,
export_format: str = 'onnx',
include_history: bool = True,
example_state: Any = None) -> BytesIO:
"""
Compiles an OrganismCapsule into a deployable agent archive (ZIP file).
Args:
capsule: The OrganismCapsule object containing the agent's state.
export_format: The format for the neural network model ('onnx', 'torchscript', 'statedict').
include_history: If True, includes more detailed history/causation data.
Returns:
BytesIO: A memory buffer containing the ZIP archive.
"""
if export_format not in self.supported_formats:
raise ValueError(f"Unsupported export format: {export_format}. Supported: {self.supported_formats}")
logger.info(f"Compiling organism {capsule.organism_id} to {export_format.upper()} format.")
# 1. Reconstruct the neural brain
brain = self._reconstruct_brain_from_capsule(capsule)
# 2. Prepare deterministic input for ONNX export (and TorchScript tracing if used)
if example_state is not None:
try:
arr = np.asarray(example_state, dtype=np.float32)
arr = arr.reshape(1, -1)
# Pad or truncate to match expected input_dim
if arr.shape[1] < brain.input_dim:
pad = np.zeros((1, brain.input_dim - arr.shape[1]), dtype=np.float32)
arr = np.concatenate([arr, pad], axis=1)
elif arr.shape[1] > brain.input_dim:
arr = arr[:, :brain.input_dim]
dummy_input = torch.from_numpy(arr)
except Exception:
dummy_input = torch.zeros(1, brain.input_dim, dtype=torch.float32)
else:
dummy_input = torch.zeros(1, brain.input_dim, dtype=torch.float32)
# 3. Export the brain to the specified format
model_buffer = BytesIO()
chosen_format = export_format
if export_format == 'onnx':
try:
self._export_onnx(brain, dummy_input, model_buffer)
logger.info(f"✓ Successfully exported to ONNX format ({model_buffer.tell()} bytes)")
except Exception as e:
# Graceful fallback: if ONNX dependencies missing, fallback to TorchScript
logger.warning(f"✗ ONNX export failed: {type(e).__name__}: {e}")
logger.warning("Falling back to TorchScript export.")
model_buffer = BytesIO()
self._export_torchscript(brain, model_buffer)
chosen_format = 'torchscript'
elif export_format == 'torchscript':
self._export_torchscript(brain, model_buffer)
elif export_format == 'statedict':
self._export_statedict(brain, model_buffer)
# 4. Create rich metadata
metadata = self._create_rich_metadata(capsule, brain)
metadata['export_format'] = chosen_format # Add (possibly updated) export format to metadata
# 4b. Compute behavioral fingerprint by sampling the brain
try:
logger.info(f"Computing behavioral fingerprint for {capsule.organism_id}...")
behavioral_fingerprint = self._compute_behavioral_fingerprint(brain, num_samples=100)
metadata['behavioral_fingerprint'] = behavioral_fingerprint
logger.info(f"Behavioral profile: {behavioral_fingerprint['personality_label']} "
f"(cooperative={behavioral_fingerprint['behavioral_tendencies']['cooperative']:.2f}, "
f"competitive={behavioral_fingerprint['behavioral_tendencies']['competitive']:.2f})")
except Exception as e:
logger.warning(f"Could not compute behavioral fingerprint: {e}")
metadata['behavioral_fingerprint'] = {'error': str(e)}
# 5. Generate runner script
runner_script = self._generate_runner_script(chosen_format, metadata)
# 5b. Build agent state payload for living runtime
agent_state_payload = self._build_agent_state_payload(capsule, metadata)
# 6. Package into ZIP archive
return self._create_agent_archive(
model_buffer,
metadata,
runner_script,
capsule,
agent_state_payload
)
if __name__ == '__main__':
# This block is for testing the AgentCompiler in isolation.
# It requires a dummy OrganismCapsule and OrganismBrain setup.
# Setup dummy brain and organism for testing
dummy_brain_arch = {
'input_dim': 24,
'hidden_dim': 64,
'output_dim': 6,
'activation': 'relu',
'dropout': 0.1,
'use_attention': False,
'num_attention_heads': 4,
'attention_dim': 64,
'vocab_size': 1000,
'use_language_head': False
}
dummy_brain = OrganismBrain(**dummy_brain_arch)
# Save dummy brain state_dict to BytesIO
dummy_state_dict_buffer = BytesIO()
torch.save(dummy_brain.state_dict(), dummy_state_dict_buffer)
dummy_state_dict_buffer.seek(0)
dummy_state_dict_b64 = base64.b64encode(dummy_state_dict_buffer.read()).decode('utf-8')
dummy_capsule = OrganismCapsule(
organism_id="test_org_001",
capsule_id=f"cap_{uuid.uuid4()}",
version="1.0",
timestamp=datetime.datetime.now().isoformat(),
neural_network_state={
'architecture': dummy_brain_arch,
'state_dict_b64': dummy_state_dict_b64,
'device': 'cpu',
'training_steps': 100,
'avg_loss': 0.05
},
genotype_hash_state={'dna': 'ATGC...'},
phenotype_summary={'size': 10, 'color': 'red'},
fitness_trajectory=[{'fitness': 0.5, 'generation': 0}, {'fitness': 0.6, 'generation': 10}],
age=10,
atomic_language_state={'concept_count': 50, 'dialect_signature': [0.1, 0.2]},
atomic_config_state={'neural': {'lr': 0.001}},
highlander_metadata={'wins': 5, 'losses': 2},
social_connections={'neighbors': 3},
environment_context={'resource_density': 0.7},
causation_digest={'events': [{'id': 'evt_1', 'type': 'born'}]},
file_path="dummy_path.json"
)
compiler = AgentCompiler()
# Test ONNX export
try:
onnx_archive = compiler.compile_capsule_to_agent(dummy_capsule, export_format='onnx')
with open("test_agent_onnx.zip", "wb") as f:
f.write(onnx_archive.read())
print("Generated test_agent_onnx.zip")
except Exception as e:
print(f"ONNX compilation failed: {e}")
# Test TorchScript export
try:
ts_archive = compiler.compile_capsule_to_agent(dummy_capsule, export_format='torchscript')
with open("test_agent_torchscript.zip", "wb") as f:
f.write(ts_archive.read())
print("Generated test_agent_torchscript.zip")
except Exception as e:
print(f"TorchScript compilation failed: {e}")
# Test StateDict export
try:
sd_archive = compiler.compile_capsule_to_agent(dummy_capsule, export_format='statedict')
with open("test_agent_statedict.zip", "wb") as f:
f.write(sd_archive.read())
print("Generated test_agent_statedict.zip")
except Exception as e:
print(f"StateDict compilation failed: {e}")

Xet Storage Details

Size:
142 kB
·
Xet hash:
8799a5e39369ad0c7c8d005eb4dd4a48618bd6b504befd11ba696404c51a4223

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.