MCP Platform

Runtime MCP local com orquestração multiagente, servidores de ferramentas distribuídos e recomendações de mídia baseadas em aprendizado de máquina.

Documentação

MCP Platform Tests

Runtime MCP local com orquestração multiagente, servidores de ferramentas distribuídos, recomendações de mídia com ML, memória persistente entre sessões e um agendador proativo de agentes.

⚠️ Experimental — destinado apenas para uso pessoal e experimental, não para implantação em produção.


Sumário


Pré-requisitos

  • Python 3.12+
  • 16GB+ de RAM recomendado
  • Ollama instalado

1. Início Rápido

Instalar Dependências

cd mcp-platform
python3 -m venv .venv
source .venv/bin/activate       # Linux/macOS
.venv\Scripts\activate          # Windows PowerShell
pip install -r requirements.txt

Nota sobre WSL2: criar .venv dentro de /mnt/c/ falha porque o NTFS não suporta os symlinks Unix que os venvs exigem. Crie o venv no sistema de arquivos Linux:

python3 -m venv /home/$USER/.virtualenvs/mcp_platform
source /home/$USER/.virtualenvs/mcp_platform/bin/activate
pip install -r requirements.txt

Backend de LLM

curl -fsSL https://ollama.com/install.sh | sh
OLLAMA_HOST=0.0.0.0:11434 ollama serve
ollama pull qwen2.5:14b-instruct-q4_K_M   # primary inference (recommended — any tool-calling model works)
ollama pull qwen2.5:0.5b                  # routing classifier (fast pre-flight intent detection)
ollama pull bge-large                     # RAG document embeddings
ollama pull qwen3-vl:8b-instruct          # vision / image analysis

Alternativa GGUF: arquivos GGUF locais (ex.: do Hugging Face) também funcionam — baixe o arquivo e execute :gguf add <path_to_file> no prompt. Não é necessário editar a configuração.

Modelos sentence-transformers (embeddings de memória + reranker RAG) são baixados automaticamente no primeiro uso — não é necessário pull manual.

Iniciar o Cliente

python client.py

Acesse a interface web em: http://localhost:9000/client/ui/index.html


2. Usando Servidores MCP com Outros Clientes

Adicione à configuração do seu cliente MCP (ex.: claude_desktop_config.json):

{
    "mcpServers": {
        "code_assistant": {
            "command": "/path/to/mcp-platform/.venv/bin/python",
            "args": ["/path/to/mcp-platform/servers/code_assistant/server.py"]
        }
    }
}

Caminhos do Windows: "command": "C:\\path\\to\\mcp-platform\\.venv\\Scripts\\python.exe"

Servidores disponíveis:

  • code_assistant - Análise, geração e refatoração de código com IA (12 ferramentas)
  • code_review - Revisão de código, busca e correção de bugs (3 ferramentas)
  • code_runner - Sandbox de execução Python/bash (4 ferramentas)
  • discord - Notificações de canal Discord via webhook (2 ferramentas) ⚠️ Requer DISCORD_WEBHOOK_URL
  • github - Clonar, navegar e limpar repositórios GitHub (4 ferramentas) ⚠️ Requer GITHUB_TOKEN para repositórios privados
  • google - Gmail + Google Calendar (13 ferramentas) ⚠️ Requer configuração do Google (Apps Script ou OAuth — veja Configuração do Google)
  • image - Busca de imagens, análise e geração com IA (6 ferramentas) ⚠️ Requer SERPER_API_KEY para busca; geração é gratuita
  • location - Clima, hora, localização (3 ferramentas) — usa Open-Meteo (gratuito, sem chave); usa OpenWeatherMap como fallback se OPENWEATHER_API_KEY estiver definido
  • plex - Biblioteca de mídia + recomendações com ML (18 ferramentas) ⚠️ Requer PLEX_URL, PLEX_TOKEN
  • rag - Busca e gerenciamento vetorial (8 ferramentas) ⚠️ Requer Ollama + bge-large
  • system - Informações do sistema e processos (3 ferramentas)
  • text - Processamento de texto e busca na web (8 ferramentas)
  • trilium - Integração com notas Trilium (11 ferramentas) ⚠️ Requer TRILIUM_URL, TRILIUM_TOKEN

3. Configuração do Cliente

Variáveis de Ambiente

Crie .env na raiz do projeto:

# === LLM Backend ===
OLLAMA_BASE_URL=http://127.0.0.1:11434  # Use 127.0.0.1 for local; LAN IP requires OLLAMA_HOST=0.0.0.0 on the server
OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct
MAX_MESSAGE_HISTORY=30
LLM_MESSAGE_WINDOW=15        # Sliding window of messages sent to LLM; older turns fall into Conversation RAG
LLM_TEMPERATURE=0.3
OLLAMA_NUM_CTX=8192          # KV cache / context window size
OLLAMA_NUM_PREDICT=4096      # Max tokens the LLM will generate per response
OLLAMA_REPEAT_PENALTY=1.1    # Penalise token repetition (1.0 = disabled)
IMAGE_MODEL=flux             # Model used for AI image generation

# === GGUF Configuration ===
GGUF_GPU_LAYERS=-1
GGUF_CONTEXT_SIZE=4096
GGUF_BATCH_SIZE=512

