fleet-memory/hindsight-api-slim/hindsight_api/engine/search/graph_retrieval.py
Nicolò Boschi 9cfdd464a9
fix(retain): preserve normalized experience fact types (#848)
* fix(retain): preserve normalized experience fact types and remove deprecated opinion type

The ExtractedFactType conversion was re-checking for raw "assistant" fact_type
after the parsing layer had already normalized it to "experience". Since
fact_from_llm.fact_type was always "experience" (never "assistant"), the ternary
always fell through to "world", silently losing experience classification.

Also removes the deprecated "opinion" fact type from internal extraction models,
database constraints/indexes (via migration), and dead code paths. The public API
surface (descriptions, response models, backwards-compat filter) is unchanged.

* refactor(retain): drop unused confidence_score column

The confidence_score column was only ever non-null for opinion facts
(which are now removed). It was always written as NULL and never read
back from the database. Remove it from:
- DB model and migration (DROP COLUMN)
- INSERT queries in fact_storage.py
- retain_async/retain_batch_async parameters
- RetainContext/RetainResult extension models
- RetainBatch dataclass
2026-04-02 12:20:37 +02:00

67 lines
2.5 KiB
Python

"""
Graph retrieval strategies for memory recall.
This module provides an abstraction for graph-based memory retrieval,
allowing different algorithms to be swapped without changing the rest
of the recall pipeline.
"""
import logging
from abc import ABC, abstractmethod
from .tags import TagGroup, TagsMatch
from .types import GraphRetrievalTimings, RetrievalResult
logger = logging.getLogger(__name__)
class GraphRetriever(ABC):
"""
Abstract base class for graph-based memory retrieval.
Implementations traverse the memory graph (entity links, temporal links,
causal links) to find relevant facts that might not be found by
semantic or keyword search alone.
"""
@property
@abstractmethod
def name(self) -> str:
"""Return identifier for this retrieval strategy (e.g., 'link_expansion')."""
pass
@abstractmethod
async def retrieve(
self,
pool,
query_embedding_str: str,
bank_id: str,
fact_type: str,
budget: int,
query_text: str | None = None,
semantic_seeds: list[RetrievalResult] | None = None,
temporal_seeds: list[RetrievalResult] | None = None,
adjacency=None, # TypedAdjacency, optional pre-loaded graph
tags: list[str] | None = None, # Visibility scope tags for filtering
tags_match: TagsMatch = "any", # How to match tags: 'any' (OR) or 'all' (AND)
tag_groups: list[TagGroup] | None = None, # Compound boolean tag filter groups
) -> tuple[list[RetrievalResult], GraphRetrievalTimings | None]:
"""
Retrieve relevant facts via graph traversal.
Args:
pool: Database connection pool
query_embedding_str: Query embedding as string (for finding entry points)
bank_id: Memory bank identifier
fact_type: Fact type to filter ('world', 'experience', 'observation')
budget: Maximum number of nodes to explore/return
query_text: Original query text (optional, for some strategies)
semantic_seeds: Pre-computed semantic entry points (from semantic retrieval)
temporal_seeds: Pre-computed temporal entry points (from temporal retrieval)
adjacency: Pre-loaded typed adjacency graph (optional)
tags: Optional list of tags for visibility filtering (OR matching)
Returns:
Tuple of (List of RetrievalResult with activation scores, optional timing info)
"""
pass