# Background Jobs

Os background jobs melhoram a qualidade da memória ao longo do tempo. Eles rodam **separadamente** de `write()` e `retrieve()` - você agenda eles (a cada algumas horas, via cron, APScheduler ou um loop simples).

### Preciso deles?

**Pra começar: não.** Os pipelines de `write()` e `retrieve()` funcionam sem background jobs. Seu agente ainda vai extrair fatos, resolver entidades e retornar contexto relevante.

**Pra produção: sim.** Sem eles, scores de importância ficam flat (0.5 pra tudo), resumos de entidades nunca são gerados, padrões e contradições passam despercebidos, e a qualidade do retrieval degrada com o tempo conforme a memória cresce.

```mermaid
flowchart LR
    A["Scheduler\n(periodic)"] --> B["Clustering"]
    A --> C["Consolidation"]
    A --> D["Importance\nScoring"]
    A --> E["Summary\nRefresh"]
```

## Visão Geral

O `arandu` fornece quatro categorias de background jobs:

| Job | Proposito | Usa LLM? | Frequencia |
|-----|-----------|----------|------------|
| **Clustering** | Agrupar fatos relacionados semanticamente | Sim (resumos) | A cada 4-8 horas |
| **Consolidation** | Detectar padroes, contradicoes, tendencias | Sim | A cada 4-8 horas |
| **Memify** | Converter fatos episodicos em conhecimento procedural/semantico | Sim | Diario |
| **Sleep-time compute** | Pontuar importancia, atualizar resumos, consolidar perfis, detectar comunidades | Parcialmente | A cada 4-8 horas |

Todos os jobs são expostos como funções async que você pode chamar diretamente ou agendar com seu task runner preferido (APScheduler, Celery, cron, etc.).

!!! info "Paralelo com neurociência"
    Os background jobs espelham o **processamento durante o sono** no cérebro. Durante o sono, o cérebro consolida memórias, transfere informações do hipocampo (curto prazo) para o neocórtex (longo prazo), poda conexões irrelevantes e fortalece as importantes. Esses jobs realizam as mesmas operações na memória do seu agente.

---

## Clustering

**Em português claro:** Agrupa fatos relacionados. Fatos sobre o trabalho, colegas e projetos de alguém ficam num cluster. Isso torna o retrieval mais contextual - quando você pergunta sobre o trabalho de alguém, o sistema sabe quais fatos são relacionados.

### Fact Clustering

```python
from arandu import cluster_user_facts, ClusteringResult

result: ClusteringResult = await cluster_user_facts(
    session=db_session,
    agent_id="user_123",
    embedding_provider=embedding_provider,
    llm_provider=llm_provider,
    config=memory_config,
)
```

!!! note "Nomes legados de funções"
    Algumas funções de background jobs ainda mantêm "user" nos nomes (ex: `cluster_user_facts`, `get_entities_for_user`). Elas aceitam `agent_id` como parâmetro - os nomes são históricos e serão aliasados em uma versão futura.

**Como funciona:**

1. Agrupa fatos por `(entity_type, entity_key)` - fatos sobre a mesma entidade ficam juntos
2. Gera um resumo de 2-3 frases por cluster usando um LLM
3. Calcula e armazena embeddings do cluster para detecção de comunidades posterior
4. Idempotente - atualiza clusters existentes em vez de criar duplicatas

### Detecção de Comunidades