# === API Keys ===
PLEX_URL=http://localhost:32400
PLEX_TOKEN=your_token_here
TRILIUM_URL=http://localhost:8888
TRILIUM_TOKEN=your_token_here
SHASHIN_BASE_URL=http://localhost:6624/
SHASHIN_API_KEY=your_key_here
SERPER_API_KEY=your_key_here
OLLAMA_TOKEN=your_token_here
LANGSEARCH_API_KEY=your_key_here   # fallback search when Ollama weekly limit is reached
OPENWEATHER_API_KEY=your_key_here  # fallback weather when Open-Meteo is unavailable
DISCORD_WEBHOOK_URL=your_webhook_url_here

# === Google Apps Script (alternative to OAuth for Gmail/Calendar) ===
# Paste PASTE_INTO_GOOGLE_APPS_SCRIPT.js into script.google.com, deploy as a Web App,
# then set the URL with your SECRET_KEY appended as ?key=...
# When set, ALL Google tools use the script instead of OAuth.
GOOGLE_APPS_SCRIPT_URL=https://script.google.com/macros/s/.../exec?key=<strong-random-secret-32-chars>

# === A2A Protocol ===
A2A_ENDPOINTS=http://localhost:8010
A2A_EXPOSED_TOOLS=

# === Performance Tuning ===
CONCURRENT_LIMIT=3
EMBEDDING_BATCH_SIZE=50
DB_FLUSH_BATCH_SIZE=50

# === Tool Control ===
DISABLED_TOOLS=plex:*

# === Location ===
DEFAULT_CITY=Vancouver
DEFAULT_STATE=BC
DEFAULT_COUNTRY=Canada
DEFAULT_TIMEZONE=America/Vancouver

Configuração Recomendada

ollama pull qwen2.5:14b-instruct-q4_K_M   # primary inference — any tool-calling model works; 14b q4 is a good default
ollama pull qwen2.5:0.5b                  # routing classifier — small/fast; set LLM_ROUTING_MODEL=qwen2.5:0.5b in .env
ollama pull bge-large                     # required for RAG
ollama pull qwen3-vl:8b-instruct          # required for image tools
# sentence-transformers models (memory embeddings + reranker) auto-download on first use

Arquivos GGUF: baixe qualquer GGUF (ex.: do Hugging Face) e execute :gguf add <path_to_file> no prompt — não é necessário editar a configuração.

.env mínimo para começar:

OLLAMA_VISION_MODEL=qwen3-vl:8b-instruct
DISABLED_TOOLS=plex:*,image_tools:shashin_analyze,shashin_random,shashin_search
OLLAMA_TOKEN=<token>
SERPER_API_KEY=<key>

Requisitos de Recursos

RecursoVariáveis de ambiente necessáriasConfiguração adicional
Gmail + Google Calendar—Configuração do Google — Apps Script (mais simples) ou OAuth (veja Configuração do Google)
Notificações do DiscordDISCORD_WEBHOOK_URLCrie webhook nas configurações do canal Discord
Ingestão e busca RAG—Ollama em execução + bge-large baixado
Biblioteca de mídia PlexPLEX_URL, PLEX_TOKENPlex Media Server em execução
Busca na web (principal)OLLAMA_TOKENConta Ollama + chave de API
Busca na web (fallback)LANGSEARCH_API_KEYConta LangSearch — painel. Usado automaticamente quando o limite semanal do Ollama é atingido ou retorna vazio.
Busca de imagensSERPER_API_KEYConta Serper + chave de API
Geração de imagens com IA—Gratuito via Pollinations.ai — sem chave necessária
Notas TriliumTRILIUM_URL, TRILIUM_TOKENServidor Trilium em execução
Galeria de fotos ShashinSHASHIN_BASE_URL, SHASHIN_API_KEYServidor Shashin em execução
Modo distribuído A2AA2A_ENDPOINTSServidor A2A remoto em execução

Comandos Disponíveis

:jobs                  - List all scheduled jobs
:jobs pause <label>    - Pause a scheduled job
:jobs enable <label>   - Resume a scheduled job
:jobs cancel <label>   - Delete a scheduled job
:jobs info <label>     - Show full job detail
:memory                        - List all memories
:memory semantic               - List permanent memories only
:memory episodic               - List session-derived memories
:memory forget <id>            - Delete a memory by ID
:memory clear                  - Clear all episodic memories
:memory clear session <id>     - Delete memories from one session
:memory consolidate <id>       - Extract memories from a session now
:memory add <fact>             - Manually add a permanent memory
:memory dedup                  - Remove duplicate memories
:commands              - List all available commands
:clear sessions        - Clear all chat history
:clear session <id>    - Clear session
:sessions              - List all sessions
:stop                  - Stop current operation
:stats                 - Show performance metrics
:tools                 - List available tools
:tools --all           - Show all tools including disabled
:tool <n>              - Get tool description
:model                 - List all available models
:model <n>             - Switch to a model
:gguf add <path>       - Register a GGUF model
:gguf remove <alias>   - Remove a GGUF model
:gguf list             - List registered GGUF models
:a2a on/off/status     - Control A2A mode
:health                - Health overview of all servers
:env                   - Show environment configuration

