# PRD: Living Entity Profiles

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

## Problem

Hoje o Arandu armazena conhecimento sobre entidades como listas planas de fatos. Caroline tem 119 fatos soltos no LoCoMo. Pra responder "who is Caroline?", o sistema precisa fazer embedding search, juntar pedaços de diferentes fatos, e torcer pro LLM montar uma resposta coerente. O campo `summary_text` no `MemoryEntity` existe mas depende de um background job (`refresh_entity_summaries` em `sleep_time.py`) que no LoCoMo não rodou — todas as 15 entidades ficaram com summary=null.

Isso causa dois problemas concretos:

1. **Queries amplas respondem mal** — "Tell me about Mel", "What do you know about Caroline?" exigem que o retrieval acerte na seleção de fatos e que o LLM sintetize uma resposta coerente a partir de uma lista. Com 119 fatos e budget de 20 no retrieval, a resposta é necessariamente incompleta e fragmentada.

2. **O pré-retrieval do PRD 1 é subótimo** — A Memory-Aware Extraction (PRD 1) precisa injetar contexto existente no prompt de extração. Sem perfis, isso significa buscar top-K fatos por embedding similarity a cada write — uma operação de busca vetorial que retorna fatos soltos, não uma visão coerente da entidade. O LLM recebe pedaços desconexos em vez de um entendimento estruturado.

A causa raiz: o sistema não tem um **modelo mental** de cada entidade. Tem dados, mas não entendimento.

## Target Audience

Desenvolvedores usando o Arandu SDK. O impacto é duplo: (1) melhora a qualidade do retrieval pra queries amplas sobre entidades, e (2) melhora a qualidade da extração informada ao dar contexto mais coerente ao LLM.

## Proposed Solution

Manter um perfil vivo por entidade (~100-300 tokens de texto coerente) que é atualizado como parte da mesma chamada LLM da extração informada (PRD 1). O perfil é simultaneamente:

1. **Input da extração** — Substitui o pré-retrieval de fatos individuais. Em vez de buscar top-K fatos por embedding similarity, o pipeline carrega o perfil da entidade (um SELECT simples).
2. **Output da extração** — O LLM recebe o perfil atual + mensagem nova e devolve o perfil atualizado junto com os fatos extraídos. Só atualiza quando há mudança (null = sem alteração).
3. **Sinal de retrieval** — Queries amplas usam o perfil diretamente. Queries específicas continuam usando os fatos atômicos.

Não há chamada LLM adicional. O perfil é gerado/atualizado dentro da mesma chamada de extração informada, como um campo a mais no output JSON. O custo marginal é ~100-200 tokens extras no output.

O ciclo é auto-alimentado: perfil informa a extração → extração atualiza o perfil → perfil melhor informa a próxima extração.

## Success Criteria

1. **Queries amplas melhoram** — Pra queries tipo "Who is [entity]?" ou "Tell me about [entity]", o sistema retorna uma resposta coerente e abrangente sem depender de background jobs. Medido qualitativamente em re-run do LoCoMo.
2. **Perfis sempre populados** — Após o primeiro write que toca uma entidade, `profile_text` nunca é null (diferente do `summary_text` atual que depende de background job).
3. **Pré-retrieval simplificado** — O write pipeline não faz embedding search pra montar contexto de extração. Carrega perfis via SELECT, latência < 5ms.
4. **Sem regressão** — Fatos atômicos continuam existindo e sendo usados pra queries específicas. O perfil é aditivo, não substitutivo.
5. **Cold start gracioso** — Primeira mensagem: sem perfil, extração cega. Após extração: perfil inicial criado. A partir da segunda mensagem: ciclo informado ativo.

## Scope v0

- **Campo `profile_text`** no `MemoryEntity`: texto livre, ~100-300 tokens. Pode reutilizar `summary_text` existente ou criar campo separado (decisão na spec).
- **Integração com extração informada (PRD 1)**: O prompt unificado recebe perfis das entidades conhecidas como contexto. O output inclui campo `updated_profiles` (dict entity_name → new_profile_text | null).
- **Upsert do perfil**: Na mesma transação do upsert de fatos, atualiza `profile_text` das entidades que tiveram perfil modificado.
- **Embedding do perfil**: Ao atualizar `profile_text`, re-embedar e armazenar no `embedding_vec` da entidade (substitui embedding do nome, que é menos útil).
- **Uso no retrieval**: O read pipeline usa `profile_text` como contexto adicional no hot tier pra queries que mencionam entidades específicas.
- **Fallback**: Se o LLM não retornar `updated_profiles` (campo ausente ou malformado), manter o perfil atual inalterado. Se perfil nunca foi gerado, o pré-retrieval cai de volta pra embedding search de fatos individuais (comportamento do PRD 1 puro).

