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>
298 lines
8.9 KiB
Python
298 lines
8.9 KiB
Python
#!/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"])
|