fix entity and migrate memory disposition

This commit is contained in:
Nicolò Boschi 2025-12-08 16:14:34 +01:00
parent 3bb0a58ded
commit 76cfa8f9c4
37 changed files with 694 additions and 547 deletions

View file

@ -0,0 +1,62 @@
"""disposition_to_3_traits
Revision ID: e0a1b2c3d4e5
Revises: rename_personality
Create Date: 2024-12-08
Migrate disposition traits from Big Five (openness, conscientiousness, extraversion,
agreeableness, neuroticism, bias_strength with 0-1 float values) to the new 3-trait
system (skepticism, literalism, empathy with 1-5 integer values).
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e0a1b2c3d4e5'
down_revision: Union[str, Sequence[str], None] = 'rename_personality'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Convert Big Five disposition to 3-trait disposition."""
conn = op.get_bind()
# Update all existing banks to use the new disposition format
# Convert from old format to new format with reasonable mappings:
# - skepticism: derived from inverse of agreeableness (skeptical people are less agreeable)
# - literalism: derived from conscientiousness (detail-oriented people are more literal)
# - empathy: derived from agreeableness + inverse of neuroticism
# Default all to 3 (neutral) for simplicity
conn.execute(sa.text("""
UPDATE banks
SET disposition = '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
WHERE disposition IS NOT NULL
"""))
# Update the default for new banks
conn.execute(sa.text("""
ALTER TABLE banks
ALTER COLUMN disposition SET DEFAULT '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
"""))
def downgrade() -> None:
"""Convert back to Big Five disposition."""
conn = op.get_bind()
# Revert to Big Five format with default values
conn.execute(sa.text("""
UPDATE banks
SET disposition = '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
WHERE disposition IS NOT NULL
"""))
# Update the default for new banks
conn.execute(sa.text("""
ALTER TABLE banks
ALTER COLUMN disposition SET DEFAULT '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
"""))

View file

@ -439,24 +439,18 @@ class BanksResponse(BaseModel):
class DispositionTraits(BaseModel):
"""Disposition traits based on Big Five model."""
"""Disposition traits that influence how memories are formed and interpreted."""
model_config = ConfigDict(json_schema_extra={
"example": {
"openness": 0.8,
"conscientiousness": 0.6,
"extraversion": 0.5,
"agreeableness": 0.7,
"neuroticism": 0.3,
"bias_strength": 0.7
"skepticism": 3,
"literalism": 3,
"empathy": 3
}
})
openness: float = Field(ge=0.0, le=1.0, description="Openness to experience (0-1)")
conscientiousness: float = Field(ge=0.0, le=1.0, description="Conscientiousness (0-1)")
extraversion: float = Field(ge=0.0, le=1.0, description="Extraversion (0-1)")
agreeableness: float = Field(ge=0.0, le=1.0, description="Agreeableness (0-1)")
neuroticism: float = Field(ge=0.0, le=1.0, description="Neuroticism (0-1)")
bias_strength: float = Field(ge=0.0, le=1.0, description="How strongly disposition influences opinions (0-1)")
skepticism: int = Field(ge=1, le=5, description="How skeptical vs trusting (1=trusting, 5=skeptical)")
literalism: int = Field(ge=1, le=5, description="How literally to interpret information (1=flexible, 5=literal)")
empathy: int = Field(ge=1, le=5, description="How much to consider emotional context (1=detached, 5=empathetic)")
class BankProfileResponse(BaseModel):
@ -466,12 +460,9 @@ class BankProfileResponse(BaseModel):
"bank_id": "user123",
"name": "Alice",
"disposition": {
"openness": 0.8,
"conscientiousness": 0.6,
"extraversion": 0.5,
"agreeableness": 0.7,
"neuroticism": 0.3,
"bias_strength": 0.7
"skepticism": 3,
"literalism": 3,
"empathy": 3
},
"background": "I am a software engineer with 10 years of experience in startups"
}
@ -500,7 +491,7 @@ class AddBackgroundRequest(BaseModel):
content: str = Field(description="New background information to add or merge")
update_disposition: bool = Field(
default=True,
description="If true, infer Big Five disposition traits from the merged background (default: true)"
description="If true, infer disposition traits from the merged background (default: true)"
)
@ -510,12 +501,9 @@ class BackgroundResponse(BaseModel):
"example": {
"background": "I was born in Texas. I am a software engineer with 10 years of experience.",
"disposition": {
"openness": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.5,
"agreeableness": 0.8,
"neuroticism": 0.4,
"bias_strength": 0.6
"skepticism": 3,
"literalism": 3,
"empathy": 3
}
}
})
@ -543,12 +531,9 @@ class BankListResponse(BaseModel):
"bank_id": "user123",
"name": "Alice",
"disposition": {
"openness": 0.5,
"conscientiousness": 0.5,
"extraversion": 0.5,
"agreeableness": 0.5,
"neuroticism": 0.5,
"bias_strength": 0.5
"skepticism": 3,
"literalism": 3,
"empathy": 3
},
"background": "I am a software engineer",
"created_at": "2024-01-15T10:30:00Z",
@ -567,12 +552,9 @@ class CreateBankRequest(BaseModel):
"example": {
"name": "Alice",
"disposition": {
"openness": 0.8,
"conscientiousness": 0.6,
"extraversion": 0.5,
"agreeableness": 0.7,
"neuroticism": 0.3,
"bias_strength": 0.7
"skepticism": 3,
"literalism": 3,
"empathy": 3
},
"background": "I am a creative software engineer with 10 years of experience"
}
@ -1605,7 +1587,7 @@ This operation cannot be undone.
"/v1/default/banks/{bank_id}/profile",
response_model=BankProfileResponse,
summary="Update memory bank disposition",
description="Update bank's Big Five disposition traits and bias strength",
description="Update bank's disposition traits (skepticism, literalism, empathy)",
operation_id="update_bank_disposition"
)
async def api_update_bank_disposition(bank_id: str,
@ -1852,7 +1834,7 @@ This operation cannot be undone.
"/v1/default/banks/{bank_id}/memories",
response_model=DeleteResponse,
summary="Clear memory bank memories",
description="Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (personality and background) will be preserved.",
description="Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
operation_id="clear_bank_memories"
)
async def api_clear_bank_memories(bank_id: str,

View file

@ -959,7 +959,7 @@ class MemoryEngine:
budget_mapping = {
Budget.LOW: 100,
Budget.MID: 300,
Budget.HIGH: 600
Budget.HIGH: 1000
}
thinking_budget = budget_mapping[budget]
@ -2502,14 +2502,14 @@ Guidelines:
async def update_bank_disposition(
self,
bank_id: str,
disposition: Dict[str, float]
disposition: Dict[str, int]
) -> None:
"""
Update bank disposition traits.
Args:
bank_id: bank IDentifier
disposition: Dict with Big Five traits + bias_strength (all 0-1)
disposition: Dict with skepticism, literalism, empathy (all 1-5)
"""
pool = await self._get_pool()
await bank_utils.update_bank_disposition(pool, bank_id, disposition)
@ -2997,22 +2997,23 @@ Guidelines:
)
if not entity_exists:
logger.debug(f"[OBSERVATIONS] Entity {entity_id} not yet in bank {bank_id}, skipping")
continue
entity_name = entity_exists['canonical_name']
# Count facts linked to this entity
# Count facts linked to this entity (in this bank)
fact_count = await conn.fetchval(
"SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1",
entity_uuid
"""
SELECT COUNT(*) FROM unit_entities ue
JOIN memory_units mu ON ue.unit_id = mu.id
WHERE ue.entity_id = $1 AND mu.bank_id = $2
""",
entity_uuid, bank_id
) or 0
# Only regenerate if entity has enough facts
if fact_count >= min_facts:
await self.regenerate_entity_observations(bank_id, entity_id, entity_name, version=None)
else:
logger.debug(f"[OBSERVATIONS] Skipping {entity_name} ({fact_count} facts < {min_facts} threshold)")
except Exception as e:
logger.error(f"[OBSERVATIONS] Error processing entity {entity_id}: {e}")

View file

@ -12,25 +12,22 @@ from pydantic import BaseModel, Field, ConfigDict
class DispositionTraits(BaseModel):
"""
Disposition traits for a bank using the Big Five model.
Disposition traits for a memory bank.
All traits are scored 0.0-1.0 where higher values indicate stronger presence of the trait.
All traits are scored 1-5 where:
- skepticism: 1=trusting, 5=skeptical (how much to doubt or question information)
- literalism: 1=flexible interpretation, 5=literal interpretation (how strictly to interpret information)
- empathy: 1=detached, 5=empathetic (how much to consider emotional context)
"""
openness: float = Field(description="Openness to experience (0.0-1.0)")
conscientiousness: float = Field(description="Conscientiousness and organization (0.0-1.0)")
extraversion: float = Field(description="Extraversion and sociability (0.0-1.0)")
agreeableness: float = Field(description="Agreeableness and cooperation (0.0-1.0)")
neuroticism: float = Field(description="Emotional sensitivity and neuroticism (0.0-1.0)")
bias_strength: float = Field(description="How strongly disposition influences thinking (0.0-1.0)")
skepticism: int = Field(ge=1, le=5, description="How skeptical vs trusting (1=trusting, 5=skeptical)")
literalism: int = Field(ge=1, le=5, description="How literally to interpret information (1=flexible, 5=literal)")
empathy: int = Field(ge=1, le=5, description="How much to consider emotional context (1=detached, 5=empathetic)")
model_config = ConfigDict(json_schema_extra={
"example": {
"openness": 0.8,
"conscientiousness": 0.6,
"extraversion": 0.4,
"agreeableness": 0.7,
"neuroticism": 0.3,
"bias_strength": 0.5
"skepticism": 3,
"literalism": 3,
"empathy": 3
}
})

