Add Phases 3-5: State transformation, OccasionLogger, OccasionManager
Phase 3 - State Transformation: - transform_state() function with alpha/beta parameters - compute_adaptive_params() for dynamic transformation - StateTransformer class for state management Phase 4 - Occasion Logger: - OccasionLog dataclass for structured logging - OccasionLogger for JSON file storage - Profile evolution tracking and statistics Phase 5 - Occasion Manager: - Full cycle: Prehension → Concrescence → Satisfaction - Search integration (thoughts, library) - State creation and logging orchestration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
247
ikario_processual/occasion_logger.py
Normal file
247
ikario_processual/occasion_logger.py
Normal file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OccasionLogger - Logging des occasions d'expérience.
|
||||
|
||||
Chaque occasion est loggée avec:
|
||||
- Trigger (type, contenu)
|
||||
- Préhension (pensées/docs récupérés)
|
||||
- Concrescence (réponse, outils utilisés)
|
||||
- Satisfaction (nouvel état, paramètres)
|
||||
- Profils avant/après
|
||||
|
||||
Les logs sont stockés en JSON pour analyse et debugging.
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class OccasionLog:
|
||||
"""Structure de log pour une occasion."""
|
||||
|
||||
# Identifiants
|
||||
occasion_id: int
|
||||
timestamp: str
|
||||
|
||||
# Trigger
|
||||
trigger_type: str # "user", "timer", "event"
|
||||
trigger_content: str
|
||||
|
||||
# Préhension
|
||||
previous_state_id: int
|
||||
prehended_thoughts_count: int
|
||||
prehended_docs_count: int
|
||||
prehended_thoughts: List[str] = field(default_factory=list) # Résumés des pensées
|
||||
|
||||
# Concrescence
|
||||
response_summary: str
|
||||
new_thoughts: List[str] = field(default_factory=list)
|
||||
tools_used: List[str] = field(default_factory=list)
|
||||
|
||||
# Satisfaction
|
||||
new_state_id: int
|
||||
alpha_used: float
|
||||
beta_used: float
|
||||
|
||||
# Profils
|
||||
profile_before: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||||
profile_after: Dict[str, Dict[str, float]] = field(default_factory=dict)
|
||||
|
||||
# Métriques
|
||||
processing_time_ms: int = 0
|
||||
token_count: Optional[int] = None
|
||||
|
||||
|
||||
class OccasionLogger:
|
||||
"""Gère le logging des occasions en fichiers JSON."""
|
||||
|
||||
def __init__(self, log_dir: str = "logs/occasions"):
|
||||
"""
|
||||
Args:
|
||||
log_dir: Répertoire de stockage des logs
|
||||
"""
|
||||
self.log_dir = Path(log_dir)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def log(self, occasion: OccasionLog) -> Path:
|
||||
"""
|
||||
Enregistre une occasion.
|
||||
|
||||
Args:
|
||||
occasion: OccasionLog à enregistrer
|
||||
|
||||
Returns:
|
||||
Chemin du fichier créé
|
||||
"""
|
||||
filename = f"occasion_{occasion.occasion_id:06d}.json"
|
||||
filepath = self.log_dir / filename
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(asdict(occasion), f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"[OccasionLogger] Occasion {occasion.occasion_id} → {filepath}")
|
||||
return filepath
|
||||
|
||||
def get_occasion(self, occasion_id: int) -> Optional[OccasionLog]:
|
||||
"""
|
||||
Récupère une occasion par son ID.
|
||||
|
||||
Args:
|
||||
occasion_id: ID de l'occasion
|
||||
|
||||
Returns:
|
||||
OccasionLog ou None si non trouvé
|
||||
"""
|
||||
filename = f"occasion_{occasion_id:06d}.json"
|
||||
filepath = self.log_dir / filename
|
||||
|
||||
if not filepath.exists():
|
||||
return None
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return OccasionLog(**data)
|
||||
|
||||
def get_recent_occasions(self, limit: int = 10) -> List[OccasionLog]:
|
||||
"""
|
||||
Récupère les N dernières occasions.
|
||||
|
||||
Args:
|
||||
limit: Nombre max d'occasions à retourner
|
||||
|
||||
Returns:
|
||||
Liste des occasions (plus récentes d'abord)
|
||||
"""
|
||||
files = sorted(self.log_dir.glob("occasion_*.json"), reverse=True)
|
||||
|
||||
occasions = []
|
||||
for f in files[:limit]:
|
||||
with open(f, 'r', encoding='utf-8') as fp:
|
||||
data = json.load(fp)
|
||||
occasions.append(OccasionLog(**data))
|
||||
|
||||
return occasions
|
||||
|
||||
def get_last_occasion_id(self) -> int:
|
||||
"""Retourne l'ID de la dernière occasion (-1 si aucune)."""
|
||||
files = sorted(self.log_dir.glob("occasion_*.json"), reverse=True)
|
||||
if not files:
|
||||
return -1
|
||||
|
||||
# Extraire l'ID du nom de fichier
|
||||
filename = files[0].stem # occasion_000042
|
||||
try:
|
||||
return int(filename.split('_')[1])
|
||||
except (IndexError, ValueError):
|
||||
return -1
|
||||
|
||||
def get_profile_evolution(
|
||||
self,
|
||||
component: str,
|
||||
last_n: int = 20
|
||||
) -> List[tuple]:
|
||||
"""
|
||||
Retourne l'évolution d'une composante sur les N dernières occasions.
|
||||
|
||||
Args:
|
||||
component: Nom de la composante (ex: "curiosity")
|
||||
last_n: Nombre d'occasions à considérer
|
||||
|
||||
Returns:
|
||||
Liste de tuples (occasion_id, valeur)
|
||||
"""
|
||||
occasions = self.get_recent_occasions(last_n)
|
||||
|
||||
evolution = []
|
||||
for occ in reversed(occasions): # Ordre chronologique
|
||||
# Chercher la composante dans le profil après
|
||||
for category, comps in occ.profile_after.items():
|
||||
if component in comps:
|
||||
evolution.append((occ.occasion_id, comps[component]))
|
||||
break
|
||||
|
||||
return evolution
|
||||
|
||||
def get_statistics(self, last_n: int = 100) -> Dict[str, Any]:
|
||||
"""
|
||||
Calcule des statistiques sur les occasions récentes.
|
||||
|
||||
Args:
|
||||
last_n: Nombre d'occasions à analyser
|
||||
|
||||
Returns:
|
||||
Dictionnaire de statistiques
|
||||
"""
|
||||
occasions = self.get_recent_occasions(last_n)
|
||||
|
||||
if not occasions:
|
||||
return {"count": 0}
|
||||
|
||||
# Statistiques de base
|
||||
processing_times = [o.processing_time_ms for o in occasions]
|
||||
thoughts_created = [len(o.new_thoughts) for o in occasions]
|
||||
tools_counts = [len(o.tools_used) for o in occasions]
|
||||
|
||||
# Répartition des triggers
|
||||
trigger_types = {}
|
||||
for o in occasions:
|
||||
trigger_types[o.trigger_type] = trigger_types.get(o.trigger_type, 0) + 1
|
||||
|
||||
return {
|
||||
"count": len(occasions),
|
||||
"processing_time": {
|
||||
"avg_ms": sum(processing_times) / len(processing_times),
|
||||
"min_ms": min(processing_times),
|
||||
"max_ms": max(processing_times),
|
||||
},
|
||||
"thoughts_created": {
|
||||
"total": sum(thoughts_created),
|
||||
"avg_per_occasion": sum(thoughts_created) / len(thoughts_created),
|
||||
},
|
||||
"tools": {
|
||||
"avg_per_occasion": sum(tools_counts) / len(tools_counts),
|
||||
},
|
||||
"trigger_distribution": trigger_types,
|
||||
}
|
||||
|
||||
|
||||
# Test
|
||||
if __name__ == "__main__":
|
||||
logger = OccasionLogger("tests/temp_logs")
|
||||
|
||||
# Créer une occasion test
|
||||
occasion = OccasionLog(
|
||||
occasion_id=1,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content="Test question sur Whitehead",
|
||||
previous_state_id=0,
|
||||
prehended_thoughts_count=5,
|
||||
prehended_docs_count=2,
|
||||
prehended_thoughts=["Pensée 1", "Pensée 2"],
|
||||
response_summary="Réponse détaillée sur le processus...",
|
||||
new_thoughts=["Nouvelle insight sur le devenir"],
|
||||
tools_used=["search_thoughts", "search_library"],
|
||||
new_state_id=1,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15,
|
||||
profile_before={"epistemic": {"curiosity": 0.5, "certainty": 0.3}},
|
||||
profile_after={"epistemic": {"curiosity": 0.55, "certainty": 0.32}},
|
||||
processing_time_ms=1500
|
||||
)
|
||||
|
||||
# Logger
|
||||
filepath = logger.log(occasion)
|
||||
print(f"Logged to: {filepath}")
|
||||
|
||||
# Relire
|
||||
loaded = logger.get_occasion(1)
|
||||
print(f"Loaded: trigger_type={loaded.trigger_type}, new_state_id={loaded.new_state_id}")
|
||||
|
||||
# Stats
|
||||
print(f"Stats: {logger.get_statistics()}")
|
||||
Reference in New Issue
Block a user