Files
linear-coding-agent/ikario_processual/tests/test_state_to_language.py
David Blanc Brioir f6fe71e2f7 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>
2026-02-01 22:30:19 +01:00

606 lines
20 KiB
Python

#!/usr/bin/env python3
"""
Tests pour le module state_to_language - Phase 5.
Le cycle de traduction :
1. Projeter StateTensor sur directions interpretables
2. Construire prompt de traduction
3. LLM en mode ZERO-REASONING
4. Valider absence de raisonnement
Executer: pytest ikario_processual/tests/test_state_to_language.py -v
"""
import json
import numpy as np
import pytest
import asyncio
from datetime import datetime
from unittest.mock import MagicMock, AsyncMock, patch
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.state_to_language import (
ProjectionDirection,
TranslationResult,
StateToLanguage,
REASONING_MARKERS,
CATEGORY_TO_DIMENSION,
create_directions_from_config,
)
def create_random_tensor(state_id: int = 0) -> StateTensor:
"""Cree un tenseur avec des vecteurs aleatoires normalises."""
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_random_direction(name: str, category: str) -> ProjectionDirection:
"""Cree une direction aleatoire normalisee."""
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
return ProjectionDirection(
name=name,
category=category,
pole_positive="positive",
pole_negative="negative",
description=f"Direction {name}",
vector=v,
)
class TestProjectionDirection:
"""Tests pour la classe ProjectionDirection."""
def test_create_direction(self):
"""Creer une direction."""
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
direction = ProjectionDirection(
name="curiosity",
category="epistemic",
pole_positive="curieux",
pole_negative="desinteresse",
description="Degre de curiosite",
vector=v,
)
assert direction.name == "curiosity"
assert direction.category == "epistemic"
assert direction.vector.shape == (EMBEDDING_DIM,)
def test_project_on_direction(self):
"""Projection sur une direction."""
v = np.random.randn(EMBEDDING_DIM)
v = v / np.linalg.norm(v)
direction = ProjectionDirection(
name="test",
category="test",
pole_positive="",
pole_negative="",
description="",
vector=v,
)
# Projeter le meme vecteur
projection = direction.project(v)
assert np.isclose(projection, 1.0)
# Projeter un vecteur oppose
projection_neg = direction.project(-v)
assert np.isclose(projection_neg, -1.0)
def test_projection_range(self):
"""Les projections sont entre -1 et 1."""
direction = create_random_direction("test", "test")
for _ in range(10):
random_vec = np.random.randn(EMBEDDING_DIM)
random_vec = random_vec / np.linalg.norm(random_vec)
projection = direction.project(random_vec)
assert -1.0 <= projection <= 1.0
class TestTranslationResult:
"""Tests pour TranslationResult."""
def test_create_result(self):
"""Creer un resultat de traduction."""
result = TranslationResult(
text="Je suis curieux.",
projections={'epistemic': {'curiosity': 0.72}},
output_type="response",
reasoning_detected=False,
json_valid=True,
processing_time_ms=50,
)
assert result.text == "Je suis curieux."
assert result.reasoning_detected is False
def test_to_dict(self):
"""to_dict() fonctionne."""
result = TranslationResult(
text="Test",
projections={'test': {'test': 0.5}},
output_type="response",
reasoning_detected=True,
json_valid=False,
processing_time_ms=100,
)
d = result.to_dict()
assert 'text' in d
assert 'projections' in d
assert d['reasoning_detected'] is True
assert d['json_valid'] is False
class TestStateToLanguage:
"""Tests pour la classe StateToLanguage."""
def test_create_translator(self):
"""Creer un traducteur."""
translator = StateToLanguage()
assert translator.directions == []
assert translator._translations_count == 0
def test_add_direction(self):
"""Ajouter des directions."""
translator = StateToLanguage()
direction1 = create_random_direction("curiosity", "epistemic")
direction2 = create_random_direction("enthusiasm", "affective")
translator.add_direction(direction1)
translator.add_direction(direction2)
assert len(translator.directions) == 2
def test_project_state(self):
"""Projeter un etat sur les directions."""
translator = StateToLanguage()
# Ajouter des directions de categories differentes
translator.add_direction(create_random_direction("curiosity", "epistemic"))
translator.add_direction(create_random_direction("certainty", "epistemic"))
translator.add_direction(create_random_direction("enthusiasm", "affective"))
X_t = create_random_tensor()
projections = translator.project_state(X_t)
# Verifier structure
assert 'epistemic' in projections
assert 'affective' in projections
assert 'curiosity' in projections['epistemic']
assert 'enthusiasm' in projections['affective']
# Verifier valeurs dans [-1, 1]
for category, components in projections.items():
for name, value in components.items():
assert -1.0 <= value <= 1.0
def test_project_state_flat(self):
"""Projection aplatie."""
translator = StateToLanguage()
translator.add_direction(create_random_direction("curiosity", "epistemic"))
translator.add_direction(create_random_direction("enthusiasm", "affective"))
X_t = create_random_tensor()
flat = translator.project_state_flat(X_t)
assert 'curiosity' in flat
assert 'enthusiasm' in flat
assert isinstance(flat['curiosity'], float)
class TestInterpretValue:
"""Tests pour interpret_value."""
def test_very_positive(self):
"""Valeur tres positive."""
assert StateToLanguage.interpret_value(0.8) == "tres"
def test_moderately_positive(self):
"""Valeur moderement positive."""
assert StateToLanguage.interpret_value(0.35) == "moderement"
def test_neutral(self):
"""Valeur neutre."""
assert StateToLanguage.interpret_value(0.0) == "neutre"
assert StateToLanguage.interpret_value(-0.1) == "neutre"
def test_moderately_negative(self):
"""Valeur moderement negative."""
assert StateToLanguage.interpret_value(-0.35) == "peu"
def test_very_negative(self):
"""Valeur tres negative."""
assert StateToLanguage.interpret_value(-0.8) == "pas du tout"
class TestBuildTranslationPrompt:
"""Tests pour build_translation_prompt."""
def test_prompt_structure(self):
"""Le prompt a la bonne structure."""
translator = StateToLanguage()
projections = {
'epistemic': {'curiosity': 0.72, 'certainty': -0.18},
'affective': {'enthusiasm': 0.45},
}
prompt = translator.build_translation_prompt(projections, "response")
assert "ETAT COGNITIF" in prompt
assert "EPISTEMIC:" in prompt
assert "AFFECTIVE:" in prompt
assert "curiosity" in prompt
assert "0.72" in prompt
assert "INSTRUCTION" in prompt
assert "NE REFLECHIS PAS" in prompt
def test_prompt_output_type(self):
"""Le type de sortie est inclus."""
translator = StateToLanguage()
prompt = translator.build_translation_prompt({}, "question")
assert "question" in prompt
class TestZeroReasoningSystemPrompt:
"""Tests pour le system prompt zero-reasoning."""
def test_strict_instructions(self):
"""Le prompt contient des instructions strictes."""
translator = StateToLanguage()
prompt = translator.build_zero_reasoning_system_prompt()
assert "NE DOIS PAS" in prompt.upper()
assert "RAISONNER" in prompt.upper()
assert "CODEC" in prompt.upper()
assert "STRICT" in prompt.upper()
def test_no_thinking_instruction(self):
"""Instruction explicite de ne pas generer de thinking."""
translator = StateToLanguage()
prompt = translator.build_zero_reasoning_system_prompt()
assert "<thinking>" in prompt.lower()
class TestJsonSystemPrompt:
"""Tests pour le system prompt JSON."""
def test_json_schema_included(self):
"""Le schema JSON est inclus dans le prompt."""
translator = StateToLanguage()
schema = {
"type": "object",
"required": ["verbalization"],
"properties": {"verbalization": {"type": "string"}},
}
prompt = translator.build_json_system_prompt(schema)
assert "JSON" in prompt
assert "verbalization" in prompt
assert "UNIQUEMENT" in prompt
class TestCheckReasoningMarkers:
"""Tests pour check_reasoning_markers."""
def test_no_markers(self):
"""Texte sans marqueurs."""
translator = StateToLanguage()
text = "Je suis curieux. Explorons cette idee."
has_reasoning, markers = translator.check_reasoning_markers(text)
assert has_reasoning is False
assert markers == []
def test_with_markers(self):
"""Texte avec marqueurs de raisonnement."""
translator = StateToLanguage()
text = "Je pense que cette approche est interessante. Apres reflexion, je suggere..."
has_reasoning, markers = translator.check_reasoning_markers(text)
assert has_reasoning is True
assert "je pense que" in markers
assert "apres reflexion" in markers
def test_case_insensitive(self):
"""Detection insensible a la casse."""
translator = StateToLanguage()
text = "IL ME SEMBLE que c'est correct."
has_reasoning, markers = translator.check_reasoning_markers(text)
assert has_reasoning is True
assert "il me semble" in markers
class TestTranslateSyncNoApi:
"""Tests pour translate_sync (mode test sans API)."""
def test_translate_sync_returns_result(self):
"""translate_sync retourne un resultat."""
translator = StateToLanguage()
translator.add_direction(create_random_direction("curiosity", "epistemic"))
X_t = create_random_tensor()
result = translator.translate_sync(X_t, output_type="response")
assert isinstance(result, TranslationResult)
assert "[RESPONSE]" in result.text.upper()
assert result.output_type == "response"
def test_translate_sync_increments_count(self):
"""translate_sync incremente le compteur."""
translator = StateToLanguage()
initial_count = translator._translations_count
translator.translate_sync(create_random_tensor())
translator.translate_sync(create_random_tensor())
assert translator._translations_count == initial_count + 2
class TestTranslateAsync:
"""Tests pour translate async avec mock."""
def test_translate_without_client(self):
"""translate sans client retourne mock."""
async def run_test():
translator = StateToLanguage()
translator.add_direction(create_random_direction("curiosity", "epistemic"))
X_t = create_random_tensor()
result = await translator.translate(X_t)
assert "[MOCK TRANSLATION]" in result.text
assert result.reasoning_detected is False
asyncio.run(run_test())
def test_translate_with_mock_client(self):
"""translate avec client mock."""
async def run_test():
# Mock du client Anthropic
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.content = [MagicMock(text="Je suis curieux.")]
mock_client.messages.create = AsyncMock(return_value=mock_response)
translator = StateToLanguage(anthropic_client=mock_client)
translator.add_direction(create_random_direction("curiosity", "epistemic"))
X_t = create_random_tensor()
result = await translator.translate(X_t)
assert result.text == "Je suis curieux."
assert mock_client.messages.create.called
# Verifier les parametres d'appel
call_kwargs = mock_client.messages.create.call_args.kwargs
assert call_kwargs['temperature'] == 0.0
assert call_kwargs['max_tokens'] == 500
asyncio.run(run_test())
def test_translate_detects_reasoning(self):
"""translate detecte le raisonnement."""
async def run_test():
# Mock avec texte contenant du raisonnement
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.content = [MagicMock(text="Je pense que c'est interessant.")]
mock_client.messages.create = AsyncMock(return_value=mock_response)
translator = StateToLanguage(anthropic_client=mock_client)
X_t = create_random_tensor()
result = await translator.translate(X_t, force_zero_reasoning=True)
assert result.reasoning_detected is True
asyncio.run(run_test())
class TestTranslateStructured:
"""Tests pour translate_structured (Amendment #14)."""
def test_translate_structured_without_client(self):
"""translate_structured sans client retourne mock."""
async def run_test():
translator = StateToLanguage()
X_t = create_random_tensor()
result = await translator.translate_structured(X_t)
assert "[MOCK JSON TRANSLATION]" in result.text
asyncio.run(run_test())
def test_translate_structured_valid_json(self):
"""translate_structured avec JSON valide."""
async def run_test():
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.content = [MagicMock(text='{"verbalization": "Je suis curieux."}')]
mock_client.messages.create = AsyncMock(return_value=mock_response)
translator = StateToLanguage(anthropic_client=mock_client)
X_t = create_random_tensor()
result = await translator.translate_structured(X_t)
assert result.text == "Je suis curieux."
assert result.json_valid is True
asyncio.run(run_test())
def test_translate_structured_extra_fields(self):
"""translate_structured detecte les champs supplementaires."""
async def run_test():
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.content = [MagicMock(
text='{"verbalization": "Texte", "extra": "pas autorise"}'
)]
mock_client.messages.create = AsyncMock(return_value=mock_response)
translator = StateToLanguage(anthropic_client=mock_client)
X_t = create_random_tensor()
result = await translator.translate_structured(X_t)
assert result.text == "Texte"
assert result.json_valid is False
asyncio.run(run_test())
def test_translate_structured_invalid_json(self):
"""translate_structured gere le JSON invalide."""
async def run_test():
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.content = [MagicMock(text="Ceci n'est pas du JSON")]
mock_client.messages.create = AsyncMock(return_value=mock_response)
translator = StateToLanguage(anthropic_client=mock_client)
X_t = create_random_tensor()
result = await translator.translate_structured(X_t)
assert result.json_valid is False
assert "Ceci n'est pas du JSON" in result.text
asyncio.run(run_test())
class TestGetStats:
"""Tests pour get_stats."""
def test_initial_stats(self):
"""Stats initiales."""
translator = StateToLanguage()
stats = translator.get_stats()
assert stats['directions_count'] == 0
assert stats['translations_count'] == 0
assert stats['reasoning_warnings'] == 0
def test_stats_with_directions(self):
"""Stats avec directions."""
translator = StateToLanguage()
translator.add_direction(create_random_direction("curiosity", "epistemic"))
translator.add_direction(create_random_direction("enthusiasm", "affective"))
stats = translator.get_stats()
assert stats['directions_count'] == 2
assert 'epistemic' in stats['categories']
assert 'affective' in stats['categories']
class TestCategoryToDimension:
"""Tests pour le mapping category -> dimension."""
def test_epistemic_maps_to_firstness(self):
"""epistemic -> firstness."""
assert CATEGORY_TO_DIMENSION['epistemic'] == 'firstness'
def test_affective_maps_to_dispositions(self):
"""affective -> dispositions."""
assert CATEGORY_TO_DIMENSION['affective'] == 'dispositions'
def test_ethical_maps_to_valeurs(self):
"""ethical -> valeurs."""
assert CATEGORY_TO_DIMENSION['ethical'] == 'valeurs'
def test_all_categories_mapped(self):
"""Toutes les categories principales sont mappees."""
expected_categories = [
'epistemic', 'affective', 'cognitive', 'relational',
'ethical', 'temporal', 'thematic', 'metacognitive',
'vital', 'ecosystemic', 'philosophical'
]
for cat in expected_categories:
assert cat in CATEGORY_TO_DIMENSION
assert CATEGORY_TO_DIMENSION[cat] in DIMENSION_NAMES
class TestReasoningMarkers:
"""Tests pour les marqueurs de raisonnement."""
def test_markers_exist(self):
"""Les marqueurs existent."""
assert len(REASONING_MARKERS) > 0
def test_markers_are_lowercase(self):
"""Les marqueurs sont en minuscules."""
for marker in REASONING_MARKERS:
assert marker == marker.lower()
class TestCreateDirectionsFromConfig:
"""Tests pour create_directions_from_config."""
def test_create_from_config(self):
"""Creer des directions depuis une config."""
# Mock du modele d'embedding avec embeddings distincts
np.random.seed(42) # Pour reproductibilite
pos_embeddings = np.random.randn(5, EMBEDDING_DIM)
neg_embeddings = np.random.randn(5, EMBEDDING_DIM) + 1.0 # Decalage pour etre distincts
mock_model = MagicMock()
# Retourner des embeddings differents pour positifs et negatifs
mock_model.encode = MagicMock(side_effect=[pos_embeddings, neg_embeddings])
config = {
"curiosity": {
"category": "epistemic",
"pole_positive": "curieux",
"pole_negative": "desinteresse",
"description": "Degre de curiosite",
"positive_examples": ["a", "b", "c", "d", "e"],
"negative_examples": ["f", "g", "h", "i", "j"],
}
}
directions = create_directions_from_config(config, mock_model)
assert len(directions) == 1
assert directions[0].name == "curiosity"
assert directions[0].category == "epistemic"
assert directions[0].vector.shape == (EMBEDDING_DIM,)
# Vecteur doit etre normalise
assert np.isclose(np.linalg.norm(directions[0].vector), 1.0)
if __name__ == "__main__":
pytest.main([__file__, "-v"])