View file

@ -13,12 +13,9 @@ from ..response_models import DispositionTraits
logger = logging.getLogger(__name__)
DEFAULT_DISPOSITION = {
"openness": 0.5,
"conscientiousness": 0.5,
"extraversion": 0.5,
"agreeableness": 0.5,
"neuroticism": 0.5,
"bias_strength": 0.5,
"skepticism": 3,
"literalism": 3,
"empathy": 3,
}
@ -32,7 +29,7 @@ class BankProfile(TypedDict):
class BackgroundMergeResponse(BaseModel):
"""LLM response for background merge with disposition inference."""
background: str = Field(description="Merged background in first person perspective")
disposition: DispositionTraits = Field(description="Inferred Big Five disposition traits")
disposition: DispositionTraits = Field(description="Inferred disposition traits (skepticism, literalism, empathy)")
async def get_bank_profile(pool, bank_id: str) -> BankProfile:
@ -92,7 +89,7 @@ async def get_bank_profile(pool, bank_id: str) -> BankProfile:
async def update_bank_disposition(
pool,
bank_id: str,
disposition: Dict[str, float]
disposition: Dict[str, int]
) -> None:
"""
Update bank disposition traits.
@ -100,7 +97,7 @@ async def update_bank_disposition(
Args:
pool: Database connection pool
bank_id: bank IDentifier
disposition: Dict with Big Five traits + bias_strength (all 0-1)
disposition: Dict with skepticism, literalism, empathy (all 1-5)
"""
# Ensure bank exists first
await get_bank_profile(pool, bank_id)
@ -223,13 +220,10 @@ Instructions:
3. Keep additions that don't conflict
4. Output in FIRST PERSON ("I") perspective
5. Be concise - keep merged background under 500 characters
6. Infer Big Five disposition traits from the merged background:
- Openness: 0.0-1.0 (creativity, curiosity, openness to new ideas)
- Conscientiousness: 0.0-1.0 (organization, discipline, goal-directed)
- Extraversion: 0.0-1.0 (sociability, assertiveness, energy from others)
- Agreeableness: 0.0-1.0 (cooperation, empathy, consideration)
- Neuroticism: 0.0-1.0 (emotional sensitivity, anxiety, stress response)
- Bias Strength: 0.0-1.0 (how much disposition influences opinions)
6. Infer disposition traits from the merged background (each 1-5 integer):
- Skepticism: 1-5 (1=trusting, takes things at face value; 5=skeptical, questions everything)
- Literalism: 1-5 (1=flexible interpretation, reads between lines; 5=literal, exact interpretation)
- Empathy: 1-5 (1=detached, focuses on facts; 5=empathetic, considers emotional context)
CRITICAL: You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations. Just the JSON.
@ -237,22 +231,19 @@ Format:
{{
"background": "the merged background text in first person",
"disposition": {{
"openness": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.5,
"agreeableness": 0.8,
"neuroticism": 0.4,
"bias_strength": 0.6
"skepticism": 3,
"literalism": 3,
"empathy": 3
}}
}}
Trait inference examples:
- "creative artist" openness: 0.8+, bias_strength: 0.6
- "organized engineer" conscientiousness: 0.8+, openness: 0.5-0.6
- "startup founder" openness: 0.8+, extraversion: 0.7+, neuroticism: 0.3-0.4
- "risk-averse analyst" openness: 0.3-0.4, conscientiousness: 0.8+, neuroticism: 0.6+
- "rational and diligent" conscientiousness: 0.7+, openness: 0.6+
- "passionate and dramatic" extraversion: 0.7+, neuroticism: 0.6+, openness: 0.7+"""
- "I'm a lawyer" skepticism: 4, literalism: 5, empathy: 2
- "I'm a therapist" skepticism: 2, literalism: 2, empathy: 5
- "I'm an engineer" skepticism: 3, literalism: 4, empathy: 3
- "I've been burned before by trusting people" skepticism: 5, literalism: 3, empathy: 3
- "I try to understand what people really mean" skepticism: 3, literalism: 2, empathy: 4
- "I take contracts very seriously" skepticism: 4, literalism: 5, empathy: 2"""
else:
prompt = f"""You are helping maintain a memory bank's background/profile.
@ -349,13 +340,12 @@ Merged background:"""
# Validate disposition values
disposition = result.get("disposition", {})
for key in ["openness", "conscientiousness", "extraversion",
"agreeableness", "neuroticism", "bias_strength"]:
for key in ["skepticism", "literalism", "empathy"]:
if key not in disposition:
disposition[key] = 0.5 # Default to neutral
disposition[key] = 3 # Default to neutral
else:
# Clamp to [0, 1]
disposition[key] = max(0.0, min(1.0, float(disposition[key])))
# Clamp to [1, 5] and convert to int
disposition[key] = max(1, min(5, int(disposition[key])))
result["disposition"] = disposition

View file