Configuração do Google

Opção A — Google Apps Script (configuração mais simples)

  1. Acesse https://script.google.com e crie um novo projeto
  2. Exclua todo o código existente e cole o conteúdo de PASTE_INTO_GOOGLE_APPS_SCRIPT.js
  3. Substitua <SECRET_KEY> no script por um segredo aleatório forte (32+ caracteres). Gere um: python3 -c "import secrets; print(secrets.token_urlsafe(32))" Qualquer pessoa que conheça esta chave pode ler seus e-mails e calendário — trate-a como uma senha.
  4. Clique em Implantar > Nova implantação > Aplicativo web
    • Executar como: Eu
    • Quem tem acesso: Qualquer pessoa
    • Clique em Implantar e copie a URL do Aplicativo Web
  5. Adicione ao .env:
    GOOGLE_APPS_SCRIPT_URL=<Web App URL>?key=<your SECRET_KEY>
    

Em edições subsequentes do script, use Implantar > Gerenciar implantações e edite a implantação existente — não crie uma nova, ou a URL mudará.

Opção B — Google OAuth

Configuração única. Após concluir estas etapas, o servidor roda sem intervenção.

  1. Acesse https://console.cloud.google.com/ e crie um projeto
  2. Ative Gmail API e Google Calendar API
  3. Crie um cliente OAuth Desktop app e baixe credentials.json
  4. Coloque em servers/google/credentials.json
  5. Publique o aplicativo como Em produção — isso é obrigatório. Aplicativos em modo "Teste" têm tokens que expiram a cada 7 dias, causando erros invalid_grant em tarefas agendadas
  6. Execute: .venv/bin/python auth_google.py
  7. Reinicie: python client.py

Se o token expirar depois: a plataforma detecta na próxima chamada de ferramenta do Google e mostra um banner de reautorização na interface com um link. Clique no link, aprove o acesso, cole o código de autorização no chat — não é necessário reiniciar o servidor. Alternativamente, exclua servers/google/token.json e execute novamente o passo 6.


4. Adicionando Ferramentas Personalizadas

Passo 1: Crie o arquivo do servidor

mkdir servers/my_tool && touch servers/my_tool/server.py

Passo 2: Implemente com @tool_meta

from mcp.server.fastmcp import FastMCP
from tools.tool_control import check_tool_enabled
from client.tool_meta import tool_meta

mcp = FastMCP("my-tool-server")

@mcp.tool()
@check_tool_enabled(category="my_tool")
@tool_meta(
    tags=["read", "search"],
    triggers=["my keyword", "my phrase"],
    template='use my_function: arg1=""',
)
def my_function(arg1: str) -> str:
    """Short description."""
    return json.dumps({"content": f"Processed {arg1}"})

if __name__ == "__main__":
    mcp.run(transport="stdio")

Reinicie o cliente e a ferramenta estará ativa — roteada, marcada e registrada automaticamente.

Referência de campos @tool_meta

CampoObrigatórioDescrição
tags✅Tags de capacidade
triggers✅Frases em linguagem natural que roteiam para esta ferramenta
templaterecomendadoTexto pré-preenchido mostrado no painel de ferramentas
text_fieldsse necessárioCampos de resposta que contêm o texto principal
rate_limitnão"100/hour", "10/day", "ollama" ou None
idempotentnãoTrue se não tiver efeitos colaterais (padrão: True)
intent_categorynãoSubstitui o nome do grupo de roteamento

Vocabulário de tags

TagSignificado
readApenas leitura de dados
writeCria ou modifica dados
destructiveExclui ou altera dados de forma irreversível
searchPropósito principal é busca
externalChama uma API externa
visionProcessa entrada de imagem
mediaOpera em áudio/vídeo/imagem
calendarInterage com dados de calendário
emailInterage com e-mail
notesInterage com sistemas de anotações
codeOpera em código-fonte
systemInterage com SO ou hardware
ragInterage com o armazenamento vetorial RAG
aiChama um LLM ou modelo de ML

Adicionando Servidores MCP Externos

Crie external_servers.json na raiz do projeto:

{
    "external_servers": {
        "deepwiki": {
            "transport": "sse",
            "url": "https://mcp.deepwiki.com/mcp",
            "enabled": true
        }
    }
}

Transportes suportados: sse, http, stdio. Convenção de variável de ambiente para autenticação por cabeçalho:

ES_SERVERNAME_TOKEN=your_token_here

5. Modo Distribuído (Protocolo A2A)

python a2a_server.py    # Terminal 1 — starts on http://localhost:8010
python client.py        # Terminal 2
A2A_ENDPOINTS=http://localhost:8010,http://gpu-server:8020
A2A_EXPOSED_TOOLS=plex,location,text   # empty = expose all

Todos os endpoints configurados são descobertos e registrados simultaneamente na inicialização via asyncio.gather() — timeouts de conexão para endpoints inacessíveis não bloqueiam mais uns aos outros.


6. Testes

Executando Testes

Ative seu virtualenv primeiro e execute a partir da raiz do projeto:

