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,297 @@
#!/usr/bin/env python3
"""
Tests pour StateTensor - Tenseur d'état 8×1024.
Exécuter: pytest ikario_processual/tests/test_state_tensor.py -v
"""
import numpy as np
import pytest
from datetime import datetime
# Import du module à tester
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from ikario_processual.state_tensor import (
StateTensor,
TensorDimension,
DIMENSION_NAMES,
EMBEDDING_DIM,
)
class TestStateTensorBasic:
"""Tests de base pour StateTensor."""
def test_create_empty_tensor(self):
"""Test création d'un tenseur vide."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
assert tensor.state_id == 0
assert tensor.firstness.shape == (EMBEDDING_DIM,)
assert tensor.valeurs.shape == (EMBEDDING_DIM,)
assert np.all(tensor.firstness == 0)
def test_create_with_values(self):
"""Test création avec valeurs."""
firstness = np.random.randn(EMBEDDING_DIM)
firstness = firstness / np.linalg.norm(firstness)
tensor = StateTensor(
state_id=1,
timestamp=datetime.now().isoformat(),
firstness=firstness,
)
assert np.allclose(tensor.firstness, firstness)
assert np.isclose(np.linalg.norm(tensor.firstness), 1.0)
def test_to_matrix(self):
"""Test conversion en matrice."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
matrix = tensor.to_matrix()
assert matrix.shape == (8, EMBEDDING_DIM)
def test_to_flat(self):
"""Test aplatissement."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
flat = tensor.to_flat()
assert flat.shape == (8 * EMBEDDING_DIM,)
assert flat.shape == (8192,)
def test_dimension_names(self):
"""Test que toutes les dimensions sont présentes."""
expected = [
"firstness", "secondness", "thirdness", "dispositions",
"orientations", "engagements", "pertinences", "valeurs"
]
assert DIMENSION_NAMES == expected
assert len(DIMENSION_NAMES) == 8
class TestStateTensorOperations:
"""Tests des opérations sur StateTensor."""
def test_copy(self):
"""Test copie profonde."""
original = StateTensor(
state_id=1,
timestamp=datetime.now().isoformat(),
firstness=np.random.randn(EMBEDDING_DIM),
)
copied = original.copy()
# Modifier l'original ne doit pas affecter la copie
original.firstness[0] = 999.0
assert copied.firstness[0] != 999.0
def test_set_dimension(self):
"""Test modification d'une dimension."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
new_vec = np.random.randn(EMBEDDING_DIM)
tensor.set_dimension(TensorDimension.VALEURS, new_vec)
# Doit être normalisé
assert np.isclose(np.linalg.norm(tensor.valeurs), 1.0)
def test_get_dimension(self):
"""Test récupération d'une dimension."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
vec = tensor.get_dimension(TensorDimension.FIRSTNESS)
assert vec.shape == (EMBEDDING_DIM,)
def test_to_dict(self):
"""Test conversion en dictionnaire."""
tensor = StateTensor(
state_id=5,
timestamp="2026-02-01T12:00:00",
trigger_type="user",
trigger_content="Hello",
)
d = tensor.to_dict()
assert d["state_id"] == 5
assert d["trigger_type"] == "user"
assert "firstness" not in d # Vecteurs pas dans properties
def test_get_vectors_dict(self):
"""Test récupération des vecteurs pour Weaviate."""
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
vectors = tensor.get_vectors_dict()
assert len(vectors) == 8
assert "firstness" in vectors
assert "valeurs" in vectors
assert len(vectors["firstness"]) == EMBEDDING_DIM
class TestStateTensorAggregation:
"""Tests des opérations d'agrégation."""
def test_weighted_mean_two_tensors(self):
"""Test moyenne pondérée de 2 tenseurs."""
t1 = StateTensor(
state_id=1,
timestamp=datetime.now().isoformat(),
)
t2 = StateTensor(
state_id=2,
timestamp=datetime.now().isoformat(),
)
# Initialiser avec des vecteurs aléatoires normalisés
for dim_name in DIMENSION_NAMES:
v1 = np.random.randn(EMBEDDING_DIM)
v1 = v1 / np.linalg.norm(v1)
setattr(t1, dim_name, v1)
v2 = np.random.randn(EMBEDDING_DIM)
v2 = v2 / np.linalg.norm(v2)
setattr(t2, dim_name, v2)
# Moyenne 50/50
result = StateTensor.weighted_mean([t1, t2], [0.5, 0.5])
# Résultat doit être normalisé
for dim_name in DIMENSION_NAMES:
vec = getattr(result, dim_name)
assert np.isclose(np.linalg.norm(vec), 1.0, atol=1e-5)
def test_blend(self):
"""Test blend 70/30."""
t1 = StateTensor(state_id=1, timestamp=datetime.now().isoformat())
t2 = StateTensor(state_id=2, timestamp=datetime.now().isoformat())
# Initialiser
for dim_name in DIMENSION_NAMES:
v1 = np.random.randn(EMBEDDING_DIM)
v1 = v1 / np.linalg.norm(v1)
setattr(t1, dim_name, v1)
v2 = np.random.randn(EMBEDDING_DIM)
v2 = v2 / np.linalg.norm(v2)
setattr(t2, dim_name, v2)
result = StateTensor.blend(t1, t2, alpha=0.7)
assert result is not None
assert result.state_id == -1 # Non défini
def test_from_matrix(self):
"""Test création depuis matrice."""
matrix = np.random.randn(8, EMBEDDING_DIM)
tensor = StateTensor.from_matrix(
matrix=matrix,
state_id=10,
timestamp="2026-02-01T12:00:00"
)
assert tensor.state_id == 10
assert np.allclose(tensor.firstness, matrix[0])
assert np.allclose(tensor.valeurs, matrix[7])
def test_from_matrix_wrong_shape(self):
"""Test erreur si matrice mauvaise forme."""
matrix = np.random.randn(4, EMBEDDING_DIM) # 4 au lieu de 8
with pytest.raises(ValueError):
StateTensor.from_matrix(matrix, state_id=0, timestamp="")
class TestStateTensorDistance:
"""Tests de calcul de distance entre tenseurs."""
def test_distance_to_self_is_zero(self):
"""Distance à soi-même = 0."""
tensor = StateTensor(state_id=0, 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)
flat = tensor.to_flat()
distance = np.linalg.norm(flat - flat)
assert distance == 0.0
def test_normalized_distance(self):
"""Test distance normalisée entre 2 tenseurs."""
t1 = StateTensor(state_id=1, timestamp=datetime.now().isoformat())
t2 = StateTensor(state_id=2, timestamp=datetime.now().isoformat())
for dim_name in DIMENSION_NAMES:
v1 = np.random.randn(EMBEDDING_DIM)
v1 = v1 / np.linalg.norm(v1)
setattr(t1, dim_name, v1)
v2 = np.random.randn(EMBEDDING_DIM)
v2 = v2 / np.linalg.norm(v2)
setattr(t2, dim_name, v2)
diff = t1.to_flat() - t2.to_flat()
distance = np.linalg.norm(diff) / np.linalg.norm(t2.to_flat())
# Distance normalisée doit être > 0 et finie
assert distance > 0
assert np.isfinite(distance)
class TestStateTensorSerialization:
"""Tests de sérialisation."""
def test_from_dict_roundtrip(self):
"""Test aller-retour dict."""
original = StateTensor(
state_id=42,
timestamp="2026-02-01T12:00:00",
previous_state_id=41,
trigger_type="user",
trigger_content="Test message",
embedding_model="BAAI/bge-m3",
)
# Simuler les vecteurs
vectors = {}
for dim_name in DIMENSION_NAMES:
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
setattr(original, dim_name, v)
vectors[dim_name] = v.tolist()
# Convertir et recréer
props = original.to_dict()
reconstructed = StateTensor.from_dict(props, vectors)
assert reconstructed.state_id == 42
assert reconstructed.trigger_type == "user"
assert np.allclose(reconstructed.firstness, original.firstness)
if __name__ == "__main__":
pytest.main([__file__, "-v"])