```python
from arandu import detect_communities, CommunityDetectionResult

result: CommunityDetectionResult = await detect_communities(
    session=db_session,
    agent_id="user_123",
    embedding_provider=embedding_provider,
    llm_provider=llm_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Compara embeddings de clusters usando similaridade de cosseno
2. Agrupa clusters acima do `community_similarity_threshold` (padrão 0.75)
3. Cria registros `MemoryMetaObservation` com tipo `"community_theme"`
4. Exemplo: uma comunidade "trabalho" pode incluir clusters sobre colegas, projetos e fatos da empresa

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `cluster_max_age_days` | `90` | Idade máxima dos fatos a incluir no clustering |
| `community_similarity_threshold` | `0.75` | Threshold de similaridade de cosseno para agrupar clusters |

??? example "Passo a passo: antes e depois do clustering"
    **Antes do clustering** - 12 fatos sobre um usuário, sem agrupamento:
    ```
    person:fernanda → "Fernanda works at Orion Tech"
    person:fernanda → "Fernanda graduated from Unicamp"
    person:fernanda → "Fernanda built the data pipeline"
    person:bruno → "Bruno works at Orion Tech"
    person:bruno → "Bruno developed the ML model"
    person:bruno → "Bruno runs marathons"
    person:marcos → "Marcos lives in Porto Alegre"
    person:marcos → "Marcos is a product manager"
    ...
    ```

    **Depois do clustering:**
    ```
    Cluster 1: "Orion Tech engineering team"
      → Fernanda works at Orion Tech
      → Bruno works at Orion Tech
      → Fernanda built the data pipeline
      → Bruno developed the ML model
      Summary: "The Orion Tech data/ML team includes Fernanda (pipeline) and Bruno (ML model)"

    Cluster 2: "Marcos's personal life"
      → Marcos lives in Porto Alegre
      → Marcos is married to Carolina
      Summary: "Marcos lives in Porto Alegre with his wife Carolina"
    ```

    **Impacto no retrieval:** Quando alguém pergunta "Me fale sobre o time da Orion Tech", o resumo do cluster e seus fatos pontuam mais alto porque estão agrupados - o sistema entende que são relacionados.

---

## Consolidation

**Em português claro:** Olha pra todos os fatos e eventos recentes e encontra padrões maiores: "Essa pessoa menciona corrida toda segunda" (padrão), "Ela disse que mora em SP mas também em RJ" (contradição), "O humor dela tá melhorando" (tendência). Armazena como meta-observações que enriquecem o retrieval.

### Consolidação Periódica (L2)

```python
from arandu import run_consolidation, ConsolidationResult