source .venv/bin/activate        # Linux/macOS
.venv\Scripts\activate           # Windows PowerShell
# WSL2: source /home/$USER/.virtualenvs/mcp_platform/bin/activate

python -m pytest                 # all tests
python -m pytest -m unit         # fast unit tests only
python -m pytest -m integration  # integration tests
python -m pytest -m e2e          # end-to-end tests
python -m pytest --no-cov        # skip coverage (faster)
python -m pytest -x              # stop on first failure
python -m pytest -k "session"    # filter by name

Estrutura de Testes

tests/
├── conftest.py
├── unit/         <- fast isolated unit tests
├── integration/  <- multi-component tests
└── e2e/          <- full conversation tests

tests/results/    <- generated after running tests
├── junit.xml
├── coverage.xml
├── test-report.html
└── coverage-report.html

CI/CD

Os testes são executados automaticamente a cada push e pull request via GitHub Actions (.github/workflows/ci.yml). Em caso de falha, uma Issue do GitHub é aberta automaticamente com um link para a execução com falha.

Para enviar cobertura ao Codecov, adicione ao workflow:

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: results/coverage.xml

7. Arquitetura

Design Multi-Servidor

servers/
├── code_assistant/   12 tools  - AI-powered code analysis, generation, and refactoring
├── code_review/       3 tools  - Code review, search, and bug fixing
├── code_runner/       4 tools  - Python/bash execution sandbox
├── discord/           2 tools  - Discord channel notifications [requires DISCORD_WEBHOOK_URL]
├── github/            4 tools  - GitHub repo clone, browse, and cleanup
├── google/           13 tools  - Gmail + Google Calendar       [requires Google setup]
├── image/             6 tools  - Image search, analysis, AI generation
├── location/          3 tools  - Weather, time, location
├── plex/             18 tools  - Media + ML recommendations    [requires PLEX_URL + PLEX_TOKEN]
├── rag/               8 tools  - Vector search and management  [requires Ollama + bge-large]
├── system/            3 tools  - System info and processes
├── text/              8 tools  - Text processing and web search
└── trilium/          11 tools  - Trilium notes integration     [requires TRILIUM_URL + TRILIUM_TOKEN]

Total: 95 ferramentas em 13 servidores

Concorrência e Paralelismo

A plataforma usa asyncio.gather() em várias camadas para executar trabalho não-LLM simultaneamente:

CamadaO que roda em paralelo
Inicialização — descoberta de servidoresVerificações de alcance TCP e sondas OAuth para todos os servidores externos
Inicialização — registro A2ATodos os A2A_ENDPOINTS são descobertos e registrados simultaneamente
Execução de tarefas multiagenteTarefas independentes (sem dependências não atendidas) rodam como um lote concorrente a cada ciclo
Execução de subtarefas A2AMesmo agrupamento concorrente com consciência de dependências do modo multiagente
Transmissão WebSocketTodos os clientes conectados recebem mensagens via uma única chamada gather()

Nota de hardware: a inferência de LLM serializa na camada GPU do Ollama independentemente da concorrência — uma inferência roda por vez em uma única GPU. O benefício do paralelismo está no trabalho limitado por I/O: chamadas de ferramentas HTTP, consultas de banco de dados e requisições de rede. Execução LLM verdadeiramente paralela exigiria múltiplas GPUs ou subagentes hospedados em nuvem.

Estrutura de Diretórios

mcp-platform/
├── servers/
├── a2a_server.py
├── client.py
├── client/
│   ├── ui/
│   ├── capability_registry.py  <- auto-populated from @tool_meta
│   ├── langgraph.py
│   ├── memory_consolidator.py  <- persistent cross-session memory
│   ├── proactive_agent.py      <- scheduler + condition triggers
│   ├── query_patterns.py       <- auto-populated from @tool_meta triggers
│   ├── tool_meta.py            <- single source of truth for tool metadata
│   ├── websocket.py
│   └── ...
├── data/
│   ├── sessions.db             <- session + message history
│   ├── memory.db               <- persistent memory (created on first run)
│   └── scheduler.db            <- scheduled jobs (created on first run)
└── tools/

8. RAG e Memória de Conversa

Como o contexto funciona

Cada chamada de LLM recebe contexto montado nesta ordem exata:

System prompt
  ├─ Persistent memory — top 5 by importance (always injected)
  ├─ Persistent memory — query-relevant (vector search, above threshold)
  └─ Original system instructions + session ID

Conversation RAG — turns that scrolled out of the window (if relevant)

Message window — last LLM_MESSAGE_WINDOW turns (always injected)

Current user message

Cadeia de consulta — o que acontece a cada pergunta:

  1. Memória persistente (sempre) — as 5 memórias de maior importância são injetadas incondicionalmente no prompt do sistema, independentemente da relevância da consulta. Isso garante que fatos essenciais (seu nome, família, preferências) nunca sejam perdidos, mesmo quando nenhuma consulta pontua bem. Memórias adicionais relevantes à consulta são adicionadas por cima via busca vetorial.

  2. Janela de mensagens (sempre) — as últimas LLM_MESSAGE_WINDOW turnos da sessão atual são incluídas diretamente como histórico de conversa.

  3. RAG de conversa (sempre) — uma busca semântica é executada contra turnos que saíram da janela. Resultados acima do limite do reranker são injetados entre o prompt do sistema e o histórico.

  4. O LLM gera a resposta usando tudo acima.