## Anti-scope

- **Perfis estruturados com seções** — v0 usa texto livre. Seções tipadas ([Identity], [Relationships], [Recent]) podem vir num v1 se necessário.
- **Perfis pra entidades non-person** — v0 foca em entidades tipo "person". Organizações, lugares, etc. podem ter perfis no futuro, mas não no v0.
- **Merge de `summary_text` e `profile_text`** — Se decidirmos por campo separado, o `summary_text` existente e o `refresh_entity_summaries` do background job ficam intocados. Não refatorar o que já existe.
- **Profile history/versioning** — Não manter versões anteriores do perfil. O perfil é uma view materializada, não um ledger. O histórico está nos fatos.
- **Profile-based retrieval signal** — v0 não cria um novo signal de retrieval baseado em embedding do perfil. O perfil entra no contexto como texto, não como signal separado no scoring.
- **Configuração de tamanho/formato do perfil** — v0 usa um guideline fixo no prompt (~100-300 tokens, texto corrido). Sem config exposta pro usuário.
- **Mudanças no background job existente** — `refresh_entity_summaries` continua como está. Não matar, não modificar.

## Technical Context

### O que já existe no codebase

- **`models.py::MemoryEntity`** — Já tem campos `summary_text` (nullable text) e `summary_refreshed_at` (nullable datetime). Também tem `embedding_vec` (Vector 1536) que hoje armazena embedding do nome da entidade.
- **`background/sleep_time.py::refresh_entity_summaries()`** — Gera summaries via LLM pra entidades com summary stale. Top 10 por importance. É o mecanismo que falhou no LoCoMo (não rodou).
- **`write/pipeline.py::run_write_pipeline()`** — Orquestrador do write. Ponto onde o perfil seria carregado (antes da extração) e persistido (após upsert).
- **`read/context_compression.py`** — Monta o contexto tiered (hot/warm/cold). Ponto onde o perfil seria injetado pra queries sobre entidades.
- **`read/retrieval.py::semantic_search()`** — Busca por embedding similarity. Continuaria sendo usado pra queries específicas; queries amplas usariam o perfil.
- **PRD 1 (Memory-Aware Extraction)** — Define o novo prompt unificado e output schema. O perfil se integra como campo adicional no input (contexto) e output (updated_profiles) dessa mesma chamada.

### Patterns a seguir

- **Pipeline orchestration**: Perfil carregado e persistido dentro do mesmo `run_write_pipeline()`, nas mesmas transações.
- **Fail-safe**: Se `updated_profiles` vier malformado, manter perfil atual. Nunca corromper perfil existente.
- **Dependency injection**: Embedding do perfil usa o mesmo `EmbeddingProvider` injetado.

### Constraints conhecidos

- **Dependência do PRD 1**: Os perfis fazem mais sentido como parte da extração informada. Se PRD 1 não for implementado, perfis ainda podem funcionar (gerados por um passo separado), mas a elegância se perde.
- **Tamanho do output JSON**: O output da extração informada já tem entities + facts + relations. Adicionar `updated_profiles` aumenta a complexidade do parse. Testar robustez com DeepSeek V3 e OpenAI.
- **Re-embedding a cada write**: Atualizar `embedding_vec` com embedding do perfil (em vez do nome) melhora o retrieval mas adiciona uma chamada de embedding a cada write que modifica perfil. Custo marginal baixo (1 embedding call), mas precisa estar no timing budget.

## Open Questions

1. **Reutilizar `summary_text` ou criar `profile_text`?** Reutilizar é mais simples (sem migration), mas mistura dois conceitos (summary do background job vs. profile do write pipeline). Campo separado é mais limpo mas requer migration. Decisão na spec.
2. **Embedding do perfil vs. embedding do nome**: Hoje `embedding_vec` armazena embedding do nome da entidade, usado no fuzzy entity resolution. Se substituirmos por embedding do perfil, o entity resolution por fuzzy embedding pode ser afetado. Avaliar se entity resolution usa `embedding_vec` e qual o impacto.
3. **Guideline de tamanho do perfil no prompt**: O prompt precisa instruir o LLM sobre tamanho/estilo do perfil. Precisa de experimentação pra achar o sweet spot (muito curto perde informação, muito longo encarece o ciclo).
