Files
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

601 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
StateTensor - Tenseur d'état 8×1024 d'Ikario v2.
Le tenseur d'état représente l'identité processuelle d'Ikario avec 8 dimensions :
- firstness : Qualia, saillances, possibles (Peirce)
- secondness : Chocs, tensions, irritations (Peirce)
- thirdness : Habitudes, positions, valeurs (Peirce)
- dispositions : Tendances à agir
- orientations : Vers quoi je tends
- engagements : Positions prises
- pertinences : Ce qui compte pour moi
- valeurs : Ce que je défends
Architecture: L'espace latent pense. Le LLM traduit.
"""
import os
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
import numpy as np
import weaviate
import weaviate.classes.config as wvc
from weaviate.classes.query import Filter
# Configuration
WEAVIATE_URL = os.getenv("WEAVIATE_URL", "http://localhost:8080")
EMBEDDING_DIM = 1024 # BGE-M3
class TensorDimension(Enum):
"""Les 8 dimensions du tenseur d'état."""
FIRSTNESS = "firstness" # Qualia, saillances, possibles
SECONDNESS = "secondness" # Chocs, tensions, irritations
THIRDNESS = "thirdness" # Habitudes, positions, valeurs
DISPOSITIONS = "dispositions" # Tendances à agir
ORIENTATIONS = "orientations" # Vers quoi je tends
ENGAGEMENTS = "engagements" # Positions prises
PERTINENCES = "pertinences" # Ce qui compte pour moi
VALEURS = "valeurs" # Ce que je défends
DIMENSION_NAMES = [d.value for d in TensorDimension]
@dataclass
class StateTensor:
"""
Tenseur d'état X_t ∈ ^(8×1024).
Chaque dimension est un vecteur BGE-M3 normalisé.
"""
state_id: int
timestamp: str
# Les 8 dimensions (chacune ∈ ^1024)
firstness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
secondness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
thirdness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
dispositions: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
orientations: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
engagements: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
pertinences: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
valeurs: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
# Métadonnées
previous_state_id: int = -1
trigger_type: str = ""
trigger_content: str = ""
embedding_model: str = "BAAI/bge-m3" # Traçabilité (Amendement #13)
def to_matrix(self) -> np.ndarray:
"""Retourne le tenseur complet (8, 1024)."""
return np.stack([
self.firstness,
self.secondness,
self.thirdness,
self.dispositions,
self.orientations,
self.engagements,
self.pertinences,
self.valeurs
])
def to_flat(self) -> np.ndarray:
"""Retourne le tenseur aplati (8192,)."""
return self.to_matrix().flatten()
def get_dimension(self, dim: TensorDimension) -> np.ndarray:
"""Récupère une dimension par enum."""
return getattr(self, dim.value)
def set_dimension(self, dim: TensorDimension, vector: np.ndarray) -> None:
"""Définit une dimension par enum."""
if vector.shape != (EMBEDDING_DIM,):
raise ValueError(f"Vector must be {EMBEDDING_DIM}-dim, got {vector.shape}")
# Normaliser
norm = np.linalg.norm(vector)
if norm > 0:
vector = vector / norm
setattr(self, dim.value, vector)
def copy(self) -> 'StateTensor':
"""Crée une copie profonde."""
return StateTensor(
state_id=self.state_id,
timestamp=self.timestamp,
firstness=self.firstness.copy(),
secondness=self.secondness.copy(),
thirdness=self.thirdness.copy(),
dispositions=self.dispositions.copy(),
orientations=self.orientations.copy(),
engagements=self.engagements.copy(),
pertinences=self.pertinences.copy(),
valeurs=self.valeurs.copy(),
previous_state_id=self.previous_state_id,
trigger_type=self.trigger_type,
trigger_content=self.trigger_content,
embedding_model=self.embedding_model,
)
def to_dict(self) -> Dict[str, Any]:
"""Convertit en dictionnaire pour stockage."""
# S'assurer que le timestamp est au format RFC3339
ts = self.timestamp
if ts and not ts.endswith('Z') and '+' not in ts:
ts = ts + 'Z' # Ajouter le suffixe UTC si absent
return {
"state_id": self.state_id,
"timestamp": ts,
"previous_state_id": self.previous_state_id,
"trigger_type": self.trigger_type,
"trigger_content": self.trigger_content,
"embedding_model": self.embedding_model,
}
def get_vectors_dict(self) -> Dict[str, List[float]]:
"""Retourne les 8 vecteurs comme dict pour Weaviate named vectors."""
return {
"firstness": self.firstness.tolist(),
"secondness": self.secondness.tolist(),
"thirdness": self.thirdness.tolist(),
"dispositions": self.dispositions.tolist(),
"orientations": self.orientations.tolist(),
"engagements": self.engagements.tolist(),
"pertinences": self.pertinences.tolist(),
"valeurs": self.valeurs.tolist(),
}
@classmethod
def from_dict(cls, props: Dict[str, Any], vectors: Dict[str, List[float]] = None) -> 'StateTensor':
"""Crée un StateTensor depuis un dictionnaire (Weaviate object)."""
tensor = cls(
state_id=props.get("state_id", 0),
timestamp=props.get("timestamp", datetime.now().isoformat()),
previous_state_id=props.get("previous_state_id", -1),
trigger_type=props.get("trigger_type", ""),
trigger_content=props.get("trigger_content", ""),
embedding_model=props.get("embedding_model", "BAAI/bge-m3"),
)
if vectors:
for dim_name in DIMENSION_NAMES:
if dim_name in vectors:
setattr(tensor, dim_name, np.array(vectors[dim_name]))
return tensor
@classmethod
def from_matrix(cls, matrix: np.ndarray, state_id: int, timestamp: str) -> 'StateTensor':
"""Crée un StateTensor depuis une matrice (8, 1024)."""
if matrix.shape != (8, EMBEDDING_DIM):
raise ValueError(f"Matrix must be (8, {EMBEDDING_DIM}), got {matrix.shape}")
return cls(
state_id=state_id,
timestamp=timestamp,
firstness=matrix[0],
secondness=matrix[1],
thirdness=matrix[2],
dispositions=matrix[3],
orientations=matrix[4],
engagements=matrix[5],
pertinences=matrix[6],
valeurs=matrix[7],
)
@staticmethod
def weighted_mean(tensors: List['StateTensor'], weights: np.ndarray) -> 'StateTensor':
"""Calcule la moyenne pondérée de plusieurs tenseurs."""
if len(tensors) != len(weights):
raise ValueError("Number of tensors must match number of weights")
weights = np.array(weights) / np.sum(weights) # Normaliser
result = StateTensor(
state_id=-1, # À définir par l'appelant
timestamp=datetime.now().isoformat(),
)
for dim_name in DIMENSION_NAMES:
weighted_sum = np.zeros(EMBEDDING_DIM)
for tensor, weight in zip(tensors, weights):
weighted_sum += weight * getattr(tensor, dim_name)
# Normaliser le résultat
norm = np.linalg.norm(weighted_sum)
if norm > 0:
weighted_sum = weighted_sum / norm
setattr(result, dim_name, weighted_sum)
return result
@staticmethod
def blend(t1: 'StateTensor', t2: 'StateTensor', alpha: float = 0.5) -> 'StateTensor':
"""Mélange deux tenseurs : alpha * t1 + (1-alpha) * t2."""
return StateTensor.weighted_mean([t1, t2], [alpha, 1 - alpha])
# ============================================================================
# WEAVIATE COLLECTION SCHEMA (API v4)
# ============================================================================
def create_state_tensor_collection(client: weaviate.WeaviateClient) -> bool:
"""
Crée la collection StateTensor dans Weaviate avec 8 vecteurs nommés.
Utilise l'API Weaviate v4 avec named vectors.
Returns:
True si créée, False si existait déjà
"""
collection_name = "StateTensor"
# Vérifier si existe déjà
if collection_name in client.collections.list_all():
print(f"[StateTensor] Collection existe déjà")
return False
# Créer la collection avec 8 vecteurs nommés
client.collections.create(
name=collection_name,
description="Tenseur d'état 8×1024 - Identité processuelle d'Ikario v2",
# 8 vecteurs nommés (Weaviate v4 API)
vector_config={
"firstness": wvc.Configure.NamedVectors.none(
name="firstness",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"secondness": wvc.Configure.NamedVectors.none(
name="secondness",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"thirdness": wvc.Configure.NamedVectors.none(
name="thirdness",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"dispositions": wvc.Configure.NamedVectors.none(
name="dispositions",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"orientations": wvc.Configure.NamedVectors.none(
name="orientations",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"engagements": wvc.Configure.NamedVectors.none(
name="engagements",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"pertinences": wvc.Configure.NamedVectors.none(
name="pertinences",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
"valeurs": wvc.Configure.NamedVectors.none(
name="valeurs",
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
),
},
# Propriétés (métadonnées)
properties=[
wvc.Property(
name="state_id",
data_type=wvc.DataType.INT,
description="Numéro séquentiel de l'état (0, 1, 2...)",
),
wvc.Property(
name="timestamp",
data_type=wvc.DataType.DATE,
description="Moment de création de cet état",
),
wvc.Property(
name="previous_state_id",
data_type=wvc.DataType.INT,
description="ID de l'état précédent (-1 pour X_0)",
),
wvc.Property(
name="trigger_type",
data_type=wvc.DataType.TEXT,
skip_vectorization=True,
description="Type: user, timer, event, initialization",
),
wvc.Property(
name="trigger_content",
data_type=wvc.DataType.TEXT,
skip_vectorization=True,
description="Contenu du déclencheur",
),
wvc.Property(
name="embedding_model",
data_type=wvc.DataType.TEXT,
skip_vectorization=True,
description="Modèle d'embedding utilisé (traçabilité)",
),
],
)
print(f"[StateTensor] Collection créée avec 8 vecteurs nommés")
return True
def delete_state_tensor_collection(client: weaviate.WeaviateClient) -> bool:
"""Supprime la collection StateTensor (pour reset)."""
try:
client.collections.delete("StateTensor")
print("[StateTensor] Collection supprimée")
return True
except Exception as e:
print(f"[StateTensor] Erreur suppression: {e}")
return False
# ============================================================================
# CRUD OPERATIONS
# ============================================================================
class StateTensorRepository:
"""
Repository pour les opérations CRUD sur StateTensor.
Utilise l'API Weaviate v4.
"""
def __init__(self, client: weaviate.WeaviateClient):
self.client = client
self.collection = client.collections.get("StateTensor")
def save(self, tensor: StateTensor) -> str:
"""
Sauvegarde un StateTensor dans Weaviate.
Returns:
UUID de l'objet créé
"""
result = self.collection.data.insert(
properties=tensor.to_dict(),
vector=tensor.get_vectors_dict(),
)
return str(result)
def get_by_state_id(self, state_id: int) -> Optional[StateTensor]:
"""Récupère un tenseur par son state_id."""
results = self.collection.query.fetch_objects(
filters=Filter.by_property("state_id").equal(state_id),
include_vector=True,
limit=1,
)
if not results.objects:
return None
obj = results.objects[0]
return StateTensor.from_dict(obj.properties, obj.vector)
def get_current(self) -> Optional[StateTensor]:
"""Récupère l'état le plus récent (state_id max)."""
from weaviate.classes.query import Sort
results = self.collection.query.fetch_objects(
sort=Sort.by_property("state_id", ascending=False),
include_vector=True,
limit=1,
)
if not results.objects:
return None
obj = results.objects[0]
return StateTensor.from_dict(obj.properties, obj.vector)
def get_current_state_id(self) -> int:
"""Retourne l'ID de l'état le plus récent (-1 si aucun)."""
current = self.get_current()
return current.state_id if current else -1
def get_history(self, limit: int = 10) -> List[StateTensor]:
"""Récupère les N derniers états."""
from weaviate.classes.query import Sort
results = self.collection.query.fetch_objects(
sort=Sort.by_property("state_id", ascending=False),
include_vector=True,
limit=limit,
)
return [
StateTensor.from_dict(obj.properties, obj.vector)
for obj in results.objects
]
def count(self) -> int:
"""Compte le nombre total d'états."""
result = self.collection.aggregate.over_all(total_count=True)
return result.total_count
# ============================================================================
# IMPACT COLLECTION (pour Secondness)
# ============================================================================
def create_impact_collection(client: weaviate.WeaviateClient) -> bool:
"""
Crée la collection Impact pour les événements de dissonance.
Un Impact représente un "choc" (Secondness) - une tension non résolue
qui demande à être intégrée.
"""
collection_name = "Impact"
if collection_name in client.collections.list_all():
print(f"[Impact] Collection existe déjà")
return False
client.collections.create(
name=collection_name,
description="Événements de dissonance (chocs, tensions) - Secondness",
# Vecteur unique pour l'impact
vectorizer_config=wvc.Configure.Vectorizer.none(),
vector_index_config=wvc.Configure.VectorIndex.hnsw(
distance_metric=wvc.VectorDistances.COSINE
),
properties=[
wvc.Property(
name="trigger_content",
data_type=wvc.DataType.TEXT,
description="Contenu déclencheur de l'impact",
),
wvc.Property(
name="trigger_type",
data_type=wvc.DataType.TEXT,
skip_vectorization=True,
description="Type: user, corpus, veille, internal",
),
wvc.Property(
name="dissonance_score",
data_type=wvc.DataType.NUMBER,
description="Score de dissonance E() [0-1]",
),
wvc.Property(
name="state_id_at_impact",
data_type=wvc.DataType.INT,
description="state_id au moment de l'impact",
),
wvc.Property(
name="dimensions_affected",
data_type=wvc.DataType.TEXT_ARRAY,
skip_vectorization=True,
description="Dimensions du tenseur affectées",
),
wvc.Property(
name="is_hard_negative",
data_type=wvc.DataType.BOOL,
description="True si contradiction détectée (NLI)",
),
wvc.Property(
name="resolved",
data_type=wvc.DataType.BOOL,
description="True si l'impact a été intégré",
),
wvc.Property(
name="resolution_state_id",
data_type=wvc.DataType.INT,
description="state_id où l'impact a été résolu",
),
wvc.Property(
name="timestamp",
data_type=wvc.DataType.DATE,
description="Moment de l'impact",
),
wvc.Property(
name="last_rumination",
data_type=wvc.DataType.DATE,
description="Dernière rumination (cooldown 24h - Amendement #9)",
),
],
)
print(f"[Impact] Collection créée")
return True
# ============================================================================
# SETUP ALL COLLECTIONS
# ============================================================================
def create_all_processual_collections(client: weaviate.WeaviateClient) -> Dict[str, bool]:
"""
Crée toutes les collections pour le système processuel v2.
Returns:
Dict avec le statut de chaque collection
"""
print("=" * 60)
print("Création des collections processuelles v2")
print("=" * 60)
results = {
"StateTensor": create_state_tensor_collection(client),
"Impact": create_impact_collection(client),
}
print("\n" + "=" * 60)
print("Resume:")
for name, created in results.items():
status = "[OK] Creee" if created else "[WARN] Existait deja"
print(f" {name}: {status}")
return results
# ============================================================================
# CLI
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Gestion des collections StateTensor")
parser.add_argument("--create", action="store_true", help="Créer les collections")
parser.add_argument("--delete", action="store_true", help="Supprimer les collections")
parser.add_argument("--status", action="store_true", help="Afficher le statut")
args = parser.parse_args()
# Connexion Weaviate
client = weaviate.connect_to_local()
try:
if args.create:
create_all_processual_collections(client)
elif args.delete:
delete_state_tensor_collection(client)
try:
client.collections.delete("Impact")
print("[Impact] Collection supprimée")
except Exception:
pass
elif args.status:
collections = client.collections.list_all()
print("Collections existantes:")
for name in sorted(collections.keys()):
if name in ["StateTensor", "Impact"]:
print(f" [OK] {name}")
if "StateTensor" in collections:
repo = StateTensorRepository(client)
print(f"\nStateTensor: {repo.count()} états")
current = repo.get_current()
if current:
print(f" État actuel: X_{current.state_id} ({current.timestamp})")
else:
parser.print_help()
finally:
client.close()