Pular para conteúdo

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.

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.).

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

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,
)

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

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
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)

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:
  3. Insights - Compreensão emergente de múltiplos fatos
  4. Padrões - Comportamentos ou preferências repetidos
  5. Contradições - Fatos conflitantes que precisam de resolução
  6. Tendências - Mudanças ao longo do tempo
  7. Gera registros MemoryMetaObservation
  8. Marca eventos com emoções (emoção, intensidade, nível de energia)

Consolidação de Perfil (L3)

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

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

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

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.

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

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.

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

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

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

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}

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

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

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.