Quando cada camada ajuda você:

SituaçãoO que ajuda
Perguntou sobre algo de 3 mensagens atrásJanela de mensagens
Perguntou sobre algo de 20 mensagens atrás, mesma sessãoRAG de conversa
Perguntou seu nome em uma nova sessãoMemória persistente (âncora)
Perguntou um fato específico de uma sessão antigaMemória persistente (relevante à consulta)
Perguntou sobre um documento ou artigo específicoRAG de conversa
Fato ainda não consolidado na memóriaNada — informe a plataforma novamente

Janela de conversa (LLM_MESSAGE_WINDOW)

Controla quantas interações recentes o LLM vê diretamente. Definido em .env:

LLM_MESSAGE_WINDOW=15   # default: 6, recommended: 15

Uma janela de 6 é apertada demais para conversas normais — informações compartilhadas no início de uma sessão saem da janela antes que você possa perguntar sobre elas. 15 cobre uma troca completa sem atingir os limites de tokens no qwen2.5:14b. Se você compartilhar algo e o LLM parecer esquecer algumas mensagens depois, aumente esse valor.

Ingestão de excedente

Quando o histórico excede a janela, interações mais antigas são automaticamente ingeridas no banco de vetores RAG como pares Human + Assistant. Elas permanecem pesquisáveis por similaridade semântica mesmo após saírem da janela.

Recuperação automática via RAG

A cada mensagem, uma busca semântica é executada contra todo o armazenamento RAG usando a mensagem atual do usuário como consulta. Trechos correspondentes (de interações antigas, documentos ingeridos, legendas do Plex ou pesquisas) são injetados no contexto automaticamente — sem necessidade de gatilho explícito de search rag.

Para que serve cada método de recuperação

Tipo de consultaMelhor ferramenta
"O que discutimos sobre X?"Auto-RAG (semântico)
"Qual foi meu primeiro prompt?"session_history_tool (banco ordenado)
"Resuma esta sessão"session_history_tool
"O que tinha naquele artigo que lemos?"Auto-RAG
"O que perguntei há 10 mensagens?"session_history_tool

session_history_tool

Adicionado ao servidor rag. Consulta o banco SQLite da sessão diretamente para obter histórico de mensagens ordenado e com carimbo de data/hora. O ID da sessão atual é sempre injetado no prompt do sistema para que o LLM possa passá-lo automaticamente.

use session_history_tool: session_id="<id>" [limit="20"] [order="asc"]

Gatilhos: first prompt, what did I ask, earlier in this session, summarise this session, session history


9. Memória Persistente e Agentes Proativos

Como memória e contexto funcionam juntos

A plataforma tem três camadas de contexto, cada uma com um propósito diferente:

CamadaO que éEscopoSobrevive à exclusão da sessão?
Janela de mensagensÚltimas N interações no contexto direto do LLMSessão atualNão
RAG de conversaInterações antigas ingeridas como vetoresPor sessãoNão
Memória persistenteFatos destilados extraídos pelo LLMTodas as sessõesSim — sobrevive à exclusão da sessão

O que isso significa na prática: Se você disser o nome do seu filho à plataforma e perguntar sobre isso 3 mensagens depois, a janela de mensagens resolve. Se perguntar 20 mensagens depois, o RAG resolve (geralmente). Se iniciar uma nova sessão amanhã, apenas a memória persistente terá essa informação.

O fluxo de memória

Passo 1 — Tenha uma conversa. Diga à plataforma coisas que você quer que ela lembre: seu nome, sua família, seus projetos, suas preferências. Quanto mais declarativo, melhor ("O nome da minha esposa é Suzy" vs "qual é o nome da minha esposa?").

Passo 2 — A memória extrai automaticamente. Após 15 minutos de inatividade, o InactivityWatcher dispara e executa o LLM sobre a transcrição da sua sessão. Ele extrai fatos e os armazena em data/memory.db com embeddings vetoriais.

A reconsolidação é inteligente: ela rastreia a contagem de mensagens na última consolidação e só reexecuta se novas mensagens foram adicionadas desde então. Ficar inativo durante a noite dispara uma extração, não dezenas.

Passo 3 — Memórias são injetadas em cada consulta. A cada nova mensagem, duas coisas acontecem: as 5 memórias de maior importância são sempre injetadas no prompt do sistema incondicionalmente (para que seu nome, família e preferências principais nunca sejam esquecidos), e então uma busca vetorial encontra memórias adicionais relevantes à consulta e as adiciona por cima. O bloco combinado fica assim:

## Persistent Memory (from past sessions)
The following facts are KNOWN and TRUE. Use them to answer directly.

◆ The user's name is Bob
○ Bob's wife is Suzy, a nurse and excellent cook
○ Bob's son Sam is 11, plays accordion, excels at hockey
...

Passo 4 — Memórias se acumulam com o tempo. Memórias episódicas acessadas 3+ vezes são promovidas ao nível semântico (permanente) durante a noite. A plataforma fica mais útil quanto mais você a usa.

