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,371 @@
#!/usr/bin/env python3
"""
Tests pour le module de dissonance - Phase 2.
Exécuter: pytest ikario_processual/tests/test_dissonance.py -v
"""
import numpy as np
import pytest
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.dissonance import (
DissonanceConfig,
DissonanceResult,
compute_dissonance,
compute_dissonance_enhanced,
compute_self_dissonance,
cosine_similarity,
Impact,
create_impact_from_dissonance,
)
def create_random_tensor() -> StateTensor:
"""Crée un tenseur avec des vecteurs aléatoires normalisés."""
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)
return tensor
def create_zero_tensor() -> StateTensor:
"""Crée un tenseur avec des vecteurs zéro."""
return StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
class TestCosineSimiliarity:
"""Tests pour la fonction cosine_similarity."""
def test_identical_vectors(self):
"""Vecteurs identiques → similarité = 1."""
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
assert np.isclose(cosine_similarity(v, v), 1.0)
def test_opposite_vectors(self):
"""Vecteurs opposés → similarité = -1."""
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
assert np.isclose(cosine_similarity(v, -v), -1.0)
def test_orthogonal_vectors(self):
"""Vecteurs orthogonaux → similarité ≈ 0."""
v1 = np.zeros(EMBEDDING_DIM)
v1[0] = 1.0
v2 = np.zeros(EMBEDDING_DIM)
v2[1] = 1.0
assert np.isclose(cosine_similarity(v1, v2), 0.0)
def test_zero_vector(self):
"""Vecteur zéro → similarité = 0."""
v1 = np.random.randn(EMBEDDING_DIM)
v2 = np.zeros(EMBEDDING_DIM)
assert cosine_similarity(v1, v2) == 0.0
class TestDissonanceConfig:
"""Tests pour DissonanceConfig."""
def test_default_weights_sum(self):
"""Les poids par défaut doivent sommer à ~1.0."""
config = DissonanceConfig()
weights = config.get_dimension_weights()
total = sum(weights.values())
assert np.isclose(total, 1.0), f"Total des poids: {total}"
def test_all_dimensions_have_weight(self):
"""Chaque dimension doit avoir un poids."""
config = DissonanceConfig()
weights = config.get_dimension_weights()
for dim in DIMENSION_NAMES:
assert dim in weights
assert weights[dim] >= 0
class TestComputeDissonance:
"""Tests pour compute_dissonance (version basique)."""
def test_self_dissonance_is_zero(self):
"""E(X_t, X_t) ≈ 0."""
X_t = create_random_tensor()
# Utiliser une dimension comme input (simuler entrée identique)
e_input = X_t.firstness.copy()
result = compute_dissonance(e_input, X_t)
# La dissonance avec firstness devrait être ~0
assert result.dissonances_by_dimension['firstness'] < 0.01
def test_orthogonal_input_high_dissonance(self):
"""Entrée orthogonale → haute dissonance."""
X_t = create_random_tensor()
# Créer un vecteur orthogonal (difficile en haute dimension, mais différent)
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
result = compute_dissonance(e_input, X_t)
# La dissonance totale devrait être significative
assert result.total > 0.1
def test_result_structure(self):
"""Vérifier la structure du résultat."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
result = compute_dissonance(e_input, X_t)
assert isinstance(result, DissonanceResult)
assert hasattr(result, 'total')
assert hasattr(result, 'is_choc')
assert hasattr(result, 'dissonances_by_dimension')
assert len(result.dissonances_by_dimension) == 8
def test_is_choc_flag(self):
"""Le flag is_choc dépend du seuil."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
# Seuil bas → plus de chocs
config_low = DissonanceConfig(choc_threshold=0.1)
result_low = compute_dissonance(e_input, X_t, config_low)
# Seuil haut → moins de chocs
config_high = DissonanceConfig(choc_threshold=0.9)
result_high = compute_dissonance(e_input, X_t, config_high)
# Avec seuil bas, plus probable d'avoir un choc
assert result_low.is_choc or result_high.is_choc is False
class TestComputeDissonanceEnhanced:
"""Tests pour compute_dissonance_enhanced avec hard negatives."""
def test_no_rag_results(self):
"""Sans résultats RAG → novelty_penalty = 1.0."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
result = compute_dissonance_enhanced(e_input, X_t, rag_results=[])
assert result.novelty_penalty == 1.0
assert result.rag_results_count == 0
def test_with_similar_rag_results(self):
"""Avec résultats RAG similaires → faible novelty."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
# Créer des résultats RAG très similaires (copie avec très peu de bruit)
rag_results = [
{'vector': e_input.copy(), 'content': 'identical'},
{'vector': e_input + np.random.randn(EMBEDDING_DIM) * 0.01, 'content': 'similar'},
]
# Normaliser les vecteurs RAG
for r in rag_results:
r['vector'] = r['vector'] / np.linalg.norm(r['vector'])
result = compute_dissonance_enhanced(e_input, X_t, rag_results)
# Le premier vecteur est identique donc max_sim ~= 1.0
assert result.max_similarity_to_corpus > 0.9
assert result.novelty_penalty == 0.0 # Pas de pénalité si > 0.3
def test_hard_negatives_detection(self):
"""Détection des hard negatives (similarité < seuil)."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
# Créer un vecteur opposé (hard negative)
opposite = -e_input
rag_results = [
{'vector': opposite, 'content': 'contradiction', 'source': 'test'},
{'vector': e_input, 'content': 'similar', 'source': 'test'},
]
result = compute_dissonance_enhanced(e_input, X_t, rag_results)
# Au moins un hard negative devrait être détecté
assert len(result.hard_negatives) >= 1
assert result.contradiction_score > 0
def test_total_dissonance_combines_all(self):
"""La dissonance totale combine base + contradiction + novelty."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
config = DissonanceConfig(
contradiction_weight=0.2,
novelty_weight=0.1
)
result = compute_dissonance_enhanced(e_input, X_t, [], config)
expected_total = (
result.base_dissonance +
config.contradiction_weight * result.contradiction_score +
config.novelty_weight * result.novelty_penalty
)
assert np.isclose(result.total, expected_total)
class TestSelfDissonance:
"""Tests pour compute_self_dissonance."""
def test_coherent_tensor(self):
"""Tenseur cohérent → faible dissonance interne."""
# Créer un tenseur où toutes les dimensions sont identiques
base_vector = np.random.randn(EMBEDDING_DIM)
base_vector = base_vector / np.linalg.norm(base_vector)
tensor = StateTensor(
state_id=0,
timestamp=datetime.now().isoformat(),
)
for dim_name in DIMENSION_NAMES:
# Utiliser le même vecteur (parfaitement cohérent)
setattr(tensor, dim_name, base_vector.copy())
dissonance = compute_self_dissonance(tensor)
# Devrait être zéro car toutes les dimensions sont identiques
assert dissonance < 0.01
def test_incoherent_tensor(self):
"""Tenseur incohérent → haute dissonance interne."""
tensor = create_random_tensor() # Dimensions aléatoires = incohérent
dissonance = compute_self_dissonance(tensor)
# Devrait être plus élevé
assert dissonance > 0.3
class TestImpact:
"""Tests pour la création d'Impact."""
def test_create_impact_from_dissonance(self):
"""Créer un Impact à partir d'un résultat de dissonance."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
dissonance_result = compute_dissonance(e_input, X_t)
impact = create_impact_from_dissonance(
dissonance=dissonance_result,
trigger_type='user',
trigger_content='Test message',
trigger_vector=e_input,
state_id=0,
impact_id=1,
)
assert impact.impact_id == 1
assert impact.trigger_type == 'user'
assert impact.dissonance_total == dissonance_result.total
assert impact.resolved is False
def test_impact_to_dict(self):
"""Impact.to_dict() retourne un dictionnaire valide."""
impact = Impact(
impact_id=1,
timestamp=datetime.now().isoformat(),
state_id_at_impact=0,
trigger_type='user',
trigger_content='Test',
dissonance_total=0.5,
)
d = impact.to_dict()
assert 'impact_id' in d
assert 'timestamp' in d
assert d['timestamp'].endswith('Z')
assert d['resolved'] is False
class TestDissonanceMonotonicity:
"""Tests de monotonie de la dissonance."""
def test_more_different_more_dissonance(self):
"""Plus différent = plus de dissonance."""
X_t = create_random_tensor()
# Entrée identique à une dimension
identical = X_t.firstness.copy()
result_identical = compute_dissonance(identical, X_t)
# Entrée légèrement différente
slightly_different = X_t.firstness + np.random.randn(EMBEDDING_DIM) * 0.1
slightly_different = slightly_different / np.linalg.norm(slightly_different)
result_slight = compute_dissonance(slightly_different, X_t)
# Entrée très différente
very_different = np.random.randn(EMBEDDING_DIM)
very_different = very_different / np.linalg.norm(very_different)
result_very = compute_dissonance(very_different, X_t)
# Vérifier la monotonie sur la dimension firstness
assert result_identical.dissonances_by_dimension['firstness'] < \
result_slight.dissonances_by_dimension['firstness']
class TestDissonanceResultSerialization:
"""Tests de sérialisation."""
def test_to_dict(self):
"""DissonanceResult.to_dict() fonctionne."""
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
result = compute_dissonance(e_input, X_t)
d = result.to_dict()
assert 'total' in d
assert 'is_choc' in d
assert 'dissonances_by_dimension' in d
def test_to_json(self):
"""DissonanceResult.to_json() produit du JSON valide."""
import json
X_t = create_random_tensor()
e_input = np.random.randn(EMBEDDING_DIM)
e_input = e_input / np.linalg.norm(e_input)
result = compute_dissonance(e_input, X_t)
json_str = result.to_json()
# Doit être parseable
parsed = json.loads(json_str)
assert parsed['total'] == result.total
if __name__ == "__main__":
pytest.main([__file__, "-v"])