diff --git a/hindsight-api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py b/hindsight-api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py new file mode 100644 index 00000000..6712f4cc --- /dev/null +++ b/hindsight-api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py @@ -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 + """)) diff --git a/hindsight-api/hindsight_api/api/http.py b/hindsight-api/hindsight_api/api/http.py index 92634137..44d6f126 100644 --- a/hindsight-api/hindsight_api/api/http.py +++ b/hindsight-api/hindsight_api/api/http.py @@ -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, diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index b5521faa..8b3d7414 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -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}") diff --git a/hindsight-api/hindsight_api/engine/response_models.py b/hindsight-api/hindsight_api/engine/response_models.py index b6ce70b0..1cf09c48 100644 --- a/hindsight-api/hindsight_api/engine/response_models.py +++ b/hindsight-api/hindsight_api/engine/response_models.py @@ -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 } }) diff --git a/hindsight-api/hindsight_api/engine/retain/bank_utils.py b/hindsight-api/hindsight_api/engine/retain/bank_utils.py index 01d3b6de..8f6be868 100644 --- a/hindsight-api/hindsight_api/engine/retain/bank_utils.py +++ b/hindsight-api/hindsight_api/engine/retain/bank_utils.py @@ -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 diff --git a/hindsight-api/hindsight_api/engine/retain/entity_processing.py b/hindsight-api/hindsight_api/engine/retain/entity_processing.py index fea40657..d238951a 100644 --- a/hindsight-api/hindsight_api/engine/retain/entity_processing.py +++ b/hindsight-api/hindsight_api/engine/retain/entity_processing.py @@ -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 diff --git a/hindsight-api/hindsight_api/engine/retain/fact_storage.py b/hindsight-api/hindsight_api/engine/retain/fact_storage.py index 280de832..442098ff 100644 --- a/hindsight-api/hindsight_api/engine/retain/fact_storage.py +++ b/hindsight-api/hindsight_api/engine/retain/fact_storage.py @@ -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}', "" ) diff --git a/hindsight-api/hindsight_api/engine/retain/link_utils.py b/hindsight-api/hindsight_api/engine/retain/link_utils.py index 77f684bb..b358fd7f 100644 --- a/hindsight-api/hindsight_api/engine/retain/link_utils.py +++ b/hindsight-api/hindsight_api/engine/retain/link_utils.py @@ -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") diff --git a/hindsight-api/hindsight_api/engine/retain/orchestrator.py b/hindsight-api/hindsight_api/engine/retain/orchestrator.py index 687f09dc..9687e12d 100644 --- a/hindsight-api/hindsight_api/engine/retain/orchestrator.py +++ b/hindsight-api/hindsight_api/engine/retain/orchestrator.py @@ -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( diff --git a/hindsight-api/hindsight_api/engine/retain/types.py b/hindsight-api/hindsight_api/engine/retain/types.py index 21b30a09..ff65efd4 100644 --- a/hindsight-api/hindsight_api/engine/retain/types.py +++ b/hindsight-api/hindsight_api/engine/retain/types.py @@ -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: """ diff --git a/hindsight-api/hindsight_api/engine/search/think_utils.py b/hindsight-api/hindsight_api/engine/search/think_utils.py index 1a539fe2..cbbb14e9 100644 --- a/hindsight-api/hindsight_api/engine/search/think_utils.py +++ b/hindsight-api/hindsight_api/engine/search/think_utils.py @@ -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." diff --git a/hindsight-api/hindsight_api/models.py b/hindsight-api/hindsight_api/models.py index b587910b..fe481306 100644 --- a/hindsight-api/hindsight_api/models.py +++ b/hindsight-api/hindsight_api/models.py @@ -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="") diff --git a/hindsight-api/tests/test_agents_api.py b/hindsight-api/tests/test_agents_api.py index 8d960cde..6bab3dd3 100644 --- a/hindsight-api/tests/test_agents_api.py +++ b/hindsight-api/tests/test_agents_api.py @@ -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( diff --git a/hindsight-api/tests/test_fact_extraction_quality.py b/hindsight-api/tests/test_fact_extraction_quality.py index 70088346..f075baba 100644 --- a/hindsight-api/tests/test_fact_extraction_quality.py +++ b/hindsight-api/tests/test_fact_extraction_quality.py @@ -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 \ No newline at end of file + # Higher skepticism expected from "very skeptical of people" + assert disposition["skepticism"] >= 3 \ No newline at end of file diff --git a/hindsight-api/tests/test_fact_ordering.py b/hindsight-api/tests/test_fact_ordering.py index 33147067..ef4aceb8 100644 --- a/hindsight-api/tests/test_fact_ordering.py +++ b/hindsight-api/tests/test_fact_ordering.py @@ -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 diff --git a/hindsight-api/tests/test_http_api_integration.py b/hindsight-api/tests/test_http_api_integration.py index 7dce716e..df305740 100644 --- a/hindsight-api/tests/test_http_api_integration.py +++ b/hindsight-api/tests/test_http_api_integration.py @@ -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() diff --git a/hindsight-api/tests/test_observations.py b/hindsight-api/tests/test_observations.py index 11c55260..1085d036 100644 --- a/hindsight-api/tests/test_observations.py +++ b/hindsight-api/tests/test_observations.py @@ -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) diff --git a/hindsight-cli/src/commands/bank.rs b/hindsight-cli/src/commands/bank.rs index 84ed1926..9d63016f 100644 --- a/hindsight-cli/src/commands/bank.rs +++ b/hindsight-cli/src/commands/bank.rs @@ -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 { diff --git a/hindsight-cli/src/ui.rs b/hindsight-cli/src/ui.rs index 34879c61..1a78cf26 100644 --- a/hindsight-cli/src/ui.rs +++ b/hindsight-cli/src/ui.rs @@ -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!(); } diff --git a/hindsight-clients/python/hindsight_client_api/api/default_api.py b/hindsight-clients/python/hindsight_client_api/api/default_api.py index 4e7c7a07..e6f212d1 100644 --- a/hindsight-clients/python/hindsight_client_api/api/default_api.py +++ b/hindsight-clients/python/hindsight_client_api/api/default_api.py @@ -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 diff --git a/hindsight-clients/python/hindsight_client_api/docs/AddBackgroundRequest.md b/hindsight-clients/python/hindsight_client_api/docs/AddBackgroundRequest.md index 0e4d4ca0..3f000fd2 100644 --- a/hindsight-clients/python/hindsight_client_api/docs/AddBackgroundRequest.md +++ b/hindsight-clients/python/hindsight_client_api/docs/AddBackgroundRequest.md @@ -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 diff --git a/hindsight-clients/python/hindsight_client_api/docs/DefaultApi.md b/hindsight-clients/python/hindsight_client_api/docs/DefaultApi.md index d2fffde4..99b77bc5 100644 --- a/hindsight-clients/python/hindsight_client_api/docs/DefaultApi.md +++ b/hindsight-clients/python/hindsight_client_api/docs/DefaultApi.md @@ -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 diff --git a/hindsight-clients/python/hindsight_client_api/docs/DispositionTraits.md b/hindsight-clients/python/hindsight_client_api/docs/DispositionTraits.md index 782bb97f..4996890c 100644 --- a/hindsight-clients/python/hindsight_client_api/docs/DispositionTraits.md +++ b/hindsight-clients/python/hindsight_client_api/docs/DispositionTraits.md @@ -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=trusting, 5=skeptical) | +**literalism** | **int** | How literally to interpret information (1=flexible, 5=literal) | +**empathy** | **int** | How much to consider emotional context (1=detached, 5=empathetic) | ## Example diff --git a/hindsight-clients/python/hindsight_client_api/models/add_background_request.py b/hindsight-clients/python/hindsight_client_api/models/add_background_request.py index f8379d31..67cd67b3 100644 --- a/hindsight-clients/python/hindsight_client_api/models/add_background_request.py +++ b/hindsight-clients/python/hindsight_client_api/models/add_background_request.py @@ -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( diff --git a/hindsight-clients/python/hindsight_client_api/models/disposition_traits.py b/hindsight-clients/python/hindsight_client_api/models/disposition_traits.py index d6a8dfff..cf858051 100644 --- a/hindsight-clients/python/hindsight_client_api/models/disposition_traits.py +++ b/hindsight-clients/python/hindsight_client_api/models/disposition_traits.py @@ -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 diff --git a/hindsight-clients/python/hindsight_client_api/test/test_background_response.py b/hindsight-clients/python/hindsight_client_api/test/test_background_response.py index 0c70b134..d48a57de 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_background_response.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_background_response.py @@ -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( diff --git a/hindsight-clients/python/hindsight_client_api/test/test_bank_list_item.py b/hindsight-clients/python/hindsight_client_api/test/test_bank_list_item.py index f4b7e855..a8b0a4f3 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_bank_list_item.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_bank_list_item.py @@ -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 = '', ) """ diff --git a/hindsight-clients/python/hindsight_client_api/test/test_bank_list_response.py b/hindsight-clients/python/hindsight_client_api/test/test_bank_list_response.py index 1989295d..3b7fc4b8 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_bank_list_response.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_bank_list_response.py @@ -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 = '', ) diff --git a/hindsight-clients/python/hindsight_client_api/test/test_bank_profile_response.py b/hindsight-clients/python/hindsight_client_api/test/test_bank_profile_response.py index 8201111a..8874401d 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_bank_profile_response.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_bank_profile_response.py @@ -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 = '', ) """ diff --git a/hindsight-clients/python/hindsight_client_api/test/test_create_bank_request.py b/hindsight-clients/python/hindsight_client_api/test/test_create_bank_request.py index 10bf2052..0e7d8157 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_create_bank_request.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_create_bank_request.py @@ -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: diff --git a/hindsight-clients/python/hindsight_client_api/test/test_disposition_traits.py b/hindsight-clients/python/hindsight_client_api/test/test_disposition_traits.py index e4ea5a6f..fbc7fd1b 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_disposition_traits.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_disposition_traits.py @@ -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, ) """ diff --git a/hindsight-clients/python/hindsight_client_api/test/test_update_disposition_request.py b/hindsight-clients/python/hindsight_client_api/test/test_update_disposition_request.py index 1686ce21..228f019d 100644 --- a/hindsight-clients/python/hindsight_client_api/test/test_update_disposition_request.py +++ b/hindsight-clients/python/hindsight_client_api/test/test_update_disposition_request.py @@ -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}, ) """ diff --git a/hindsight-clients/rust/Cargo.lock b/hindsight-clients/rust/Cargo.lock index 98cc87d1..e158a2b4 100644 --- a/hindsight-clients/rust/Cargo.lock +++ b/hindsight-clients/rust/Cargo.lock @@ -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", diff --git a/hindsight-clients/typescript/generated/sdk.gen.ts b/hindsight-clients/typescript/generated/sdk.gen.ts index c1b1fb2c..80d05893 100644 --- a/hindsight-clients/typescript/generated/sdk.gen.ts +++ b/hindsight-clients/typescript/generated/sdk.gen.ts @@ -183,7 +183,7 @@ export const getBankProfile = (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 = (options: Options) => (options.client ?? client).put({ url: '/v1/default/banks/{bank_id}/profile', diff --git a/hindsight-clients/typescript/generated/types.gen.ts b/hindsight-clients/typescript/generated/types.gen.ts index 55561b48..2536b9ab 100644 --- a/hindsight-clients/typescript/generated/types.gen.ts +++ b/hindsight-clients/typescript/generated/types.gen.ts @@ -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; }; /** diff --git a/hindsight-dev/benchmarks/common/benchmark_runner.py b/hindsight-dev/benchmarks/common/benchmark_runner.py index 7b0aea9d..121b14d7 100644 --- a/hindsight-dev/benchmarks/common/benchmark_runner.py +++ b/hindsight-dev/benchmarks/common/benchmark_runner.py @@ -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 diff --git a/openapi.json b/openapi.json index f3157f5c..7d0376ab 100644 --- a/openapi.json +++ b/openapi.json @@ -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": {