Quando a memória não dispara automaticamente

O observador de inatividade dispara 15 minutos após sua última mensagem. Se você precisar que as memórias sejam extraídas imediatamente:

:memory consolidate <session_id>

Use :sessions para encontrar o ID da sessão. O comando limpa o sinalizador de consolidação e reexecuta a extração independentemente da contagem de mensagens.

Comandos de memória

:memory                        — list all memories (sorted by relevance)
:memory semantic               — permanent memories only
:memory episodic               — session-derived memories
:memory forget <id>            — delete one memory by ID
:memory clear                  — delete all episodic memories
:memory clear session <id>     — delete memories from one session
:memory consolidate <id>       — extract memories from a session now
:memory add <fact>             — manually add a permanent memory
:memory dedup                  — remove duplicate memories

Memórias adicionadas manualmente (:memory add) são armazenadas no nível semantic com importância 1.0 — elas sempre ficam em primeiro lugar na recuperação.

Se o LLM esquecer algo no meio da sessão

Aumente LLM_MESSAGE_WINDOW em .env. O padrão de 6 é apertado demais — 15 é recomendado. Informações compartilhadas no início de uma sessão saem da janela antes que você possa perguntar sobre elas.

Quando uma interação sai da janela, ela passa para o RAG de conversa — ela ainda está lá, mas agora é recuperada por similaridade semântica em vez de contexto direto. Isso significa que a formulação da consulta precisa estar próxima o suficiente do conteúdo original para que o reranker a encontre. Se o LLM ainda não conseguir encontrar algo que foi dito antes na mesma sessão, tente reformular a pergunta usando as mesmas palavras-chave da declaração original.

Por exemplo: se você disse "Meu filho Sam toca acordeão" e depois perguntar "Qual instrumento meu filho toca?", a correspondência semântica é forte. Mas "E o Sam?" é vago demais para o RAG retornar com confiança o fato do acordeão — seja específico.

Dois níveis de memória

NívelComo é criadoPersiste
episodicExtraído automaticamente das sessõesPermanente — sobrevive à exclusão da sessão
semanticPromovido do episódico (3+ acessos) ou adicionado via :memory addPermanente

O limite de promoção é configurável: MEMORY_PROMOTE_THRESHOLD=3 em .env.

Excluir uma sessão não exclui suas memórias. Os trechos RAG são removidos, mas os fatos extraídos permanecem. Para remover memórias de uma sessão específica antes de excluí-la:

:memory clear session <id>

Lendo a saída de :memory

PERSISTENT MEMORY
────────────────────────────────────────────────
[11] The user's name is Bob
    ◆ semantic  |  importance: 1.0  |  accessed 163x
[22] Bob's wife Suzy is a nurse and an excellent cook.
    ○ episodic  |  importance: 0.9  |  accessed 89x

Cada entrada mostra:

  • [id] — o ID da memória, usado com :memory forget <id> para excluí-la
  • Conteúdo — o fato extraído, escrito em inglês simples
  • ◆ semantic / ○ episodic — nível: semântico (permanente) ou episódico (derivado da sessão)
  • importance — pontuação atribuída pelo LLM no momento da extração (0.0–1.0). Maior = recuperado primeiro
  • accessed Nx — quantas vezes esta memória foi retornada em uma busca de recuperação. Alta contagem de acessos impulsiona a promoção de episódico → semântico

Memórias são ordenadas por pontuação de recuperação (combinação de similaridade vetorial e importância), não por ID ou data de criação. As memórias mais relevantes para sua última consulta aparecem primeiro.

Memórias duplicadas podem aparecer quando a mesma sessão é consolidada várias vezes, ou quando duas sessões contêm fatos semelhantes. Elas não causam erros — o LLM vê ambas — mas desperdiçam espaço de contexto. Limpe-as com:

:memory dedup        — removes exact duplicates, keeps highest importance copy
:memory forget <id>  — removes near-duplicates manually (same fact, different wording)

Memória Persistente

A plataforma lembra fatos, preferências e resultados entre sessões. Após o término de uma sessão (ou após 15 minutos de inatividade), o LLM extrai informações memoráveis da transcrição e as armazena em data/memory.db. Em cada nova sessão, memórias relevantes são injetadas no prompt do sistema automaticamente — sem necessidade de reexplicar.

Automação Agêntica (Agendador de Agentes Proativos)

A plataforma pode agir de forma autônoma ou semiautônoma — executando ferramentas em um cronograma, verificando condições e envolvendo o LLM para interpretar resultados antes de apresentá-los a você. Esta é a base para coisas como resumos diários, monitoramento de e-mail e, eventualmente, fluxos de trabalho totalmente autônomos.

Basta descrever o que você quer:

show me my daily briefing every day at 6am
check for new emails every 5 minutes and summarize anything urgent
show me a random photo from my gallery every morning with a short commentary
alert me when I have unread emails
run the weather check every morning at 7am and write a friendly summary

O LLM classifica se sua mensagem é uma solicitação de agendamento, interpreta a intenção em uma definição de tarefa e a apresenta para confirmação antes de salvar qualquer coisa. Solicitações ambíguas são esclarecidas com uma pergunta em vez de adivinhar.

