Add Ikario Architecture v2 - Phases 1-8 complete

Implements the processual architecture based on Whitehead's Process
Philosophy and Peirce's Semiotics. Core paradigm: "L'espace latent
pense. Le LLM traduit." (The latent space thinks. The LLM translates.)

Phase 1-4: Core semiotic cycle
- StateTensor 8x1024 (8 Peircean dimensions)
- Dissonance computation with hard negatives
- Fixation via 4 Peircean methods (Tenacity, Authority, A Priori, Science)
- LatentEngine orchestrating the full cycle

Phase 5: StateToLanguage
- LLM as pure translator (zero-reasoning, T=0)
- Projection on interpretable directions
- Reasoning markers detection (Amendment #4)

Phase 6: Vigilance
- x_ref (David) as guard-rail, NOT attractor
- Drift detection per dimension and globally
- Alerts: ok, warning, critical

Phase 7: Autonomous Daemon
- Two modes: CONVERSATION (always verbalize), AUTONOMOUS (~1000 cycles/day)
- Amendment #5: 50% probability on unresolved impacts
- TriggerGenerator with weighted random selection

Phase 8: Integration & Metrics
- ProcessMetrics for daily/weekly reports
- Health status monitoring
- Integration tests validating all modules

297 tests passing, version 0.7.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 21:18:40 +01:00
parent 9c2145bcf2
commit f6fe71e2f7
19 changed files with 9887 additions and 9 deletions

View File

@@ -0,0 +1,674 @@
#!/usr/bin/env python3
"""
LatentEngine - Moteur de pensée latent d'Ikario.
Phase 4 du plan processuel v2.
Implémente le cycle sémiotique Peircien :
1. FIRSTNESS : Vectoriser l'entrée, extraire saillances
2. SECONDNESS : Calculer dissonance, créer Impacts si choc
3. THIRDNESS : Appliquer fixation, calculer δ, mettre à jour X_t
4. SÉMIOSE : Créer Thoughts, préparer cycle suivant
C'est ici que la pensée a lieu - PAS dans le LLM.
Le LLM ne fait que traduire le résultat en langage.
"""
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import json
import numpy as np
from .state_tensor import (
StateTensor,
StateTensorRepository,
DIMENSION_NAMES,
EMBEDDING_DIM,
)
from .dissonance import (
DissonanceConfig,
DissonanceResult,
compute_dissonance_enhanced,
Impact,
ImpactRepository,
create_impact_from_dissonance,
)
from .fixation import (
FixationConfig,
FixationResult,
Authority,
compute_delta,
apply_delta_all_dimensions,
)
# ============================================================================
# THOUGHT - Pensée créée pendant un cycle
# ============================================================================
@dataclass
class Thought:
"""
Une pensée créée pendant un cycle sémiotique.
Les Thoughts sont les "traces" du processus de pensée latent.
Elles ne sont pas le produit du LLM, mais du cycle vectoriel.
"""
thought_id: int
timestamp: str
state_id: int # État au moment de la création
# Contenu
content: str # Description textuelle (générée pour logging)
thought_type: str # reflection, insight, question, resolution
# Origine
trigger_type: str
trigger_summary: str
# Métriques
delta_magnitude: float
dissonance_total: float
dimensions_affected: List[str]
# Vecteur
vector: Optional[np.ndarray] = None
def to_dict(self) -> Dict[str, Any]:
"""Convertit en dictionnaire pour Weaviate."""
ts = self.timestamp
if ts and not ts.endswith('Z') and '+' not in ts:
ts = ts + 'Z'
return {
'thought_id': self.thought_id,
'timestamp': ts,
'state_id': self.state_id,
'content': self.content,
'thought_type': self.thought_type,
'trigger_type': self.trigger_type,
'trigger_summary': self.trigger_summary[:200],
'delta_magnitude': self.delta_magnitude,
'dissonance_total': self.dissonance_total,
'dimensions_affected': self.dimensions_affected,
}
# ============================================================================
# CYCLE RESULT
# ============================================================================
@dataclass
class CycleResult:
"""Résultat complet d'un cycle sémiotique."""
# Nouvel état
new_state: StateTensor
previous_state_id: int
# Dissonance
dissonance: DissonanceResult
# Fixation
fixation: FixationResult
# Impacts créés
impacts: List[Impact]
# Thoughts créées
thoughts: List[Thought]
# Verbalisation
should_verbalize: bool
verbalization_reason: str
# Métriques
processing_time_ms: int
cycle_number: int
# Saillances extraites
saillances: Dict[str, float] = field(default_factory=dict)
def to_dict(self) -> Dict[str, Any]:
"""Résumé du cycle."""
return {
'cycle_number': self.cycle_number,
'new_state_id': self.new_state.state_id,
'previous_state_id': self.previous_state_id,
'dissonance_total': self.dissonance.total,
'is_choc': self.dissonance.is_choc,
'delta_magnitude': self.fixation.magnitude,
'was_clamped': self.fixation.was_clamped,
'impacts_count': len(self.impacts),
'thoughts_count': len(self.thoughts),
'should_verbalize': self.should_verbalize,
'verbalization_reason': self.verbalization_reason,
'processing_time_ms': self.processing_time_ms,
}
# ============================================================================
# CYCLE LOGGER
# ============================================================================
class CycleLogger:
"""Logger pour les cycles sémiotiques."""
def __init__(self, max_history: int = 100):
self.history: List[Dict[str, Any]] = []
self.max_history = max_history
self.total_cycles = 0
def log_cycle(self, result: CycleResult) -> None:
"""Enregistre un cycle."""
self.total_cycles += 1
entry = {
'cycle_number': self.total_cycles,
'timestamp': datetime.now().isoformat(),
**result.to_dict(),
}
self.history.append(entry)
# Limiter la taille
if len(self.history) > self.max_history:
self.history = self.history[-self.max_history:]
def get_stats(self) -> Dict[str, Any]:
"""Retourne les statistiques."""
if not self.history:
return {'total_cycles': 0}
dissonances = [h['dissonance_total'] for h in self.history]
times = [h['processing_time_ms'] for h in self.history]
return {
'total_cycles': self.total_cycles,
'recent_cycles': len(self.history),
'avg_dissonance': float(np.mean(dissonances)),
'max_dissonance': float(max(dissonances)),
'avg_processing_time_ms': float(np.mean(times)),
'total_impacts': sum(h['impacts_count'] for h in self.history),
'total_thoughts': sum(h['thoughts_count'] for h in self.history),
}
# ============================================================================
# LATENT ENGINE
# ============================================================================
class LatentEngine:
"""
Moteur de pensée latent d'Ikario.
Implémente le cycle sémiotique Peircien.
C'est ici que la pensée a lieu - PAS dans le LLM.
Usage:
engine = LatentEngine(client, model)
result = engine.run_cycle({
'type': 'user',
'content': 'Que penses-tu de Whitehead?'
})
"""
def __init__(
self,
weaviate_client,
embedding_model,
dissonance_config: DissonanceConfig = None,
fixation_config: FixationConfig = None,
authority: Authority = None,
vigilance_system=None, # Pour Phase 6
):
"""
Args:
weaviate_client: Client Weaviate connecté
embedding_model: Modèle SentenceTransformer
dissonance_config: Configuration dissonance
fixation_config: Configuration fixation
authority: Instance Authority pré-configurée
vigilance_system: Système de vigilance x_ref (Phase 6)
"""
self.client = weaviate_client
self.model = embedding_model
self.dissonance_config = dissonance_config or DissonanceConfig()
self.fixation_config = fixation_config or FixationConfig()
# Authority avec les vecteurs du Pacte
self.authority = authority or Authority(
embedding_model=embedding_model,
config=self.fixation_config
)
self.vigilance = vigilance_system
# Repositories
self.state_repo = StateTensorRepository(weaviate_client)
self.impact_repo = ImpactRepository(weaviate_client)
# Logger
self.logger = CycleLogger()
# Compteurs
self._impact_counter = 0
self._thought_counter = 0
def run_cycle(self, trigger: Dict[str, Any]) -> CycleResult:
"""
Exécute un cycle sémiotique complet.
Args:
trigger: {
'type': 'user' | 'corpus' | 'veille' | 'internal' | 'timer',
'content': str,
'metadata': dict (optional)
}
Returns:
CycleResult avec tous les détails du cycle
"""
start_time = time.time()
# Valider le trigger
trigger_type = trigger.get('type', 'unknown')
trigger_content = trigger.get('content', '')
if not trigger_content:
raise ValueError("Trigger content is required")
# === PHASE 1: FIRSTNESS ===
# Récupérer l'état actuel
X_t = self._get_current_state()
previous_state_id = X_t.state_id
# Vectoriser l'entrée
e_input = self._vectorize_input(trigger_content)
# Extraire les saillances
saillances = self._extract_saillances(e_input, X_t)
# === PHASE 2: SECONDNESS ===
# Récupérer contexte RAG
rag_results = self._retrieve_context(e_input, trigger_content)
# Calculer dissonance avec hard negatives
dissonance = compute_dissonance_enhanced(
e_input,
X_t,
rag_results,
self.dissonance_config
)
# Créer Impact si choc
impacts = []
if dissonance.is_choc:
impact = self._create_impact(
trigger_type=trigger_type,
trigger_content=trigger_content,
trigger_vector=e_input,
dissonance=dissonance,
state_id=X_t.state_id
)
impacts.append(impact)
# === PHASE 3: THIRDNESS ===
# Calculer delta via les 4 méthodes de fixation
fixation_result = compute_delta(
e_input=e_input,
X_t=X_t,
dissonance=dissonance,
rag_results=rag_results,
config=self.fixation_config,
authority=self.authority
)
# Appliquer le delta pour créer X_{t+1}
X_new = apply_delta_all_dimensions(
X_t=X_t,
e_input=e_input,
fixation_result=fixation_result
)
# Mettre à jour les métadonnées
X_new.trigger_type = trigger_type
X_new.trigger_content = trigger_content[:500]
X_new.timestamp = datetime.now().isoformat()
# Persister le nouvel état
self._persist_state(X_new)
# === PHASE 4: SÉMIOSE ===
# Créer Thought si delta significatif
thoughts = []
if fixation_result.magnitude > 0.0005:
thought = self._create_thought(
trigger_type=trigger_type,
trigger_content=trigger_content,
fixation_result=fixation_result,
dissonance=dissonance,
state_id=X_new.state_id
)
thoughts.append(thought)
# Décider si verbalisation nécessaire
should_verbalize, reason = self._should_verbalize(
trigger=trigger,
dissonance=dissonance,
fixation_result=fixation_result,
X_new=X_new
)
# Calculer le temps
processing_time_ms = int((time.time() - start_time) * 1000)
# Créer le résultat
result = CycleResult(
new_state=X_new,
previous_state_id=previous_state_id,
dissonance=dissonance,
fixation=fixation_result,
impacts=impacts,
thoughts=thoughts,
should_verbalize=should_verbalize,
verbalization_reason=reason,
processing_time_ms=processing_time_ms,
cycle_number=self.logger.total_cycles + 1,
saillances=saillances,
)
# Logger le cycle
self.logger.log_cycle(result)
return result
def _get_current_state(self) -> StateTensor:
"""Récupère l'état actuel depuis Weaviate."""
current = self.state_repo.get_current()
if current is None:
raise RuntimeError(
"No current state found. Run create_initial_tensor.py first."
)
return current
def _vectorize_input(self, content: str) -> np.ndarray:
"""Vectorise le contenu d'entrée."""
# Tronquer si trop long
if len(content) > 2000:
content = content[:2000]
embedding = self.model.encode(content)
# Normaliser
norm = np.linalg.norm(embedding)
if norm > 0:
embedding = embedding / norm
return embedding
def _extract_saillances(
self,
e_input: np.ndarray,
X_t: StateTensor
) -> Dict[str, float]:
"""
Extrait les saillances de l'entrée par rapport à l'état.
Les saillances indiquent quelles dimensions sont les plus
"touchées" par l'entrée.
"""
saillances = {}
for dim_name in DIMENSION_NAMES:
dim_vec = getattr(X_t, dim_name)
# Similarité = saillance
sim = float(np.dot(e_input, dim_vec))
saillances[dim_name] = sim
return saillances
def _retrieve_context(
self,
e_input: np.ndarray,
content: str,
limit: int = 5
) -> List[Dict[str, Any]]:
"""
Récupère le contexte RAG pertinent.
Cherche dans Thought et Message les contenus similaires.
"""
rag_results = []
try:
# Chercher dans Thought
thought_collection = self.client.collections.get("Thought")
thought_results = thought_collection.query.near_vector(
near_vector=e_input.tolist(),
limit=limit,
include_vector=True,
)
for obj in thought_results.objects:
rag_results.append({
'content': obj.properties.get('content', ''),
'vector': obj.vector.get('default') if isinstance(obj.vector, dict) else obj.vector,
'source': 'thought',
})
except Exception:
pass # Collection might not exist
try:
# Chercher dans Message
message_collection = self.client.collections.get("Message")
message_results = message_collection.query.near_vector(
near_vector=e_input.tolist(),
limit=limit,
include_vector=True,
)
for obj in message_results.objects:
rag_results.append({
'content': obj.properties.get('content', ''),
'vector': obj.vector.get('default') if isinstance(obj.vector, dict) else obj.vector,
'source': 'message',
'input_text': content, # Pour NLI si nécessaire
})
except Exception:
pass
return rag_results
def _create_impact(
self,
trigger_type: str,
trigger_content: str,
trigger_vector: np.ndarray,
dissonance: DissonanceResult,
state_id: int
) -> Impact:
"""Crée et sauvegarde un Impact."""
self._impact_counter += 1
impact = create_impact_from_dissonance(
dissonance=dissonance,
trigger_type=trigger_type,
trigger_content=trigger_content,
trigger_vector=trigger_vector,
state_id=state_id,
impact_id=self._impact_counter
)
# Sauvegarder dans Weaviate
try:
self.impact_repo.save(impact)
except Exception as e:
print(f"[WARN] Could not save impact: {e}")
return impact
def _create_thought(
self,
trigger_type: str,
trigger_content: str,
fixation_result: FixationResult,
dissonance: DissonanceResult,
state_id: int
) -> Thought:
"""Crée une Thought basée sur le cycle."""
self._thought_counter += 1
# Déterminer le type de thought
if dissonance.is_choc:
thought_type = 'insight'
elif fixation_result.was_clamped:
thought_type = 'resolution'
else:
thought_type = 'reflection'
# Dimensions les plus affectées
contributions = fixation_result.contributions
affected = sorted(contributions.keys(), key=lambda k: contributions[k], reverse=True)[:3]
# Générer un contenu descriptif
content = self._generate_thought_content(
trigger_type=trigger_type,
trigger_content=trigger_content,
dissonance=dissonance,
fixation_result=fixation_result,
thought_type=thought_type
)
thought = Thought(
thought_id=self._thought_counter,
timestamp=datetime.now().isoformat(),
state_id=state_id,
content=content,
thought_type=thought_type,
trigger_type=trigger_type,
trigger_summary=trigger_content[:100],
delta_magnitude=fixation_result.magnitude,
dissonance_total=dissonance.total,
dimensions_affected=affected,
)
return thought
def _generate_thought_content(
self,
trigger_type: str,
trigger_content: str,
dissonance: DissonanceResult,
fixation_result: FixationResult,
thought_type: str
) -> str:
"""Génère le contenu textuel d'une thought (sans LLM)."""
# Description basée sur les métriques
if thought_type == 'insight':
return (
f"Choc détecté (dissonance={dissonance.total:.3f}). "
f"L'entrée '{trigger_content[:50]}...' a provoqué une tension "
f"avec {len(dissonance.hard_negatives)} contradictions potentielles."
)
elif thought_type == 'resolution':
return (
f"Résolution d'une tension. Delta limité à {fixation_result.magnitude:.6f} "
f"pour maintenir la stabilité. Contributions: "
f"Science={fixation_result.contributions['science']:.4f}, "
f"Authority={fixation_result.contributions['authority']:.4f}."
)
else:
return (
f"Réflexion sur '{trigger_content[:50]}...'. "
f"Dissonance={dissonance.total:.3f}, "
f"intégration via les 4 méthodes de fixation."
)
def _persist_state(self, X_new: StateTensor) -> None:
"""Sauvegarde le nouvel état dans Weaviate."""
self.state_repo.save(X_new)
def _should_verbalize(
self,
trigger: Dict[str, Any],
dissonance: DissonanceResult,
fixation_result: FixationResult,
X_new: StateTensor
) -> Tuple[bool, str]:
"""
Décide si le cycle doit produire une verbalisation.
TOUJOURS verbaliser si:
- trigger.type == 'user' (conversation)
PEUT verbaliser si (mode autonome):
- Dissonance très haute (découverte importante)
- Alerte de dérive (vigilance)
- Question à poser (tension irrésoluble)
"""
trigger_type = trigger.get('type', 'unknown')
# Mode conversation → toujours verbaliser
if trigger_type == 'user':
return True, "conversation_mode"
# Mode autonome : vérifier critères
if dissonance.total > 0.6:
return True, "high_dissonance_discovery"
# Vérifier vigilance si disponible
if self.vigilance is not None:
alert = self.vigilance.check_drift(X_new)
if alert.level in ('warning', 'critical'):
return True, f"drift_alert_{alert.level}"
# Hard negatives nombreux → potentielle découverte
if len(dissonance.hard_negatives) >= 3:
return True, "multiple_contradictions"
return False, "silent_processing"
def get_stats(self) -> Dict[str, Any]:
"""Retourne les statistiques du moteur."""
return {
**self.logger.get_stats(),
'impacts_created': self._impact_counter,
'thoughts_created': self._thought_counter,
}
# ============================================================================
# CONVENIENCE FUNCTIONS
# ============================================================================
def create_engine(
weaviate_client,
embedding_model,
load_authority: bool = True
) -> LatentEngine:
"""
Factory pour créer un LatentEngine configuré.
Args:
weaviate_client: Client Weaviate connecté
embedding_model: Modèle SentenceTransformer
load_authority: Si True, charge les vecteurs du Pacte
Returns:
LatentEngine configuré
"""
authority = None
if load_authority:
authority = Authority(embedding_model=embedding_model)
return LatentEngine(
weaviate_client=weaviate_client,
embedding_model=embedding_model,
authority=authority
)