Files
David Blanc Brioir f6fe71e2f7 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>
2026-02-01 22:30:19 +01:00

770 lines
26 KiB
Python

#!/usr/bin/env python3
"""
IkarioDaemon - Daemon d'individuation autonome.
Phase 7 de l'architecture processuelle v2.
Ikario pense meme quand personne ne lui parle.
Deux modes:
1. CONVERSATION : Reponse a un humain (toujours verbaliser)
2. AUTONOME : Pensee silencieuse (~1000 cycles/jour)
En mode autonome, Ikario :
- Traite la veille Tavily
- Lit le corpus philosophique
- RUMINE SES TENSIONS NON RESOLUES (Amendment #5)
- Evolue sans parler
Verbalise SEULEMENT si:
- Alerte de derive (vigilance x_ref)
- Decouverte importante (haute dissonance + resolution)
- Question a poser a David
"""
import asyncio
import logging
import random
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple
import numpy as np
from .state_tensor import StateTensor, DIMENSION_NAMES
from .latent_engine import LatentEngine, CycleResult
from .vigilance import VigilanceSystem, VigilanceAlert
from .state_to_language import StateToLanguage, TranslationResult
# Logger
logger = logging.getLogger(__name__)
class TriggerType(Enum):
"""Types de triggers pour le daemon."""
USER = "user" # Message utilisateur
VEILLE = "veille" # Actualites Tavily
CORPUS = "corpus" # Corpus philosophique
RUMINATION = "rumination" # Impact non resolu
RUMINATION_FREE = "rumination_free" # Pensee ancienne
TIMER = "timer" # Tick periodique
EMPTY = "empty" # Aucune entree
class DaemonMode(Enum):
"""Modes du daemon."""
CONVERSATION = "conversation" # Interactif avec humain
AUTONOMOUS = "autonomous" # Pensee silencieuse
PAUSED = "paused" # En pause
@dataclass
class DaemonConfig:
"""Configuration du daemon."""
# Intervalle entre cycles autonomes (secondes)
cycle_interval_seconds: float = 90.0 # ~1000 cycles/jour
# Intervalle de veille Tavily (secondes)
veille_interval_seconds: float = 3600.0 # 1x/heure
# Intervalle de check vigilance (secondes)
vigilance_interval_seconds: float = 300.0 # 5 min
# Probabilites de triggers autonomes (Amendment #5)
prob_unresolved_impact: float = 0.50 # 50%
prob_corpus: float = 0.30 # 30%
prob_rumination_free: float = 0.20 # 20%
# Seuil d'anciennete pour priorite haute (jours)
old_impact_threshold_days: int = 7
# Nombre max d'impacts non resolus a considerer
max_unresolved_impacts: int = 10
# Seuil de dissonance pour verbalisation autonome
verbalization_dissonance_threshold: float = 0.6
def validate(self) -> bool:
"""Verifie que la config est valide."""
probs_sum = self.prob_unresolved_impact + self.prob_corpus + self.prob_rumination_free
return (
self.cycle_interval_seconds > 0 and
np.isclose(probs_sum, 1.0, atol=0.01)
)
@dataclass
class DaemonStats:
"""Statistiques du daemon."""
total_cycles: int = 0
conversation_cycles: int = 0
autonomous_cycles: int = 0
verbalizations: int = 0
silent_cycles: int = 0
vigilance_alerts: int = 0
impacts_ruminated: int = 0
corpus_processed: int = 0
veille_items_processed: int = 0
start_time: str = field(default_factory=lambda: datetime.now().isoformat())
last_cycle_time: str = ""
def to_dict(self) -> Dict[str, Any]:
"""Serialise en dictionnaire."""
return {
'total_cycles': self.total_cycles,
'conversation_cycles': self.conversation_cycles,
'autonomous_cycles': self.autonomous_cycles,
'verbalizations': self.verbalizations,
'silent_cycles': self.silent_cycles,
'vigilance_alerts': self.vigilance_alerts,
'impacts_ruminated': self.impacts_ruminated,
'corpus_processed': self.corpus_processed,
'veille_items_processed': self.veille_items_processed,
'start_time': self.start_time,
'last_cycle_time': self.last_cycle_time,
'uptime_seconds': self._compute_uptime(),
}
def _compute_uptime(self) -> float:
"""Calcule le temps d'execution."""
try:
start = datetime.fromisoformat(self.start_time.replace('Z', ''))
return (datetime.now() - start).total_seconds()
except Exception:
return 0.0
@dataclass
class Trigger:
"""Trigger pour le daemon."""
type: TriggerType
content: str
source: str = ""
metadata: Dict[str, Any] = field(default_factory=dict)
timestamp: str = field(default_factory=lambda: datetime.now().isoformat() + "Z")
priority: int = 0 # 0=normal, 1=high, 2=urgent
def to_dict(self) -> Dict[str, Any]:
"""Convertit en dictionnaire pour le LatentEngine."""
return {
'type': self.type.value,
'content': self.content,
'source': self.source,
'metadata': self.metadata,
'timestamp': self.timestamp,
}
@dataclass
class VerbalizationEvent:
"""Evenement de verbalisation."""
text: str
reason: str
trigger_type: str
state_id: int
dissonance: float
timestamp: str = field(default_factory=lambda: datetime.now().isoformat() + "Z")
def to_dict(self) -> Dict[str, Any]:
"""Serialise en dictionnaire."""
return {
'text': self.text,
'reason': self.reason,
'trigger_type': self.trigger_type,
'state_id': self.state_id,
'dissonance': self.dissonance,
'timestamp': self.timestamp,
}
class TriggerGenerator:
"""
Generateur de triggers autonomes.
Amendment #5 : Rumination sur Impacts Non Resolus.
La rumination est CRUCIALE pour la subjectivite. Un Impact non resolu
= une TENSION INTERNE que je dois continuer a explorer.
"""
def __init__(
self,
config: DaemonConfig,
weaviate_client=None,
):
self.config = config
self.weaviate = weaviate_client
self._last_corpus_id: Optional[str] = None
self._last_thought_id: Optional[str] = None
async def generate_autonomous_trigger(self) -> Trigger:
"""
Genere un trigger pour pensee autonome.
PRIORITE (Amendment #5):
1. Impacts non resolus (surtout si anciens >7 jours) -> HAUTE PRIORITE
2. Corpus philosophique -> priorite moyenne
3. Rumination libre -> priorite basse
"""
# Selection ponderee
generators = [
(self._trigger_from_unresolved_impact, self.config.prob_unresolved_impact),
(self._trigger_from_corpus, self.config.prob_corpus),
(self._trigger_rumination_free, self.config.prob_rumination_free),
]
# Choisir generateur selon probabilites
chosen_generator = random.choices(
[g[0] for g in generators],
weights=[g[1] for g in generators],
k=1
)[0]
return await chosen_generator()
async def _trigger_from_unresolved_impact(self) -> Trigger:
"""
Rumine sur les Impacts non resolus.
Un Impact est "resolu" quand :
1. Sa dissonance initiale a diminue
2. Un Thought explicatif a ete cree
3. Marque manuellement comme resolu
Si Impact ancien (>7 jours) et non resolu -> priorite haute
"""
if self.weaviate is None:
return await self._trigger_from_corpus()
try:
collection = self.weaviate.collections.get("Impact")
# Query impacts non resolus
results = collection.query.fetch_objects(
limit=self.config.max_unresolved_impacts,
filters=None, # Idealement filtrer sur resolved=False
)
# Filtrer les non resolus (si le champ existe)
unresolved = []
for obj in results.objects:
props = obj.properties
if not props.get('resolved', False):
unresolved.append(props)
if unresolved:
# Trier par anciennete (plus ancien d'abord)
unresolved.sort(
key=lambda x: x.get('timestamp', ''),
reverse=False
)
oldest = unresolved[0]
# Calculer anciennete
try:
impact_time = datetime.fromisoformat(
oldest.get('timestamp', '').replace('Z', '')
)
days_unresolved = (datetime.now() - impact_time).days
except Exception:
days_unresolved = 0
# Priorite haute si ancien
priority = 1 if days_unresolved > self.config.old_impact_threshold_days else 0
return Trigger(
type=TriggerType.RUMINATION,
content=oldest.get('trigger_content', 'Impact sans contenu'),
source='impact_rumination',
priority=priority,
metadata={
'impact_id': oldest.get('impact_id', 0),
'original_dissonance': oldest.get('dissonance_total', 0),
'days_unresolved': days_unresolved,
'is_old_tension': days_unresolved > self.config.old_impact_threshold_days,
'dissonance_breakdown': oldest.get('dissonance_breakdown', '{}'),
}
)
except Exception as e:
logger.warning(f"Erreur acces impacts: {e}")
# Fallback sur corpus
return await self._trigger_from_corpus()
async def _trigger_from_corpus(self) -> Trigger:
"""Tire un passage aleatoire du corpus philosophique."""
if self.weaviate is None:
return self._create_fallback_trigger()
try:
collection = self.weaviate.collections.get("Chunk")
# Recuperer un chunk aleatoire
results = collection.query.fetch_objects(limit=10)
if results.objects:
# Choisir aleatoirement parmi les resultats
chunk = random.choice(results.objects)
props = chunk.properties
return Trigger(
type=TriggerType.CORPUS,
content=props.get('text', ''),
source=props.get('source_id', 'corpus'),
metadata={
'author': props.get('author', ''),
'work': props.get('work_title', ''),
'chunk_id': str(chunk.uuid) if hasattr(chunk, 'uuid') else '',
}
)
except Exception as e:
logger.warning(f"Erreur acces corpus: {e}")
# Fallback
return await self._trigger_rumination_free()
async def _trigger_rumination_free(self) -> Trigger:
"""Rumination libre : revisite une pensee ancienne."""
if self.weaviate is None:
return self._create_fallback_trigger()
try:
collection = self.weaviate.collections.get("Thought")
# Recuperer une pensee ancienne
results = collection.query.fetch_objects(limit=10)
if results.objects:
thought = random.choice(results.objects)
props = thought.properties
# Calculer age
try:
thought_time = datetime.fromisoformat(
props.get('timestamp', '').replace('Z', '')
)
age_days = (datetime.now() - thought_time).days
except Exception:
age_days = 0
return Trigger(
type=TriggerType.RUMINATION_FREE,
content=props.get('content', ''),
source='thought_rumination',
metadata={
'original_thought_id': str(thought.uuid) if hasattr(thought, 'uuid') else '',
'thought_type': props.get('thought_type', ''),
'age_days': age_days,
}
)
except Exception as e:
logger.warning(f"Erreur acces thoughts: {e}")
return self._create_fallback_trigger()
def _create_fallback_trigger(self) -> Trigger:
"""Trigger de fallback quand aucune source disponible."""
return Trigger(
type=TriggerType.EMPTY,
content="Silence. Aucune entree disponible. Etat contemplatif.",
source='fallback',
)
def create_user_trigger(self, content: str, metadata: Dict = None) -> Trigger:
"""Cree un trigger utilisateur."""
return Trigger(
type=TriggerType.USER,
content=content,
source='user',
priority=2, # Priorite maximale
metadata=metadata or {},
)
def create_veille_trigger(
self,
title: str,
snippet: str,
url: str,
source: str = 'tavily'
) -> Trigger:
"""Cree un trigger de veille."""
return Trigger(
type=TriggerType.VEILLE,
content=f"{title}. {snippet}",
source=source,
metadata={
'url': url,
'title': title,
}
)
class IkarioDaemon:
"""
Daemon d'individuation autonome.
Orchestre tous les composants de l'architecture processuelle:
- LatentEngine : Cycle semiotique
- VigilanceSystem : Surveillance derive x_ref
- StateToLanguage : Traduction vecteur -> texte
- TriggerGenerator : Generation triggers autonomes
"""
def __init__(
self,
latent_engine: LatentEngine,
vigilance: VigilanceSystem,
translator: StateToLanguage,
config: Optional[DaemonConfig] = None,
weaviate_client=None,
notification_callback: Optional[Callable] = None,
):
"""
Initialise le daemon.
Args:
latent_engine: Moteur du cycle semiotique
vigilance: Systeme de vigilance x_ref
translator: Traducteur vecteur -> texte
config: Configuration du daemon
weaviate_client: Client Weaviate (optionnel)
notification_callback: Callback pour notifications David
"""
self.engine = latent_engine
self.vigilance = vigilance
self.translator = translator
self.config = config or DaemonConfig()
self.weaviate = weaviate_client
self.trigger_generator = TriggerGenerator(self.config, weaviate_client)
self._notification_callback = notification_callback
# Etat du daemon
self.running = False
self.mode = DaemonMode.PAUSED
self._trigger_queue: asyncio.Queue = asyncio.Queue()
self._verbalization_history: List[VerbalizationEvent] = []
# Statistiques
self.stats = DaemonStats()
# Tasks async
self._tasks: List[asyncio.Task] = []
async def start(self) -> None:
"""Demarre le daemon."""
if self.running:
logger.warning("Daemon deja en cours d'execution")
return
logger.info("Demarrage du daemon Ikario...")
self.running = True
self.mode = DaemonMode.AUTONOMOUS
self.stats = DaemonStats()
# Lancer les boucles async
self._tasks = [
asyncio.create_task(self._conversation_loop()),
asyncio.create_task(self._autonomous_loop()),
asyncio.create_task(self._vigilance_loop()),
]
logger.info("Daemon Ikario demarre")
async def stop(self) -> None:
"""Arrete le daemon proprement."""
if not self.running:
return
logger.info("Arret du daemon Ikario...")
self.running = False
self.mode = DaemonMode.PAUSED
# Annuler les tasks
for task in self._tasks:
task.cancel()
# Attendre annulation
await asyncio.gather(*self._tasks, return_exceptions=True)
self._tasks = []
logger.info("Daemon Ikario arrete")
async def run(self, duration_seconds: Optional[float] = None) -> None:
"""
Execute le daemon.
Args:
duration_seconds: Duree d'execution (None = infini)
"""
await self.start()
try:
if duration_seconds:
await asyncio.sleep(duration_seconds)
else:
# Attendre indefiniment
await asyncio.gather(*self._tasks)
except asyncio.CancelledError:
pass
finally:
await self.stop()
async def send_message(self, content: str, metadata: Dict = None) -> VerbalizationEvent:
"""
Envoie un message utilisateur au daemon.
Args:
content: Contenu du message
metadata: Metadonnees optionnelles
Returns:
Evenement de verbalisation (reponse)
"""
trigger = self.trigger_generator.create_user_trigger(content, metadata)
await self._trigger_queue.put(trigger)
# Attendre et retourner la reponse
# Note: Dans une impl reelle, on utiliserait un Future/Event
# Ici on traite directement
return await self._process_conversation_trigger(trigger)
async def _conversation_loop(self) -> None:
"""Traite les messages utilisateur (prioritaire)."""
while self.running:
try:
# Attendre un trigger avec timeout
trigger = await asyncio.wait_for(
self._trigger_queue.get(),
timeout=1.0
)
if trigger.type == TriggerType.USER:
await self._process_conversation_trigger(trigger)
elif trigger.type == TriggerType.VEILLE:
await self._process_veille_trigger(trigger)
except asyncio.TimeoutError:
continue
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Erreur conversation loop: {e}")
async def _process_conversation_trigger(self, trigger: Trigger) -> VerbalizationEvent:
"""Traite un trigger de conversation (toujours verbalise)."""
self.mode = DaemonMode.CONVERSATION
# Executer cycle semiotique
result = await self._run_cycle(trigger)
# Mode conversation = TOUJOURS verbaliser
translation = await self.translator.translate(
result.new_state,
output_type="response",
context=trigger.content[:200],
)
event = VerbalizationEvent(
text=translation.text,
reason="conversation_mode",
trigger_type=trigger.type.value,
state_id=result.new_state.state_id,
dissonance=result.dissonance.total if result.dissonance else 0,
)
self._verbalization_history.append(event)
self.stats.conversation_cycles += 1
self.stats.verbalizations += 1
self.mode = DaemonMode.AUTONOMOUS
return event
async def _process_veille_trigger(self, trigger: Trigger) -> None:
"""Traite un trigger de veille (silencieux sauf decouverte)."""
result = await self._run_cycle(trigger)
self.stats.veille_items_processed += 1
# Verbaliser seulement si haute dissonance
if result.should_verbalize:
await self._verbalize_autonomous(result, trigger, "veille_discovery")
async def _autonomous_loop(self) -> None:
"""Cycles autonomes de pensee silencieuse."""
while self.running:
try:
await asyncio.sleep(self.config.cycle_interval_seconds)
if self.mode == DaemonMode.CONVERSATION:
# Ne pas interrompre une conversation
continue
# Generer trigger autonome
trigger = await self.trigger_generator.generate_autonomous_trigger()
# Executer cycle
result = await self._run_cycle(trigger)
self.stats.autonomous_cycles += 1
# Mettre a jour stats selon type
if trigger.type == TriggerType.RUMINATION:
self.stats.impacts_ruminated += 1
elif trigger.type == TriggerType.CORPUS:
self.stats.corpus_processed += 1
# Verbaliser SEULEMENT si necessaire
if result.should_verbalize:
await self._verbalize_autonomous(
result,
trigger,
result.verbalization_reason
)
else:
self.stats.silent_cycles += 1
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Erreur autonomous loop: {e}")
async def _vigilance_loop(self) -> None:
"""Verifie periodiquement la derive par rapport a x_ref."""
while self.running:
try:
await asyncio.sleep(self.config.vigilance_interval_seconds)
# Obtenir l'etat actuel
current_state = self.engine._get_current_state()
if current_state is None:
continue
# Verifier derive
alert = self.vigilance.check_drift(current_state)
if alert.is_alert:
self.stats.vigilance_alerts += 1
logger.warning(f"Alerte vigilance: {alert.message}")
# Notifier David si critique
if alert.level == "critical":
await self._notify_david_alert(alert)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Erreur vigilance loop: {e}")
async def _run_cycle(self, trigger: Trigger) -> CycleResult:
"""Execute un cycle semiotique."""
self.stats.total_cycles += 1
self.stats.last_cycle_time = datetime.now().isoformat()
result = await self.engine.run_cycle(trigger.to_dict())
return result
async def _verbalize_autonomous(
self,
result: CycleResult,
trigger: Trigger,
reason: str,
) -> None:
"""Verbalise en mode autonome."""
translation = await self.translator.translate(
result.new_state,
output_type="autonomous_verbalization",
context=f"{reason}: {trigger.content[:100]}",
)
event = VerbalizationEvent(
text=translation.text,
reason=reason,
trigger_type=trigger.type.value,
state_id=result.new_state.state_id,
dissonance=result.dissonance.total if result.dissonance else 0,
)
self._verbalization_history.append(event)
self.stats.verbalizations += 1
# Notifier David
await self._notify_david(event)
async def _notify_david(self, event: VerbalizationEvent) -> None:
"""Notifie David d'une verbalisation autonome."""
if self._notification_callback:
try:
await self._notification_callback(event)
except Exception as e:
logger.error(f"Erreur notification: {e}")
logger.info(f"Verbalisation autonome [{event.reason}]: {event.text[:100]}...")
async def _notify_david_alert(self, alert: VigilanceAlert) -> None:
"""Notifie David d'une alerte de vigilance."""
message = f"ALERTE VIGILANCE [{alert.level.upper()}]: {alert.message}"
if self._notification_callback:
try:
# Creer un evenement special pour l'alerte
event = VerbalizationEvent(
text=message,
reason=f"vigilance_{alert.level}",
trigger_type="vigilance",
state_id=alert.state_id,
dissonance=0,
)
await self._notification_callback(event)
except Exception as e:
logger.error(f"Erreur notification alerte: {e}")
logger.warning(message)
def get_stats(self) -> Dict[str, Any]:
"""Retourne les statistiques du daemon."""
return self.stats.to_dict()
def get_verbalization_history(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Retourne l'historique des verbalisations."""
return [v.to_dict() for v in self._verbalization_history[-limit:]]
@property
def is_running(self) -> bool:
"""True si le daemon est en cours d'execution."""
return self.running
@property
def current_mode(self) -> DaemonMode:
"""Mode actuel du daemon."""
return self.mode
def create_daemon(
latent_engine: LatentEngine,
vigilance: VigilanceSystem,
translator: StateToLanguage,
config: Optional[DaemonConfig] = None,
weaviate_client=None,
notification_callback: Optional[Callable] = None,
) -> IkarioDaemon:
"""
Factory pour creer un daemon configure.
Args:
latent_engine: Moteur du cycle semiotique
vigilance: Systeme de vigilance
translator: Traducteur etat -> langage
config: Configuration
weaviate_client: Client Weaviate
notification_callback: Callback notifications
Returns:
IkarioDaemon configure
"""
return IkarioDaemon(
latent_engine=latent_engine,
vigilance=vigilance,
translator=translator,
config=config,
weaviate_client=weaviate_client,
notification_callback=notification_callback,
)