# Read Pipeline

The read pipeline finds facts relevant to a query using multiple signals in parallel. Every call to `memory.retrieve()` runs five stages: **Plan → Retrieve → Enhance → Rerank → Format**.

```mermaid
flowchart LR
    A["Query"] --> B["Plan"]
    B --> C["Retrieve\n(3 signals)"]
    C --> D["Enhance"]
    D --> E["Rerank"]
    E --> F["RetrieveResult"]
```

## Overview

When you call `memory.retrieve(user_id, query)`, the pipeline:

1. **Plans the retrieval** — An LLM planner analyzes the query and decides the strategy, reformulates the query for semantic search, and identifies relevant entities for graph traversal.
2. **Retrieves candidates** — Three parallel signals (semantic, keyword, graph) find candidate facts.
3. **Enhances results** — Spreading activation expands context from seed facts along entity relationships.
4. **Reranks** — An optional LLM reranker refines the ranking based on query intent.
5. **Formats** — Facts are scored, compressed within a token budget, and formatted into a context string.

---

## Stage 1: Retrieval Agent (Planner)

The retrieval agent is an LLM-powered planner that decides **how** to retrieve, not just **what** to retrieve. It analyzes the query and produces a `RetrievalPlan`.

### What the Planner Decides

| Field | Description | Example |
|-------|-------------|---------|
| `strategy` | Retrieval strategy | `"multi_signal"` (default) or `"skip"` (for greetings) |
| `similarity_query` | Reformulated query for semantic search | "user location city" (from "where do I live?") |
| `entities` | Entity keys for graph signal | `["person:ana", "organization:acme"]` |
| `as_of_range` | Time-travel window (optional) | `{"start": "2024-01-01", "end": "2024-06-30"}` |
| `broad_query` | Whether to expand graph scope | `true` for "tell me everything about..." |
| `reason` | Explanation of the strategy | For debugging and observability |

### Query Reformulation

The planner doesn't just pass the user's query to semantic search. It **reformulates** it to improve vector similarity matching:

- `"where do I live?"` → `"user location city residence"`
- `"what does Ana do for work?"` → `"Ana profession occupation job role"`

This bridges the vocabulary gap between how users ask questions and how facts are stored.

### Schema-Aware Planning

The planner inspects the actual memory schema for this user:

- Which entity types exist (persons, organizations, places...)
- Which entities have the most facts
- What attributes are stored

This grounds the plan in reality — the planner won't search for entities that don't exist.

### Skip Strategy

For greetings and casual messages ("hi", "how are you?"), the planner returns `strategy: "skip"`, short-circuiting the pipeline. No database queries, no LLM calls, instant response.

!!! info "Neuroscience parallel"
    The retrieval agent mirrors **retrieval cues** in cognitive psychology. When you try to remember something, your brain doesn't do an exhaustive search — it uses contextual cues to narrow down the search space. The planner identifies entities and reformulates queries as cues that guide the retrieval signals.

---

## Stage 2: Multi-Signal Retrieval

Three independent signals run **in parallel** via `asyncio.gather()`, each finding candidates from a different angle:

```mermaid
flowchart TD
    P["RetrievalPlan"] --> S["Semantic Search\n(pgvector cosine)"]
    P --> K["Keyword Search\n(SQL ILIKE)"]
    P --> G["Graph Traversal\n(BFS 2-hop)"]
    S --> M["Merge & Rank\n(RRF + weighted scoring)"]
    K --> M
    G --> M
```

### Signal 1: Semantic Search

Uses pgvector cosine similarity to find facts whose embeddings are close to the query embedding.

- Embeds the reformulated query (from the planner)
- Searches the `MemoryFact` table with HNSW index
- Returns top-N candidates above `min_similarity` threshold
- Filters: `user_id`, active facts (`valid_to IS NULL`), confidence ≥ `min_confidence`

This is the primary signal — it finds facts that are **semantically similar** to the query, even if they don't share exact keywords.

### Signal 2: Keyword Search

SQL ILIKE matching on `fact_text` for exact or partial keyword hits.

- Extracts significant words (> 2 characters) from the query
- Matches against fact text (up to 5 keywords)
- Score = fraction of query words found in the fact

This complements semantic search by catching exact matches that embedding similarity might miss (e.g., proper nouns, technical terms, abbreviations).

### Signal 3: Graph Retrieval

Traverses entity relationships to find facts connected to the query entities.

- Starts from entities identified by the planner
- BFS traversal up to 2 hops through `MemoryEntityRelationship`
- Scoring formula: `edge_strength × recency_factor × edge_recency_factor × query_bonus`
- `query_bonus`: 1.5× when the entity name appears in the query text

Graph retrieval excels at finding **contextual** facts. When you ask about a person, it also finds facts about their workplace, their relationships, and their projects.

### Merge & Rank

After all three signals return, results are merged:

1. **Deduplicate** by fact ID (same fact may appear in multiple signals)
2. **Apply recency decay** — Exponential decay with configurable half-life (`recency_half_life_days`, default 14)
3. **Apply confidence decay** — Older facts with lower confidence are penalized
4. **Compute combined score** — Weighted sum:

```python
score = (
    score_weights["semantic"]   * semantic_score +    # default 0.70
    score_weights["recency"]    * recency_score +     # default 0.20
    score_weights["importance"] * importance_score     # default 0.10
)
```

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `topk_facts` | `20` | Maximum facts to return |
| `topk_events` | `8` | Maximum events to consider |
| `min_similarity` | `0.20` | Minimum cosine similarity for semantic results |
| `min_confidence` | `0.55` | Minimum fact confidence |
| `recency_half_life_days` | `14` | Half-life for recency decay |
| `score_weights` | See above | Weights for each scoring signal |
| `enable_reranker` | `True` | Whether to use LLM reranking |

!!! info "Neuroscience parallel"
    Multi-signal retrieval mirrors **spreading activation** in semantic networks (Collins & Loftus, 1975). When you think of "doctor", activation spreads to related concepts ("hospital", "medicine", "appointment") through associative links. Similarly, graph retrieval spreads from query entities along relationship edges, while semantic search activates facts through embedding proximity.

---

## Stage 3: Enhancement

### Spreading Activation

Starting from the top-K seed facts, the pipeline expands context by following entity relationships:

- For each seed fact, find its entity's relationships
- Traverse relationships for N hops (`spreading_activation_hops`, default 2)
- Apply decay factor per hop (`spreading_decay_factor`, default 0.50)
- Return up to M additional facts per entity

This catches important context that wasn't directly matched. If you ask "what does Rafael do?", spreading activation might surface facts about his workplace, team, and projects.

### Pattern Signal

Facts that have been reinforced multiple times (confirmed by NOOP decisions in write) get an additive score boost:

- High reinforcement count → up to 0.10 extra score
- Captures frequently mentioned, well-established facts

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `spreading_activation_hops` | `2` | Maximum hops from seed facts |
| `spreading_decay_factor` | `0.50` | Score decay per hop (0.5 = halved each hop) |

---

## Stage 4: Reranking (Optional)

When `enable_reranker=True`, the top candidates are reranked by an LLM that considers query intent:

- Respects the semantic meaning of the query (not just keyword overlap)
- Can promote facts that are indirectly relevant but important
- Graceful degradation: if the reranker fails (timeout, error), the original ranking is preserved

The reranker is the most expensive stage but provides the highest quality improvement for complex queries.

---

## Stage 5: Formatting

The final stage converts ranked facts into consumable output.

### Context Compression

Facts are divided into three tiers within a token budget (`context_max_tokens`):

| Tier | Budget share | Content |
|------|-------------|---------|
| **Hot** | 50% | Most relevant facts (highest scores) |
| **Warm** | 30% | Supporting context |
| **Cold** | 20% | Background facts (lower scores but still relevant) |

### Configuration

| Parameter | Default | Description |
|-----------|---------|-------------|
| `context_budget_tokens` | `3000` | Total token budget for retrieval |
| `context_max_tokens` | `2000` | Maximum tokens in formatted context |
| `hot_tier_ratio` | `0.50` | Share of budget for top facts |
| `warm_tier_ratio` | `0.30` | Share of budget for supporting facts |

### Output Format

The `context` string is formatted for direct injection into LLM prompts:

```
## Known facts about the user:
- Lives in São Paulo (confidence: 0.95)
- Works at Acme Corp as a backend engineer (confidence: 0.90)
- Wife's name is Ana (confidence: 0.92)
```

---

## RetrieveResult

```python
result = await memory.retrieve(user_id="user_123", query="...")

# Pre-formatted context (ready for LLM prompts)
print(result.context)

# Individual facts with scores
for fact in result.facts:
    print(f"[{fact.score:.2f}] {fact.entity_name}: {fact.value}")
    print(f"  Scores: {fact.scores}")  # {"semantic": 0.85, "recency": 0.72, ...}

# Pipeline stats
print(f"Candidates evaluated: {result.total_candidates}")
print(f"Duration: {result.duration_ms:.0f}ms")
```

---

## Pipeline Diagram (Complete)

```mermaid
flowchart TD
    Q["User query"] --> AG["Retrieval Agent\n(LLM planner)"]
    AG -->|skip| SKIP["Return empty\n(greeting/casual)"]
    AG -->|multi_signal| PAR["Parallel retrieval"]
    PAR --> SEM["Semantic Search\n(pgvector cosine)"]
    PAR --> KW["Keyword Search\n(SQL ILIKE)"]
    PAR --> GR["Graph Traversal\n(BFS 2-hop)"]
    SEM --> MERGE["Merge & Rank\n(dedup + weighted scoring)"]
    KW --> MERGE
    GR --> MERGE
    MERGE --> SA["Spreading Activation\n(expand context along edges)"]
    SA --> RR{"Reranker\nenabled?"}
    RR -->|yes| RERANK["LLM Rerank"]
    RR -->|no| FMT["Format & Compress"]
    RERANK --> FMT
    FMT --> RES["RetrieveResult"]
```

!!! info "Neuroscience parallel"
    The tiered compression (hot/warm/cold) mirrors **levels of activation** in working memory. In Cowan's embedded-process model, a small number of items are in the focus of attention (hot tier), surrounded by activated long-term memory (warm tier), with the rest of long-term memory available but not active (cold tier). The token budget acts as the capacity limit of working memory.
