# PRD: Memory-Aware Extraction

> Generated via /vibeflow:discover on 2026-03-30

## Problem

O write pipeline do Arandu trata extração como uma tarefa de NLP isolada: o LLM recebe uma mensagem e extrai "todos os fatos". Ele não sabe o que o sistema já sabe. Isso causa três problemas concretos, evidenciados no benchmark LoCoMo (F1=0.012, conv-26):

1. **Duplicatas massivas** — "Mel friend of Caroline" foi extraído 8 vezes em sessões diferentes. O extrator não sabe que essa informação já existe, então extrai de novo. A reconciliação posterior (passo separado) deveria pegar isso como NOOP, mas falha em parte dos casos.
2. **Ruído conversacional** — 10% dos fatos extraídos são ações efêmeras ("Caroline is greeting Mel", "Caroline thanked Melanie") que não têm valor de memória a longo prazo. O extrator não tem contexto pra saber que greetings são padrão recorrente.
3. **Explosão de relationships** — 44 tipos únicos pra 56 relações. O LLM inventa sinônimos ("admires_work_of", "admires_art_of", "appreciates_work_of") porque não vê as relações que já existem.

A causa raiz é arquitetural: a extração é `f(mensagem)` quando deveria ser `f(mensagem, conhecimento_existente)`. O pipeline compensa com etapas posteriores (dedup semântico, reconciliação LLM), mas isso é patch — adiciona latência (4 chamadas LLM), custo, e pontos de falha sem resolver a causa raiz.

## Target Audience

Desenvolvedores usando o Arandu SDK como sistema de memória conversacional de longo prazo. O impacto é direto na qualidade do retrieval (e portanto na experiência do usuário final do agente/chatbot que consome o SDK).

## Proposed Solution

Redesenhar o write pipeline pra fundir extração e reconciliação num passo único informado pela memória existente. Em vez de 4 chamadas LLM sequenciais (entity scan + fact extraction + relation extraction + reconciliation), o pipeline passa a ter:

1. **Alias lookup (sem LLM)** — Escaneia a mensagem procurando nomes/aliases já conhecidos no cache de entidades. String matching, microsegundos.
2. **Pré-retrieval (sem LLM)** — Pra cada entidade conhecida encontrada, busca os top-K fatos existentes mais relevantes à mensagem via embedding similarity (pgvector). ~10ms.
3. **Extração informada (1 chamada LLM)** — O LLM recebe a mensagem + speaker context + fatos existentes relevantes. Num único passo: identifica entidades (inclusive novas), extrai fatos com ação (NEW/UPDATE), e extrai relações. Fatos já conhecidos simplesmente não são extraídos.

Resultado: de 4 chamadas LLM pra 1, eliminação da etapa de reconciliação, redução drástica de duplicatas e ruído na fonte.

## Success Criteria

1. **Redução de duplicatas** — Em um re-run do LoCoMo conv-26 (mesmas 92 turns, mesmo LLM), duplicatas exatas caem de 19 (7%) pra <=2 (<1%).
2. **Redução de ruído** — Fatos conversacionais sem valor caem de 26 (10%) pra <=8 (<=5%).
3. **Menos chamadas LLM no write** — De 4 chamadas por turno pra 1 chamada + 1 DB lookup.
4. **Sem regressão de recall** — Fatos úteis extraídos no pipeline atual continuam sendo extraídos no novo (>= 95% dos fatos úteis preservados).
5. **Cold start transparente** — Na primeira mensagem (base vazia), o comportamento é equivalente à extração cega atual. Sem lógica especial, sem degradação.

## Scope v0

- **Alias lookup**: Reutilizar o cache de aliases existente (`MemoryEntityAlias`) pra detectar entidades conhecidas na mensagem via string matching (case-insensitive, normalized).
- **Pré-retrieval**: Embedding search nos fatos existentes das entidades detectadas. Top-K por entidade (K=10), budget total de ~800 tokens de contexto.
- **Novo prompt de extração informada**: Um prompt unificado que recebe mensagem + speaker context + contexto existente e produz entities + facts (com action: NEW/UPDATE) + relations num único JSON.
- **Novo output schema**: `FactCandidate` ganha campo `action` (NEW/UPDATE), campo opcional `updates` (texto do fato existente que está sendo atualizado), e campo `importance` (classificação semântica de importância pelo LLM).
- **Importance at extraction**: O LLM classifica cada fato extraído com uma categoria de importância semântica (ex: `biographical_milestone`, `stable_preference`, `routine_activity`, `conversational`). Isso define o `importance` inicial do fato no banco. O `compute_dynamic_importance()` existente no read pipeline continua operando em cima desse valor base — importância semântica (extração) + importância de uso (retrieval) se complementam.
- **Remoção da etapa de reconciliação**: `reconcile.py` deixa de ser chamado no pipeline. O upsert interpreta `action` direto do output da extração.
- **Fallback**: Se a extração informada falhar (timeout, parse error), cair no comportamento atual (extração cega + reconciliação separada) como fail-safe.
- **Dedup semântico pós-extração**: Mantém como safety net no v0, mas com expectativa de que seja quase no-op.