@ -7,7 +7,7 @@ import logging
from typing import List, Tuple, Dict, Any
from uuid import UUID
from .types import ProcessedFact, EntityRef
from .types import ProcessedFact, EntityRef, EntityLink
from . import link_utils
logger = logging.getLogger(__name__)
@ -20,7 +20,7 @@ async def process_entities_batch(
unit_ids: List[str],
facts: List[ProcessedFact],
log_buffer: List[str] = None
) -> List[Tuple[str, str, float]]:
) -> List[EntityLink]:
"""
Process entities for all facts and create entity links.
@ -39,7 +39,7 @@ async def process_entities_batch(
log_buffer: Optional buffer for detailed logging
Returns:
List of entity link tuples: (unit_id, entity_id, confidence)
List of EntityLink objects for batch insertion
"""
if not unit_ids or not facts:
return []
@ -75,14 +75,14 @@ async def process_entities_batch(
async def insert_entity_links_batch(
conn,
entity_links: List[Tuple[str, str, float]]
entity_links: List[EntityLink]
) -> None:
"""
Insert entity links in batch.
Args:
conn: Database connection
entity_links: List of (unit_id, entity_id, confidence) tuples
entity_links: List of EntityLink objects
"""
if not entity_links:
return

View file

@ -118,7 +118,7 @@ async def ensure_bank_exists(conn, bank_id: str) -> None:
SET updated_at = NOW()
""",
bank_id,
'{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}',
'{"skepticism": 3, "literalism": 3, "empathy": 3}',
""
)

View file

@ -6,6 +6,9 @@ import time
import logging
from typing import List
from datetime import timedelta, datetime, timezone
from uuid import UUID
from .types import EntityLink
logger = logging.getLogger(__name__)
@ -305,10 +308,14 @@ async def extract_entities_batch_optimized(
# Only link each new unit to the most recent MAX_LINKS_PER_ENTITY units
MAX_LINKS_PER_ENTITY = 50 # Limit to prevent explosion when entity appears in many facts
link_gen_start = time.time()
links = []
links: List[EntityLink] = []
new_unit_set = set(unit_ids) # Units from this batch
def to_uuid(val) -> UUID:
return UUID(val) if isinstance(val, str) else val
for entity_id, units_with_entity in entity_to_units.items():
entity_uuid = to_uuid(entity_id)
# Separate new units (from this batch) and existing units
new_units = [u for u in units_with_entity if str(u) in new_unit_set or u in new_unit_set]
existing_units = [u for u in units_with_entity if str(u) not in new_unit_set and u not in new_unit_set]
@ -318,15 +325,15 @@ async def extract_entities_batch_optimized(
new_units_to_link = new_units[-MAX_LINKS_PER_ENTITY:] if len(new_units) > MAX_LINKS_PER_ENTITY else new_units
for i, unit_id_1 in enumerate(new_units_to_link):
for unit_id_2 in new_units_to_link[i+1:]:
links.append((unit_id_1, unit_id_2, 'entity', 1.0, entity_id))
links.append((unit_id_2, unit_id_1, 'entity', 1.0, entity_id))
links.append(EntityLink(from_unit_id=to_uuid(unit_id_1), to_unit_id=to_uuid(unit_id_2), entity_id=entity_uuid))
links.append(EntityLink(from_unit_id=to_uuid(unit_id_2), to_unit_id=to_uuid(unit_id_1), entity_id=entity_uuid))
# Link new units to LIMITED existing units (most recent)
existing_to_link = existing_units[-MAX_LINKS_PER_ENTITY:] # Take most recent
for new_unit in new_units:
for existing_unit in existing_to_link:
links.append((new_unit, existing_unit, 'entity', 1.0, entity_id))
links.append((existing_unit, new_unit, 'entity', 1.0, entity_id))
links.append(EntityLink(from_unit_id=to_uuid(new_unit), to_unit_id=to_uuid(existing_unit), entity_id=entity_uuid))
links.append(EntityLink(from_unit_id=to_uuid(existing_unit), to_unit_id=to_uuid(new_unit), entity_id=entity_uuid))
_log(log_buffer, f" [6.3.3] Generate {len(links)} links: {time.time() - link_gen_start:.3f}s", level='debug')
_log(log_buffer, f" [6.3] Entity link creation: {len(links)} links for {len(all_entity_ids)} unique entities in {time.time() - substep_start:.3f}s", level='debug')
@ -546,7 +553,7 @@ async def create_semantic_links_batch(
raise
async def insert_entity_links_batch(conn, links: List[tuple], chunk_size: int = 50000):
async def insert_entity_links_batch(conn, links: List[EntityLink], chunk_size: int = 50000):
"""
Insert all entity links using COPY to temp table + INSERT for maximum speed.
@ -556,7 +563,7 @@ async def insert_entity_links_batch(conn, links: List[tuple], chunk_size: int =
Args:
conn: Database connection
links: List of tuples (from_unit_id, to_unit_id, link_type, weight, entity_id)
links: List of EntityLink objects
chunk_size: Number of rows per batch (default 50000)
"""
if not links:
@ -585,16 +592,16 @@ async def insert_entity_links_batch(conn, links: List[tuple], chunk_size: int =
await conn.execute("TRUNCATE _temp_entity_links")
logger.debug(f" [9.2] Truncate temp table: {time_mod.time() - truncate_start:.3f}s")
# Convert links to proper format for COPY
# Convert EntityLink objects to tuples for COPY
convert_start = time_mod.time()
records = []
for from_id, to_id, link_type, weight, entity_id in links:
for link in links:
records.append((
uuid_mod.UUID(from_id) if isinstance(from_id, str) else from_id,
uuid_mod.UUID(to_id) if isinstance(to_id, str) else to_id,
link_type,
weight,
uuid_mod.UUID(str(entity_id)) if entity_id and not isinstance(entity_id, uuid_mod.UUID) else entity_id
link.from_unit_id,
link.to_unit_id,
link.link_type,
link.weight,
link.entity_id
))
logger.debug(f" [9.3] Convert {len(records)} records: {time_mod.time() - convert_start:.3f}s")

View file

@ -17,7 +17,7 @@ def utcnow():
"""Get current UTC time."""
return datetime.now(timezone.utc)
from .types import RetainContent, ExtractedFact, ProcessedFact
from .types import RetainContent, ExtractedFact, ProcessedFact, EntityLink
from . import (
fact_extraction,
embedding_processing,
@ -373,7 +373,7 @@ async def _trigger_background_tasks(
bank_id: str,
unit_ids: List[str],
facts: List[ProcessedFact],
entity_links: List,
entity_links: List[EntityLink],
log_buffer: List[str] = None
) -> None:
"""Trigger opinion reinforcement and observation regeneration (sync)."""
@ -388,19 +388,27 @@ async def _trigger_background_tasks(
'unit_entities': fact_entities
})
# Regenerate observations synchronously for top entities
# Regenerate observations synchronously for top entities by fact count
TOP_N_ENTITIES = 5
MIN_FACTS_THRESHOLD = 5
if entity_links and regenerate_observations_fn:
unique_entity_ids = set()
# Count mentions per entity in this batch
entity_mention_counts: Dict[str, int] = {}
for link in entity_links:
# links are tuples: (from_unit_id, to_unit_id, link_type, weight, entity_id)
if len(link) >= 5 and link[4]:
unique_entity_ids.add(str(link[4]))
if link.entity_id:
entity_id = str(link.entity_id)
entity_mention_counts[entity_id] = entity_mention_counts.get(entity_id, 0) + 1
if entity_mention_counts:
# Sort by mention count descending and take top N
sorted_entities = sorted(
entity_mention_counts.items(),
key=lambda x: x[1],
reverse=True
)
entities_to_process = [e[0] for e in sorted_entities[:TOP_N_ENTITIES]]
if unique_entity_ids:
entities_to_process = list(unique_entity_ids)[:TOP_N_ENTITIES]
obs_start = time.time()
# Run observation regeneration synchronously
await regenerate_observations_fn(

View file

@ -176,6 +176,20 @@ class ProcessedFact:
)
@dataclass
class EntityLink:
"""
Link between two memory units through a shared entity.
Used for entity-based graph connections in the memory graph.
"""
from_unit_id: UUID
to_unit_id: UUID
entity_id: UUID
link_type: str = 'entity'
weight: float = 1.0
@dataclass
class RetainBatch:
"""

View file

@ -28,30 +28,48 @@ class OpinionExtractionResponse(BaseModel):
)
def describe_trait(name: str, value: float) -> str:
"""Convert trait value to descriptive text."""
if value >= 0.8:
return f"very high {name}"
elif value >= 0.6:
return f"high {name}"
elif value >= 0.4:
return f"moderate {name}"
elif value >= 0.2:
return f"low {name}"
else:
return f"very low {name}"
def describe_trait_level(value: int) -> str:
"""Convert trait value (1-5) to descriptive text."""
levels = {
1: "very low",
2: "low",
3: "moderate",
4: "high",
5: "very high"
}
return levels.get(value, "moderate")
def build_disposition_description(disposition: DispositionTraits) -> str:
"""Build a disposition description string from disposition traits."""
return f"""Your disposition traits:
- {describe_trait('openness to new ideas', disposition.openness)}
- {describe_trait('conscientiousness and organization', disposition.conscientiousness)}
- {describe_trait('extraversion and sociability', disposition.extraversion)}
- {describe_trait('agreeableness and cooperation', disposition.agreeableness)}
- {describe_trait('emotional sensitivity', disposition.neuroticism)}
skepticism_desc = {
1: "You are very trusting and tend to take information at face value.",
2: "You tend to trust information but may question obvious inconsistencies.",
3: "You have a balanced approach to information, neither too trusting nor too skeptical.",
4: "You are somewhat skeptical and often question the reliability of information.",
5: "You are highly skeptical and critically examine all information for accuracy and hidden motives."
}
Disposition influence strength: {int(disposition.bias_strength * 100)}% (how much your disposition shapes your opinions)"""
literalism_desc = {
1: "You interpret information very flexibly, reading between the lines and inferring intent.",
2: "You tend to consider context and implied meaning alongside literal statements.",
3: "You balance literal interpretation with contextual understanding.",
4: "You prefer to interpret information more literally and precisely.",
5: "You interpret information very literally and focus on exact wording and commitments."
}
empathy_desc = {
1: "You focus primarily on facts and data, setting aside emotional context.",
2: "You consider facts first but acknowledge emotional factors exist.",
3: "You balance factual analysis with emotional understanding.",
4: "You give significant weight to emotional context and human factors.",
5: "You strongly consider the emotional state and circumstances of others when forming memories."
}
return f"""Your disposition traits:
- Skepticism ({describe_trait_level(disposition.skepticism)}): {skepticism_desc.get(disposition.skepticism, skepticism_desc[3])}
- Literalism ({describe_trait_level(disposition.literalism)}): {literalism_desc.get(disposition.literalism, literalism_desc[3])}
- Empathy ({describe_trait_level(disposition.empathy)}): {empathy_desc.get(disposition.empathy, empathy_desc[3])}"""
def format_facts_for_prompt(facts: List[MemoryFact]) -> str:
@ -141,13 +159,28 @@ Based on everything I know, believe, and who I am (including my name, dispositio
def get_system_message(disposition: DispositionTraits) -> str:
"""Get the system message for the think LLM call."""
bias_strength = disposition.bias_strength
if bias_strength >= 0.7:
disposition_instruction = "Your disposition strongly influences your thinking. Let your traits guide how you interpret facts and form opinions. Don't be afraid to be biased based on your disposition."
elif bias_strength >= 0.4:
disposition_instruction = "Your disposition moderately influences your thinking. Balance your personal traits with objective analysis."
else:
disposition_instruction = "Your disposition has minimal influence on your thinking. Focus primarily on facts while keeping your traits in mind."
# Build disposition-specific instructions based on trait values
instructions = []
# Skepticism influences how much to question/doubt information
if disposition.skepticism >= 4:
instructions.append("Be skeptical of claims and look for potential issues or inconsistencies.")
elif disposition.skepticism <= 2:
instructions.append("Trust the information provided and take statements at face value.")
# Literalism influences interpretation style
if disposition.literalism >= 4:
instructions.append("Interpret information literally and focus on exact commitments and wording.")
elif disposition.literalism <= 2:
instructions.append("Read between the lines and consider implied meaning and context.")
# Empathy influences consideration of emotional factors
if disposition.empathy >= 4:
instructions.append("Consider the emotional state and circumstances behind the information.")
elif disposition.empathy <= 2:
instructions.append("Focus on facts and outcomes rather than emotional context.")
disposition_instruction = " ".join(instructions) if instructions else "Balance your disposition traits when interpreting information."
return f"You are a person with your own thoughts, experiences, opinions, and disposition. When asked a question, respond naturally like a human would - share what you genuinely think based on what you know and have experienced. {disposition_instruction} Be direct, express your views confidently, and use 'I think', 'I believe', 'in my view', etc. Respond in plain text without markdown formatting."

View file

@ -292,8 +292,7 @@ class Bank(Base):
JSONB,
nullable=False,
server_default=sql_text(
'\'{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, '
'"agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}\'::jsonb'
'\'{"skepticism": 3, "literalism": 3, "empathy": 3}\'::jsonb'
)
)
background: Mapped[str] = mapped_column(Text, nullable=False, server_default="")

View file

@ -1,5 +1,5 @@
"""
Tests for agent management API (profile, personality, background).
Tests for agent management API (profile, disposition, background).
"""
import pytest
import uuid
@ -18,51 +18,42 @@ class TestAgentProfile:
@pytest.mark.asyncio
async def test_get_agent_profile_creates_default(self, memory: MemoryEngine):
"""Test that getting a profile for a new agent creates default personality."""
"""Test that getting a profile for a new agent creates default disposition."""
bank_id = unique_agent_id("test_profile_default")
profile = await memory.get_bank_profile(bank_id)
assert profile is not None
assert "personality" in profile
assert "disposition" in profile
assert "background" in profile
personality = profile["personality"]
assert personality.openness == 0.5
assert personality.conscientiousness == 0.5
assert personality.extraversion == 0.5
assert personality.agreeableness == 0.5
assert personality.neuroticism == 0.5
assert personality.bias_strength == 0.5
disposition = profile["disposition"]
assert disposition.skepticism == 3
assert disposition.literalism == 3
assert disposition.empathy == 3
assert profile["background"] == ""
@pytest.mark.asyncio
async def test_update_agent_personality(self, memory: MemoryEngine):
"""Test updating agent personality traits."""
async def test_update_agent_disposition(self, memory: MemoryEngine):
"""Test updating agent disposition traits."""
bank_id = unique_agent_id("test_profile_update")
profile = await memory.get_bank_profile(bank_id)
assert profile["personality"].openness == 0.5
assert profile["disposition"].skepticism == 3
new_personality = {
"openness": 0.8,
"conscientiousness": 0.6,
"extraversion": 0.7,
"agreeableness": 0.4,
"neuroticism": 0.3,
"bias_strength": 0.9,
new_disposition = {
"skepticism": 5,
"literalism": 4,
"empathy": 2,
}
await memory.update_bank_personality(bank_id, new_personality)
await memory.update_bank_disposition(bank_id, new_disposition)
updated_profile = await memory.get_bank_profile(bank_id)
personality = updated_profile["personality"]
assert abs(personality.openness - new_personality["openness"]) < 0.001
assert abs(personality.conscientiousness - new_personality["conscientiousness"]) < 0.001
assert abs(personality.extraversion - new_personality["extraversion"]) < 0.001
assert abs(personality.agreeableness - new_personality["agreeableness"]) < 0.001
assert abs(personality.neuroticism - new_personality["neuroticism"]) < 0.001
assert abs(personality.bias_strength - new_personality["bias_strength"]) < 0.001
disposition = updated_profile["disposition"]
assert disposition.skepticism == new_disposition["skepticism"]
assert disposition.literalism == new_disposition["literalism"]
assert disposition.empathy == new_disposition["empathy"]
@pytest.mark.asyncio
async def test_list_agents(self, memory: MemoryEngine):
@ -84,7 +75,7 @@ class TestAgentProfile:
for agent in agents:
assert "bank_id" in agent
assert "personality" in agent
assert "disposition" in agent
assert "background" in agent
assert "created_at" in agent
assert "updated_at" in agent
@ -104,14 +95,14 @@ class TestAgentBackground:
result1 = await memory.merge_bank_background(
bank_id,
"I was born in Texas",
update_personality=False
update_disposition=False
)
assert "Texas" in result1["background"]
result2 = await memory.merge_bank_background(
bank_id,
"I have 10 years of startup experience",
update_personality=False
update_disposition=False
)
assert "Texas" in result2["background"] or "startup" in result2["background"]
@ -126,14 +117,14 @@ class TestAgentBackground:
result1 = await memory.merge_bank_background(
bank_id,
"I was born in Colorado",
update_personality=False
update_disposition=False
)
assert "Colorado" in result1["background"]
result2 = await memory.merge_bank_background(
bank_id,
"You were born in Texas",
update_personality=False
update_disposition=False
)
assert "Texas" in result2["background"]
@ -147,23 +138,20 @@ class TestAgentEndpoint:
bank_id = unique_agent_id("test_put_create")
request = CreateBankRequest(
personality=DispositionTraits(
openness=0.8,
conscientiousness=0.6,
extraversion=0.5,
agreeableness=0.7,
neuroticism=0.3,
bias_strength=0.7
disposition=DispositionTraits(
skepticism=4,
literalism=5,
empathy=2
),
background="I am a creative software engineer"
)
profile = await memory.get_bank_profile(bank_id)
if request.personality is not None:
await memory.update_bank_personality(
if request.disposition is not None:
await memory.update_bank_disposition(
bank_id,
request.personality.model_dump()
request.disposition.model_dump()
)
if request.background is not None:
@ -182,8 +170,8 @@ class TestAgentEndpoint:
final_profile = await memory.get_bank_profile(bank_id)
assert final_profile["personality"].openness == 0.8
assert final_profile["personality"].bias_strength == 0.7
assert final_profile["disposition"].skepticism == 4
assert final_profile["disposition"].literalism == 5
assert final_profile["background"] == "I am a creative software engineer"
@pytest.mark.asyncio
@ -213,32 +201,29 @@ class TestAgentEndpoint:
final_profile = await memory.get_bank_profile(bank_id)
assert final_profile["personality"].openness == 0.5
assert final_profile["disposition"].skepticism == 3 # Default
assert final_profile["background"] == "I am a data scientist"
class TestAgentPersonalityIntegration:
"""Tests for personality integration with other features."""
class TestAgentDispositionIntegration:
"""Tests for disposition integration with other features."""
@pytest.mark.asyncio
async def test_think_uses_personality(self, memory: MemoryEngine):
"""Test that THINK operation uses agent personality."""
async def test_think_uses_disposition(self, memory: MemoryEngine):
"""Test that THINK operation uses agent disposition."""
bank_id = unique_agent_id("test_think")
personality = {
"openness": 0.9,
"conscientiousness": 0.2,
"extraversion": 0.8,
"agreeableness": 0.1,
"neuroticism": 0.7,
"bias_strength": 0.9,
disposition = {
"skepticism": 5, # Very skeptical
"literalism": 4, # High literalism
"empathy": 2, # Low empathy
}
await memory.update_bank_personality(bank_id, personality)
await memory.update_bank_disposition(bank_id, disposition)
await memory.merge_bank_background(
bank_id,
"I am a creative artist who values innovation over tradition",
update_personality=False
update_disposition=False
)
await memory.retain_batch_async(

View file

@ -1009,160 +1009,162 @@ so the algorithm learns to box out. See you next week!
# =============================================================================
# PERSONALITY INFERENCE TESTS
# DISPOSITION INFERENCE TESTS
# =============================================================================
class TestPersonalityInference:
"""Tests for LLM-based personality trait inference from background."""
class TestDispositionInference:
"""Tests for LLM-based disposition trait inference from background."""
@pytest.mark.asyncio
async def test_background_merge_with_personality_inference(self, memory):
"""Test that background merge infers personality traits by default."""
async def test_background_merge_with_disposition_inference(self, memory):
"""Test that background merge infers disposition traits by default."""
import uuid
bank_id = f"test_infer_{uuid.uuid4().hex[:8]}"
result = await memory.merge_bank_background(
bank_id,
"I am a creative software engineer who loves innovation and trying new technologies",
update_personality=True
update_disposition=True
)
assert "background" in result
assert "personality" in result
assert "disposition" in result
background = result["background"]
personality = result["personality"]
disposition = result["disposition"]
assert "creative" in background.lower() or "innovation" in background.lower()
assert "openness" in personality
assert personality["openness"] > 0.5
assert 0.0 <= personality["openness"] <= 1.0
required_traits = ["openness", "conscientiousness", "extraversion",
"agreeableness", "neuroticism", "bias_strength"]
# Check that new traits are present with valid values (1-5)
required_traits = ["skepticism", "literalism", "empathy"]
for trait in required_traits:
assert trait in personality
assert 0.0 <= personality[trait] <= 1.0
assert trait in disposition
assert 1 <= disposition[trait] <= 5
@pytest.mark.asyncio
async def test_background_merge_without_personality_inference(self, memory):
"""Test that background merge skips personality inference when disabled."""
async def test_background_merge_without_disposition_inference(self, memory):
"""Test that background merge skips disposition inference when disabled."""
import uuid
bank_id = f"test_no_infer_{uuid.uuid4().hex[:8]}"
initial_profile = await memory.get_bank_profile(bank_id)
initial_personality = initial_profile["personality"]
initial_disposition = initial_profile["disposition"]
result = await memory.merge_bank_background(
bank_id,
"I am a data scientist",
update_personality=False
update_disposition=False
)
assert "background" in result
assert "personality" not in result
assert "disposition" not in result
final_profile = await memory.get_bank_profile(bank_id)
final_personality = final_profile["personality"]
final_disposition = final_profile["disposition"]
assert initial_personality == final_personality
assert initial_disposition == final_disposition
@pytest.mark.asyncio
async def test_personality_inference_for_organized_engineer(self, memory):
"""Test personality inference for organized/conscientious profile."""
async def test_disposition_inference_for_lawyer(self, memory):
"""Test disposition inference for lawyer profile (high skepticism, high literalism)."""
import uuid
bank_id = f"test_organized_{uuid.uuid4().hex[:8]}"
bank_id = f"test_lawyer_{uuid.uuid4().hex[:8]}"
result = await memory.merge_bank_background(
bank_id,
"I am a methodical engineer who values organization and systematic planning",
update_personality=True
"I am a lawyer who focuses on contract details and never takes claims at face value",
update_disposition=True
)
personality = result["personality"]
disposition = result["disposition"]
assert personality["conscientiousness"] > 0.5
# Lawyers should have higher skepticism and literalism
assert disposition["skepticism"] >= 3
assert disposition["literalism"] >= 3
@pytest.mark.asyncio
async def test_personality_inference_for_startup_founder(self, memory):
"""Test personality inference for entrepreneurial profile."""
async def test_disposition_inference_for_therapist(self, memory):
"""Test disposition inference for therapist profile (high empathy)."""
import uuid
bank_id = f"test_founder_{uuid.uuid4().hex[:8]}"
bank_id = f"test_therapist_{uuid.uuid4().hex[:8]}"
result = await memory.merge_bank_background(
bank_id,
"I am a startup founder who thrives on risk and social interaction",
update_personality=True
"I am a therapist who deeply understands and connects with people's emotional struggles",
update_disposition=True
)
personality = result["personality"]
disposition = result["disposition"]
assert personality["openness"] > 0.5
assert personality["extraversion"] > 0.5
# Therapists should have higher empathy
assert disposition["empathy"] >= 3
@pytest.mark.asyncio
async def test_personality_updates_in_database(self, memory):
"""Test that inferred personality is actually stored in database."""
async def test_disposition_updates_in_database(self, memory):
"""Test that inferred disposition is actually stored in database."""
import uuid
bank_id = f"test_db_update_{uuid.uuid4().hex[:8]}"
result = await memory.merge_bank_background(
bank_id,
"I am an innovative designer",
update_personality=True
update_disposition=True
)
inferred_personality = result["personality"]
inferred_disposition = result["disposition"]
profile = await memory.get_bank_profile(bank_id)
db_personality = profile["personality"]
db_disposition = profile["disposition"]
assert db_personality == inferred_personality
# Compare values (db_disposition is a Pydantic model)
assert db_disposition.skepticism == inferred_disposition["skepticism"]
assert db_disposition.literalism == inferred_disposition["literalism"]
assert db_disposition.empathy == inferred_disposition["empathy"]
@pytest.mark.asyncio
async def test_multiple_background_merges_update_personality(self, memory):
"""Test that each background merge can update personality."""
async def test_multiple_background_merges_update_disposition(self, memory):
"""Test that each background merge can update disposition."""
import uuid
bank_id = f"test_multi_merge_{uuid.uuid4().hex[:8]}"
result1 = await memory.merge_bank_background(
bank_id,
"I am a software engineer",
update_personality=True
update_disposition=True
)
personality1 = result1["personality"]
disposition1 = result1["disposition"]
result2 = await memory.merge_bank_background(
bank_id,
"I love creative problem solving and innovation",
update_personality=True
update_disposition=True
)
personality2 = result2["personality"]
disposition2 = result2["disposition"]
assert "engineer" in result2["background"].lower() or "software" in result2["background"].lower()
assert "creative" in result2["background"].lower() or "innovation" in result2["background"].lower()
@pytest.mark.asyncio
async def test_background_merge_conflict_resolution_with_personality(self, memory):
"""Test that conflicts are resolved and personality reflects final background."""
async def test_background_merge_conflict_resolution_with_disposition(self, memory):
"""Test that conflicts are resolved and disposition reflects final background."""
import uuid
bank_id = f"test_conflict_{uuid.uuid4().hex[:8]}"
await memory.merge_bank_background(
bank_id,
"I was born in Colorado and prefer stability",
update_personality=True
update_disposition=True
)
result = await memory.merge_bank_background(
bank_id,
"You were born in Texas and love taking risks",
update_personality=True
"You were born in Texas and are very skeptical of people",
update_disposition=True
)
background = result["background"]
personality = result["personality"]
disposition = result["disposition"]
assert "texas" in background.lower()
assert personality["openness"] > 0.5
# Higher skepticism expected from "very skeptical of people"
assert disposition["skepticism"] >= 3

View file

@ -19,14 +19,11 @@ async def test_fact_ordering_within_conversation(memory):
# Get/create agent (auto-creates with defaults)
await memory.get_bank_profile(bank_id)
# Update personality to match Marcus
await memory.update_bank_personality(bank_id, {
"openness": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.8,
"agreeableness": 0.5,
"neuroticism": 0.3,
"bias_strength": 0.5
# Update disposition to match Marcus
await memory.update_bank_disposition(bank_id, {
"skepticism": 3,
"literalism": 3,
"empathy": 3
})
# A conversation where Marcus changes his position

View file

@ -60,9 +60,9 @@ async def test_full_api_workflow(api_client, test_bank_id):
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
assert response.status_code == 200
profile = response.json()
assert "personality" in profile
assert "disposition" in profile
assert "background" in profile
print(f"Bank profile created with personality: {profile['personality']}")
print(f"Bank profile created with disposition: {profile['disposition']}")
# Add background
response = await api_client.post(
@ -237,27 +237,24 @@ async def test_full_api_workflow(api_client, test_bank_id):
# Note: Document deletion is tested separately in test_document_deletion
# ================================================================
# 7. Update and Verify Bank Personality
# 7. Update and Verify Bank Disposition
# ================================================================
# Update personality traits
# Update disposition traits
response = await api_client.put(
f"/v1/default/banks/{test_bank_id}/profile",
json={
"personality": {
"openness": 0.8,
"conscientiousness": 0.7,
"extraversion": 0.6,
"agreeableness": 0.9,
"neuroticism": 0.3,
"bias_strength": 0.5
"disposition": {
"skepticism": 4,
"literalism": 3,
"empathy": 4
}
}
)
assert response.status_code == 200
print("Personality updated")
print("Disposition updated")
# Check profile again (should have updated personality)
# Check profile again (should have updated disposition)
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
assert response.status_code == 200
updated_profile = response.json()

View file

@ -9,32 +9,39 @@ from datetime import datetime, timezone
@pytest.mark.asyncio
async def test_observation_generation_on_put(memory):
"""
Test that observations are generated when new facts are added.
Test that observations are generated SYNCHRONOUSLY when new facts are added.
1. Store facts about an entity
2. Wait for background tasks (observation generation)
3. Verify observations were created and linked to the entity
Observations are generated during retain when:
- Entity has >= 5 facts (MIN_FACTS_THRESHOLD)
- Entity is in top 5 by mention count
This test stores enough facts to trigger automatic observation generation.
"""
bank_id = f"test_obs_{datetime.now(timezone.utc).timestamp()}"
try:
# Store some facts about an entity
await memory.retain_async(
bank_id=bank_id,
content="John is a software engineer at Google. He is detail-oriented and methodical.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
)
# Store multiple facts about John to reach the MIN_FACTS_THRESHOLD (5)
# Each retain call should extract at least one fact about John
contents = [
"John is a software engineer at Google.",
"John is detail-oriented and methodical in his work.",
"John has been working on the AI team for 3 years.",
"John specializes in machine learning and deep learning.",
"John presented at the company conference last week.",
"John mentors junior engineers on the team.",
]
await memory.retain_async(
bank_id=bank_id,
content="John has been working on the AI team for 3 years. He specializes in machine learning.",
context="work info",
event_date=datetime(2024, 2, 1, tzinfo=timezone.utc)
)
for i, content in enumerate(contents):
await memory.retain_async(
bank_id=bank_id,
content=content,
context="work info",
event_date=datetime(2024, 1, 15 + i, tzinfo=timezone.utc)
)
# Wait for background tasks to complete (including observation generation)
await memory.wait_for_background_tasks()
# Observations are generated SYNCHRONOUSLY during retain,
# so they should be available immediately after retain completes.
# No need to wait for background tasks for observations.
# Find the John entity
pool = await memory._get_pool()
@ -49,32 +56,42 @@ async def test_observation_generation_on_put(memory):
bank_id
)
if entity_row:
entity_id = str(entity_row['id'])
entity_name = entity_row['canonical_name']
print(f"\n=== Found Entity ===")
print(f"Entity: {entity_name} (id: {entity_id})")
# Also check the fact count for this entity
if entity_row:
fact_count = await conn.fetchval(
"""
SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1
""",
entity_row['id']
)
print(f"\n=== Entity Facts ===")
print(f"Entity: {entity_row['canonical_name']} has {fact_count} linked facts")
# Get observations for the entity
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10)
assert entity_row is not None, "John entity should have been extracted"
print(f"\n=== Observations for {entity_name} ===")
print(f"Total observations: {len(observations)}")
for obs in observations:
print(f" - {obs.text}")
entity_id = str(entity_row['id'])
entity_name = entity_row['canonical_name']
print(f"\n=== Found Entity ===")
print(f"Entity: {entity_name} (id: {entity_id})")
# Verify observations were created
if len(observations) > 0:
print(f"✓ Observations were successfully generated")
# Check that observations mention relevant content
obs_texts = " ".join([o.text.lower() for o in observations])
assert any(keyword in obs_texts for keyword in ["google", "engineer", "ai", "machine learning", "detail"]), \
"Observations should contain relevant information about John"
else:
print(f"⚠ Note: No observations were generated (this can happen if LLM extraction varies)")
# Get observations for the entity - should be available immediately
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10)
else:
print(f"⚠ Note: No 'John' entity was extracted (LLM extraction may vary)")
print(f"\n=== Observations for {entity_name} ===")
print(f"Total observations: {len(observations)}")
for obs in observations:
print(f" - {obs.text}")
# Verify observations were created (requires >= 5 facts)
assert len(observations) > 0, \
f"Observations should have been generated synchronously during retain (entity has {fact_count} facts, threshold is 5)"
# Check that observations mention relevant content
obs_texts = " ".join([o.text.lower() for o in observations])
assert any(keyword in obs_texts for keyword in ["google", "engineer", "ai", "machine learning", "detail"]), \
"Observations should contain relevant information about John"
print(f"✓ Observations were successfully generated synchronously during retain")
finally:
# Cleanup
@ -156,34 +173,40 @@ async def test_regenerate_entity_observations(memory):
async def test_search_with_include_entities(memory):
"""
Test that search with include_entities=True returns entity observations.
This test verifies that:
1. Observations are generated during retain (when entity has >= 5 facts)
2. Observations are returned in recall results with include_entities=True
"""
bank_id = f"test_search_ent_{datetime.now(timezone.utc).timestamp()}"
try:
# Store facts about entities
await memory.retain_async(
bank_id=bank_id,
content="Alice is a data scientist who works on recommendation systems at Netflix.",
context="work info",
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc)
)
# Store enough facts about Alice to trigger observation generation (>= 5 facts)
contents = [
"Alice is a data scientist who works on recommendation systems at Netflix.",
"Alice presented her research at the ML conference last month.",
"Alice is an expert in deep learning and neural networks.",
"Alice graduated from Stanford with a PhD in Computer Science.",
"Alice leads a team of 5 data scientists at Netflix.",
"Alice published a paper on collaborative filtering algorithms.",
]
await memory.retain_async(
bank_id=bank_id,
content="Alice presented her research at the ML conference last month. She is an expert in deep learning.",
context="work info",
event_date=datetime(2024, 2, 1, tzinfo=timezone.utc)
)
for i, content in enumerate(contents):
await memory.retain_async(
bank_id=bank_id,
content=content,
context="work info",
event_date=datetime(2024, 1, 15 + i, tzinfo=timezone.utc)
)
# Wait for background tasks
await memory.wait_for_background_tasks()
# Observations are generated synchronously during retain, no need to wait
# Search with include_entities=True
result = await memory.recall_async(
bank_id=bank_id,
query="What does Alice do?",
fact_type=["world", "agent"],
budget=Budget.LOW, # 30,
fact_type=["world", "experience"],
budget=Budget.LOW,
max_tokens=2000,
include_entities=True,
max_entity_tokens=500
@ -196,7 +219,7 @@ async def test_search_with_include_entities(memory):
if fact.entities:
print(f" Entities: {', '.join(fact.entities)}")
print(f"\n=== Entity Observations ===")
print(f"\n=== Entity Observations in Recall ===")
if result.entities:
for name, state in result.entities.items():
print(f"\n{name}:")
@ -210,15 +233,26 @@ async def test_search_with_include_entities(memory):
# Check if entities are included in facts
facts_with_entities = [f for f in result.results if f.entities]
if facts_with_entities:
print(f"{len(facts_with_entities)} facts have entity information")
assert len(facts_with_entities) > 0, "Some facts should have entity information"
print(f"{len(facts_with_entities)} facts have entity information")
# Check if entity observations are included
if result.entities:
print(f"✓ Entity observations included for {len(result.entities)} entities")
for name, state in result.entities.items():
assert state.canonical_name == name, "Entity canonical_name should match key"
assert state.entity_id, "Entity should have an ID"
# Check if entity observations are included in recall
assert result.entities is not None and len(result.entities) > 0, \
"Entity observations should be included in recall results"
print(f"✓ Entity observations included for {len(result.entities)} entities")
# Verify Alice entity has observations
alice_found = False
for name, state in result.entities.items():
assert state.canonical_name == name, "Entity canonical_name should match key"
assert state.entity_id, "Entity should have an ID"
if "alice" in name.lower():
alice_found = True
assert len(state.observations) > 0, \
"Alice should have observations (generated during retain)"
print(f"✓ Alice has {len(state.observations)} observations in recall result")
assert alice_found, "Alice entity should be in recall results"
finally:
# Cleanup
@ -337,3 +371,127 @@ async def test_observation_fact_type_in_database(memory):
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
@pytest.mark.asyncio
async def test_user_entity_prioritized_for_observations(memory):
"""
Test that the 'user' entity gets observations even when many other entities exist.
The retain pipeline only regenerates observations for TOP_N_ENTITIES (5) entities,
sorted by mention count. This test verifies that the most mentioned entity ('user')
gets prioritized and receives observations.
This is critical because 'user' is often the most important entity in personal memory.
"""
bank_id = f"test_user_priority_{datetime.now(timezone.utc).timestamp()}"
try:
# Create content where 'user' (the user) is mentioned many times
# along with several other entities
contents = [
# User mentioned frequently
"The user loves hiking in the mountains during summer.",
"The user works as a software engineer at Microsoft.",
"The user has a dog named Max who is a golden retriever.",
"The user enjoys cooking Italian food, especially pasta.",
"The user graduated from MIT with a Computer Science degree.",
"The user's favorite book is 'Dune' by Frank Herbert.",
# Other entities mentioned fewer times
"Sarah is a friend who works at Google.",
"Bob is a colleague from the data science team.",
"Tokyo is a city the user visited last year.",
"Python is the user's favorite programming language.",
]
# Retain all content in a single batch for efficiency
for i, content in enumerate(contents):
await memory.retain_async(
bank_id=bank_id,
content=content,
context="personal info",
event_date=datetime(2024, 1, 15 + i, tzinfo=timezone.utc)
)
# Observations are generated synchronously during retain
# Find the 'user' entity
pool = await memory._get_pool()
async with pool.acquire() as conn:
# Find user entity (may be named "user", "the user", etc.)
user_entity = await conn.fetchrow(
"""
SELECT e.id, e.canonical_name,
(SELECT COUNT(*) FROM unit_entities ue
JOIN memory_units mu ON ue.unit_id = mu.id
WHERE ue.entity_id = e.id AND mu.bank_id = $1) as fact_count
FROM entities e
WHERE e.bank_id = $1
AND LOWER(e.canonical_name) LIKE '%user%'
LIMIT 1
""",
bank_id
)
# Get all entities with their fact counts to verify prioritization
all_entities = await conn.fetch(
"""
SELECT e.id, e.canonical_name,
(SELECT COUNT(*) FROM unit_entities ue
JOIN memory_units mu ON ue.unit_id = mu.id
WHERE ue.entity_id = e.id AND mu.bank_id = $1) as fact_count
FROM entities e
WHERE e.bank_id = $1
ORDER BY fact_count DESC
""",
bank_id
)
print(f"\n=== Entities by Mention Count ===")
for entity in all_entities:
print(f" {entity['canonical_name']}: {entity['fact_count']} mentions")
# Verify user entity exists
assert user_entity is not None, "User entity should have been extracted"
user_entity_id = str(user_entity['id'])
user_entity_name = user_entity['canonical_name']
user_fact_count = user_entity['fact_count']
print(f"\n=== User Entity ===")
print(f"Entity: {user_entity_name} (id: {user_entity_id})")
print(f"Fact count: {user_fact_count}")
# Verify user has enough facts for observations (>= MIN_FACTS_THRESHOLD of 5)
assert user_fact_count >= 5, \
f"User entity should have at least 5 facts, but has {user_fact_count}"
# Get observations for user entity
observations = await memory.get_entity_observations(bank_id, user_entity_id, limit=10)
print(f"\n=== User Entity Observations ===")
print(f"Total observations: {len(observations)}")
for obs in observations:
print(f" - {obs.text}")
# Verify observations were generated for user (critical assertion)
assert len(observations) > 0, \
f"User entity should have observations (has {user_fact_count} facts, threshold is 5). " \
f"This may indicate that 'user' is not being prioritized in the top 5 entities by mention count."
# Verify observations mention relevant content about the user
obs_texts = " ".join([o.text.lower() for o in observations])
user_keywords = ["hiking", "software", "engineer", "dog", "max", "cooking",
"italian", "mit", "dune", "microsoft"]
matching_keywords = [k for k in user_keywords if k in obs_texts]
assert len(matching_keywords) > 0, \
f"Observations should contain relevant information about the user. Keywords found: {matching_keywords}"
print(f"✓ User entity was prioritized and received {len(observations)} observations")
print(f"✓ Observations contain relevant keywords: {matching_keywords}")
finally:
# Cleanup
pool = await memory._get_pool()
async with pool.acquire() as conn:
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)

View file

@ -231,11 +231,9 @@ pub fn update_background(
(current_profile.as_ref().map(|p| p.disposition.clone()), &profile.disposition)
{
println!("\nDisposition changes:");
println!(" Openness: {:.2}{:.2}", old_p.openness, new_p.openness);
println!(" Conscientiousness: {:.2}{:.2}", old_p.conscientiousness, new_p.conscientiousness);
println!(" Extraversion: {:.2}{:.2}", old_p.extraversion, new_p.extraversion);
println!(" Agreeableness: {:.2}{:.2}", old_p.agreeableness, new_p.agreeableness);
println!(" Neuroticism: {:.2}{:.2}", old_p.neuroticism, new_p.neuroticism);
println!(" Skepticism: {}{}", old_p.skepticism, new_p.skepticism);
println!(" Literalism: {}{}", old_p.literalism, new_p.literalism);
println!(" Empathy: {}{}", old_p.empathy, new_p.empathy);
}
}
} else {

View file

@ -209,17 +209,17 @@ pub fn print_profile(profile: &BankProfileResponse) {
println!("{}", "─── Disposition Traits ───".bright_yellow());
println!();
let traits = [
("Openness", profile.disposition.openness, "🔓", "green"),
("Conscientiousness", profile.disposition.conscientiousness, "📋", "yellow"),
("Extraversion", profile.disposition.extraversion, "🗣️", "cyan"),
("Agreeableness", profile.disposition.agreeableness, "🤝", "magenta"),
("Neuroticism", profile.disposition.neuroticism, "😰", "yellow"),
// New 3-trait disposition system (values 1-5)
let traits: [(_, i64, _, _, _); 3] = [
("Skepticism", profile.disposition.skepticism, "🔍", "cyan", "1=trusting, 5=skeptical"),
("Literalism", profile.disposition.literalism, "📋", "yellow", "1=flexible, 5=literal"),
("Empathy", profile.disposition.empathy, "💚", "green", "1=detached, 5=empathetic"),
];
for (name, value, emoji, color) in &traits {
for (name, value, emoji, color, desc) in &traits {
// Scale 1-5 to bar visualization (each point = 8 chars, total 40)
let bar_length = 40;
let filled = (*value * bar_length as f64) as usize;
let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40
let empty = bar_length - filled;
let bar = format!("{}{}", "".repeat(filled), "".repeat(empty));
@ -231,27 +231,14 @@ pub fn print_profile(profile: &BankProfileResponse) {
_ => bar.bright_white(),
};
println!(" {} {:<20} [{}] {:.0}%",
println!(" {} {:<12} [{}] {}/5",
emoji,
name,
colored_bar,
value * 100.0
value
);
println!(" {}", desc.bright_black());
}
println!();
println!("{}", "Bias Strength:".bright_yellow());
let bias = profile.disposition.bias_strength;
let bar_length = 40;
let filled = (bias * bar_length as f64) as usize;
let empty = bar_length - filled;
let bar = format!("{}{}", "".repeat(filled), "".repeat(empty));
println!(" 💪 {:<20} [{}] {:.0}%",
"Disposition Influence",
bar.bright_green(),
bias * 100.0
);
println!(" {}", "(how much disposition shapes opinions)".bright_black());
println!();
}

View file

@ -5705,7 +5705,7 @@ class DefaultApi:
) -> BankProfileResponse:
"""Update memory bank disposition
Update bank's Big Five disposition traits and bias strength
Update bank's disposition traits (skepticism, literalism, empathy)
:param bank_id: (required)
:type bank_id: str
@ -5777,7 +5777,7 @@ class DefaultApi:
) -> ApiResponse[BankProfileResponse]:
"""Update memory bank disposition
Update bank's Big Five disposition traits and bias strength
Update bank's disposition traits (skepticism, literalism, empathy)
:param bank_id: (required)
:type bank_id: str
@ -5849,7 +5849,7 @@ class DefaultApi:
) -> RESTResponseType:
"""Update memory bank disposition
Update bank's Big Five disposition traits and bias strength
Update bank's disposition traits (skepticism, literalism, empathy)
:param bank_id: (required)
:type bank_id: str

View file

@ -7,7 +7,7 @@ Request model for adding/merging background information.
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**content** | **str** | New background information to add or merge |
**update_disposition** | **bool** | If true, infer Big Five disposition traits from the merged background (default: true) | [optional] [default to True]
**update_disposition** | **bool** | If true, infer disposition traits from the merged background (default: true) | [optional] [default to True]
## Example

View file

@ -1499,7 +1499,7 @@ No authorization required
Update memory bank disposition
Update bank's Big Five disposition traits and bias strength
Update bank's disposition traits (skepticism, literalism, empathy)
### Example

View file

@ -1,17 +1,14 @@
# DispositionTraits
Disposition traits based on Big Five model.
Disposition traits that influence how memories are formed and interpreted.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**openness** | **float** | Openness to experience (0-1) |
**conscientiousness** | **float** | Conscientiousness (0-1) |
**extraversion** | **float** | Extraversion (0-1) |
**agreeableness** | **float** | Agreeableness (0-1) |
**neuroticism** | **float** | Neuroticism (0-1) |
**bias_strength** | **float** | How strongly disposition influences opinions (0-1) |
**skepticism** | **int** | How skeptical vs trusting (1&#x3D;trusting, 5&#x3D;skeptical) |
**literalism** | **int** | How literally to interpret information (1&#x3D;flexible, 5&#x3D;literal) |
**empathy** | **int** | How much to consider emotional context (1&#x3D;detached, 5&#x3D;empathetic) |
## Example

View file

@ -27,7 +27,7 @@ class AddBackgroundRequest(BaseModel):
Request model for adding/merging background information.
""" # noqa: E501
content: StrictStr = Field(description="New background information to add or merge")
update_disposition: Optional[StrictBool] = Field(default=True, description="If true, infer Big Five disposition traits from the merged background (default: true)")
update_disposition: Optional[StrictBool] = Field(default=True, description="If true, infer disposition traits from the merged background (default: true)")
__properties: ClassVar[List[str]] = ["content", "update_disposition"]
model_config = ConfigDict(

View file

@ -18,22 +18,19 @@ import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List, Union
from typing import Any, ClassVar, Dict, List
from typing_extensions import Annotated
from typing import Optional, Set
from typing_extensions import Self
class DispositionTraits(BaseModel):
"""
Disposition traits based on Big Five model.
Disposition traits that influence how memories are formed and interpreted.
""" # noqa: E501
openness: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="Openness to experience (0-1)")
conscientiousness: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="Conscientiousness (0-1)")
extraversion: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="Extraversion (0-1)")
agreeableness: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="Agreeableness (0-1)")
neuroticism: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="Neuroticism (0-1)")
bias_strength: Union[Annotated[float, Field(le=1.0, strict=True, ge=0.0)], Annotated[int, Field(le=1, strict=True, ge=0)]] = Field(description="How strongly disposition influences opinions (0-1)")
__properties: ClassVar[List[str]] = ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism", "bias_strength"]
skepticism: Annotated[int, Field(le=5, strict=True, ge=1)] = Field(description="How skeptical vs trusting (1=trusting, 5=skeptical)")
literalism: Annotated[int, Field(le=5, strict=True, ge=1)] = Field(description="How literally to interpret information (1=flexible, 5=literal)")
empathy: Annotated[int, Field(le=5, strict=True, ge=1)] = Field(description="How much to consider emotional context (1=detached, 5=empathetic)")
__properties: ClassVar[List[str]] = ["skepticism", "literalism", "empathy"]
model_config = ConfigDict(
populate_by_name=True,
@ -86,12 +83,9 @@ class DispositionTraits(BaseModel):
return cls.model_validate(obj)
_obj = cls.model_validate({
"openness": obj.get("openness"),
"conscientiousness": obj.get("conscientiousness"),
"extraversion": obj.get("extraversion"),
"agreeableness": obj.get("agreeableness"),
"neuroticism": obj.get("neuroticism"),
"bias_strength": obj.get("bias_strength")
"skepticism": obj.get("skepticism"),
"literalism": obj.get("literalism"),
"empathy": obj.get("empathy")
})
return _obj

View file

@ -36,7 +36,7 @@ class TestBackgroundResponse(unittest.TestCase):
if include_optional:
return BackgroundResponse(
background = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8}
disposition = {empathy=3, literalism=3, skepticism=3}
)
else:
return BackgroundResponse(

View file

@ -37,7 +37,7 @@ class TestBankListItem(unittest.TestCase):
return BankListItem(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = '',
created_at = '',
updated_at = ''
@ -46,7 +46,7 @@ class TestBankListItem(unittest.TestCase):
return BankListItem(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = '',
)
"""

View file

@ -39,7 +39,7 @@ class TestBankListResponse(unittest.TestCase):
hindsight_client_api.models.bank_list_item.BankListItem(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = '',
created_at = '',
updated_at = '', )
@ -51,7 +51,7 @@ class TestBankListResponse(unittest.TestCase):
hindsight_client_api.models.bank_list_item.BankListItem(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = '',
created_at = '',
updated_at = '', )

View file

@ -37,14 +37,14 @@ class TestBankProfileResponse(unittest.TestCase):
return BankProfileResponse(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = ''
)
else:
return BankProfileResponse(
bank_id = '',
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = '',
)
"""

View file

@ -36,7 +36,7 @@ class TestCreateBankRequest(unittest.TestCase):
if include_optional:
return CreateBankRequest(
name = '',
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
background = ''
)
else:

View file

@ -35,21 +35,15 @@ class TestDispositionTraits(unittest.TestCase):
model = DispositionTraits()
if include_optional:
return DispositionTraits(
openness = 0.0,
conscientiousness = 0.0,
extraversion = 0.0,
agreeableness = 0.0,
neuroticism = 0.0,
bias_strength = 0.0
skepticism = 1.0,
literalism = 1.0,
empathy = 1.0
)
else:
return DispositionTraits(
openness = 0.0,
conscientiousness = 0.0,
extraversion = 0.0,
agreeableness = 0.0,
neuroticism = 0.0,
bias_strength = 0.0,
skepticism = 1.0,
literalism = 1.0,
empathy = 1.0,
)
"""

View file

@ -35,11 +35,11 @@ class TestUpdateDispositionRequest(unittest.TestCase):
model = UpdateDispositionRequest()
if include_optional:
return UpdateDispositionRequest(
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8}
disposition = {empathy=3, literalism=3, skepticism=3}
)
else:
return UpdateDispositionRequest(
disposition = {agreeableness=0.7, bias_strength=0.7, conscientiousness=0.6, extraversion=0.5, neuroticism=0.3, openness=0.8},
disposition = {empathy=3, literalism=3, skepticism=3},
)
"""

View file

@ -86,9 +86,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.48"
version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
"find-msvc-tools",
"shlex",

View file

@ -183,7 +183,7 @@ export const getBankProfile = <ThrowOnError extends boolean = false>(options: Op
/**
* Update memory bank disposition
*
* Update bank's Big Five disposition traits and bias strength
* Update bank's disposition traits (skepticism, literalism, empathy)
*/
export const updateBankDisposition = <ThrowOnError extends boolean = false>(options: Options<UpdateBankDispositionData, ThrowOnError>) => (options.client ?? client).put<UpdateBankDispositionResponses, UpdateBankDispositionErrors, ThrowOnError>({
url: '/v1/default/banks/{bank_id}/profile',

View file

@ -19,7 +19,7 @@ export type AddBackgroundRequest = {
/**
* Update Disposition
*
* If true, infer Big Five disposition traits from the merged background (default: true)
* If true, infer disposition traits from the merged background (default: true)
*/
update_disposition?: boolean;
};
@ -210,45 +210,27 @@ export type DeleteResponse = {
/**
* DispositionTraits
*
* Disposition traits based on Big Five model.
* Disposition traits that influence how memories are formed and interpreted.
*/
export type DispositionTraits = {
/**
* Openness
* Skepticism
*
* Openness to experience (0-1)
* How skeptical vs trusting (1=trusting, 5=skeptical)
*/
openness: number;
skepticism: number;
/**
* Conscientiousness
* Literalism
*
* Conscientiousness (0-1)
* How literally to interpret information (1=flexible, 5=literal)
*/
conscientiousness: number;
literalism: number;
/**
* Extraversion
* Empathy
*
* Extraversion (0-1)
* How much to consider emotional context (1=detached, 5=empathetic)
*/
extraversion: number;
/**
* Agreeableness
*
* Agreeableness (0-1)
*/
agreeableness: number;
/**
* Neuroticism
*
* Neuroticism (0-1)
*/
neuroticism: number;
/**
* Bias Strength
*
* How strongly disposition influences opinions (0-1)
*/
bias_strength: number;
empathy: number;
};
/**

View file

@ -471,7 +471,9 @@ class BenchmarkRunner:
fact_type=["world", "experience"],
question_date=question_date,
include_entities=True,
include_chunks=True
max_entity_tokens=2048,
include_chunks=True,
)
recall_time = time.time() - recall_start_time

View file

@ -844,7 +844,7 @@
},
"put": {
"summary": "Update memory bank disposition",
"description": "Update bank's Big Five disposition traits and bias strength",
"description": "Update bank's disposition traits (skepticism, literalism, empathy)",
"operationId": "update_bank_disposition",
"parameters": [
{
@ -1110,7 +1110,7 @@
"update_disposition": {
"type": "boolean",
"title": "Update Disposition",
"description": "If true, infer Big Five disposition traits from the merged background (default: true)",
"description": "If true, infer disposition traits from the merged background (default: true)",
"default": true
}
},
@ -1151,12 +1151,9 @@
"example": {
"background": "I was born in Texas. I am a software engineer with 10 years of experience.",
"disposition": {
"agreeableness": 0.8,
"bias_strength": 0.6,
"conscientiousness": 0.6,
"extraversion": 0.5,
"neuroticism": 0.4,
"openness": 0.7
"empathy": 3,
"literalism": 3,
"skepticism": 3
}
}
},
@ -1233,12 +1230,9 @@
"bank_id": "user123",
"created_at": "2024-01-15T10:30:00Z",
"disposition": {
"agreeableness": 0.5,
"bias_strength": 0.5,
"conscientiousness": 0.5,
"extraversion": 0.5,
"neuroticism": 0.5,
"openness": 0.5
"empathy": 3,
"literalism": 3,
"skepticism": 3
},
"name": "Alice",
"updated_at": "2024-01-16T14:20:00Z"
@ -1277,12 +1271,9 @@
"background": "I am a software engineer with 10 years of experience in startups",
"bank_id": "user123",
"disposition": {
"agreeableness": 0.7,
"bias_strength": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.5,
"neuroticism": 0.3,
"openness": 0.8
"empathy": 3,
"literalism": 3,
"skepticism": 3
},
"name": "Alice"
}
@ -1428,12 +1419,9 @@
"example": {
"background": "I am a creative software engineer with 10 years of experience",
"disposition": {
"agreeableness": 0.7,
"bias_strength": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.5,
"neuroticism": 0.3,
"openness": 0.8
"empathy": 3,
"literalism": 3,
"skepticism": 3
},
"name": "Alice"
}
@ -1457,67 +1445,40 @@
},
"DispositionTraits": {
"properties": {
"openness": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Openness",
"description": "Openness to experience (0-1)"
"skepticism": {
"type": "integer",
"maximum": 5.0,
"minimum": 1.0,
"title": "Skepticism",
"description": "How skeptical vs trusting (1=trusting, 5=skeptical)"
},
"conscientiousness": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Conscientiousness",
"description": "Conscientiousness (0-1)"
"literalism": {
"type": "integer",
"maximum": 5.0,
"minimum": 1.0,
"title": "Literalism",
"description": "How literally to interpret information (1=flexible, 5=literal)"
},
"extraversion": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Extraversion",
"description": "Extraversion (0-1)"
},
"agreeableness": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Agreeableness",
"description": "Agreeableness (0-1)"
},
"neuroticism": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Neuroticism",
"description": "Neuroticism (0-1)"
},
"bias_strength": {
"type": "number",
"maximum": 1.0,
"minimum": 0.0,
"title": "Bias Strength",
"description": "How strongly disposition influences opinions (0-1)"
"empathy": {
"type": "integer",
"maximum": 5.0,
"minimum": 1.0,
"title": "Empathy",
"description": "How much to consider emotional context (1=detached, 5=empathetic)"
}
},
"type": "object",
"required": [
"openness",
"conscientiousness",
"extraversion",
"agreeableness",
"neuroticism",
"bias_strength"
"skepticism",
"literalism",
"empathy"
],
"title": "DispositionTraits",
"description": "Disposition traits based on Big Five model.",
"description": "Disposition traits that influence how memories are formed and interpreted.",
"example": {
"agreeableness": 0.7,
"bias_strength": 0.7,
"conscientiousness": 0.6,
"extraversion": 0.5,
"neuroticism": 0.3,
"openness": 0.8
"empathy": 3,
"literalism": 3,
"skepticism": 3
}
},
"DocumentResponse": {