result: ConsolidationResult = await run_consolidation(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Analisa eventos e fatos em uma janela de lookback (`consolidation_lookback_days`)
2. Detecta padrões entre fatos:
   - **Insights** - Compreensão emergente de múltiplos fatos
   - **Padrões** - Comportamentos ou preferências repetidos
   - **Contradições** - Fatos conflitantes que precisam de resolução
   - **Tendências** - Mudanças ao longo do tempo
3. Gera registros `MemoryMetaObservation`
4. Marca eventos com emoções (emoção, intensidade, nível de energia)

### Consolidação de Perfil (L3)

```python
from arandu import run_profile_consolidation

await run_profile_consolidation(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
)
```

**Como funciona:**

1. Atualiza resumos de entidades via LLM - uma visão de nível mais alto de cada entidade
2. Atualiza a visão geral do perfil
3. Disparado periodicamente (menos frequente que L2)

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `consolidation_min_events` | `3` | Mínimo de eventos antes de rodar consolidation |
| `consolidation_lookback_days` | `7` | Quantos dias olhar para trás em busca de padrões |

!!! info "Paralelo com neurociência"
    A consolidation espelha a **consolidação de memória durante o sono** do cérebro. O hipocampo repete experiências recentes, o neocórtex detecta padrões e os integra em estruturas de conhecimento existentes, e contradições são sinalizadas para resolução. A consolidação L2 é análoga ao replay durante o sono de ondas lentas (SWS), enquanto a consolidação de perfil L3 é análoga ao papel do sono REM na integração de memórias em conhecimento semântico.

---

## Memify

**Em português claro:** Com o tempo, detalhes específicos ("foi num meetup de Python dia 5 de março") viram conhecimento geral ("frequenta meetups de tech regularmente"). O Memify destila fatos episódicos em conhecimento de nível mais alto e poda fatos que não são mencionados há tempo.

### Executar Memify

```python
from arandu import run_memify, MemifyResult

result: MemifyResult = await run_memify(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    embedding_provider=embedding_provider,
    config=memory_config,
)
```

**Como funciona:**

1. Agrupa fatos relacionados por entidade e tópico
2. Gera resumos destilados (conhecimento procedural/semântico)
3. Verifica vitalidade - fatos mencionados recentemente são mantidos; fatos obsoletos podem ser deprecados
4. Mescla procedimentos similares para prevenir fragmentação de conhecimento

### Scoring de Vitalidade

```python
from arandu import compute_vitality

# Síncrono, por fato — NÃO é async
score = compute_vitality(fact)          # usa datetime.now(UTC)
score = compute_vitality(fact, now=ts)  # timestamp customizado
```

A vitalidade mede quão "vivo" um fato está com base em:

- **Retrieval** (0.30) - Escala logarítmica de times_retrieved
- **Recência** (0.25) - Decay exponencial a partir de last_retrieved_at (half-life de 30 dias)
- **Confiança** (0.20) - Valor bruto de confidence
- **Reforço** (0.15) - Contagem limitada de reinforcement (até 5)

Penalidade por correção: `0.8 ^ user_correction_count`. Fatos supersedidos (valid_to is not None) retornam 0.0.

!!! info "Paralelo com neurociência"
    O memify espelha a **curva de esquecimento** descrita por Hermann Ebbinghaus (1885). Memórias decaem exponencialmente ao longo do tempo a menos que sejam reforçadas através de prática de recuperação. Fatos com alta vitalidade (acessados frequentemente) resistem ao decay, enquanto fatos de baixa vitalidade vão se apagando gradualmente - assim como o cérebro poda conexões sinápticas para informações não utilizadas.

---

## Sleep-Time Compute

**Em portugues claro:** Quatro jobs de manutencao que mantem o retrieval afiado: (1) pontuar quais entidades sao mais importantes, (2) atualizar resumos das entidades importantes, (3) consolidar perfis de entidade a partir de todos os fatos, (4) detectar comunidades de entidades relacionadas. O primeiro e SQL puro (barato), os outros tres usam LLM.

### Job 1: Entity Importance Scoring

```python
from arandu import compute_entity_importance, EntityImportanceResult

result: EntityImportanceResult = await compute_entity_importance(
    session=db_session,
    agent_id="user_123",
    config=memory_config,
)
```

Computação pura em SQL (sem chamadas LLM). Pontua cada entidade de 0.0 a 1.0 usando quatro sinais normalizados:

| Sinal | Peso | Descrição |
|-------|------|-----------|
| Densidade de fatos | 0.30 | Número de fatos linkados à entidade (via `MemoryFactEntityLink`). Inclui fatos onde a entidade é subject primário E fatos que apenas a mencionam. |
| Recência | 0.25 | Decay exponencial (half-life de 30 dias) |
| Frequência de retrieval | 0.25 | Quão frequentemente fatos sobre essa entidade são recuperados |
| Grau de relacionamento | 0.20 | Número de relacionamentos entrantes + saintes |

O importance score é usado como sinal no scoring de retrieval e como fator de prioridade para atualização de resumos.

??? example "Passo a passo: como o importance scoring muda o retrieval"
    **Antes do importance scoring** - todas as entidades com importância padrão = 0.5:
    ```
    person:fernanda  → importance: 0.50 (padrão)
    person:marcos    → importance: 0.50 (padrão)
    organization:vertix → importance: 0.50 (padrão)
    product:xgboost  → importance: 0.50 (padrão)
    ```

    **Depois do importance scoring:**
    ```
    person:fernanda  → importance: 0.85 (12 fatos, recente, alta frequência de retrieval)
    organization:vertix → importance: 0.78 (8 fatos, muitos relacionamentos)
    person:marcos    → importance: 0.55 (4 fatos, atividade moderada)
    product:xgboost  → importance: 0.25 (1 fato, mencionado uma vez, sem relacionamentos)
    ```

    **Impacto no retrieval:** Quando dois fatos têm scores semânticos similares, o sobre Fernanda (importance=0.85) ranqueia acima do sobre XGBoost (importance=0.25). Isso reflete a realidade de que Fernanda é uma entidade central na memória deste usuário enquanto XGBoost é um detalhe periférico.

    **Impacto no summary refresh:** Fernanda e Vertix recebem atualização de resumo primeiro (maior prioridade). XGBoost pode nunca receber um resumo - a prioridade é muito baixa.

### Job 2: Entity Summary Refresh

```python
from arandu import refresh_entity_summaries, SummaryRefreshResult

result: SummaryRefreshResult = await refresh_entity_summaries(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

Atualiza resumos obsoletos de entidades:

- **Condição de obsolescência**: `summary_text IS NULL` ou última atualização > 7 dias atrás
- **Prioridade**: entidades com `importance_score` mais alto são atualizadas primeiro
- **Limite**: 10 entidades por execução (previne timeout)
- Gera resumos de 2-3 frases a partir dos fatos da entidade usando um LLM

### Job 3: Consolidacao de Perfis de Entidade

```python
from arandu import consolidate_entity_profiles, ProfileConsolidationResult

result: ProfileConsolidationResult = await consolidate_entity_profiles(
    session=db_session,
    agent_id="user_123",
    llm_provider=llm_provider,
    config=memory_config,
)
```

Reconstroi perfis de entidade (`profile_text`) a partir de **todos** os fatos ativos de cada entidade. Diferente do write pipeline (que apenas semeia perfis curtos para entidades novas sem perfil), este job gera perfis abrangentes que cobrem todos os aspectos relevantes da entidade.

**Como funciona:**

1. Seleciona entidades com mais fatos primeiro (priorizacao por densidade)
2. Filtra entidades cujo perfil e `NULL` ou esta obsoleto (mais antigo que `summary_refresh_interval_days`)
3. Carrega ate 50 fatos ativos por entidade
4. Gera perfil abrangente via LLM (timeout de 15s por entidade)
5. Processa ate 20 entidades por execucao

**Relacao com o write pipeline:** O write pipeline so semeia perfis para entidades que ainda nao possuem um -- nunca sobrescreve perfis existentes. Este background job e a **fonte autoritativa** para perfis. Na pratica, o fluxo e:

1. Primeira mensagem sobre "Ana" → write pipeline cria perfil inicial simples
2. Mensagens subsequentes sobre "Ana" → write pipeline nao toca o perfil
3. Background job roda → le todos os fatos sobre "Ana" e gera perfil abrangente, sobrescrevendo o perfil inicial

### Job 4: Entity Community Detection

```python
from arandu import detect_entity_communities

result = await detect_entity_communities(
    session=db_session,
    agent_id="user_123",
    config=memory_config,
)
```

Clustering por componentes conectados no grafo de relacionamentos de entidades (sem chamadas LLM):

1. Carrega entidades ativas e arestas (strength >= 0.3)
2. Executa Union-Find (com path compression + union by rank) para encontrar componentes conectados
3. Agrupa entidades em comunidades (componentes com >= 2 membros)
4. Retorna `{"communities_found": int, "total_entities_assigned": int}`

!!! info "Paralelo com neurociência"
    O sleep-time compute espelha o **processamento offline durante o sono**. O cérebro não apenas armazena memórias passivamente durante o sono - ele as reorganiza ativamente. O importance scoring é análogo ao processo de **homeostase sináptica** (Tononi & Cirelli), onde sinapses fortemente ativadas são mantidas enquanto fracamente ativadas são podadas. O summary refresh espelha a formação de **memórias de essência** - representações comprimidas que capturam a essência de episódios detalhados.

---

## Agendamento

O `arandu` não inclui um scheduler - você traz o seu. Todas as funções de background são simples callables async que podem ser integradas com qualquer sistema de agendamento.

### Exemplo: APScheduler

```python
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from arandu import (
    cluster_user_facts,
    run_consolidation,
    compute_entity_importance,
    consolidate_entity_profiles,
    refresh_entity_summaries,
)

scheduler = AsyncIOScheduler()

async def maintenance_cycle():
    async with get_session() as session:
        for agent_id in await get_active_users(session):
            await compute_entity_importance(session, agent_id, config=config)
            await refresh_entity_summaries(session, agent_id, llm_provider=llm, config=config)
            await consolidate_entity_profiles(session, agent_id, llm_provider=llm, config=config)
            await cluster_user_facts(session, agent_id, embeddings, llm, config)
            await run_consolidation(session, agent_id, llm_provider=llm, config=config)

scheduler.add_job(maintenance_cycle, "interval", hours=4)
scheduler.start()
```

### Exemplo: Loop Simples

```python
import asyncio

async def background_loop():
    while True:
        await maintenance_cycle()
        await asyncio.sleep(4 * 3600)  # a cada 4 horas
```

### Cadência Recomendada

| Job | Frequencia | Custo |
|-----|-----------|-------|
| Entity importance | A cada 4h | Barato (apenas SQL) |
| Summary refresh | A cada 4h | Moderado (LLM, limitado a 10/execucao) |
| Profile consolidation | A cada 4h | Moderado (LLM, limitado a 20/execucao) |
| Clustering | A cada 4-8h | Moderado (LLM para resumos) |
| Consolidation | A cada 4-8h | Moderado (LLM para deteccao de padroes) |
| Memify | Diario | Moderado (LLM para destilacao) |
| Community detection | Diario | Moderado (LLM + embeddings) |

Execute importance scoring primeiro - sua saída é usada pelo summary refresh para priorizar entidades.
