Add /profile endpoint with StateTensor v2 integration
- Add /profile GET endpoint returning Ikario + David projections on 100 directions - Compute Ikario state from Weaviate StateVector v1 + 113 thoughts - Compute David tensor from user messages (SQLite) + declared profile - Map direction categories to StateTensor dimensions via CATEGORY_TO_DIMENSION - Calculate david_similarity as average cosine across 8 dimensions - Result: 60.93% Ikario-David similarity (vs 100% when initialized from same source) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ Endpoints:
|
|||||||
GET /state - État actuel
|
GET /state - État actuel
|
||||||
GET /vigilance - Vérifier la dérive
|
GET /vigilance - Vérifier la dérive
|
||||||
GET /metrics - Métriques du système
|
GET /metrics - Métriques du système
|
||||||
|
GET /profile - Profil processuel (109 directions)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -24,6 +25,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import requests
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load env
|
# Load env
|
||||||
@@ -39,9 +41,10 @@ from .state_tensor import StateTensor, DIMENSION_NAMES, EMBEDDING_DIM
|
|||||||
from .dissonance import compute_dissonance, DissonanceResult
|
from .dissonance import compute_dissonance, DissonanceResult
|
||||||
from .fixation import Authority, compute_delta, apply_delta
|
from .fixation import Authority, compute_delta, apply_delta
|
||||||
from .vigilance import VigilanceSystem, VigilanceConfig, create_vigilance_system
|
from .vigilance import VigilanceSystem, VigilanceConfig, create_vigilance_system
|
||||||
from .state_to_language import StateToLanguage, ProjectionDirection
|
from .state_to_language import StateToLanguage, ProjectionDirection, CATEGORY_TO_DIMENSION
|
||||||
from .daemon import TriggerType, DaemonConfig
|
from .daemon import TriggerType, DaemonConfig
|
||||||
from .metrics import ProcessMetrics, create_metrics
|
from .metrics import ProcessMetrics, create_metrics
|
||||||
|
from .projection_directions import get_all_directions
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -51,11 +54,13 @@ from .metrics import ProcessMetrics, create_metrics
|
|||||||
_embedding_model = None
|
_embedding_model = None
|
||||||
_current_state: Optional[StateTensor] = None
|
_current_state: Optional[StateTensor] = None
|
||||||
_initial_state: Optional[StateTensor] = None
|
_initial_state: Optional[StateTensor] = None
|
||||||
|
_x_ref: Optional[StateTensor] = None # David reference
|
||||||
_vigilance: Optional[VigilanceSystem] = None
|
_vigilance: Optional[VigilanceSystem] = None
|
||||||
_translator: Optional[StateToLanguage] = None
|
_translator: Optional[StateToLanguage] = None
|
||||||
_metrics: Optional[ProcessMetrics] = None
|
_metrics: Optional[ProcessMetrics] = None
|
||||||
_authority: Optional[Authority] = None
|
_authority: Optional[Authority] = None
|
||||||
_startup_time: Optional[datetime] = None
|
_startup_time: Optional[datetime] = None
|
||||||
|
_directions: List[Dict] = [] # 109 directions from Weaviate
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -125,6 +130,15 @@ class HealthResponse(BaseModel):
|
|||||||
embedding_model: str
|
embedding_model: str
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileResponse(BaseModel):
|
||||||
|
"""Profil processuel (format compatible frontend)."""
|
||||||
|
state_id: int
|
||||||
|
directions_count: int
|
||||||
|
profile: Dict[str, Dict[str, Any]]
|
||||||
|
david_profile: Dict[str, Dict[str, Any]]
|
||||||
|
david_similarity: float
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# INITIALIZATION
|
# INITIALIZATION
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -148,44 +162,376 @@ def load_embedding_model():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def initialize_state():
|
def _fetch_ikario_state_from_weaviate() -> Optional[StateTensor]:
|
||||||
"""Initialise l'état depuis le profil David ou crée un état aléatoire."""
|
"""
|
||||||
global _current_state, _initial_state, _vigilance, _metrics
|
Récupère l'état d'Ikario depuis Weaviate (thoughts + messages).
|
||||||
|
|
||||||
# Chercher le profil David
|
Stratégie:
|
||||||
|
1. Récupère le StateVector v1 existant (agrégat de thoughts/messages)
|
||||||
|
2. Récupère les thoughts par catégorie pour enrichir les dimensions
|
||||||
|
3. Construit un StateTensor 8×1024
|
||||||
|
"""
|
||||||
|
WEAVIATE_URL = os.getenv("WEAVIATE_URL", "http://localhost:8080")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Récupérer le StateVector v1 le plus récent
|
||||||
|
state_query = {
|
||||||
|
"query": """{
|
||||||
|
Get {
|
||||||
|
StateVector(
|
||||||
|
sort: [{ path: ["state_id"], order: desc }]
|
||||||
|
limit: 1
|
||||||
|
) {
|
||||||
|
state_id
|
||||||
|
timestamp
|
||||||
|
_additional { vector }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{WEAVIATE_URL}/v1/graphql",
|
||||||
|
json=state_query,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
print(f"[API] Weaviate StateVector query failed: {response.status_code}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
states = data.get("data", {}).get("Get", {}).get("StateVector", [])
|
||||||
|
|
||||||
|
if not states:
|
||||||
|
print("[API] No StateVector found in Weaviate")
|
||||||
|
return None
|
||||||
|
|
||||||
|
state_v1 = states[0]
|
||||||
|
base_vector = np.array(state_v1.get("_additional", {}).get("vector", []))
|
||||||
|
|
||||||
|
if len(base_vector) != EMBEDDING_DIM:
|
||||||
|
print(f"[API] Invalid StateVector dimension: {len(base_vector)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"[API] Loaded StateVector v1 (state_id={state_v1.get('state_id')})")
|
||||||
|
|
||||||
|
# 2. Récupérer les thoughts par catégorie pour enrichir les dimensions
|
||||||
|
thoughts_query = {
|
||||||
|
"query": """{
|
||||||
|
Get {
|
||||||
|
Thought(limit: 500) {
|
||||||
|
content
|
||||||
|
thought_type
|
||||||
|
evolution_stage
|
||||||
|
_additional { vector }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
}
|
||||||
|
|
||||||
|
thoughts_response = requests.post(
|
||||||
|
f"{WEAVIATE_URL}/v1/graphql",
|
||||||
|
json=thoughts_query,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
thoughts = []
|
||||||
|
if thoughts_response.status_code == 200:
|
||||||
|
thoughts_data = thoughts_response.json()
|
||||||
|
thoughts = thoughts_data.get("data", {}).get("Get", {}).get("Thought", [])
|
||||||
|
print(f"[API] Loaded {len(thoughts)} thoughts from Weaviate")
|
||||||
|
|
||||||
|
# 3. Construire le StateTensor avec les thoughts catégorisés
|
||||||
|
tensor = StateTensor(
|
||||||
|
state_id=state_v1.get("state_id", 0),
|
||||||
|
timestamp=state_v1.get("timestamp", datetime.now().isoformat()),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mapping thought_type -> dimension
|
||||||
|
TYPE_TO_DIMENSION = {
|
||||||
|
"reflection": "firstness", # Qualités immédiates
|
||||||
|
"question": "secondness", # Réactions, résistances
|
||||||
|
"insight": "thirdness", # Médiations, lois
|
||||||
|
"emotion": "dispositions", # États émotionnels
|
||||||
|
"intention": "orientations", # Intentions, buts
|
||||||
|
"dialogue": "engagements", # Relations, contexte
|
||||||
|
"observation": "pertinences", # Saillances, focus
|
||||||
|
"principle": "valeurs", # Principes éthiques
|
||||||
|
}
|
||||||
|
|
||||||
|
# Agréger les vecteurs par dimension
|
||||||
|
dim_vectors = {dim: [] for dim in DIMENSION_NAMES}
|
||||||
|
|
||||||
|
for thought in thoughts:
|
||||||
|
thought_vec = thought.get("_additional", {}).get("vector", [])
|
||||||
|
if len(thought_vec) != EMBEDDING_DIM:
|
||||||
|
continue
|
||||||
|
|
||||||
|
thought_type = thought.get("thought_type", "reflection")
|
||||||
|
target_dim = TYPE_TO_DIMENSION.get(thought_type, "thirdness")
|
||||||
|
dim_vectors[target_dim].append(np.array(thought_vec))
|
||||||
|
|
||||||
|
# Construire chaque dimension
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
vectors = dim_vectors[dim_name]
|
||||||
|
|
||||||
|
if vectors:
|
||||||
|
# Moyenne des thoughts de cette catégorie
|
||||||
|
avg_vec = np.mean(vectors, axis=0)
|
||||||
|
# Mélanger avec le vecteur de base (70% base, 30% thoughts)
|
||||||
|
combined = 0.7 * base_vector + 0.3 * avg_vec
|
||||||
|
else:
|
||||||
|
# Pas de thoughts pour cette dimension, utiliser le vecteur de base
|
||||||
|
# avec une légère perturbation pour différencier
|
||||||
|
noise = np.random.randn(EMBEDDING_DIM) * 0.05
|
||||||
|
combined = base_vector + noise
|
||||||
|
|
||||||
|
# Normaliser
|
||||||
|
combined = combined / np.linalg.norm(combined)
|
||||||
|
setattr(tensor, dim_name, combined)
|
||||||
|
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Error fetching state from Weaviate: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_david_from_messages() -> Optional[np.ndarray]:
|
||||||
|
"""
|
||||||
|
Récupère le vecteur David depuis ses messages dans les conversations.
|
||||||
|
|
||||||
|
Utilise SQLite pour récupérer les messages utilisateur récents,
|
||||||
|
puis les embed avec le modèle BGE-M3.
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
# Trouver la base de données SQLite (claude-clone.db contient les messages)
|
||||||
|
db_paths = [
|
||||||
|
Path(__file__).parent.parent / "generations" / "ikario" / "server" / "data" / "claude-clone.db",
|
||||||
|
Path("C:/Users/david/SynologyDrive/Linear_coding_ikario/generations/ikario/server/data/claude-clone.db"),
|
||||||
|
]
|
||||||
|
|
||||||
|
db_path = None
|
||||||
|
for p in db_paths:
|
||||||
|
if p.exists():
|
||||||
|
db_path = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not db_path:
|
||||||
|
print("[API] SQLite database not found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Récupérer les messages utilisateur récents (excluant les tests)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT m.content
|
||||||
|
FROM messages m
|
||||||
|
JOIN conversations c ON m.conversation_id = c.id
|
||||||
|
WHERE m.role = 'user'
|
||||||
|
AND c.is_deleted = 0
|
||||||
|
AND LENGTH(m.content) > 20
|
||||||
|
AND LOWER(c.title) NOT LIKE '%test%'
|
||||||
|
AND LOWER(m.content) NOT LIKE '%test%'
|
||||||
|
ORDER BY m.created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("[API] No user messages found in database")
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(f"[API] Found {len(rows)} user messages for David")
|
||||||
|
|
||||||
|
# Concaténer les messages (max ~8000 chars)
|
||||||
|
concatenated = ""
|
||||||
|
for (content,) in rows:
|
||||||
|
if len(concatenated) + len(content) > 8000:
|
||||||
|
break
|
||||||
|
concatenated += content + "\n\n"
|
||||||
|
|
||||||
|
# Embed avec le modèle
|
||||||
|
if _embedding_model is None:
|
||||||
|
print("[API] Embedding model not loaded")
|
||||||
|
return None
|
||||||
|
|
||||||
|
david_vector = _embedding_model.encode([concatenated])[0]
|
||||||
|
david_vector = david_vector / np.linalg.norm(david_vector)
|
||||||
|
|
||||||
|
print(f"[API] David vector computed from messages (dim={len(david_vector)})")
|
||||||
|
return david_vector
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Error fetching David messages: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_david_tensor(base_vector: np.ndarray, declared_profile: dict) -> StateTensor:
|
||||||
|
"""
|
||||||
|
Crée le StateTensor de David à partir de son vecteur de messages
|
||||||
|
et le complète avec son profil déclaré.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_vector: Vecteur 1024-dim depuis les messages
|
||||||
|
declared_profile: Dict des valeurs déclarées par catégorie
|
||||||
|
"""
|
||||||
|
tensor = StateTensor(
|
||||||
|
state_id=-1, # x_ref a toujours state_id = -1
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mapping catégorie déclarée -> dimension StateTensor
|
||||||
|
CATEGORY_TO_DIM = {
|
||||||
|
'epistemic': 'firstness',
|
||||||
|
'affective': 'dispositions',
|
||||||
|
'cognitive': 'thirdness',
|
||||||
|
'relational': 'engagements',
|
||||||
|
'ethical': 'valeurs',
|
||||||
|
'temporal': 'orientations',
|
||||||
|
'thematic': 'pertinences',
|
||||||
|
'metacognitive': 'secondness',
|
||||||
|
'vital': 'dispositions',
|
||||||
|
'ecosystemic': 'engagements',
|
||||||
|
'philosophical': 'thirdness',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialiser toutes les dimensions avec le vecteur de base
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
setattr(tensor, dim_name, base_vector.copy())
|
||||||
|
|
||||||
|
# Si on a les directions chargées, ajuster avec le profil déclaré
|
||||||
|
if _directions and declared_profile:
|
||||||
|
# Construire un map direction_name -> vector
|
||||||
|
direction_map = {}
|
||||||
|
for d in _directions:
|
||||||
|
name = d.get("name")
|
||||||
|
vec = d.get("_additional", {}).get("vector", [])
|
||||||
|
category = d.get("category", "unknown")
|
||||||
|
if name and len(vec) == EMBEDDING_DIM:
|
||||||
|
direction_map[name] = {
|
||||||
|
"vector": np.array(vec),
|
||||||
|
"category": category,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajuster chaque dimension en fonction des valeurs déclarées
|
||||||
|
for category, directions_values in declared_profile.items():
|
||||||
|
target_dim = CATEGORY_TO_DIM.get(category, "thirdness")
|
||||||
|
current_vec = getattr(tensor, target_dim).copy()
|
||||||
|
|
||||||
|
for name, declared_value in directions_values.items():
|
||||||
|
if declared_value is None or name not in direction_map:
|
||||||
|
continue
|
||||||
|
|
||||||
|
dir_info = direction_map[name]
|
||||||
|
dir_vec = dir_info["vector"]
|
||||||
|
|
||||||
|
# Valeur déclarée: -10 à +10, convertir en -0.167 à +0.167
|
||||||
|
declared_scaled = declared_value / 60.0
|
||||||
|
|
||||||
|
# Projection actuelle
|
||||||
|
current_proj = float(np.dot(current_vec, dir_vec))
|
||||||
|
|
||||||
|
# Delta à appliquer
|
||||||
|
delta = declared_scaled - current_proj
|
||||||
|
|
||||||
|
# Ajuster le vecteur (facteur 0.5 pour ne pas trop modifier)
|
||||||
|
current_vec = current_vec + delta * dir_vec * 0.5
|
||||||
|
|
||||||
|
# Normaliser et stocker
|
||||||
|
current_vec = current_vec / np.linalg.norm(current_vec)
|
||||||
|
setattr(tensor, target_dim, current_vec)
|
||||||
|
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_state():
|
||||||
|
"""
|
||||||
|
Initialise l'état d'Ikario et la référence David.
|
||||||
|
|
||||||
|
Ikario: Calculé depuis Weaviate (thoughts + messages)
|
||||||
|
David (x_ref): Messages utilisateur + profil déclaré
|
||||||
|
"""
|
||||||
|
global _current_state, _initial_state, _x_ref, _vigilance, _metrics
|
||||||
|
|
||||||
|
# 1. Charger le profil déclaré de David (pour compléter)
|
||||||
profile_path = Path(__file__).parent / "david_profile_declared.json"
|
profile_path = Path(__file__).parent / "david_profile_declared.json"
|
||||||
|
declared_profile = None
|
||||||
|
|
||||||
if profile_path.exists():
|
if profile_path.exists():
|
||||||
print(f"[API] Loading David profile from {profile_path}")
|
import json
|
||||||
|
with open(profile_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
declared_profile = data.get("profile", {})
|
||||||
|
print(f"[API] Loaded David declared profile ({len(declared_profile)} categories)")
|
||||||
|
|
||||||
|
# 2. Créer x_ref (David) depuis ses messages + profil déclaré
|
||||||
|
david_vector = _fetch_david_from_messages()
|
||||||
|
|
||||||
|
if david_vector is not None:
|
||||||
|
_x_ref = _create_david_tensor(david_vector, declared_profile)
|
||||||
|
print("[API] David tensor created from messages + declared profile")
|
||||||
|
elif declared_profile:
|
||||||
|
# Fallback: utiliser uniquement le profil déclaré
|
||||||
|
print("[API] Falling back to declared profile only for David")
|
||||||
from .vigilance import DavidReference
|
from .vigilance import DavidReference
|
||||||
x_ref = DavidReference.create_from_declared_profile(str(profile_path))
|
_x_ref = DavidReference.create_from_declared_profile(str(profile_path))
|
||||||
|
|
||||||
# Créer l'état initial comme copie de x_ref
|
|
||||||
_initial_state = x_ref.copy()
|
|
||||||
_initial_state.state_id = 0
|
|
||||||
_current_state = _initial_state.copy()
|
|
||||||
|
|
||||||
# Créer le système de vigilance
|
|
||||||
_vigilance = VigilanceSystem(x_ref=x_ref)
|
|
||||||
else:
|
else:
|
||||||
print(f"[API] No David profile found, creating random state")
|
print("[API] No David profile available, x_ref will be set later")
|
||||||
|
_x_ref = None
|
||||||
|
|
||||||
|
# 2. Charger l'état d'Ikario depuis Weaviate (thoughts + messages)
|
||||||
|
ikario_state = _fetch_ikario_state_from_weaviate()
|
||||||
|
|
||||||
|
if ikario_state is not None:
|
||||||
|
print(f"[API] Ikario state loaded from Weaviate: S({ikario_state.state_id})")
|
||||||
|
_initial_state = ikario_state
|
||||||
|
_current_state = ikario_state.copy()
|
||||||
|
else:
|
||||||
|
# Fallback: créer un état aléatoire
|
||||||
|
print("[API] Creating random initial state (no Weaviate data)")
|
||||||
_initial_state = StateTensor(
|
_initial_state = StateTensor(
|
||||||
state_id=0,
|
state_id=0,
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
)
|
)
|
||||||
# Initialiser avec des vecteurs aléatoires normalisés
|
|
||||||
for dim_name in DIMENSION_NAMES:
|
for dim_name in DIMENSION_NAMES:
|
||||||
v = np.random.randn(EMBEDDING_DIM)
|
v = np.random.randn(EMBEDDING_DIM)
|
||||||
v = v / np.linalg.norm(v)
|
v = v / np.linalg.norm(v)
|
||||||
setattr(_initial_state, dim_name, v)
|
setattr(_initial_state, dim_name, v)
|
||||||
|
|
||||||
_current_state = _initial_state.copy()
|
_current_state = _initial_state.copy()
|
||||||
_vigilance = create_vigilance_system()
|
|
||||||
|
|
||||||
# Créer les métriques
|
# 3. Si pas de x_ref, utiliser l'état initial comme référence
|
||||||
|
if _x_ref is None:
|
||||||
|
_x_ref = _initial_state.copy()
|
||||||
|
_x_ref.state_id = -1
|
||||||
|
|
||||||
|
# 4. Créer le système de vigilance
|
||||||
|
_vigilance = VigilanceSystem(x_ref=_x_ref)
|
||||||
|
|
||||||
|
# 5. Créer les métriques
|
||||||
_metrics = create_metrics(S_0=_initial_state, x_ref=_vigilance.x_ref)
|
_metrics = create_metrics(S_0=_initial_state, x_ref=_vigilance.x_ref)
|
||||||
|
|
||||||
print(f"[API] State initialized: S({_current_state.state_id})")
|
print(f"[API] State initialized: Ikario=S({_current_state.state_id}), David=x_ref")
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_directions():
|
||||||
|
"""Charge les 109 directions depuis Weaviate."""
|
||||||
|
global _directions
|
||||||
|
|
||||||
|
try:
|
||||||
|
_directions = get_all_directions()
|
||||||
|
print(f"[API] Loaded {len(_directions)} directions from Weaviate")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[API] Failed to load directions: {e}")
|
||||||
|
_directions = []
|
||||||
|
|
||||||
|
|
||||||
def initialize_authority():
|
def initialize_authority():
|
||||||
@@ -230,6 +576,7 @@ async def lifespan(app: FastAPI):
|
|||||||
initialize_state()
|
initialize_state()
|
||||||
initialize_authority()
|
initialize_authority()
|
||||||
initialize_translator()
|
initialize_translator()
|
||||||
|
initialize_directions()
|
||||||
|
|
||||||
print("[API] Ikario API ready")
|
print("[API] Ikario API ready")
|
||||||
|
|
||||||
@@ -450,6 +797,97 @@ async def reset_state():
|
|||||||
return {"status": "ok", "state_id": _current_state.state_id}
|
return {"status": "ok", "state_id": _current_state.state_id}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/profile", response_model=ProfileResponse)
|
||||||
|
async def get_profile():
|
||||||
|
"""
|
||||||
|
Récupérer le profil processuel projeté sur les 109 directions.
|
||||||
|
|
||||||
|
Pour chaque direction:
|
||||||
|
- Utilise CATEGORY_TO_DIMENSION pour mapper la catégorie à la dimension du StateTensor
|
||||||
|
- Projette le vecteur de cette dimension sur le vecteur de la direction
|
||||||
|
|
||||||
|
Retourne le profil de l'état courant ET le profil de x_ref (David).
|
||||||
|
"""
|
||||||
|
if not _directions:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="Directions not loaded from Weaviate"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Projeter l'état courant sur toutes les directions
|
||||||
|
ikario_profile = _compute_tensor_profile(_current_state)
|
||||||
|
|
||||||
|
# Projeter x_ref (David) sur toutes les directions
|
||||||
|
david_profile = _compute_tensor_profile(_x_ref)
|
||||||
|
|
||||||
|
# Calculer la similarité globale (moyenne des cosines sur les 8 dimensions)
|
||||||
|
similarity = _compute_tensor_similarity(_current_state, _x_ref)
|
||||||
|
|
||||||
|
return ProfileResponse(
|
||||||
|
state_id=_current_state.state_id,
|
||||||
|
directions_count=len(_directions),
|
||||||
|
profile=ikario_profile,
|
||||||
|
david_profile=david_profile,
|
||||||
|
david_similarity=similarity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_tensor_profile(tensor: StateTensor) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Calcule le profil d'un StateTensor sur les 109 directions.
|
||||||
|
|
||||||
|
Pour chaque direction:
|
||||||
|
- Récupère sa catégorie (epistemic, affective, etc.)
|
||||||
|
- Utilise CATEGORY_TO_DIMENSION pour trouver la dimension correspondante
|
||||||
|
- Projette le vecteur de cette dimension sur le vecteur de la direction
|
||||||
|
"""
|
||||||
|
profile = {}
|
||||||
|
|
||||||
|
for d in _directions:
|
||||||
|
category = d.get("category", "unknown")
|
||||||
|
name = d.get("name", "unknown")
|
||||||
|
direction_vec = d.get("_additional", {}).get("vector", [])
|
||||||
|
|
||||||
|
if not direction_vec:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction_vec = np.array(direction_vec)
|
||||||
|
|
||||||
|
# Mapper la catégorie à la dimension du StateTensor
|
||||||
|
dim_name = CATEGORY_TO_DIMENSION.get(category, "thirdness")
|
||||||
|
state_vec = getattr(tensor, dim_name)
|
||||||
|
|
||||||
|
# Projection (cosine similarity)
|
||||||
|
projection = float(np.dot(state_vec, direction_vec))
|
||||||
|
|
||||||
|
if category not in profile:
|
||||||
|
profile[category] = {}
|
||||||
|
profile[category][name] = {
|
||||||
|
"value": round(projection, 4),
|
||||||
|
"dimension": dim_name,
|
||||||
|
"pole_positive": d.get("pole_positive", ""),
|
||||||
|
"pole_negative": d.get("pole_negative", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_tensor_similarity(t1: StateTensor, t2: StateTensor) -> float:
|
||||||
|
"""
|
||||||
|
Calcule la similarité globale entre deux StateTensors.
|
||||||
|
Moyenne des similarités cosinus sur les 8 dimensions.
|
||||||
|
"""
|
||||||
|
similarities = []
|
||||||
|
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
v1 = getattr(t1, dim_name)
|
||||||
|
v2 = getattr(t2, dim_name)
|
||||||
|
sim = float(np.dot(v1, v2))
|
||||||
|
similarities.append(sim)
|
||||||
|
|
||||||
|
return round(sum(similarities) / len(similarities), 4)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# MAIN
|
# MAIN
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user