## Anti-scope

- **Entity profiles / living summaries** — Será um PRD separado. O v0 injeta fatos individuais, não perfis consolidados.
- **Importance decay / forgetting** — O v0 não implementa esquecimento ativo. Importância na extração define o valor inicial, dynamic importance modifica no retrieval, mas fatos não são arquivados/deletados automaticamente.
- **Mudanças no read pipeline** — Intocado. Nenhuma alteração em retrieval, reranking, ou context compression.
- **Mudanças no modelo de dados** — `MemoryFact`, `MemoryEntity`, etc. ficam iguais. Não há novas tabelas ou colunas (exceto o campo `action` no schema intermediário de extração).
- **Remoção do `reconcile.py`** — Fica no codebase como fallback. Não deletar no v0.
- **Vocabulário controlado de relationships** — Não entra neste PRD. A extração informada pode reduzir a explosão naturalmente (LLM vê rels existentes), mas não forçamos vocabulário fechado.
- **Background jobs** — Nenhuma mudança em consolidation, clustering, memify, sleep-time.
- **Otimização do alias lookup** — v0 usa scan simples. Não investir em trie, fuzzy matching, ou NER no alias lookup.

## Technical Context

### O que já existe no codebase

- **`write/pipeline.py`** — Orquestrador do write pipeline. Chama extract → resolve_entities → reconcile → upsert. Ponto de entrada: `run_write_pipeline()`.
- **`write/extract.py`** — Extração multi-pass atual (3 passes: entity scan, fact extraction, relation extraction). Prompts inline como constantes. Dedup exato + normalização de subjects.
- **`write/reconcile.py`** — Reconciliação Mem0-style. Busca fatos existentes por embedding similarity (threshold 0.5), chama LLM pra decidir ADD/UPDATE/NOOP/DELETE. Output: `ReconcileDecision`.
- **`write/entity_resolution.py`** — Entity resolution 3-fase (exact → fuzzy → LLM). Já tem alias cache e string matching.
- **`write/upsert.py`** — Persistência de fatos. Já implementa ADD, UPDATE (close old + create new), NOOP (confirm), DELETE. Cascade invalidation de relationships.
- **`constants.py`** — Schemas Pydantic (`FactCandidate`, `ExtractionOutput`, `ExtractedEntity`). Tipos canônicos de relationship.
- **`config.py`** — `MemoryConfig` com todos os parâmetros tunable (thresholds, timeouts, top-K).
- **`models.py`** — `MemoryEntityAlias` com campo `alias` (normalized) e `canonical_entity_key`.

### Patterns a seguir

- **Pipeline orchestration**: `run_write_pipeline()` orquestra stages, mede timing, acumula tokens. Stages usam savepoints.
- **LLM interaction**: Prompts inline, JSON parse com `_strip_markdown_fences()`, fail-safe (retorna defaults em caso de erro).
- **Dependency injection**: LLM e embeddings injetados via protocols. Sem imports diretos de providers.
- **Error handling**: Savepoint per-operation, fail-safe com defaults seguros, exception chaining.

### Constraints conhecidos

- O pré-retrieval precisa de um `AsyncSession` ativo (já disponível no pipeline).
- O alias lookup precisa carregar aliases do banco. Pode cachear em memória durante o pipeline.
- O prompt unificado será maior que os prompts individuais atuais. Testar com DeepSeek V3 e OpenAI pra garantir que a qualidade do output se mantém.
- O output JSON unificado é mais complexo (entities + facts com action + relations). Risco de parse errors maior — o fallback pra extração cega é essencial.

## Open Questions

1. **Merge de fact + relation extraction no mesmo prompt**: A extração atual separa fatos e relações em passes diferentes por questões de qualidade. Testar se um prompt unificado mantém a qualidade de ambos, ou se relations deveriam continuar como passo separado (seria 2 LLM calls em vez de 1, ainda melhor que 4).
2. **Threshold do pré-retrieval**: Qual similaridade mínima pra incluir um fato existente no contexto? Se muito baixo, injeta fatos irrelevantes. Se muito alto, perde contexto útil. Precisa de experimentação.
3. **Comportamento com muitas entidades**: Se uma mensagem menciona 5+ entidades conhecidas, o budget de contexto (800 tokens) pode não comportar. Definir estratégia de priorização (entidades com mais menções na mensagem? Entidades mais recentes?).
4. **Categorias de importância**: Definir o set de categorias (ex: `biographical_milestone`, `relationship_change`, `stable_preference`, `specific_event`, `routine_activity`, `conversational`) e o mapeamento pra valores numéricos de importance. Precisa de experimentação pra calibrar.