Três tipos de gatilho:

TipoComo funcionaExemplo
cronDispara repetidamente em um cronograma fixo"todos os dias às 6h"
conditionVerifica uma ferramenta de checagem em um intervalo; dispara apenas quando a condição é verdadeira"quando eu tiver e-mails não lidos"
onceDispara uma vez em uma data/hora específica e depois se remove"hoje às 8h", "me lembre amanhã ao meio-dia"

Envolvimento do LLM: Cada tarefa pode ter um llm_prompt — uma instrução passada ao LLM após a execução da ferramenta. Em vez de transmitir a saída bruta da ferramenta, o LLM a interpreta primeiro:

  • shashin_random_tool + "escreva um breve comentário sobre esta foto incluindo local e clima"
  • gmail_get_unread + "resuma estes e-mails e sinalize qualquer coisa que precise de resposta"
  • get_weather_tool + "escreva um resumo meteorológico matinal amigável"

Pipelines de ferramentas: Encadeie várias ferramentas em sequência usando >> no llm_prompt. A saída de cada etapa é automaticamente passada para a próxima — sem necessidade de LLM:

use get_day_briefing >> use discord_notify
use gmail_get_unread >> use discord_notify
use calendar_get_today >> use discord_notify

Ao agendar, basta descrever o que você quer:

Every day at 7am, use get_day_briefing then use discord_notify to send me the result
At 8am today, use get_day_briefing then use discord_notify to send me today's schedule

O analisador do agendador produzirá automaticamente a sintaxe de cadeia >>.

Expressões de condição têm acesso a contexto rico da saída JSON da ferramenta:

total_unread > 0       — fires when Gmail has unread emails
len_results > 0        — fires when a results list is non-empty
result > 10            — fires when a numeric value exceeds 10
result_len > 100       — fires when the raw output is non-trivial

Comandos de gerenciamento de tarefas:

:jobs                    — list all scheduled jobs
:jobs pause <label>      — pause a job without deleting it
:jobs enable <label>     — resume a paused job
:jobs cancel <label>     — delete a job permanently
:jobs info <label>       — full job detail including cron and last run

Arquivos:

  • client/memory_consolidator.py — extração, armazenamento, injeção de memória e comandos :memory
  • client/proactive_agent.py — agendador, gatilhos de condição, analisador de intenção baseado em LLM e comandos :jobs

Dependência necessária:

pip install apscheduler

Arquivos de dados (criados automaticamente na primeira execução):

data/memory.db      — persistent memory store
data/scheduler.db   — scheduled jobs store (label, tool, cron, llm_prompt, etc.)

10. Padrões de Intenção e Solução de Problemas

Padrões de Intenção

O roteamento é conduzido pelo classificador LLM e triggers no decorador @tool_meta de cada ferramenta — sem necessidade de edição manual de padrões. Os gatilhos suportam strings simples e padrões regex prefixados com r:.

Forçar uma ferramenta específica:

Using shashin_search_tool, find photos of Sam
Using web_image_search_tool, show me a red panda

Desvio conversacional: Consultas que o LLM classifica como conversacionais (saudações, opiniões, escrita criativa) ignoram o roteamento de ferramentas completamente — respondidas a partir do contexto sem ferramentas vinculadas.

Detecção de agendamento: O LLM determina se uma mensagem é uma solicitação de agendamento/automação — sem listas de palavras-chave. Se detectado, é roteado para o ScheduleParser em vez do agente.


Solução de Problemas

Ollama inacessível / conexão recusada:

  • Use OLLAMA_BASE_URL=http://127.0.0.1:11434 em .env para acesso local
  • Para WSL2 + acesso LAN, inicie o Ollama com OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Adicione proxies de porta do Windows (PowerShell elevado):
    # Get WSL2 IP (use ip addr — hostname -I is not available on all WSL2 distros)
    $wslIp = (wsl -- ip -4 addr show eth0 | Select-String "inet ") -replace '.*inet (\d+\.\d+\.\d+\.\d+).*','$1'
    netsh interface portproxy add v4tov4 listenport=11434 listenaddress=0.0.0.0 connectport=11434 connectaddress=$wslIp
    New-NetFirewallRule -DisplayName "WSL2 Ollama" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 11434
    
  • O IP do WSL2 muda na reinicialização — reexecute os comandos portproxy acima após cada reinicialização

Modelos do Ollama não aparecendo:

  • Se OLLAMA_BASE_URL aponta para um IP LAN, mas o Ollama está vinculado apenas a 127.0.0.1, a lista de modelos ficará vazia
  • Correção: inicie o Ollama com OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Verifique: ollama list

Interface web não acessível pela LAN:

A plataforma vincula as portas 9000 (HTTP), 8765 e 8766 (WebSockets) a 0.0.0.0 dentro do processo. Se outros dispositivos na sua LAN podem alcançar essas portas depende do seu ambiente.

Linux nativo:

  • Deve funcionar imediatamente — verifique se seu firewall permite entrada nessas portas:
    sudo ufw allow 9000 && sudo ufw allow 8765 && sudo ufw allow 8766
    

