fix: MCP tools migrated from StateVector V1 to StateTensor V2
- identity_tools.py: rewritten to read StateTensor (8x1024 named vectors) instead of StateVector (single 1024-dim). Uses CATEGORY_TO_DIMENSION mapping. - mcp_server.py: get_state_vector renamed to get_state_tensor - __init__.py: updated exports Now returns S(30) with architecture v2_tensor instead of S(2) from V1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,15 @@ from memory.mcp import (
|
|||||||
trace_concept_evolution_handler,
|
trace_concept_evolution_handler,
|
||||||
check_consistency_handler,
|
check_consistency_handler,
|
||||||
update_thought_evolution_stage_handler,
|
update_thought_evolution_stage_handler,
|
||||||
|
# Identity tools (state tensors and profiles)
|
||||||
|
GetStateProfileInput,
|
||||||
|
GetDavidProfileInput,
|
||||||
|
CompareProfilesInput,
|
||||||
|
GetStateTensorInput,
|
||||||
|
get_state_profile_handler,
|
||||||
|
get_david_profile_handler,
|
||||||
|
compare_profiles_handler,
|
||||||
|
get_state_tensor_handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -1019,6 +1028,201 @@ async def update_thought_evolution_stage(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Identity Tools (State Vectors and Profiles)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_state_profile(
|
||||||
|
state_id: int | None = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get Ikario's current state profile projected onto interpretable directions.
|
||||||
|
|
||||||
|
Uses StateTensor v2 architecture (8x1024 named vectors). Each category
|
||||||
|
is projected onto its correct tensor dimension via CATEGORY_TO_DIMENSION mapping.
|
||||||
|
|
||||||
|
Returns Ikario's processual identity as a profile organized by categories
|
||||||
|
(epistemic, affective, cognitive, relational, etc.) with values for each
|
||||||
|
interpretable direction (curiosity, certainty, enthusiasm, empathy, etc.).
|
||||||
|
|
||||||
|
Each value represents how strongly Ikario's current state aligns with that
|
||||||
|
direction (-1 to +1 scale, where 0 is neutral).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state_id: Specific state ID to retrieve (default: latest state).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- success: Whether query succeeded
|
||||||
|
- state_id: The state ID retrieved
|
||||||
|
- timestamp: When this state was created
|
||||||
|
- trigger_type: What triggered this state
|
||||||
|
- profile: Dict[category, Dict[direction, value]]
|
||||||
|
- epistemic: {curiosity: 0.72, certainty: -0.18, ...}
|
||||||
|
- affective: {enthusiasm: 0.45, serenity: 0.23, ...}
|
||||||
|
- cognitive: {divergence: 0.31, intuition: -0.12, ...}
|
||||||
|
- relational: {engagement: 0.56, empathy: 0.41, ...}
|
||||||
|
- ... (11 categories, ~109 directions total)
|
||||||
|
- directions_count: Total number of directions analyzed
|
||||||
|
- architecture: "v2_tensor"
|
||||||
|
- dimensions_loaded: List of 8 tensor dimension names
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Get current profile::
|
||||||
|
|
||||||
|
get_state_profile()
|
||||||
|
|
||||||
|
Get specific past state::
|
||||||
|
|
||||||
|
get_state_profile(state_id=5)
|
||||||
|
"""
|
||||||
|
input_data = GetStateProfileInput(state_id=state_id)
|
||||||
|
result = await get_state_profile_handler(input_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_david_profile(
|
||||||
|
include_declared: bool = True,
|
||||||
|
max_messages: int = 100,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get David's profile computed from his messages and optionally declared values.
|
||||||
|
|
||||||
|
Analyzes David's recent messages to compute his embedding, then projects it
|
||||||
|
onto the same interpretable directions used for Ikario. Optionally includes
|
||||||
|
David's declared profile values from the questionnaire.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
include_declared: Include declared profile from david_profile_declared.json (default True).
|
||||||
|
max_messages: Maximum number of David's messages to analyze (10-500, default 100).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- success: Whether query succeeded
|
||||||
|
- profile: Dict[category, Dict[direction, {computed, declared?, declared_normalized?}]]
|
||||||
|
- computed: Value computed from messages (-1 to +1)
|
||||||
|
- declared: Raw declared value from questionnaire (-10 to +10) if available
|
||||||
|
- declared_normalized: Declared value normalized to -1 to +1
|
||||||
|
- similarity_with_ikario: Similarity percentage (0-100%)
|
||||||
|
- messages_analyzed: Number of messages used
|
||||||
|
- has_declared_profile: Whether declared profile was found
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Get full profile with declared values::
|
||||||
|
|
||||||
|
get_david_profile()
|
||||||
|
|
||||||
|
Get computed-only profile::
|
||||||
|
|
||||||
|
get_david_profile(include_declared=False)
|
||||||
|
"""
|
||||||
|
input_data = GetDavidProfileInput(
|
||||||
|
include_declared=include_declared,
|
||||||
|
max_messages=max_messages,
|
||||||
|
)
|
||||||
|
result = await get_david_profile_handler(input_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def compare_profiles(
|
||||||
|
categories: list[str] | None = None,
|
||||||
|
state_id: int | None = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compare Ikario and David profiles to understand similarities and differences.
|
||||||
|
|
||||||
|
Computes both profiles and returns detailed comparison including:
|
||||||
|
- Overall similarity percentage
|
||||||
|
- Per-direction comparison with deltas
|
||||||
|
- Top 5 convergent dimensions (most similar)
|
||||||
|
- Top 5 divergent dimensions (most different)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
categories: Filter to specific categories (e.g., ["epistemic", "affective"]).
|
||||||
|
If None, compares all categories.
|
||||||
|
state_id: Ikario state ID to compare (default: latest).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- success: Whether comparison succeeded
|
||||||
|
- similarity: Overall similarity percentage (0-100%)
|
||||||
|
- comparison: Dict[category, Dict[direction, {ikario, david, delta}]]
|
||||||
|
- convergent_dimensions: Top 5 dimensions where Ikario and David are most similar
|
||||||
|
- [{name, category, ikario, david}, ...]
|
||||||
|
- divergent_dimensions: Top 5 dimensions where they differ most
|
||||||
|
- [{name, category, ikario, david}, ...]
|
||||||
|
- categories_compared: List of categories analyzed
|
||||||
|
- directions_compared: Total number of directions compared
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Compare full profiles::
|
||||||
|
|
||||||
|
compare_profiles()
|
||||||
|
|
||||||
|
Compare only epistemic and affective dimensions::
|
||||||
|
|
||||||
|
compare_profiles(categories=["epistemic", "affective"])
|
||||||
|
"""
|
||||||
|
input_data = CompareProfilesInput(
|
||||||
|
categories=categories,
|
||||||
|
state_id=state_id,
|
||||||
|
)
|
||||||
|
result = await compare_profiles_handler(input_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_state_tensor(
|
||||||
|
state_id: int | None = None,
|
||||||
|
entity: str = "ikario",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get raw 8x1024 state tensor (advanced usage).
|
||||||
|
|
||||||
|
Returns the 8 named dimension vectors (firstness, secondness, thirdness,
|
||||||
|
dispositions, orientations, engagements, pertinences, valeurs) for Ikario,
|
||||||
|
or a single embedding for David. This is useful for advanced analysis,
|
||||||
|
custom projections, or debugging.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
state_id: State ID for Ikario (ignored for David). Default: latest.
|
||||||
|
entity: Which entity's tensor to retrieve: "ikario" or "david".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing:
|
||||||
|
- success: Whether query succeeded
|
||||||
|
- entity: The entity ("ikario" or "david")
|
||||||
|
- For Ikario: dimensions dict with 8 named vectors (first 10 values each)
|
||||||
|
- For David: single vector (first 10 values)
|
||||||
|
- metadata: Additional information
|
||||||
|
- For Ikario: state_id, timestamp, trigger_type
|
||||||
|
- For David: source, messages_count
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Get Ikario's current state tensor::
|
||||||
|
|
||||||
|
get_state_tensor(entity="ikario")
|
||||||
|
|
||||||
|
Get David's computed vector::
|
||||||
|
|
||||||
|
get_state_tensor(entity="david")
|
||||||
|
|
||||||
|
Get specific Ikario state::
|
||||||
|
|
||||||
|
get_state_tensor(state_id=3, entity="ikario")
|
||||||
|
"""
|
||||||
|
input_data = GetStateTensorInput(
|
||||||
|
state_id=state_id,
|
||||||
|
entity=entity,
|
||||||
|
)
|
||||||
|
result = await get_state_tensor_handler(input_data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Signal Handlers
|
# Signal Handlers
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ from memory.mcp.unified_tools import (
|
|||||||
update_thought_evolution_stage_handler,
|
update_thought_evolution_stage_handler,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from memory.mcp.identity_tools import (
|
||||||
|
GetStateProfileInput,
|
||||||
|
GetDavidProfileInput,
|
||||||
|
CompareProfilesInput,
|
||||||
|
GetStateTensorInput,
|
||||||
|
get_state_profile_handler,
|
||||||
|
get_david_profile_handler,
|
||||||
|
compare_profiles_handler,
|
||||||
|
get_state_tensor_handler,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Thought tools
|
# Thought tools
|
||||||
"AddThoughtInput",
|
"AddThoughtInput",
|
||||||
@@ -74,4 +85,14 @@ __all__ = [
|
|||||||
"trace_concept_evolution_handler",
|
"trace_concept_evolution_handler",
|
||||||
"check_consistency_handler",
|
"check_consistency_handler",
|
||||||
"update_thought_evolution_stage_handler",
|
"update_thought_evolution_stage_handler",
|
||||||
|
|
||||||
|
# Identity tools (state tensors and profiles)
|
||||||
|
"GetStateProfileInput",
|
||||||
|
"GetDavidProfileInput",
|
||||||
|
"CompareProfilesInput",
|
||||||
|
"GetStateTensorInput",
|
||||||
|
"get_state_profile_handler",
|
||||||
|
"get_david_profile_handler",
|
||||||
|
"compare_profiles_handler",
|
||||||
|
"get_state_tensor_handler",
|
||||||
]
|
]
|
||||||
|
|||||||
659
memory/mcp/identity_tools.py
Normal file
659
memory/mcp/identity_tools.py
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
"""
|
||||||
|
Identity MCP Tools - Handlers for reading Ikario and David state tensors.
|
||||||
|
|
||||||
|
Provides tools for:
|
||||||
|
- get_state_profile: Read Ikario's state tensor projected onto 109 interpretable directions
|
||||||
|
- get_david_profile: Read David's profile from messages + declared profile
|
||||||
|
- compare_profiles: Compare Ikario and David profiles
|
||||||
|
- get_state_tensor: Get raw 8x1024 state tensor (advanced usage)
|
||||||
|
|
||||||
|
Architecture v2: StateTensor (8 named vectors x 1024 dims) replaces StateVector (single 1024-dim).
|
||||||
|
Each category maps to a dimension via CATEGORY_TO_DIMENSION for proper projection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import weaviate
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from memory.core import get_embedder
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Category -> Dimension mapping (must match state_to_language.py)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
CATEGORY_TO_DIMENSION = {
|
||||||
|
'epistemic': 'firstness',
|
||||||
|
'affective': 'dispositions',
|
||||||
|
'cognitive': 'thirdness',
|
||||||
|
'relational': 'engagements',
|
||||||
|
'ethical': 'valeurs',
|
||||||
|
'temporal': 'orientations',
|
||||||
|
'thematic': 'pertinences',
|
||||||
|
'metacognitive': 'secondness',
|
||||||
|
'vital': 'dispositions',
|
||||||
|
'ecosystemic': 'engagements',
|
||||||
|
'philosophical': 'thirdness',
|
||||||
|
}
|
||||||
|
|
||||||
|
DIMENSION_NAMES = [
|
||||||
|
'firstness', 'secondness', 'thirdness',
|
||||||
|
'dispositions', 'orientations', 'engagements',
|
||||||
|
'pertinences', 'valeurs',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Input Models
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class GetStateProfileInput(BaseModel):
|
||||||
|
"""Input for get_state_profile tool."""
|
||||||
|
|
||||||
|
state_id: Optional[int] = Field(
|
||||||
|
default=None,
|
||||||
|
description="State ID to retrieve (default: latest state)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetDavidProfileInput(BaseModel):
|
||||||
|
"""Input for get_david_profile tool."""
|
||||||
|
|
||||||
|
include_declared: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Include declared profile values from david_profile_declared.json"
|
||||||
|
)
|
||||||
|
max_messages: int = Field(
|
||||||
|
default=500,
|
||||||
|
ge=10,
|
||||||
|
le=1000,
|
||||||
|
description="Maximum number of David's messages to analyze"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CompareProfilesInput(BaseModel):
|
||||||
|
"""Input for compare_profiles tool."""
|
||||||
|
|
||||||
|
categories: Optional[List[str]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Filter to specific categories (e.g., ['epistemic', 'affective'])"
|
||||||
|
)
|
||||||
|
state_id: Optional[int] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Ikario state ID to compare (default: latest)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetStateTensorInput(BaseModel):
|
||||||
|
"""Input for get_state_tensor tool (advanced usage)."""
|
||||||
|
|
||||||
|
state_id: Optional[int] = Field(
|
||||||
|
default=None,
|
||||||
|
description="State ID (default: latest)"
|
||||||
|
)
|
||||||
|
entity: str = Field(
|
||||||
|
default="ikario",
|
||||||
|
description="Entity to retrieve: 'ikario' or 'david'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Helper Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_state_tensor(client: weaviate.WeaviateClient) -> tuple[dict, dict]:
|
||||||
|
"""
|
||||||
|
Get the latest StateTensor from Weaviate (v2 architecture).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (properties dict, named_vectors dict[dim_name -> list[float]])
|
||||||
|
"""
|
||||||
|
collection = client.collections.get("StateTensor")
|
||||||
|
|
||||||
|
result = collection.query.fetch_objects(
|
||||||
|
limit=100,
|
||||||
|
include_vector=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.objects:
|
||||||
|
raise ValueError("No StateTensor found in Weaviate")
|
||||||
|
|
||||||
|
# Find the one with highest state_id
|
||||||
|
latest = max(result.objects, key=lambda o: o.properties.get("state_id", -1))
|
||||||
|
|
||||||
|
# Extract named vectors
|
||||||
|
named_vectors = {}
|
||||||
|
if isinstance(latest.vector, dict):
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
if dim_name in latest.vector:
|
||||||
|
named_vectors[dim_name] = latest.vector[dim_name]
|
||||||
|
|
||||||
|
if not named_vectors:
|
||||||
|
raise ValueError(f"StateTensor S({latest.properties.get('state_id')}) has no named vectors")
|
||||||
|
|
||||||
|
return latest.properties, named_vectors
|
||||||
|
|
||||||
|
|
||||||
|
def get_state_tensor_by_id(
|
||||||
|
client: weaviate.WeaviateClient,
|
||||||
|
state_id: int
|
||||||
|
) -> tuple[dict, dict]:
|
||||||
|
"""
|
||||||
|
Get a specific StateTensor by state_id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (properties dict, named_vectors dict[dim_name -> list[float]])
|
||||||
|
"""
|
||||||
|
collection = client.collections.get("StateTensor")
|
||||||
|
|
||||||
|
from weaviate.classes.query import Filter
|
||||||
|
|
||||||
|
result = collection.query.fetch_objects(
|
||||||
|
filters=Filter.by_property("state_id").equal(state_id),
|
||||||
|
limit=1,
|
||||||
|
include_vector=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.objects:
|
||||||
|
raise ValueError(f"StateTensor with state_id={state_id} not found")
|
||||||
|
|
||||||
|
obj = result.objects[0]
|
||||||
|
|
||||||
|
named_vectors = {}
|
||||||
|
if isinstance(obj.vector, dict):
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
if dim_name in obj.vector:
|
||||||
|
named_vectors[dim_name] = obj.vector[dim_name]
|
||||||
|
|
||||||
|
if not named_vectors:
|
||||||
|
raise ValueError(f"StateTensor S({state_id}) has no named vectors")
|
||||||
|
|
||||||
|
return obj.properties, named_vectors
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_projection_directions(client: weaviate.WeaviateClient) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Get all ProjectionDirection objects from Weaviate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of direction objects with properties and vectors
|
||||||
|
"""
|
||||||
|
collection = client.collections.get("ProjectionDirection")
|
||||||
|
|
||||||
|
result = collection.query.fetch_objects(
|
||||||
|
limit=200,
|
||||||
|
include_vector=True
|
||||||
|
)
|
||||||
|
|
||||||
|
directions = []
|
||||||
|
for obj in result.objects:
|
||||||
|
directions.append({
|
||||||
|
"name": obj.properties.get("name"),
|
||||||
|
"category": obj.properties.get("category"),
|
||||||
|
"pole_positive": obj.properties.get("pole_positive"),
|
||||||
|
"pole_negative": obj.properties.get("pole_negative"),
|
||||||
|
"description": obj.properties.get("description"),
|
||||||
|
"vector": obj.vector["default"]
|
||||||
|
})
|
||||||
|
|
||||||
|
return directions
|
||||||
|
|
||||||
|
|
||||||
|
def compute_projection(state_vector: list, direction_vector: list) -> float:
|
||||||
|
"""
|
||||||
|
Compute projection (dot product) of state onto direction.
|
||||||
|
|
||||||
|
Both vectors should be normalized (cosine similarity).
|
||||||
|
"""
|
||||||
|
state = np.array(state_vector)
|
||||||
|
direction = np.array(direction_vector)
|
||||||
|
|
||||||
|
return float(np.dot(state, direction))
|
||||||
|
|
||||||
|
|
||||||
|
def build_tensor_profile(
|
||||||
|
named_vectors: dict,
|
||||||
|
directions: list[dict]
|
||||||
|
) -> dict[str, dict[str, float]]:
|
||||||
|
"""
|
||||||
|
Build a profile by projecting each direction onto the correct tensor dimension.
|
||||||
|
|
||||||
|
Uses CATEGORY_TO_DIMENSION to map each direction's category to the right
|
||||||
|
dimension of the 8x1024 state tensor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[category, Dict[direction_name, projection_value]]
|
||||||
|
"""
|
||||||
|
profile = {}
|
||||||
|
|
||||||
|
for direction in directions:
|
||||||
|
category = direction["category"]
|
||||||
|
name = direction["name"]
|
||||||
|
dir_vector = direction["vector"]
|
||||||
|
|
||||||
|
# Map category to tensor dimension
|
||||||
|
dim_name = CATEGORY_TO_DIMENSION.get(category, "thirdness")
|
||||||
|
state_vector = named_vectors.get(dim_name)
|
||||||
|
|
||||||
|
if state_vector is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
projection = compute_projection(state_vector, dir_vector)
|
||||||
|
|
||||||
|
if category not in profile:
|
||||||
|
profile[category] = {}
|
||||||
|
|
||||||
|
profile[category][name] = round(projection, 4)
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def get_david_messages(client: weaviate.WeaviateClient, max_messages: int) -> list[str]:
|
||||||
|
"""
|
||||||
|
Get David's messages from Weaviate Message collection.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of message contents
|
||||||
|
"""
|
||||||
|
collection = client.collections.get("Message")
|
||||||
|
|
||||||
|
from weaviate.classes.query import Filter
|
||||||
|
|
||||||
|
result = collection.query.fetch_objects(
|
||||||
|
filters=Filter.by_property("role").equal("user"),
|
||||||
|
limit=max_messages
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
for obj in result.objects:
|
||||||
|
content = obj.properties.get("content", "")
|
||||||
|
if len(content) > 20:
|
||||||
|
messages.append(content)
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
def load_declared_profile() -> dict | None:
|
||||||
|
"""
|
||||||
|
Load David's declared profile from JSON file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Profile dict or None if not found
|
||||||
|
"""
|
||||||
|
possible_paths = [
|
||||||
|
Path(__file__).parent.parent.parent / "ikario_processual" / "david_profile_declared.json",
|
||||||
|
Path("ikario_processual/david_profile_declared.json"),
|
||||||
|
Path("david_profile_declared.json"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possible_paths:
|
||||||
|
if path.exists():
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Handlers
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
async def get_state_profile_handler(input_data: GetStateProfileInput) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get Ikario's state profile projected onto interpretable directions.
|
||||||
|
|
||||||
|
Uses StateTensor (8x1024) with CATEGORY_TO_DIMENSION mapping for proper projection.
|
||||||
|
Returns profile organized by categories (epistemic, affective, etc.)
|
||||||
|
with values for each direction (curiosity, certainty, etc.).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = weaviate.connect_to_local()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Get StateTensor (8 named vectors)
|
||||||
|
if input_data.state_id is not None:
|
||||||
|
properties, named_vectors = get_state_tensor_by_id(
|
||||||
|
client, input_data.state_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
properties, named_vectors = get_latest_state_tensor(client)
|
||||||
|
|
||||||
|
# 2. Get all ProjectionDirections
|
||||||
|
directions = get_all_projection_directions(client)
|
||||||
|
|
||||||
|
if not directions:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No ProjectionDirection found in Weaviate. Run phase2_projection_directions.py first."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Build profile using tensor dimensions
|
||||||
|
profile = build_tensor_profile(named_vectors, directions)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"state_id": properties.get("state_id"),
|
||||||
|
"timestamp": str(properties.get("timestamp", "")),
|
||||||
|
"trigger_type": properties.get("trigger_type", "unknown"),
|
||||||
|
"profile": profile,
|
||||||
|
"directions_count": len(directions),
|
||||||
|
"categories": list(profile.keys()),
|
||||||
|
"architecture": "v2_tensor",
|
||||||
|
"dimensions_loaded": list(named_vectors.keys())
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_david_profile_handler(input_data: GetDavidProfileInput) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get David's profile from his messages and optionally declared profile.
|
||||||
|
|
||||||
|
Computes David's embedding from his messages, projects onto directions,
|
||||||
|
and optionally merges with declared profile values.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = weaviate.connect_to_local()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Get David's messages
|
||||||
|
messages = get_david_messages(client, input_data.max_messages)
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No messages from David found in Weaviate"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Concatenate and embed
|
||||||
|
text = "\n\n".join(messages)[:5000]
|
||||||
|
|
||||||
|
embedder = get_embedder()
|
||||||
|
david_vector = embedder.embed_batch([text])[0].tolist()
|
||||||
|
|
||||||
|
# 3. Get directions and compute profile
|
||||||
|
directions = get_all_projection_directions(client)
|
||||||
|
|
||||||
|
if not directions:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No ProjectionDirection found. Run phase2_projection_directions.py first."
|
||||||
|
}
|
||||||
|
|
||||||
|
# For David, use same vector for all dimensions (single embedding)
|
||||||
|
david_named_vectors = {dim: david_vector for dim in DIMENSION_NAMES}
|
||||||
|
computed_profile = build_tensor_profile(david_named_vectors, directions)
|
||||||
|
|
||||||
|
# 4. Load declared profile if requested
|
||||||
|
declared_profile = None
|
||||||
|
has_declared = False
|
||||||
|
|
||||||
|
if input_data.include_declared:
|
||||||
|
declared_data = load_declared_profile()
|
||||||
|
if declared_data:
|
||||||
|
declared_profile = declared_data.get("profile", {})
|
||||||
|
has_declared = True
|
||||||
|
|
||||||
|
# 5. Merge profiles (declared takes precedence for display)
|
||||||
|
final_profile = {}
|
||||||
|
for category, directions_dict in computed_profile.items():
|
||||||
|
final_profile[category] = {}
|
||||||
|
for name, computed_value in directions_dict.items():
|
||||||
|
entry = {
|
||||||
|
"computed": computed_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if declared_profile and category in declared_profile:
|
||||||
|
declared_value = declared_profile[category].get(name)
|
||||||
|
if declared_value is not None:
|
||||||
|
entry["declared"] = declared_value
|
||||||
|
entry["declared_normalized"] = round(declared_value / 10, 2)
|
||||||
|
|
||||||
|
final_profile[category][name] = entry
|
||||||
|
|
||||||
|
# 6. Compute similarity with Ikario
|
||||||
|
try:
|
||||||
|
_, ikario_vectors = get_latest_state_tensor(client)
|
||||||
|
# Cosine similarity across all dimensions
|
||||||
|
similarities = []
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
if dim_name in ikario_vectors:
|
||||||
|
sim = float(np.dot(david_vector, ikario_vectors[dim_name]))
|
||||||
|
similarities.append(sim)
|
||||||
|
similarity_percent = round(np.mean(similarities) * 100, 1) if similarities else None
|
||||||
|
except Exception:
|
||||||
|
similarity_percent = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"profile": final_profile,
|
||||||
|
"similarity_with_ikario": similarity_percent,
|
||||||
|
"messages_analyzed": len(messages),
|
||||||
|
"has_declared_profile": has_declared,
|
||||||
|
"categories": list(final_profile.keys()),
|
||||||
|
"directions_count": len(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def compare_profiles_handler(input_data: CompareProfilesInput) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Compare Ikario and David profiles.
|
||||||
|
|
||||||
|
Returns similarity score and detailed comparison by direction,
|
||||||
|
including convergent and divergent dimensions.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = weaviate.connect_to_local()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Get Ikario's state tensor
|
||||||
|
if input_data.state_id is not None:
|
||||||
|
_, ikario_vectors = get_state_tensor_by_id(client, input_data.state_id)
|
||||||
|
else:
|
||||||
|
_, ikario_vectors = get_latest_state_tensor(client)
|
||||||
|
|
||||||
|
# 2. Get David's messages and embed
|
||||||
|
messages = get_david_messages(client, max_messages=100)
|
||||||
|
if not messages:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No messages from David found"
|
||||||
|
}
|
||||||
|
|
||||||
|
text = "\n\n".join(messages)[:5000]
|
||||||
|
embedder = get_embedder()
|
||||||
|
david_vector = embedder.embed_batch([text])[0].tolist()
|
||||||
|
david_named_vectors = {dim: david_vector for dim in DIMENSION_NAMES}
|
||||||
|
|
||||||
|
# 3. Get directions
|
||||||
|
directions = get_all_projection_directions(client)
|
||||||
|
if not directions:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No ProjectionDirection found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Filter directions by category if specified
|
||||||
|
if input_data.categories:
|
||||||
|
directions = [
|
||||||
|
d for d in directions
|
||||||
|
if d["category"] in input_data.categories
|
||||||
|
]
|
||||||
|
|
||||||
|
# 5. Compute projections for both using tensor dimensions
|
||||||
|
ikario_profile = build_tensor_profile(ikario_vectors, directions)
|
||||||
|
david_profile = build_tensor_profile(david_named_vectors, directions)
|
||||||
|
|
||||||
|
# 6. Build comparison
|
||||||
|
comparison = {}
|
||||||
|
all_deltas = []
|
||||||
|
|
||||||
|
for category in ikario_profile.keys():
|
||||||
|
comparison[category] = {}
|
||||||
|
for name in ikario_profile[category].keys():
|
||||||
|
ikario_val = ikario_profile[category][name]
|
||||||
|
david_val = david_profile[category].get(name, 0)
|
||||||
|
delta = round(abs(ikario_val - david_val), 4)
|
||||||
|
|
||||||
|
comparison[category][name] = {
|
||||||
|
"ikario": ikario_val,
|
||||||
|
"david": david_val,
|
||||||
|
"delta": delta
|
||||||
|
}
|
||||||
|
|
||||||
|
all_deltas.append({
|
||||||
|
"name": name,
|
||||||
|
"category": category,
|
||||||
|
"ikario": ikario_val,
|
||||||
|
"david": david_val,
|
||||||
|
"delta": delta
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7. Find convergent and divergent dimensions
|
||||||
|
sorted_by_delta = sorted(all_deltas, key=lambda x: x["delta"])
|
||||||
|
|
||||||
|
convergent = sorted_by_delta[:5]
|
||||||
|
divergent = sorted_by_delta[-5:][::-1]
|
||||||
|
|
||||||
|
# 8. Compute overall similarity (mean across dimensions)
|
||||||
|
similarities = []
|
||||||
|
for dim_name in DIMENSION_NAMES:
|
||||||
|
if dim_name in ikario_vectors:
|
||||||
|
sim = float(np.dot(david_vector, ikario_vectors[dim_name]))
|
||||||
|
similarities.append(sim)
|
||||||
|
similarity_percent = round(np.mean(similarities) * 100, 1) if similarities else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"similarity": similarity_percent,
|
||||||
|
"comparison": comparison,
|
||||||
|
"convergent_dimensions": [
|
||||||
|
{
|
||||||
|
"name": d["name"],
|
||||||
|
"category": d["category"],
|
||||||
|
"ikario": d["ikario"],
|
||||||
|
"david": d["david"]
|
||||||
|
}
|
||||||
|
for d in convergent
|
||||||
|
],
|
||||||
|
"divergent_dimensions": [
|
||||||
|
{
|
||||||
|
"name": d["name"],
|
||||||
|
"category": d["category"],
|
||||||
|
"ikario": d["ikario"],
|
||||||
|
"david": d["david"]
|
||||||
|
}
|
||||||
|
for d in divergent
|
||||||
|
],
|
||||||
|
"categories_compared": list(comparison.keys()),
|
||||||
|
"directions_compared": len(all_deltas)
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_state_tensor_handler(input_data: GetStateTensorInput) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get raw 8x1024 state tensor (advanced usage).
|
||||||
|
|
||||||
|
Returns the 8 named dimension vectors for Ikario or a single embedding for David.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = weaviate.connect_to_local()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if input_data.entity == "ikario":
|
||||||
|
if input_data.state_id is not None:
|
||||||
|
properties, named_vectors = get_state_tensor_by_id(
|
||||||
|
client, input_data.state_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
properties, named_vectors = get_latest_state_tensor(client)
|
||||||
|
|
||||||
|
# Return first 10 values per dimension (truncated for readability)
|
||||||
|
truncated = {
|
||||||
|
dim: list(vec[:10]) if hasattr(vec, '__len__') else vec
|
||||||
|
for dim, vec in named_vectors.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"entity": "ikario",
|
||||||
|
"dimensions": truncated,
|
||||||
|
"dimension_count": len(named_vectors),
|
||||||
|
"vector_size": 1024,
|
||||||
|
"metadata": {
|
||||||
|
"state_id": properties.get("state_id"),
|
||||||
|
"timestamp": str(properties.get("timestamp", "")),
|
||||||
|
"trigger_type": properties.get("trigger_type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elif input_data.entity == "david":
|
||||||
|
messages = get_david_messages(client, max_messages=100)
|
||||||
|
if not messages:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "No messages from David found"
|
||||||
|
}
|
||||||
|
|
||||||
|
text = "\n\n".join(messages)[:5000]
|
||||||
|
embedder = get_embedder()
|
||||||
|
david_vector = embedder.embed_batch([text])[0].tolist()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"entity": "david",
|
||||||
|
"vector": david_vector[:10], # Truncated
|
||||||
|
"dimension": len(david_vector),
|
||||||
|
"metadata": {
|
||||||
|
"source": "messages_embedding",
|
||||||
|
"messages_count": len(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unknown entity: {input_data.entity}. Use 'ikario' or 'david'."
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user