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:
140
ikario_processual/tests/test_phase3_transformation.py
Normal file
140
ikario_processual/tests/test_phase3_transformation.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests pour Phase 3 - Transformation d'état."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from ..state_transformation import (
|
||||
transform_state,
|
||||
compute_adaptive_params,
|
||||
StateTransformer
|
||||
)
|
||||
|
||||
|
||||
class TestTransformState:
|
||||
"""Tests de la fonction de transformation."""
|
||||
|
||||
def test_transform_preserves_norm(self):
|
||||
"""Le vecteur transformé doit rester normalisé."""
|
||||
s_prev = np.random.randn(1024)
|
||||
s_prev = s_prev / np.linalg.norm(s_prev)
|
||||
|
||||
occasion = np.random.randn(1024)
|
||||
occasion = occasion / np.linalg.norm(occasion)
|
||||
|
||||
s_new = transform_state(s_prev, occasion)
|
||||
|
||||
assert abs(np.linalg.norm(s_new) - 1.0) < 0.001
|
||||
|
||||
def test_high_alpha_preserves_identity(self):
|
||||
"""Alpha élevé = peu de changement."""
|
||||
s_prev = np.random.randn(1024)
|
||||
s_prev = s_prev / np.linalg.norm(s_prev)
|
||||
|
||||
occasion = np.random.randn(1024)
|
||||
occasion = occasion / np.linalg.norm(occasion)
|
||||
|
||||
s_new = transform_state(s_prev, occasion, alpha=0.99, beta=0.01)
|
||||
|
||||
similarity = np.dot(s_prev, s_new)
|
||||
assert similarity > 0.98, f"Trop de changement: similarity={similarity}"
|
||||
|
||||
def test_low_alpha_allows_change(self):
|
||||
"""Alpha bas = plus de changement."""
|
||||
s_prev = np.random.randn(1024)
|
||||
s_prev = s_prev / np.linalg.norm(s_prev)
|
||||
|
||||
# Occasion très différente
|
||||
occasion = -s_prev + 0.1 * np.random.randn(1024)
|
||||
occasion = occasion / np.linalg.norm(occasion)
|
||||
|
||||
s_new = transform_state(s_prev, occasion, alpha=0.5, beta=0.5)
|
||||
|
||||
similarity = np.dot(s_prev, s_new)
|
||||
assert similarity < 0.9, f"Pas assez de changement: similarity={similarity}"
|
||||
|
||||
def test_identical_occasion_increases_identity(self):
|
||||
"""Si l'occasion est identique à l'état, l'identité est renforcée."""
|
||||
s_prev = np.random.randn(1024)
|
||||
s_prev = s_prev / np.linalg.norm(s_prev)
|
||||
|
||||
s_new = transform_state(s_prev, s_prev.copy(), alpha=0.85, beta=0.15)
|
||||
|
||||
# Doit rester très similaire
|
||||
similarity = np.dot(s_prev, s_new)
|
||||
assert similarity > 0.99
|
||||
|
||||
|
||||
class TestAdaptiveParams:
|
||||
"""Tests des paramètres adaptatifs."""
|
||||
|
||||
def test_default_params(self):
|
||||
"""Paramètres par défaut."""
|
||||
alpha, beta = compute_adaptive_params({})
|
||||
assert abs(alpha + beta - 1.0) < 0.001
|
||||
assert 0.8 < alpha < 0.9
|
||||
assert 0.1 < beta < 0.2
|
||||
|
||||
def test_more_thoughts_increases_beta(self):
|
||||
"""Plus de pensées = plus de beta."""
|
||||
alpha1, beta1 = compute_adaptive_params({'thoughts_created': 0})
|
||||
alpha2, beta2 = compute_adaptive_params({'thoughts_created': 5})
|
||||
|
||||
assert beta2 > beta1
|
||||
assert alpha2 < alpha1
|
||||
|
||||
def test_timer_reduces_intensity(self):
|
||||
"""Timer = moins d'intensité."""
|
||||
alpha_user, beta_user = compute_adaptive_params({
|
||||
'trigger_type': 'user',
|
||||
'thoughts_created': 3
|
||||
})
|
||||
alpha_timer, beta_timer = compute_adaptive_params({
|
||||
'trigger_type': 'timer',
|
||||
'thoughts_created': 3
|
||||
})
|
||||
|
||||
assert beta_timer < beta_user
|
||||
assert alpha_timer > alpha_user
|
||||
|
||||
def test_params_sum_to_one(self):
|
||||
"""Alpha + beta = 1 toujours."""
|
||||
test_cases = [
|
||||
{'thoughts_created': 0},
|
||||
{'thoughts_created': 10},
|
||||
{'trigger_type': 'timer'},
|
||||
{'trigger_type': 'user', 'trigger_content': 'x' * 300},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
alpha, beta = compute_adaptive_params(case)
|
||||
assert abs(alpha + beta - 1.0) < 0.001, f"Cas: {case}"
|
||||
|
||||
|
||||
class TestStateTransformer:
|
||||
"""Tests du StateTransformer (nécessite Weaviate)."""
|
||||
|
||||
@pytest.fixture
|
||||
def transformer(self):
|
||||
"""Créer un transformer sans modèle (tests unitaires)."""
|
||||
return StateTransformer(embedding_model=None)
|
||||
|
||||
def test_get_current_state_id(self, transformer):
|
||||
"""Test de récupération de l'ID courant."""
|
||||
# Ce test nécessite Weaviate
|
||||
state_id = transformer.get_current_state_id()
|
||||
assert isinstance(state_id, int)
|
||||
# -1 si pas d'état, sinon >= 0
|
||||
assert state_id >= -1
|
||||
|
||||
@pytest.mark.skip(reason="Nécessite Weaviate avec S(0)")
|
||||
def test_get_state_vector(self, transformer):
|
||||
"""Test de récupération du vecteur d'état."""
|
||||
vector = transformer.get_state_vector(0)
|
||||
if vector is not None:
|
||||
assert len(vector) == 1024
|
||||
assert abs(np.linalg.norm(vector) - 1.0) < 0.01
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
215
ikario_processual/tests/test_phase4_logging.py
Normal file
215
ikario_processual/tests/test_phase4_logging.py
Normal file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests pour Phase 4 - Logging des occasions."""
|
||||
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ..occasion_logger import OccasionLogger, OccasionLog
|
||||
|
||||
|
||||
class TestOccasionLog:
|
||||
"""Tests de la structure OccasionLog."""
|
||||
|
||||
def test_create_occasion_log(self):
|
||||
"""Créer un OccasionLog basique."""
|
||||
log = OccasionLog(
|
||||
occasion_id=1,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content="Test",
|
||||
previous_state_id=0,
|
||||
prehended_thoughts_count=5,
|
||||
prehended_docs_count=2,
|
||||
response_summary="Response",
|
||||
new_state_id=1,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15,
|
||||
processing_time_ms=1000
|
||||
)
|
||||
|
||||
assert log.occasion_id == 1
|
||||
assert log.trigger_type == "user"
|
||||
assert log.alpha_used == 0.85
|
||||
|
||||
def test_default_lists(self):
|
||||
"""Les listes par défaut sont vides."""
|
||||
log = OccasionLog(
|
||||
occasion_id=1,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content="Test",
|
||||
previous_state_id=0,
|
||||
prehended_thoughts_count=0,
|
||||
prehended_docs_count=0,
|
||||
response_summary="Response",
|
||||
new_state_id=1,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15
|
||||
)
|
||||
|
||||
assert log.new_thoughts == []
|
||||
assert log.tools_used == []
|
||||
assert log.prehended_thoughts == []
|
||||
|
||||
|
||||
class TestOccasionLogger:
|
||||
"""Tests du logger d'occasions."""
|
||||
|
||||
@pytest.fixture
|
||||
def temp_logger(self):
|
||||
"""Créer un logger avec répertoire temporaire."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield OccasionLogger(tmpdir)
|
||||
|
||||
def test_log_and_retrieve(self, temp_logger):
|
||||
"""Logger et relire une occasion."""
|
||||
occasion = OccasionLog(
|
||||
occasion_id=42,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content="Question test",
|
||||
previous_state_id=5,
|
||||
prehended_thoughts_count=3,
|
||||
prehended_docs_count=1,
|
||||
response_summary="Réponse test",
|
||||
new_thoughts=["Nouvelle pensée"],
|
||||
tools_used=["search_thoughts"],
|
||||
new_state_id=6,
|
||||
alpha_used=0.82,
|
||||
beta_used=0.18,
|
||||
processing_time_ms=2500
|
||||
)
|
||||
|
||||
# Logger
|
||||
filepath = temp_logger.log(occasion)
|
||||
assert filepath.exists()
|
||||
|
||||
# Relire
|
||||
loaded = temp_logger.get_occasion(42)
|
||||
assert loaded is not None
|
||||
assert loaded.occasion_id == 42
|
||||
assert loaded.trigger_content == "Question test"
|
||||
assert loaded.new_thoughts == ["Nouvelle pensée"]
|
||||
|
||||
def test_get_nonexistent(self, temp_logger):
|
||||
"""Récupérer une occasion inexistante retourne None."""
|
||||
loaded = temp_logger.get_occasion(99999)
|
||||
assert loaded is None
|
||||
|
||||
def test_get_recent_occasions(self, temp_logger):
|
||||
"""Récupérer les occasions récentes."""
|
||||
# Créer plusieurs occasions
|
||||
for i in range(5):
|
||||
occasion = OccasionLog(
|
||||
occasion_id=i,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content=f"Question {i}",
|
||||
previous_state_id=max(0, i - 1),
|
||||
prehended_thoughts_count=i,
|
||||
prehended_docs_count=1,
|
||||
response_summary=f"Réponse {i}",
|
||||
new_state_id=i,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15,
|
||||
processing_time_ms=1000 + i * 100
|
||||
)
|
||||
temp_logger.log(occasion)
|
||||
|
||||
# Récupérer les 3 dernières
|
||||
recent = temp_logger.get_recent_occasions(3)
|
||||
assert len(recent) == 3
|
||||
|
||||
# Vérifier l'ordre (plus récent d'abord)
|
||||
assert recent[0].occasion_id == 4
|
||||
assert recent[1].occasion_id == 3
|
||||
assert recent[2].occasion_id == 2
|
||||
|
||||
def test_get_last_occasion_id(self, temp_logger):
|
||||
"""Récupérer l'ID de la dernière occasion."""
|
||||
# Vide
|
||||
assert temp_logger.get_last_occasion_id() == -1
|
||||
|
||||
# Ajouter une occasion
|
||||
occasion = OccasionLog(
|
||||
occasion_id=10,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content="Test",
|
||||
previous_state_id=0,
|
||||
prehended_thoughts_count=0,
|
||||
prehended_docs_count=0,
|
||||
response_summary="Response",
|
||||
new_state_id=1,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15
|
||||
)
|
||||
temp_logger.log(occasion)
|
||||
|
||||
assert temp_logger.get_last_occasion_id() == 10
|
||||
|
||||
def test_profile_evolution(self, temp_logger):
|
||||
"""Tracer l'évolution d'une composante."""
|
||||
# Créer des occasions avec évolution de curiosité
|
||||
for i in range(5):
|
||||
occasion = OccasionLog(
|
||||
occasion_id=i,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user",
|
||||
trigger_content=f"Question {i}",
|
||||
previous_state_id=max(0, i - 1),
|
||||
prehended_thoughts_count=0,
|
||||
prehended_docs_count=0,
|
||||
response_summary=f"Réponse {i}",
|
||||
new_state_id=i,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15,
|
||||
profile_before={"epistemic": {"curiosity": 0.5 + i * 0.05}},
|
||||
profile_after={"epistemic": {"curiosity": 0.5 + (i + 1) * 0.05}}
|
||||
)
|
||||
temp_logger.log(occasion)
|
||||
|
||||
evolution = temp_logger.get_profile_evolution("curiosity", last_n=5)
|
||||
|
||||
assert len(evolution) == 5
|
||||
# Vérifier que la curiosité augmente
|
||||
values = [v for _, v in evolution]
|
||||
assert values[-1] > values[0]
|
||||
|
||||
def test_statistics(self, temp_logger):
|
||||
"""Calculer des statistiques."""
|
||||
# Créer quelques occasions
|
||||
for i in range(3):
|
||||
occasion = OccasionLog(
|
||||
occasion_id=i,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
trigger_type="user" if i % 2 == 0 else "timer",
|
||||
trigger_content=f"Question {i}",
|
||||
previous_state_id=max(0, i - 1),
|
||||
prehended_thoughts_count=0,
|
||||
prehended_docs_count=0,
|
||||
response_summary=f"Réponse {i}",
|
||||
new_thoughts=["t1"] if i == 0 else [],
|
||||
tools_used=["tool1", "tool2"],
|
||||
new_state_id=i,
|
||||
alpha_used=0.85,
|
||||
beta_used=0.15,
|
||||
processing_time_ms=1000 + i * 500
|
||||
)
|
||||
temp_logger.log(occasion)
|
||||
|
||||
stats = temp_logger.get_statistics()
|
||||
|
||||
assert stats["count"] == 3
|
||||
assert "processing_time" in stats
|
||||
assert "thoughts_created" in stats
|
||||
assert "trigger_distribution" in stats
|
||||
assert stats["trigger_distribution"]["user"] == 2
|
||||
assert stats["trigger_distribution"]["timer"] == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
167
ikario_processual/tests/test_phase5_occasion_manager.py
Normal file
167
ikario_processual/tests/test_phase5_occasion_manager.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests pour Phase 5 - OccasionManager."""
|
||||
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from ..occasion_manager import OccasionManager, get_state_profile
|
||||
|
||||
|
||||
class TestGetStateProfile:
|
||||
"""Tests de la fonction get_state_profile."""
|
||||
|
||||
@pytest.mark.skip(reason="Nécessite Weaviate avec S(0) et directions")
|
||||
def test_get_profile_s0(self):
|
||||
"""Récupérer le profil de S(0)."""
|
||||
profile = get_state_profile(0)
|
||||
|
||||
assert isinstance(profile, dict)
|
||||
# Devrait avoir des catégories
|
||||
assert len(profile) > 0
|
||||
|
||||
# Les valeurs doivent être dans [-1, 1]
|
||||
for category, components in profile.items():
|
||||
for name, value in components.items():
|
||||
assert -1 <= value <= 1, f"{name} = {value} hors limites"
|
||||
|
||||
|
||||
class TestOccasionManager:
|
||||
"""Tests de l'OccasionManager."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Créer un manager avec répertoire temporaire."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield OccasionManager(log_dir=tmpdir, embedding_model=None)
|
||||
|
||||
def test_manager_initialization(self, manager):
|
||||
"""Test d'initialisation du manager."""
|
||||
assert manager.current_occasion_id >= 0
|
||||
assert manager.logger is not None
|
||||
assert manager.transformer is not None
|
||||
|
||||
def test_prehend_structure(self, manager):
|
||||
"""Test de la structure de préhension."""
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Test question",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
prehension = manager._prehend(trigger)
|
||||
|
||||
assert "previous_state_id" in prehension
|
||||
assert "thoughts" in prehension
|
||||
assert "documents" in prehension
|
||||
assert isinstance(prehension["thoughts"], list)
|
||||
assert isinstance(prehension["documents"], list)
|
||||
|
||||
def test_concresce_simulation(self, manager):
|
||||
"""Test de la concrescence (simulation)."""
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Test question sur Whitehead",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
prehension = {
|
||||
"previous_state_id": 0,
|
||||
"previous_state_vector": None,
|
||||
"thoughts": [{"content": "Pensée 1"}],
|
||||
"documents": []
|
||||
}
|
||||
|
||||
concrescence = manager._concresce(trigger, prehension)
|
||||
|
||||
assert "response" in concrescence
|
||||
assert "new_thoughts" in concrescence
|
||||
assert "tools_used" in concrescence
|
||||
assert "[Simulation]" in concrescence["response"]
|
||||
|
||||
@pytest.mark.skip(reason="Nécessite Weaviate avec S(0)")
|
||||
def test_run_occasion_full(self, manager):
|
||||
"""Test d'un cycle complet d'occasion."""
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Qu'est-ce que le processus selon Whitehead ?",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
result = manager.run_occasion(trigger)
|
||||
|
||||
assert "occasion_id" in result
|
||||
assert "response" in result
|
||||
assert "new_state_id" in result
|
||||
assert "profile" in result
|
||||
assert "processing_time_ms" in result
|
||||
assert result["processing_time_ms"] > 0
|
||||
|
||||
@pytest.mark.skip(reason="Nécessite Weaviate avec S(0)")
|
||||
def test_state_evolution_after_occasion(self, manager):
|
||||
"""Vérifier que l'état évolue après une occasion."""
|
||||
initial_state_id = manager.transformer.get_current_state_id()
|
||||
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Je suis très curieux à propos de la philosophie",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
result = manager.run_occasion(trigger)
|
||||
|
||||
assert result["new_state_id"] == initial_state_id + 1
|
||||
|
||||
@pytest.mark.skip(reason="Nécessite Weaviate")
|
||||
def test_occasion_logged(self, manager):
|
||||
"""Vérifier que l'occasion est loggée."""
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Test logging",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
result = manager.run_occasion(trigger)
|
||||
|
||||
logged = manager.logger.get_occasion(result["occasion_id"])
|
||||
assert logged is not None
|
||||
assert logged.trigger_content == "Test logging"
|
||||
|
||||
|
||||
class TestOccasionManagerTriggerTypes:
|
||||
"""Tests des différents types de triggers."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
yield OccasionManager(log_dir=tmpdir, embedding_model=None)
|
||||
|
||||
def test_user_trigger(self, manager):
|
||||
"""Test trigger utilisateur."""
|
||||
trigger = {
|
||||
"type": "user",
|
||||
"content": "Question utilisateur",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
prehension = manager._prehend(trigger)
|
||||
concrescence = manager._concresce(trigger, prehension)
|
||||
|
||||
assert "Question utilisateur" in concrescence["response"]
|
||||
|
||||
def test_timer_trigger(self, manager):
|
||||
"""Test trigger timer (auto-réflexion)."""
|
||||
trigger = {
|
||||
"type": "timer",
|
||||
"content": "Moment d'auto-réflexion",
|
||||
"metadata": {"auto": True}
|
||||
}
|
||||
|
||||
prehension = manager._prehend(trigger)
|
||||
concrescence = manager._concresce(trigger, prehension)
|
||||
|
||||
assert concrescence["response"] is not None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user