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:
483
ikario_processual/tests/test_vigilance.py
Normal file
483
ikario_processual/tests/test_vigilance.py
Normal file
@@ -0,0 +1,483 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests pour le module de vigilance - Phase 6.
|
||||
|
||||
Systeme de vigilance x_ref (David) :
|
||||
1. x_ref N'EST PAS un attracteur (Ikario ne tend pas vers David)
|
||||
2. x_ref EST un garde-fou (alerte si distance > seuil)
|
||||
3. Alertes : ok, warning, critical
|
||||
|
||||
Executer: pytest ikario_processual/tests/test_vigilance.py -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
import pytest
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from ikario_processual.state_tensor import StateTensor, DIMENSION_NAMES, EMBEDDING_DIM
|
||||
from ikario_processual.vigilance import (
|
||||
VigilanceAlert,
|
||||
VigilanceConfig,
|
||||
VigilanceSystem,
|
||||
DavidReference,
|
||||
VigilanceVisualizer,
|
||||
create_vigilance_system,
|
||||
)
|
||||
|
||||
|
||||
def create_random_tensor(state_id: int = 0, seed: int = None) -> StateTensor:
|
||||
"""Cree un tenseur avec des vecteurs aleatoires normalises."""
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
|
||||
tensor = StateTensor(
|
||||
state_id=state_id,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
)
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
v = np.random.randn(EMBEDDING_DIM)
|
||||
v = v / np.linalg.norm(v)
|
||||
setattr(tensor, dim_name, v)
|
||||
return tensor
|
||||
|
||||
|
||||
def create_similar_tensor(reference: StateTensor, noise: float = 0.01) -> StateTensor:
|
||||
"""Cree un tenseur similaire a la reference avec un peu de bruit."""
|
||||
tensor = reference.copy()
|
||||
tensor.state_id = reference.state_id + 1
|
||||
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
vec = getattr(tensor, dim_name).copy()
|
||||
# Ajouter du bruit
|
||||
vec += np.random.randn(EMBEDDING_DIM) * noise
|
||||
# Re-normaliser
|
||||
vec = vec / np.linalg.norm(vec)
|
||||
setattr(tensor, dim_name, vec)
|
||||
|
||||
return tensor
|
||||
|
||||
|
||||
def create_different_tensor(reference: StateTensor, offset: float = 0.5) -> StateTensor:
|
||||
"""Cree un tenseur different de la reference."""
|
||||
tensor = reference.copy()
|
||||
tensor.state_id = reference.state_id + 1
|
||||
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
# Vecteur orthogonal approximatif
|
||||
vec = np.random.randn(EMBEDDING_DIM)
|
||||
vec = vec / np.linalg.norm(vec)
|
||||
setattr(tensor, dim_name, vec)
|
||||
|
||||
return tensor
|
||||
|
||||
|
||||
class TestVigilanceAlert:
|
||||
"""Tests pour VigilanceAlert."""
|
||||
|
||||
def test_create_alert(self):
|
||||
"""Creer une alerte."""
|
||||
alert = VigilanceAlert(
|
||||
level="warning",
|
||||
message="Derive detectee",
|
||||
cumulative_drift=0.015,
|
||||
state_id=5,
|
||||
)
|
||||
|
||||
assert alert.level == "warning"
|
||||
assert alert.cumulative_drift == 0.015
|
||||
assert alert.is_alert is True
|
||||
|
||||
def test_ok_not_alert(self):
|
||||
"""'ok' n'est pas une alerte."""
|
||||
alert = VigilanceAlert(level="ok")
|
||||
assert alert.is_alert is False
|
||||
|
||||
def test_warning_is_alert(self):
|
||||
"""'warning' est une alerte."""
|
||||
alert = VigilanceAlert(level="warning")
|
||||
assert alert.is_alert is True
|
||||
|
||||
def test_critical_is_alert(self):
|
||||
"""'critical' est une alerte."""
|
||||
alert = VigilanceAlert(level="critical")
|
||||
assert alert.is_alert is True
|
||||
|
||||
def test_to_dict(self):
|
||||
"""to_dict() fonctionne."""
|
||||
alert = VigilanceAlert(
|
||||
level="critical",
|
||||
message="Test",
|
||||
dimensions={'firstness': 0.1},
|
||||
cumulative_drift=0.025,
|
||||
)
|
||||
|
||||
d = alert.to_dict()
|
||||
assert 'level' in d
|
||||
assert 'message' in d
|
||||
assert 'dimensions' in d
|
||||
assert d['cumulative_drift'] == 0.025
|
||||
|
||||
|
||||
class TestVigilanceConfig:
|
||||
"""Tests pour VigilanceConfig."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Configuration par defaut."""
|
||||
config = VigilanceConfig()
|
||||
|
||||
assert config.threshold_cumulative == 0.01 # 1%
|
||||
assert config.threshold_per_cycle == 0.002 # 0.2%
|
||||
assert config.threshold_per_dimension == 0.05 # 5%
|
||||
assert config.critical_multiplier == 2.0
|
||||
|
||||
def test_validate_default(self):
|
||||
"""La config par defaut est valide."""
|
||||
config = VigilanceConfig()
|
||||
assert config.validate() is True
|
||||
|
||||
def test_validate_invalid(self):
|
||||
"""Config invalide."""
|
||||
config = VigilanceConfig(threshold_cumulative=2.0) # > 1
|
||||
assert config.validate() is False
|
||||
|
||||
|
||||
class TestVigilanceSystem:
|
||||
"""Tests pour VigilanceSystem."""
|
||||
|
||||
def test_create_system(self):
|
||||
"""Creer un systeme de vigilance."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
assert system.x_ref is x_ref
|
||||
assert system.cumulative_drift == 0.0
|
||||
assert len(system.history) == 0
|
||||
|
||||
def test_no_drift_when_identical(self):
|
||||
"""Pas de derive si X_t == x_ref."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Premier check avec x_ref lui-meme
|
||||
alert = system.check_drift(x_ref)
|
||||
|
||||
assert alert.level == "ok"
|
||||
assert alert.cumulative_drift == 0.0
|
||||
|
||||
def test_warning_when_drifting(self):
|
||||
"""Alerte warning quand derive > seuil."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(
|
||||
x_ref=x_ref,
|
||||
config=VigilanceConfig(threshold_cumulative=0.001) # Seuil bas
|
||||
)
|
||||
|
||||
# Premier check etablit X_prev
|
||||
system.check_drift(x_ref)
|
||||
|
||||
# Creer un etat different
|
||||
X_t = create_different_tensor(x_ref)
|
||||
alert = system.check_drift(X_t)
|
||||
|
||||
# Devrait etre au moins warning ou critical
|
||||
assert alert.level in ("warning", "critical")
|
||||
|
||||
def test_critical_when_high_drift(self):
|
||||
"""Alerte critical quand derive >> seuil."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(
|
||||
x_ref=x_ref,
|
||||
config=VigilanceConfig(
|
||||
threshold_cumulative=0.0001, # Seuil tres bas
|
||||
critical_multiplier=1.5
|
||||
)
|
||||
)
|
||||
|
||||
# Premier check
|
||||
system.check_drift(x_ref)
|
||||
|
||||
# Plusieurs checks avec etats differents pour accumuler drift
|
||||
for i in range(3):
|
||||
X_t = create_different_tensor(x_ref)
|
||||
X_t.state_id = i + 1
|
||||
alert = system.check_drift(X_t)
|
||||
|
||||
assert alert.level == "critical"
|
||||
|
||||
def test_cumulative_drift_increases(self):
|
||||
"""La derive cumulative augmente."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Premier check
|
||||
system.check_drift(x_ref)
|
||||
|
||||
# Plusieurs checks avec de petites differences
|
||||
for i in range(5):
|
||||
X_t = create_similar_tensor(x_ref, noise=0.1)
|
||||
X_t.state_id = i + 1
|
||||
system.check_drift(X_t)
|
||||
|
||||
assert system.cumulative_drift > 0
|
||||
|
||||
def test_reset_cumulative(self):
|
||||
"""Reset de la derive cumulative."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Accumuler de la derive
|
||||
system.check_drift(x_ref)
|
||||
X_t = create_different_tensor(x_ref)
|
||||
system.check_drift(X_t)
|
||||
|
||||
assert system.cumulative_drift > 0
|
||||
|
||||
# Reset
|
||||
system.reset_cumulative()
|
||||
assert system.cumulative_drift == 0.0
|
||||
|
||||
def test_history_recorded(self):
|
||||
"""L'historique des alertes est enregistre."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
for i in range(3):
|
||||
X_t = create_similar_tensor(x_ref, noise=0.05)
|
||||
X_t.state_id = i
|
||||
system.check_drift(X_t)
|
||||
|
||||
assert len(system.history) == 3
|
||||
|
||||
|
||||
class TestDistanceCalculations:
|
||||
"""Tests pour les calculs de distance."""
|
||||
|
||||
def test_distance_per_dimension(self):
|
||||
"""Distance par dimension."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Distance avec soi-meme = 0
|
||||
distances = system._distance_per_dimension(x_ref)
|
||||
|
||||
for dim_name, dist in distances.items():
|
||||
assert np.isclose(dist, 0.0, atol=1e-6)
|
||||
|
||||
def test_distance_opposite_vectors(self):
|
||||
"""Distance avec vecteurs opposes."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Creer un tenseur avec vecteurs opposes
|
||||
X_opposite = x_ref.copy()
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
setattr(X_opposite, dim_name, -getattr(x_ref, dim_name))
|
||||
|
||||
distances = system._distance_per_dimension(X_opposite)
|
||||
|
||||
# Distance cosine avec vecteur oppose = 2 (1 - (-1))
|
||||
for dim_name, dist in distances.items():
|
||||
assert np.isclose(dist, 2.0, atol=1e-6)
|
||||
|
||||
def test_global_distance_self(self):
|
||||
"""Distance globale avec soi-meme = 0."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
dist = system._global_distance(x_ref)
|
||||
assert np.isclose(dist, 0.0, atol=1e-6)
|
||||
|
||||
def test_global_distance_different(self):
|
||||
"""Distance globale avec tenseur different > 0."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
X_different = create_random_tensor(state_id=1, seed=123)
|
||||
dist = system._global_distance(X_different)
|
||||
|
||||
assert dist > 0
|
||||
|
||||
|
||||
class TestTopDriftingDimensions:
|
||||
"""Tests pour l'identification des dimensions en derive."""
|
||||
|
||||
def test_identifies_drifting_dims(self):
|
||||
"""Identifie les dimensions qui derivent."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
# Creer un tenseur ou certaines dimensions sont tres differentes
|
||||
X_t = x_ref.copy()
|
||||
# Inverser seulement 'firstness' et 'valeurs'
|
||||
X_t.firstness = -x_ref.firstness
|
||||
X_t.valeurs = -x_ref.valeurs
|
||||
|
||||
alert = system.check_drift(X_t)
|
||||
|
||||
# Les dimensions inversees devraient etre dans le top
|
||||
assert 'firstness' in alert.top_drifting_dimensions
|
||||
assert 'valeurs' in alert.top_drifting_dimensions
|
||||
|
||||
|
||||
class TestDavidReference:
|
||||
"""Tests pour DavidReference."""
|
||||
|
||||
def test_create_from_declared_profile_no_model(self):
|
||||
"""Creer x_ref depuis profil sans modele d'embedding."""
|
||||
# Creer un fichier profil temporaire
|
||||
profile = {
|
||||
"profile": {
|
||||
"epistemic": {"curiosity": 8, "certainty": 3},
|
||||
"affective": {"enthusiasm": 5},
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(profile, f)
|
||||
profile_path = f.name
|
||||
|
||||
x_ref = DavidReference.create_from_declared_profile(profile_path)
|
||||
|
||||
assert x_ref.state_id == -1
|
||||
assert x_ref.firstness.shape == (EMBEDDING_DIM,)
|
||||
# Vecteurs normalises
|
||||
assert np.isclose(np.linalg.norm(x_ref.firstness), 1.0)
|
||||
|
||||
def test_create_hybrid_fallback(self):
|
||||
"""create_hybrid sans weaviate retourne profil declare."""
|
||||
profile = {"profile": {"epistemic": {"curiosity": 5}}}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(profile, f)
|
||||
profile_path = f.name
|
||||
|
||||
# Sans weaviate, utilise create_hybrid avec mock
|
||||
x_declared = DavidReference.create_from_declared_profile(profile_path)
|
||||
|
||||
assert x_declared is not None
|
||||
assert x_declared.state_id == -1
|
||||
|
||||
|
||||
class TestVigilanceVisualizer:
|
||||
"""Tests pour VigilanceVisualizer."""
|
||||
|
||||
def test_format_distance_report(self):
|
||||
"""format_distance_report genere un rapport."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
X_t = create_similar_tensor(x_ref, noise=0.1)
|
||||
|
||||
report = VigilanceVisualizer.format_distance_report(X_t, x_ref, 0.005)
|
||||
|
||||
assert "RAPPORT VIGILANCE" in report
|
||||
assert "Derive cumulative" in report
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
assert dim_name in report
|
||||
|
||||
def test_format_report_includes_bars(self):
|
||||
"""Le rapport inclut des barres de progression."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
X_t = create_different_tensor(x_ref)
|
||||
|
||||
report = VigilanceVisualizer.format_distance_report(X_t, x_ref)
|
||||
|
||||
# Devrait avoir des barres (caracteres # et -)
|
||||
assert "#" in report or "-" in report
|
||||
|
||||
|
||||
class TestCreateVigilanceSystem:
|
||||
"""Tests pour la factory create_vigilance_system."""
|
||||
|
||||
def test_create_without_args(self):
|
||||
"""Creer un systeme sans arguments (mode test)."""
|
||||
system = create_vigilance_system()
|
||||
|
||||
assert system is not None
|
||||
assert system.x_ref is not None
|
||||
assert system.x_ref.state_id == -1
|
||||
|
||||
def test_create_with_profile(self):
|
||||
"""Creer un systeme avec profil."""
|
||||
profile = {"profile": {"epistemic": {"curiosity": 7}}}
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
json.dump(profile, f)
|
||||
profile_path = f.name
|
||||
|
||||
system = create_vigilance_system(profile_path=profile_path)
|
||||
|
||||
assert system is not None
|
||||
assert system.x_ref.state_id == -1
|
||||
|
||||
def test_create_with_custom_config(self):
|
||||
"""Creer un systeme avec config personnalisee."""
|
||||
config = VigilanceConfig(
|
||||
threshold_cumulative=0.02,
|
||||
threshold_per_cycle=0.005
|
||||
)
|
||||
|
||||
system = create_vigilance_system(config=config)
|
||||
|
||||
assert system.config.threshold_cumulative == 0.02
|
||||
assert system.config.threshold_per_cycle == 0.005
|
||||
|
||||
|
||||
class TestGetStats:
|
||||
"""Tests pour get_stats."""
|
||||
|
||||
def test_initial_stats(self):
|
||||
"""Stats initiales."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
stats = system.get_stats()
|
||||
|
||||
assert stats['cumulative_drift'] == 0.0
|
||||
assert stats['total_checks'] == 0
|
||||
assert stats['alerts_count'] == {'ok': 0, 'warning': 0, 'critical': 0}
|
||||
|
||||
def test_stats_after_checks(self):
|
||||
"""Stats apres plusieurs checks."""
|
||||
x_ref = create_random_tensor(state_id=-1, seed=42)
|
||||
system = VigilanceSystem(x_ref=x_ref)
|
||||
|
||||
for i in range(5):
|
||||
X_t = create_similar_tensor(x_ref, noise=0.05)
|
||||
X_t.state_id = i
|
||||
system.check_drift(X_t)
|
||||
|
||||
stats = system.get_stats()
|
||||
|
||||
assert stats['total_checks'] == 5
|
||||
assert len(stats['recent_alerts']) <= 10
|
||||
|
||||
|
||||
class TestIntegrationWithRealProfile:
|
||||
"""Tests d'integration avec le vrai profil David."""
|
||||
|
||||
def test_load_real_profile(self):
|
||||
"""Charger le vrai profil david_profile_declared.json."""
|
||||
profile_path = Path(__file__).parent.parent / "david_profile_declared.json"
|
||||
|
||||
if not profile_path.exists():
|
||||
pytest.skip("david_profile_declared.json not found")
|
||||
|
||||
x_ref = DavidReference.create_from_declared_profile(str(profile_path))
|
||||
|
||||
assert x_ref is not None
|
||||
assert x_ref.state_id == -1
|
||||
|
||||
# Verifier que toutes les dimensions sont initialisees
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
vec = getattr(x_ref, dim_name)
|
||||
assert vec.shape == (EMBEDDING_DIM,)
|
||||
assert np.isclose(np.linalg.norm(vec), 1.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user