WSL2 no Windows: O WSL2 roda em uma rede virtual privada (172.x.x.x) que não é diretamente roteável a partir da sua LAN. O Windows deve encaminhar o tráfego da LAN para o WSL2 usando netsh portproxy. Execute isso em um PowerShell elevado toda vez que o IP do WSL2 mudar (ele muda a cada reinicialização):

# 1. Find your current WSL2 IP (hostname -I is not available on all WSL2 distros)
$wslIp = (wsl -- ip -4 addr show eth0 | Select-String "inet ") -replace '.*inet (\d+\.\d+\.\d+\.\d+).*','$1'
Write-Host "WSL2 IP: $wslIp"

# 2. Add port forwarding for each port (both 0.0.0.0 and specific LAN IP — both are needed)
netsh interface portproxy add v4tov4 listenport=9000  listenaddress=0.0.0.0 connectport=9000  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8765  listenaddress=0.0.0.0 connectport=8765  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8766  listenaddress=0.0.0.0 connectport=8766  connectaddress=$wslIp
$lanIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -like "192.168.*" } | Select-Object -First 1).IPAddress
netsh interface portproxy add v4tov4 listenport=9000  listenaddress=$lanIp connectport=9000  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8765  listenaddress=$lanIp connectport=8765  connectaddress=$wslIp
netsh interface portproxy add v4tov4 listenport=8766  listenaddress=$lanIp connectport=8766  connectaddress=$wslIp

# 3. Allow inbound through Windows Firewall (first time only)
New-NetFirewallRule -DisplayName "MCP Platform 9000" -Direction Inbound -Protocol TCP -LocalPort 9000 -Action Allow
New-NetFirewallRule -DisplayName "MCP Platform 8765" -Direction Inbound -Protocol TCP -LocalPort 8765 -Action Allow
New-NetFirewallRule -DisplayName "MCP Platform 8766" -Direction Inbound -Protocol TCP -LocalPort 8766 -Action Allow

# 4. Allow inbound through WSL2 firewall (first time only — run inside WSL2)
# sudo ufw allow 9000 && sudo ufw allow 8765 && sudo ufw allow 8766

Acesse a partir de dispositivos LAN usando seu IP LAN do Windows (ex.: 192.168.0.x), não o IP do WSL2:

http://192.168.0.x:9000/client/ui/index.html

Para encontrar seu IP LAN do Windows: ipconfig → procure o endereço 192.168.x.x no seu adaptador principal.

Dica: se parar de funcionar após uma reinicialização, basta reexecutar os comandos netsh interface portproxy add — as regras de firewall persistem, mas o IP do WSL2 terá mudado. A interface web não carrega localmente:

netstat -an | grep LISTEN   # check ports 8765, 8766, 9000

Pesquisa na web retornando sem resultados / respostas vazias:

  • O nível gratuito do Ollama tem um limite de uso semanal — quando atingido, a API retorna HTTP 200 com corpo vazio
  • A plataforma automaticamente recorre ao LangSearch quando isso acontece
  • Defina LANGSEARCH_API_KEY em .env para habilitar o fallback (conta gratuita em https://langsearch.com/dashboard)
  • Para confirmar qual provedor está sendo usado, verifique os logs para 🔍 Ollama web search vs 🔍 LangSearch web search

Problemas com Google / OAuth:

  • Confirme se servers/google/token.json existe — se não, execute novamente auth_google.py
  • Confirme se a API do Gmail e a API do Google Agenda estão habilitadas no Google Cloud Console
  • Confirme se o aplicativo OAuth está publicado como Em produção (tokens do modo de teste expiram a cada 7 dias)
  • invalid_grant em tempo de execução: o banner de reautenticação aparecerá automaticamente na interface — siga o link, aprove, cole o código no chat

RAG não funcionando:

  • Garanta que o Ollama esteja em execução: OLLAMA_HOST=0.0.0.0:11434 ollama serve
  • Baixe o bge-large se estiver faltando: ollama pull bge-large

Auto-RAG retornando contexto irrelevante:

  • Aumente o limite de min_score na chamada rag_search_tool dentro de run_agent
  • Verifique se o armazenamento RAG não está poluído: use rag_browse_tool
  • Remova uma fonte: use rag_delete_source_tool: source="<source>"

session_history_tool não encontrando mensagens:

  • Confirme se o ID da sessão no prompt do sistema corresponde à sessão ativa
  • Verifique se data/sessions.db existe e é gravável

Ferramentas Plex retornando erros:

curl $PLEX_URL/identity?X-Plex-Token=$PLEX_TOKEN

O modelo GGUF não carrega:

  • Reduza as camadas de GPU: export GGUF_GPU_LAYERS=20
  • Somente CPU: export GGUF_GPU_LAYERS=0

Servidor A2A não conectando:

curl http://localhost:8010/.well-known/agent-card.json

Histórico de conversa não funcionando:

  • Mude para um modelo maior: :model qwen2.5:14b-instruct-q4_K_M
  • Evite qwen2.5:3b / qwen2.5:7b para tarefas que dependem do histórico

Ferramentas não aparecendo:

:tools --all        # check if disabled
# check DISABLED_TOOLS in .env
python client.py    # restart

Licença

Licença MIT