Add Phases 3-5: State transformation, OccasionLogger, OccasionManager
Phase 3 - State Transformation: - transform_state() function with alpha/beta parameters - compute_adaptive_params() for dynamic transformation - StateTransformer class for state management Phase 4 - Occasion Logger: - OccasionLog dataclass for structured logging - OccasionLogger for JSON file storage - Profile evolution tracking and statistics Phase 5 - Occasion Manager: - Full cycle: Prehension → Concrescence → Satisfaction - Search integration (thoughts, library) - State creation and logging orchestration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
258
ikario_processual/state_transformation.py
Normal file
258
ikario_processual/state_transformation.py
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
StateTransformation - Fonction de transition d'état S(t-1) → S(t).
|
||||
|
||||
Implémente la transformation processuelle selon Whitehead:
|
||||
- Préhension: récupérer le contexte
|
||||
- Concrescence: intégrer l'occasion
|
||||
- Satisfaction: nouvel état stable
|
||||
|
||||
La formule de base:
|
||||
S(t) = normalize(alpha * S(t-1) + beta * occasion_embedding)
|
||||
|
||||
où alpha (inertie) + beta (nouveauté) = 1
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Dict, Any, Optional
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
WEAVIATE_URL = os.getenv("WEAVIATE_URL", "http://localhost:8080")
|
||||
|
||||
|
||||
def transform_state(
|
||||
s_prev: np.ndarray,
|
||||
occasion_embedding: np.ndarray,
|
||||
alpha: float = 0.85,
|
||||
beta: float = 0.15
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Transforme S(t-1) en S(t) via l'occasion.
|
||||
|
||||
Le résultat est renormalisé pour rester sur l'hypersphère unitaire.
|
||||
|
||||
Args:
|
||||
s_prev: Vecteur d'état précédent (normalisé)
|
||||
occasion_embedding: Embedding de l'occasion/réponse (normalisé)
|
||||
alpha: Coefficient d'inertie (conservation de l'identité)
|
||||
beta: Coefficient de nouveauté (intégration de l'occasion)
|
||||
|
||||
Returns:
|
||||
Nouveau vecteur d'état normalisé
|
||||
"""
|
||||
# Combinaison pondérée
|
||||
s_new = alpha * s_prev + beta * occasion_embedding
|
||||
|
||||
# Renormalisation sur l'hypersphère
|
||||
norm = np.linalg.norm(s_new)
|
||||
if norm > 0:
|
||||
s_new = s_new / norm
|
||||
|
||||
return s_new
|
||||
|
||||
|
||||
def compute_adaptive_params(
|
||||
occasion: Dict[str, Any],
|
||||
base_alpha: float = 0.85,
|
||||
base_beta: float = 0.15
|
||||
) -> Tuple[float, float]:
|
||||
"""
|
||||
Calcule alpha/beta adaptatifs selon l'intensité de l'occasion.
|
||||
|
||||
Heuristiques:
|
||||
- Plus de pensées créées → plus de beta (plus de changement)
|
||||
- Trigger "timer" → moins de beta (auto-réflexion douce)
|
||||
- Trigger "user" significatif → plus de beta
|
||||
|
||||
Args:
|
||||
occasion: Dictionnaire avec trigger_type, thoughts_created, etc.
|
||||
base_alpha: Alpha de base
|
||||
base_beta: Beta de base
|
||||
|
||||
Returns:
|
||||
Tuple (alpha, beta) ajustés
|
||||
"""
|
||||
# Ajuster selon le nombre de pensées créées
|
||||
thoughts_count = occasion.get('thoughts_created', 0)
|
||||
intensity = min(thoughts_count * 0.03, 0.10) # Max +10% de beta
|
||||
|
||||
# Trigger timer = plus d'inertie (réflexion douce)
|
||||
if occasion.get('trigger_type') == 'timer':
|
||||
intensity = intensity * 0.5
|
||||
|
||||
# Trigger user avec contenu long = plus d'impact
|
||||
trigger_content = occasion.get('trigger_content', '')
|
||||
if occasion.get('trigger_type') == 'user' and len(trigger_content) > 200:
|
||||
intensity = intensity + 0.02
|
||||
|
||||
alpha = base_alpha - intensity
|
||||
beta = base_beta + intensity
|
||||
|
||||
# S'assurer que alpha + beta = 1
|
||||
total = alpha + beta
|
||||
alpha = alpha / total
|
||||
beta = beta / total
|
||||
|
||||
return alpha, beta
|
||||
|
||||
|
||||
class StateTransformer:
|
||||
"""Gère les transformations d'état et la persistance."""
|
||||
|
||||
def __init__(self, embedding_model=None):
|
||||
"""
|
||||
Args:
|
||||
embedding_model: Modèle SentenceTransformer (chargé à la demande si None)
|
||||
"""
|
||||
self._model = embedding_model
|
||||
self._model_loaded = embedding_model is not None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Charge le modèle d'embedding à la demande."""
|
||||
if not self._model_loaded:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
print("[StateTransformer] Chargement du modèle BGE-M3...")
|
||||
self._model = SentenceTransformer('BAAI/bge-m3')
|
||||
self._model_loaded = True
|
||||
return self._model
|
||||
|
||||
def get_current_state_id(self) -> int:
|
||||
"""Retourne l'ID de l'état le plus récent."""
|
||||
url = f"{WEAVIATE_URL}/v1/objects?class=StateVector&limit=100"
|
||||
response = requests.get(url)
|
||||
|
||||
if response.status_code != 200:
|
||||
return -1
|
||||
|
||||
objects = response.json().get("objects", [])
|
||||
if not objects:
|
||||
return -1
|
||||
|
||||
return max(obj.get("properties", {}).get("state_id", -1) for obj in objects)
|
||||
|
||||
def get_state_vector(self, state_id: int) -> Optional[np.ndarray]:
|
||||
"""Récupère le vecteur d'un état."""
|
||||
query = {
|
||||
"query": """
|
||||
{
|
||||
Get {
|
||||
StateVector(where: {
|
||||
path: ["state_id"],
|
||||
operator: Equal,
|
||||
valueInt: %d
|
||||
}) {
|
||||
_additional {
|
||||
vector
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" % state_id
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{WEAVIATE_URL}/v1/graphql",
|
||||
json=query,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
data = response.json()
|
||||
states = data.get("data", {}).get("Get", {}).get("StateVector", [])
|
||||
|
||||
if not states:
|
||||
return None
|
||||
|
||||
vector = states[0].get("_additional", {}).get("vector")
|
||||
return np.array(vector) if vector else None
|
||||
|
||||
def create_new_state(
|
||||
self,
|
||||
occasion: Dict[str, Any],
|
||||
response_text: str,
|
||||
thoughts_created: int = 0
|
||||
) -> int:
|
||||
"""
|
||||
Crée un nouvel état à partir de l'occasion.
|
||||
|
||||
Args:
|
||||
occasion: {trigger_type, trigger_content, summary}
|
||||
response_text: Texte de la réponse générée
|
||||
thoughts_created: Nombre de pensées créées
|
||||
|
||||
Returns:
|
||||
new_state_id
|
||||
"""
|
||||
# 1. Récupérer S(t-1)
|
||||
current_id = self.get_current_state_id()
|
||||
s_prev = self.get_state_vector(current_id)
|
||||
|
||||
if s_prev is None:
|
||||
raise ValueError(f"État S({current_id}) non trouvé")
|
||||
|
||||
# 2. Calculer l'embedding de la réponse
|
||||
occasion_embedding = self.model.encode(response_text)
|
||||
occasion_embedding = occasion_embedding / np.linalg.norm(occasion_embedding)
|
||||
|
||||
# 3. Calculer les paramètres adaptatifs
|
||||
alpha, beta = compute_adaptive_params({
|
||||
'thoughts_created': thoughts_created,
|
||||
'trigger_type': occasion.get('trigger_type', 'user'),
|
||||
'trigger_content': occasion.get('trigger_content', '')
|
||||
})
|
||||
|
||||
# 4. Transformer
|
||||
s_new = transform_state(s_prev, occasion_embedding, alpha, beta)
|
||||
|
||||
# 5. Persister
|
||||
new_state_id = current_id + 1
|
||||
state_obj = {
|
||||
"state_id": new_state_id,
|
||||
"timestamp": datetime.now().isoformat() + "Z",
|
||||
"previous_state_id": current_id,
|
||||
"trigger_type": occasion.get('trigger_type', 'user'),
|
||||
"trigger_content": occasion.get('trigger_content', '')[:500],
|
||||
"occasion_summary": occasion.get('summary', '')[:500],
|
||||
"response_summary": response_text[:500],
|
||||
"thoughts_created": thoughts_created,
|
||||
"source_thoughts_count": 0,
|
||||
"source_messages_count": 0,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{WEAVIATE_URL}/v1/objects",
|
||||
json={
|
||||
"class": "StateVector",
|
||||
"properties": state_obj,
|
||||
"vector": s_new.tolist()
|
||||
},
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
raise RuntimeError(f"Erreur création S({new_state_id}): {response.text}")
|
||||
|
||||
print(f"[StateTransformer] État S({new_state_id}) créé (alpha={alpha:.2f}, beta={beta:.2f})")
|
||||
return new_state_id
|
||||
|
||||
|
||||
# Test simple
|
||||
if __name__ == "__main__":
|
||||
# Test de la transformation
|
||||
s_prev = np.random.randn(1024)
|
||||
s_prev = s_prev / np.linalg.norm(s_prev)
|
||||
|
||||
occasion = np.random.randn(1024)
|
||||
occasion = occasion / np.linalg.norm(occasion)
|
||||
|
||||
s_new = transform_state(s_prev, occasion, alpha=0.85, beta=0.15)
|
||||
|
||||
print(f"Norme s_prev: {np.linalg.norm(s_prev):.4f}")
|
||||
print(f"Norme s_new: {np.linalg.norm(s_new):.4f}")
|
||||
print(f"Similarité s_prev/s_new: {np.dot(s_prev, s_new):.4f}")
|
||||
Reference in New Issue
Block a user