From 084242a6ddd16a55a2203373e494218cda76bf0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Tue, 27 Jan 2026 15:03:25 +0100 Subject: [PATCH] chore: drop dead code (#210) --- .../hindsight_api/engine/memory_engine.py | 314 +----------------- .../hindsight_api/engine/reflect/__init__.py | 6 +- .../hindsight_api/engine/reflect/models.py | 29 +- .../hindsight_api/engine/search/scoring.py | 134 -------- hindsight-api/hindsight_api/engine/utils.py | 126 ------- 5 files changed, 7 insertions(+), 602 deletions(-) delete mode 100644 hindsight-api/hindsight_api/engine/search/scoring.py diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index 5c301ba2..d23e8682 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -141,7 +141,6 @@ from .entity_resolver import EntityResolver from .llm_wrapper import LLMConfig from .query_analyzer import QueryAnalyzer from .reflect import run_reflect_agent -from .reflect.models import ObservationInput from .reflect.tools import tool_expand, tool_recall, tool_search_mental_models, tool_search_observations from .response_models import ( VALID_RECALL_FACT_TYPES, @@ -545,7 +544,7 @@ class MemoryEngine(MemoryEngineInterface): """ Handler for consolidation tasks. - Consolidates new memories into learnings for a bank. + Consolidates new memories into mental models for a bank. Args: task_dict: Dict with 'bank_id' @@ -1625,7 +1624,6 @@ class MemoryEngine(MemoryEngineInterface): ) # Filter out 'opinion' - opinions are no longer returned from recall - # (learnings are now stored as mental models instead) fact_type = [ft for ft in fact_type if ft != "opinion"] if not fact_type: # All requested types were opinions - return empty result @@ -3534,7 +3532,7 @@ class MemoryEngine(MemoryEngineInterface): ReflectResult containing: - text: Plain text answer - based_on: Empty dict (agent retrieves facts dynamically) - - new_opinions: Empty list (learnings stored as mental models) + - new_opinions: Empty list - structured_output: None (not yet supported for agentic reflect) """ # Use cached LLM config @@ -4377,314 +4375,6 @@ class MemoryEngine(MemoryEngineInterface): ) return updated_count - # ========================================================================= - # LEARNINGS CRUD - # ========================================================================= - - async def list_learnings( - self, - bank_id: str, - *, - tags: list[str] | None = None, - tags_match: str = "any", - limit: int = 100, - offset: int = 0, - request_context: "RequestContext", - ) -> list[dict[str, Any]]: - """List learnings for a bank. - - Args: - bank_id: Bank identifier - tags: Optional tags to filter by - tags_match: How to match tags - 'any', 'all', or 'exact' - limit: Maximum number of results - offset: Offset for pagination - request_context: Request context for authentication - - Returns: - List of learning dicts - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - async with acquire_with_retry(pool) as conn: - # Build tag filter - tag_filter = "" - params: list[Any] = [bank_id, limit, offset] - if tags: - if tags_match == "all": - tag_filter = " AND tags @> $4::varchar[]" - elif tags_match == "exact": - tag_filter = " AND tags = $4::varchar[]" - else: # any - tag_filter = " AND tags && $4::varchar[]" - params.append(tags) - - rows = await conn.fetch( - f""" - SELECT id, bank_id, text, proof_count, history, mission_context, - pre_mission_change, tags, created_at, updated_at - FROM {fq_table("learnings")} - WHERE bank_id = $1 {tag_filter} - ORDER BY proof_count DESC, updated_at DESC - LIMIT $2 OFFSET $3 - """, - *params, - ) - - return [self._row_to_learning(row) for row in rows] - - async def get_learning( - self, - bank_id: str, - learning_id: str, - *, - request_context: "RequestContext", - ) -> dict[str, Any] | None: - """Get a single learning by ID. - - Args: - bank_id: Bank identifier - learning_id: Learning UUID - request_context: Request context for authentication - - Returns: - Learning dict or None if not found - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - async with acquire_with_retry(pool) as conn: - row = await conn.fetchrow( - f""" - SELECT id, bank_id, text, proof_count, history, mission_context, - pre_mission_change, tags, created_at, updated_at - FROM {fq_table("learnings")} - WHERE bank_id = $1 AND id = $2 - """, - bank_id, - learning_id, - ) - - return self._row_to_learning(row) if row else None - - async def create_learning( - self, - bank_id: str, - text: str, - *, - proof_count: int = 1, - tags: list[str] | None = None, - mission_context: str | None = None, - request_context: "RequestContext", - ) -> dict[str, Any]: - """Create a new learning. - - Args: - bank_id: Bank identifier - text: The learning text - proof_count: Initial proof count (default 1) - tags: Optional tags for scoped visibility - mission_context: Hash of mission when created - request_context: Request context for authentication - - Returns: - The created learning dict - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - # Generate embedding for the learning text - embedding = await embedding_utils.generate_embeddings_batch(self.embeddings, [text]) - # Convert embedding to string for asyncpg vector type - embedding_str = str(embedding[0]) if embedding else None - - async with acquire_with_retry(pool) as conn: - row = await conn.fetchrow( - f""" - INSERT INTO {fq_table("learnings")} - (bank_id, text, proof_count, mission_context, embedding, tags) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, bank_id, text, proof_count, history, mission_context, - pre_mission_change, tags, created_at, updated_at - """, - bank_id, - text, - proof_count, - mission_context, - embedding_str, - tags or [], - ) - - logger.info(f"[LEARNINGS] Created learning for bank {bank_id}: {text[:50]}...") - return self._row_to_learning(row) - - async def update_learning( - self, - bank_id: str, - learning_id: str, - *, - text: str | None = None, - increment_proof: bool = False, - add_history: dict[str, Any] | None = None, - mark_pre_mission_change: bool = False, - request_context: "RequestContext", - ) -> dict[str, Any] | None: - """Update a learning. - - Args: - bank_id: Bank identifier - learning_id: Learning UUID - text: New text (if changing) - increment_proof: Whether to increment proof_count - add_history: History entry to append (for contradictions) - mark_pre_mission_change: Whether to mark as pre-mission-change - request_context: Request context for authentication - - Returns: - Updated learning dict or None if not found - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - async with acquire_with_retry(pool) as conn: - # Build dynamic update - updates = ["updated_at = NOW()"] - params: list[Any] = [bank_id, learning_id] - param_idx = 3 - - if text is not None: - updates.append(f"text = ${param_idx}") - params.append(text) - param_idx += 1 - # Also update embedding (convert to string for asyncpg vector type) - embedding = await embedding_utils.generate_embeddings_batch(self.embeddings, [text]) - if embedding: - updates.append(f"embedding = ${param_idx}") - params.append(str(embedding[0])) - param_idx += 1 - - if increment_proof: - updates.append("proof_count = proof_count + 1") - - if add_history: - import json - - updates.append(f"history = history || ${param_idx}::jsonb") - params.append(json.dumps([add_history])) - param_idx += 1 - - if mark_pre_mission_change: - updates.append("pre_mission_change = TRUE") - - query = f""" - UPDATE {fq_table("learnings")} - SET {", ".join(updates)} - WHERE bank_id = $1 AND id = $2 - RETURNING id, bank_id, text, proof_count, history, mission_context, - pre_mission_change, tags, created_at, updated_at - """ - - row = await conn.fetchrow(query, *params) - - return self._row_to_learning(row) if row else None - - async def delete_learning( - self, - bank_id: str, - learning_id: str, - *, - request_context: "RequestContext", - ) -> bool: - """Delete a learning. - - Args: - bank_id: Bank identifier - learning_id: Learning UUID - request_context: Request context for authentication - - Returns: - True if deleted, False if not found - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - async with acquire_with_retry(pool) as conn: - result = await conn.execute( - f"DELETE FROM {fq_table('learnings')} WHERE bank_id = $1 AND id = $2", - bank_id, - learning_id, - ) - - return result == "DELETE 1" - - def _row_to_learning(self, row) -> dict[str, Any]: - """Convert a database row to a learning dict.""" - import json - - # Parse history - asyncpg may return JSONB as string in some cases - history = row["history"] - if isinstance(history, str): - history = json.loads(history) - elif history is None: - history = [] - - return { - "id": str(row["id"]), - "bank_id": row["bank_id"], - "text": row["text"], - "proof_count": row["proof_count"], - "history": history, - "mission_context": row["mission_context"], - "pre_mission_change": row["pre_mission_change"], - "tags": row["tags"] or [], - "created_at": row["created_at"].isoformat() if row["created_at"] else None, - "updated_at": row["updated_at"].isoformat() if row["updated_at"] else None, - } - - async def mark_learnings_pre_mission_change( - self, - bank_id: str, - *, - request_context: "RequestContext", - ) -> int: - """Mark all learnings as pre-mission-change when mission changes. - - Args: - bank_id: Bank identifier - request_context: Request context for authentication - - Returns: - Number of learnings marked - """ - await self._authenticate_tenant(request_context) - pool = await self._get_pool() - - async with acquire_with_retry(pool) as conn: - result = await conn.execute( - f""" - UPDATE {fq_table("learnings")} - SET pre_mission_change = TRUE, updated_at = NOW() - WHERE bank_id = $1 AND pre_mission_change = FALSE - """, - bank_id, - ) - - # Also update bank's mission_changed_at - await conn.execute( - f""" - UPDATE {fq_table("banks")} - SET mission_changed_at = NOW() - WHERE bank_id = $1 - """, - bank_id, - ) - - count = int(result.split()[-1]) if result and "UPDATE" in result else 0 - if count > 0: - logger.info(f"[LEARNINGS] Marked {count} learnings as pre-mission-change for bank {bank_id}") - return count - # ========================================================================= # MENTAL MODELS (CONSOLIDATED) - Read-only access to auto-consolidated mental models # ========================================================================= diff --git a/hindsight-api/hindsight_api/engine/reflect/__init__.py b/hindsight-api/hindsight_api/engine/reflect/__init__.py index 0c1ac1dd..52680a2a 100644 --- a/hindsight-api/hindsight_api/engine/reflect/__init__.py +++ b/hindsight-api/hindsight_api/engine/reflect/__init__.py @@ -4,17 +4,15 @@ Reflect agent module for agentic reflection with tools. The reflect agent uses an iterative loop with tools to: 1. Lookup mental models (existing knowledge) 2. Recall facts (semantic + temporal search) -3. Learn new insights (create/update observations) -4. Expand memories (get chunk/document context) +3. Expand memories (get chunk/document context) """ from .agent import ReflectAgentResult, run_reflect_agent -from .models import ObservationInput, ReflectAction, ReflectActionBatch +from .models import ReflectAction, ReflectActionBatch __all__ = [ "run_reflect_agent", "ReflectAgentResult", "ReflectAction", "ReflectActionBatch", - "ObservationInput", ] diff --git a/hindsight-api/hindsight_api/engine/reflect/models.py b/hindsight-api/hindsight_api/engine/reflect/models.py index af030fc9..04d7f6de 100644 --- a/hindsight-api/hindsight_api/engine/reflect/models.py +++ b/hindsight-api/hindsight_api/engine/reflect/models.py @@ -15,41 +15,18 @@ class ObservationSection(BaseModel): memory_ids: list[str] = Field(default_factory=list, description="Memory IDs supporting this section") -class ObservationInput(BaseModel): - """Input for the learn tool to create an observation placeholder. - - The agent only specifies name and description - the actual content/sections - are generated during refresh, similar to pinned models. - """ - - name: str = Field(description="Human-readable name for the observation") - description: str = Field(description="What to track - used as prompt for content generation during refresh") - entity_id: str | None = Field(default=None, description="Optional link to existing entity ID") - - -class AnswerSection(BaseModel): - """A section of the answer with its supporting evidence (DEPRECATED).""" - - title: str = Field(description="Section header/title") - text: str = Field(description="Section content") - memory_ids: list[str] = Field(default_factory=list, description="Memory IDs supporting this section") - model_ids: list[str] = Field(default_factory=list, description="Mental model IDs supporting this section") - - class ReflectAction(BaseModel): """Single action the reflect agent can take.""" - tool: Literal["list_observations", "get_observation", "recall", "learn", "expand", "done"] = Field( - description="Tool to invoke: list_observations, get_observation, recall, learn, expand, or done" + tool: Literal["list_observations", "get_observation", "recall", "expand", "done"] = Field( + description="Tool to invoke: list_observations, get_observation, recall, expand, or done" ) # Tool-specific parameters observation_id: str | None = Field(default=None, description="Observation ID for get_observation") query: str | None = Field(default=None, description="Search query for recall") max_tokens: int | None = Field(default=None, description="Max tokens for recall results (default 2048)") - observation: ObservationInput | None = Field(default=None, description="Observation to create/update for learn") memory_ids: list[str] | None = Field(default=None, description="Memory unit IDs for expand (batched)") depth: Literal["chunk", "document"] | None = Field(default=None, description="Expansion depth for expand") - sections: list[AnswerSection] | None = Field(default=None, description="DEPRECATED: Use answer field instead") observation_sections: list[ObservationSection] | None = Field( default=None, description="Observation sections for done action (when output_mode=observations)" ) @@ -73,7 +50,7 @@ class ReflectActionBatch(BaseModel): class ToolCall(BaseModel): """A single tool call made during reflect.""" - tool: str = Field(description="Tool name: lookup, recall, learn, expand") + tool: str = Field(description="Tool name: lookup, recall, expand") input: dict = Field(description="Tool input parameters") output: dict = Field(description="Tool output/result") duration_ms: int = Field(description="Execution time in milliseconds") diff --git a/hindsight-api/hindsight_api/engine/search/scoring.py b/hindsight-api/hindsight_api/engine/search/scoring.py deleted file mode 100644 index a8c5f361..00000000 --- a/hindsight-api/hindsight_api/engine/search/scoring.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -Scoring functions for memory search and retrieval. - -Includes recency weighting, frequency weighting, temporal proximity, -and similarity calculations used in memory activation and ranking. -""" - -from datetime import datetime - - -def cosine_similarity(vec1: list[float], vec2: list[float]) -> float: - """ - Calculate cosine similarity between two vectors. - - Args: - vec1: First vector - vec2: Second vector - - Returns: - Similarity score between 0 and 1 - """ - if len(vec1) != len(vec2): - raise ValueError("Vectors must have same dimension") - - dot_product = sum(a * b for a, b in zip(vec1, vec2)) - magnitude1 = sum(a * a for a in vec1) ** 0.5 - magnitude2 = sum(b * b for b in vec2) ** 0.5 - - if magnitude1 == 0 or magnitude2 == 0: - return 0.0 - - return dot_product / (magnitude1 * magnitude2) - - -def calculate_recency_weight(days_since: float, half_life_days: float = 365.0) -> float: - """ - Calculate recency weight using logarithmic decay. - - This provides much better differentiation over long time periods compared to - exponential decay. Uses a log-based decay where the half-life parameter controls - when memories reach 50% weight. - - Examples: - - Today (0 days): 1.0 - - 1 year (365 days): ~0.5 (with default half_life=365) - - 2 years (730 days): ~0.33 - - 5 years (1825 days): ~0.17 - - 10 years (3650 days): ~0.09 - - This ensures that 2-year-old and 5-year-old memories have meaningfully - different weights, unlike exponential decay which makes them both ~0. - - Args: - days_since: Number of days since the memory was created - half_life_days: Number of days for weight to reach 0.5 (default: 1 year) - - Returns: - Weight between 0 and 1 - """ - import math - - # Logarithmic decay: 1 / (1 + log(1 + days_since/half_life)) - # This decays much slower than exponential, giving better long-term differentiation - normalized_age = days_since / half_life_days - return 1.0 / (1.0 + math.log1p(normalized_age)) - - -def calculate_temporal_anchor(occurred_start: datetime, occurred_end: datetime) -> datetime: - """ - Calculate a single temporal anchor point from a temporal range. - - Used for spreading activation - we need a single representative date - to calculate temporal proximity between facts. This simplifies the - range-to-range distance problem. - - Strategy: Use midpoint of the range for balanced representation. - - Args: - occurred_start: Start of temporal range - occurred_end: End of temporal range - - Returns: - Single datetime representing the temporal anchor (midpoint) - - Examples: - - Point event (July 14): start=July 14, end=July 14 → anchor=July 14 - - Month range (February): start=Feb 1, end=Feb 28 → anchor=Feb 14 - - Year range (2023): start=Jan 1, end=Dec 31 → anchor=July 1 - """ - # Calculate midpoint - time_delta = occurred_end - occurred_start - midpoint = occurred_start + (time_delta / 2) - return midpoint - - -def calculate_temporal_proximity(anchor_a: datetime, anchor_b: datetime, half_life_days: float = 30.0) -> float: - """ - Calculate temporal proximity between two temporal anchors. - - Used for spreading activation to determine how "close" two facts are - in time. Uses logarithmic decay so that temporal similarity doesn't - drop off too quickly. - - Args: - anchor_a: Temporal anchor of first fact - anchor_b: Temporal anchor of second fact - half_life_days: Number of days for proximity to reach 0.5 - (default: 30 days = 1 month) - - Returns: - Proximity score in [0, 1] where: - - 1.0 = same day - - 0.5 = ~half_life days apart - - 0.0 = very distant in time - - Examples: - - Same day: 1.0 - - 1 week apart (half_life=30): ~0.7 - - 1 month apart (half_life=30): ~0.5 - - 1 year apart (half_life=30): ~0.2 - """ - import math - - days_apart = abs((anchor_a - anchor_b).days) - - if days_apart == 0: - return 1.0 - - # Logarithmic decay: 1 / (1 + log(1 + days_apart/half_life)) - # Similar to calculate_recency_weight but for proximity between events - normalized_distance = days_apart / half_life_days - proximity = 1.0 / (1.0 + math.log1p(normalized_distance)) - - return proximity diff --git a/hindsight-api/hindsight_api/engine/utils.py b/hindsight-api/hindsight_api/engine/utils.py index 204622ee..b35d4819 100644 --- a/hindsight-api/hindsight_api/engine/utils.py +++ b/hindsight-api/hindsight_api/engine/utils.py @@ -65,129 +65,3 @@ async def extract_facts( return [], chunks return facts, chunks - - -def cosine_similarity(vec1: list[float], vec2: list[float]) -> float: - """ - Calculate cosine similarity between two vectors. - - Args: - vec1: First vector - vec2: Second vector - - Returns: - Similarity score between 0 and 1 - """ - if len(vec1) != len(vec2): - raise ValueError("Vectors must have same dimension") - - dot_product = sum(a * b for a, b in zip(vec1, vec2)) - magnitude1 = sum(a * a for a in vec1) ** 0.5 - magnitude2 = sum(b * b for b in vec2) ** 0.5 - - if magnitude1 == 0 or magnitude2 == 0: - return 0.0 - - return dot_product / (magnitude1 * magnitude2) - - -def calculate_recency_weight(days_since: float, half_life_days: float = 365.0) -> float: - """ - Calculate recency weight using logarithmic decay. - - This provides much better differentiation over long time periods compared to - exponential decay. Uses a log-based decay where the half-life parameter controls - when memories reach 50% weight. - - Examples: - - Today (0 days): 1.0 - - 1 year (365 days): ~0.5 (with default half_life=365) - - 2 years (730 days): ~0.33 - - 5 years (1825 days): ~0.17 - - 10 years (3650 days): ~0.09 - - This ensures that 2-year-old and 5-year-old memories have meaningfully - different weights, unlike exponential decay which makes them both ~0. - - Args: - days_since: Number of days since the memory was created - half_life_days: Number of days for weight to reach 0.5 (default: 1 year) - - Returns: - Weight between 0 and 1 - """ - import math - - # Logarithmic decay: 1 / (1 + log(1 + days_since/half_life)) - # This decays much slower than exponential, giving better long-term differentiation - normalized_age = days_since / half_life_days - return 1.0 / (1.0 + math.log1p(normalized_age)) - - -def calculate_temporal_anchor(occurred_start: datetime, occurred_end: datetime) -> datetime: - """ - Calculate a single temporal anchor point from a temporal range. - - Used for spreading activation - we need a single representative date - to calculate temporal proximity between facts. This simplifies the - range-to-range distance problem. - - Strategy: Use midpoint of the range for balanced representation. - - Args: - occurred_start: Start of temporal range - occurred_end: End of temporal range - - Returns: - Single datetime representing the temporal anchor (midpoint) - - Examples: - - Point event (July 14): start=July 14, end=July 14 → anchor=July 14 - - Month range (February): start=Feb 1, end=Feb 28 → anchor=Feb 14 - - Year range (2023): start=Jan 1, end=Dec 31 → anchor=July 1 - """ - # Calculate midpoint - time_delta = occurred_end - occurred_start - midpoint = occurred_start + (time_delta / 2) - return midpoint - - -def calculate_temporal_proximity(anchor_a: datetime, anchor_b: datetime, half_life_days: float = 30.0) -> float: - """ - Calculate temporal proximity between two temporal anchors. - - Used for spreading activation to determine how "close" two facts are - in time. Uses logarithmic decay so that temporal similarity doesn't - drop off too quickly. - - Args: - anchor_a: Temporal anchor of first fact - anchor_b: Temporal anchor of second fact - half_life_days: Number of days for proximity to reach 0.5 - (default: 30 days = 1 month) - - Returns: - Proximity score in [0, 1] where: - - 1.0 = same day - - 0.5 = ~half_life days apart - - 0.0 = very distant in time - - Examples: - - Same day: 1.0 - - 1 week apart (half_life=30): ~0.7 - - 1 month apart (half_life=30): ~0.5 - - 1 year apart (half_life=30): ~0.2 - """ - import math - - days_apart = abs((anchor_a - anchor_b).days) - - if days_apart == 0: - return 1.0 - - # Logarithmic decay: 1 / (1 + log(1 + days_apart/half_life)) - # Similar to calculate_recency_weight but for proximity between events - normalized_distance = days_apart / half_life_days - proximity = 1.0 / (1.0 + math.log1p(normalized_distance)) - - return proximity