nanobot — Arquitetura

Como funciona por dentro o assistente pessoal ultra-leve em ~3.400 linhas de Python

Visao Geral

O nanobot funciona como um telefone com secretaria inteligente:

  1. Voce manda uma mensagem pelo Telegram (ou Discord, WhatsApp...)
  2. A mensagem entra numa fila de espera
  3. Um agente pega a mensagem, pensa (usando uma IA como GPT/Claude/Qwen), e pode usar ferramentas (ler arquivos, executar comandos, buscar na web)
  4. A resposta volta pela mesma fila e chega no seu Telegram

O codigo todo tem ~3.400 linhas e e dividido em 7 pecas principais que se conectam como engrenagens.

As 7 Pecas do Nanobot

+-----------------------------------------------------------+
|                VOCE (Telegram, Discord...)                 |
+-----------------------------+-----------------------------+
                              |
                         (1) CHANNELS
                        (recebe e envia)
                              |
                         (2) MESSAGE BUS
                        (fila de espera)
                              |
                         (3) AGENT LOOP
                        (cerebro principal)
                              |
                  +-----------+-----------+
                  |           |           |
             (4) PROVIDER  (5) TOOLS  (6) CONTEXT
             (chama a IA)  (faz coisas) (memoria)
                  |           |           |
                  +-----------+-----------+
                              |
                         (7) SESSIONS
                        (historico salvo)

1. Channels — A porta de entrada

Arquivo principal: nanobot/channels/telegram.py (e discord.py, whatsapp.py, etc.)

Pense no channel como um porteiro. Cada plataforma (Telegram, Discord, WhatsApp) tem seu porteiro, e todos fazem a mesma coisa:

  1. Ficam escutando — o Telegram usa "polling" (a cada poucos segundos pergunta "tem mensagem nova?")
  2. Verificam seguranca — confere se o remetente esta na lista allowFrom. Se nao esta, ignora
  3. Limitam velocidade — se alguem mandar 100 mensagens por segundo, bloqueia (rate limit)
  4. Padronizam — converte a mensagem de cada plataforma para um formato unico interno

O que o channel cria

InboundMessage(
    channel   = "telegram",        # de onde veio
    sender_id = "123456789",      # quem mandou
    chat_id   = "123456789",      # qual chat
    content   = "Oi, tudo bem?",   # a mensagem
    media     = [],                # fotos, audios...
)

Todos os channels herdam de BaseChannel — um "molde" que ja traz seguranca e rate limit prontos. Cada plataforma so precisa implementar como conectar e como enviar mensagem de volta.

Channels implementados

ChannelComo recebeBiblioteca
TelegramPolling (pergunta a cada X segundos)python-telegram-bot
WhatsAppWebSocket para bridge Node.jsbaileys (JS)
DiscordGateway WebSocketdiscord.py
FeishuWebSocket longa conexaoSDK proprio
DingTalkStream modeSDK proprio

2. Message Bus — A fila de espera

Arquivo: nanobot/bus/queue.py (~60 linhas)

O bus e a peca mais simples: sao duas filas (como a fila do banco).

FILA DE ENTRADA (inbound):    Channels colocam → Agent retira
FILA DE SAIDA   (outbound):   Agent coloca     → Channels retiram
class MessageBus:
    inbound  = asyncio.Queue(maxsize=1000)   # fila de entrada
    outbound = asyncio.Queue(maxsize=1000)   # fila de saida

Por que o bus existe?

Desacoplamento. O Telegram nao precisa esperar o agente responder. Ele joga a mensagem na fila e volta a escutar. O agente processa quando puder. Se chegarem 10 mensagens ao mesmo tempo, ficam na fila esperando a vez.

Se a fila encher (1000 mensagens), as novas sao descartadas com um aviso no log.

3. Agent Loop — O cerebro

Arquivo: nanobot/agent/loop.py

Este e o coracao do nanobot. Funciona assim:

LOOP INFINITO:
  1. Pega proxima mensagem da fila de entrada
  2. Busca o historico da conversa (sessao)
  3. Monta o "contexto" (personalidade + memoria + skills)
  4. Manda tudo para a IA (provider)
  5. A IA responde com TEXTO ou pedido de FERRAMENTA
  6. Se pediu ferramenta → executa → volta pro passo 4 com o resultado
  7. Se respondeu com texto → salva no historico → coloca na fila de saida

Codigo simplificado

async def run(self):
    while True:
        msg = await bus.consume_inbound()         # 1. espera mensagem
        await self._process_message(msg)           # 2-7. processa

async def _process_message(self, msg):
    session = session_manager.get_or_create(msg.session_key)
    messages = context_builder.build(session, msg)

    for iteration in range(20):                    # max 20 rodadas
        response = await provider.chat(messages, tools)

        if response.tool_calls:                    # IA quer ferramenta?
            for tool_call in response.tool_calls:
                result = await tools.execute(
                    tool_call.name, tool_call.args
                )
                messages.append({
                    "role": "tool", "content": result
                })
        else:                                      # IA respondeu texto
            await bus.publish_outbound(OutboundMessage(
                channel=msg.channel,
                chat_id=msg.chat_id,
                content=response.content
            ))
            break

    session.save()

Ponto-chave

A IA pode pedir para usar ferramentas, e o loop executa e devolve o resultado. Isso pode acontecer ate 20 vezes numa unica mensagem (por exemplo: a IA le um arquivo, depois executa um comando, depois busca na web, e finalmente responde).

4. Provider — A conexao com a IA

Arquivo: nanobot/providers/litellm_provider.py

O provider e quem faz a chamada para a API do modelo de linguagem (GPT, Claude, Qwen, etc.).

O nanobot usa uma biblioteca chamada LiteLLM que funciona como um "tradutor universal" — voce manda no formato OpenAI e ele traduz para qualquer provider:

class LiteLLMProvider:
    async def chat(self, messages, tools, model):
        response = await litellm.acompletion(
            model       = "openrouter/qwen/qwen3-coder-next",
            messages    = messages,       # conversa inteira
            tools       = tools,          # ferramentas disponiveis
            temperature = 0.7,
            max_tokens  = 8192,
        )
        return LLMResponse(
            content    = response.content,
            tool_calls = response.tool_calls,
        )

O Registry de Providers

Arquivo: nanobot/providers/registry.py

Cada provider tem uma "spec" que diz como configurar:

ProviderSpec(
    name="openrouter",
    env_key="OPENROUTER_API_KEY",
    keywords=("openrouter",),
    is_gateway=True,               # aceita qualquer modelo
    detect_by_key_prefix="sk-or-", # detecta pelo prefixo da chave
)

Isso permite que adicionar um provider novo precise de apenas 2 mudancas (a spec + um campo na config).

Providers suportados

ProviderTipoModelos
OpenRouterGatewayTodos (Claude, GPT, Qwen, Llama...)
AiHubMixGatewayTodos
AnthropicDiretoClaude
OpenAIDiretoGPT
DeepSeekDiretoDeepSeek
GroqDiretoLlama, Whisper (voz)
GeminiDiretoGemini
DashscopeDiretoQwen
MoonshotDiretoKimi
vLLMLocalQualquer (servidor local)

5. Tools — As ferramentas do agente

Arquivos: nanobot/agent/tools/ (6 arquivos)

Tools sao as "maos" do agente. Sem elas, ele so fala. Com elas, ele faz coisas.

Cada tool e uma classe Python:

class ReadFileTool(Tool):
    name = "read_file"
    description = "Read contents of a file"
    parameters = {
        "path": {"type": "string", "description": "File path"}
    }

    async def execute(self, path):
        return open(path).read()

Tools disponiveis

ToolO que fazProtecoes
read_fileLe um arquivo (max 10MB)Restrito ao workspace
write_fileCria/escreve arquivoRestrito ao workspace
edit_fileEdita linhas especificasRestrito ao workspace
list_dirLista diretorioRestrito ao workspace
execExecuta comando no terminalBloqueia rm -rf, dd, fork bombs
web_searchBusca na web (Brave Search)Precisa de API key
web_fetchLe uma pagina webProtecao SSRF
messageEnvia mensagem ao usuario
spawnCria sub-agente backgroundMax 5 simultaneos
cronAgenda tarefas recorrentes

Como a IA usa as tools

A IA recebe uma lista de "funcoes disponiveis" no formato JSON Schema. Quando decide usar uma, responde assim:

{
    "tool_calls": [{
        "id": "call_abc123",
        "function": {
            "name": "read_file",
            "arguments": "{\"path\": \"/workspace/arquivo.txt\"}"
        }
    }]
}

O agent loop executa, pega o resultado, e manda de volta pra IA pra ela continuar pensando.

Seguranca do exec

O tool de terminal tem a protecao mais pesada:

# Padroes bloqueados:
BLOCKED = ["rm -rf /", "dd if=", ":(){ :|:", "mkfs", "> /dev/"]

# Injecao de comandos detectada:
INJECTION = [";", "&&", "||", "|", "`", "$("]

# Se restrict_to_workspace = true:
# Todo comando roda dentro de ~/.nanobot/workspace/ apenas

6. Context — Personalidade e Memoria

Arquivo: nanobot/agent/context.py

Quando o agente vai responder, ele precisa de contexto — quem ele e, o que sabe, o que o usuario ja disse. O ContextBuilder monta tudo isso num "system prompt".

O que vai no contexto (nesta ordem)

#ConteudoOrigem
1Identidade — "Voce e o nanobot, assistente pessoal"Hardcoded + data/hora
2Bootstrap files — AGENTS.md, SOUL.md, USER.md~/.nanobot/workspace/
3Memoria — MEMORY.md + notas dos ultimos 7 dias~/.nanobot/workspace/memory/
4Skills — lista resumida ou conteudo completonanobot/skills/
5Historico — ultimas 50 mensagens deste chatSessao (.jsonl)
6Mensagem atual do usuarioInboundMessage

Memoria persistente

Arquivo: nanobot/agent/memory.py

~/.nanobot/workspace/memory/
+-- MEMORY.md           ← Memoria permanente (agente pode editar)
+-- 2026-02-10.md       ← Notas de hoje (agente pode adicionar)

O agente pode escrever na memoria usando o tool write_file. Na proxima conversa, o contexto inclui essas memorias automaticamente.

7. Sessions — Historico Salvo

Arquivo: nanobot/session/manager.py

Cada conversa e uma sessao identificada por channel:chat_id (ex: telegram:123456789).

As sessoes ficam salvas como arquivos .jsonl (uma linha JSON por mensagem):

~/.nanobot/sessions/
+-- telegram_123456789.jsonl

Conteudo do arquivo:

{"_type":"metadata","created_at":"2026-02-10T05:30:00"}
{"role":"user","content":"Oi!","timestamp":"2026-02-10T05:31:00"}
{"role":"assistant","content":"Ola! Como posso ajudar?","timestamp":"2026-02-10T05:31:05"}

Limites

LimiteValor
Mensagens por sessao1000 (descarta as mais antigas)
Tamanho max por mensagem100KB
Sessoes em cache na RAM100 (LRU — remove as menos usadas)
Validade30 dias (auto-deletadas)
GravacaoAtomica (temp → rename, nunca corrompe)

Pecas Extras

Cron — Tarefas agendadas

Arquivo: nanobot/cron/service.py

Um timer que roda a cada 10-30 segundos verificando se tem tarefa pra executar:

CronJob(
    name     = "bom dia",
    schedule = CronSchedule(kind="cron", expr="0 9 * * *"),  # todo dia 9h
    payload  = CronPayload(message="Diga bom dia!"),
)

Quando chega a hora, o cron manda a mensagem do job como se fosse um usuario → o agente processa normalmente → a resposta vai pro channel configurado.

Tipos de agendamento:

TipoExemploDescricao
everyevery_ms: 3600000Repete a cada N milissegundos (min 60s)
cron"0 9 * * *"Expressao cron padrao
atat_ms: 1739145600000Uma vez na data/hora especifica

Heartbeat — Batimento cardiaco

Arquivo: nanobot/heartbeat/service.py

A cada 30 minutos, o heartbeat "cutuca" o agente: "leia o HEARTBEAT.md e faca o que tiver la". Se nao tem nada, o agente responde HEARTBEAT_OK e volta a dormir.

Subagent — Tarefas em background

Arquivo: nanobot/agent/subagent.py

O agente pode "se clonar" pra fazer tarefas demoradas em paralelo:

Agente recebe: "pesquise sobre X e me mande um relatorio"
  → Cria subagent com a tarefa
  → Responde imediatamente: "Estou pesquisando em background!"
  → Subagent roda independente (com acesso a tools)
  → Quando termina, manda o resultado pro chat do usuario

Maximo 5 subagents simultaneos.

Skills — Habilidades do agente

Arquivo: nanobot/agent/skills.py

Skills sao pacotes de conhecimento em Markdown (SKILL.md) que o agente carrega conforme necessario:

  1. Skills always-on → conteudo completo no prompt
  2. Skills disponiveis → resumo no prompt, agente carrega via read_file se precisar
  3. Skills indisponiveis → listados com o que falta instalar
SkillDescricaoRequer
remotionCriacao de videos em React (35 regras + 3 exemplos)npx
agent-browserAutomacao de browseragent-browser
githubGitHub via gh CLIgh
summarizeResumir URLs e videossummarize
weatherClima e previsao
tmuxSessoes tmuxtmux
cronAgendar lembretes
skill-creatorCriar novos skills

Fluxo Completo: Da mensagem a resposta

Exemplo: voce digita "Qual o clima em SP?" no Telegram.

CHANNEL — Telegram recebe O polling detecta a mensagem. Verifica: esta no allowFrom? Sim. Cria InboundMessage.
BUS — Entra na fila bus.publish_inbound(msg) coloca na fila de entrada.
AGENT — Pega da fila bus.consume_inbound() retira. Busca sessao telegram:123456789.
CONTEXT — Monta o prompt Sistema + SOUL.md + memoria + skills + ultimas 50 msgs + "Qual o clima em SP?"
PROVIDER — Chama a IA provider.chat() → API OpenRouter → qwen3-coder-next. IA responde: tool_call("web_fetch", url="wttr.in/Sao+Paulo")
TOOL — Executa ferramenta web_fetch busca a pagina do wttr.in. Resultado: "Sao Paulo: 28 C, parcialmente nublado..."
PROVIDER — Chama a IA de novo Agora com o resultado do wttr.in. IA responde com texto: "O clima em Sao Paulo esta 28 C, parcialmente nublado"
SESSION — Salva historico Mensagem do usuario + resposta salvos no .jsonl
BUS — Fila de saida bus.publish_outbound() coloca a resposta na fila de saida.
CHANNEL — Telegram envia TelegramChannel.send() converte para HTML e envia via API do Telegram.

Tempo total

5-30 segundos dependendo da velocidade da IA e dos tools usados.

Tecnologias Utilizadas

ParteTecnologiaPor que
LinguagemPython 3.11+Async/await nativo, ecossistema IA
CLITyper + RichInterface bonita com cores
AsyncasyncioMultiplos channels simultaneos sem threads
LLMLiteLLMSuporta 100+ providers com uma API so
ConfigPydantic v2Validacao automatica do JSON
SessoesJSONLUm arquivo por conversa, append-only
Telegrampython-telegram-botOficial, bem mantida
WhatsAppBaileys (Node.js)Unica opcao gratuita
SegurancaRate limit + allowlist + workspace restrictionDefense in depth

Por que so ~3.400 linhas?

O projeto faz escolhas minimalistas: usa LiteLLM em vez de implementar cada provider, usa asyncio.Queue em vez de Redis/RabbitMQ, e mantem cada componente com responsabilidade unica. Nada de frameworks pesados — so Python puro com algumas bibliotecas bem escolhidas.

Diagrama Completo

+-------------------------------------------------------------+
|                    PLATAFORMAS DO USUARIO                    |
|       (Telegram, Discord, WhatsApp, Feishu, DingTalk)       |
+-----------------------------+-------------------------------+
                              |
                              v
                +---------------------------+
                |      CHANNEL LAYER        |
                |   TelegramChannel          |
                |   WhatsAppChannel          |
                |   DiscordChannel           |
                |                           |
                |  - Polling / WebSockets   |
                |  - Conversao de formato   |
                |  - Seguranca (allowFrom)  |
                +------------+--------------+
                             |
                +------------v--------------+
                |     MESSAGE BUS           |
                |                           |
                | inbound.Queue  ← channels |
                | outbound.Queue → channels |
                |                           |
                +------------+--------------+
                             |
                +------------v--------------+
                |       AGENT LOOP          |
                |                           |
                | 1. consume_inbound()      |
                | 2. build_messages()       |
                | 3. while iteration < 20:  |
                |    - LLM.chat()           |
                |    - if tool_calls:       |
                |      execute_tools()      |
                |    - else: break          |
                | 4. save_session()         |
                | 5. publish_outbound()     |
                +------------+--------------+
                             |
      +----------+-----------+-----------+----------+
      |          |           |           |          |
+-----v----+ +--v------+ +-v--------+ +v-------+ +v---------+
| Provider | | Tools   | | Context  | |Sessions| | Skills   |
| (LiteLLM)| | (6 tipos)| | Builder  | |(.jsonl)| | (.md)    |
+----------+ +---------+ +----------+ +--------+ +----------+
      |          |           |
+-----v----+ +--v------+ +-v--------+
|  OpenAI  | | exec    | | SOUL.md  |
|  Claude  | | files   | | AGENTS.md|
|  Qwen    | | web     | | MEMORY.md|
|  Llama   | | message | | USER.md  |
|  ...     | | spawn   | | Skills   |
+----------+ | cron    | +----------+
             +---------+

Mapa de Arquivos

Estrutura de diretorios do codigo-fonte

nanobot/                          ← Pacote principal Python
+-- __init__.py
+-- __main__.py                   ← Entry point (python -m nanobot)
|
+-- cli/
|   +-- commands.py               ← Todos os comandos CLI (gateway, agent, status...)
|
+-- agent/                        ← Cerebro do sistema
|   +-- loop.py                   ← Motor principal (recebe msg, chama LLM, executa tools)
|   +-- context.py                ← Monta o prompt (bootstrap + memoria + skills + historico)
|   +-- memory.py                 ← Memoria de longo prazo e notas diarias
|   +-- skills.py                 ← Descoberta e carregamento de skills
|   +-- subagent.py               ← Gerencia subagentes em background (max 5)
|   +-- tools/                    ← Ferramentas do agente
|       +-- registry.py           ← Registro central de tools
|       +-- base.py               ← Classe base Tool
|       +-- filesystem.py         ← read_file, write_file, edit_file, list_dir
|       +-- shell.py              ← exec (executa comandos no terminal)
|       +-- web.py                ← web_search, web_fetch
|       +-- message.py            ← message (envia msg ao usuario)
|       +-- spawn.py              ← spawn (cria subagentes)
|       +-- cron.py               ← cron (agenda tarefas)
|
+-- bus/                          ← Fila de mensagens
|   +-- queue.py                  ← MessageBus (asyncio.Queue, max 1000)
|   +-- events.py                 ← InboundMessage, OutboundMessage
|
+-- channels/                     ← Portas de entrada (plataformas)
|   +-- base.py                   ← BaseChannel (seguranca, rate limit)
|   +-- manager.py                ← Gerencia todos os channels ativos
|   +-- telegram.py               ← Canal Telegram (polling)
|   +-- discord.py                ← Canal Discord (WebSocket)
|   +-- whatsapp.py               ← Canal WhatsApp (bridge Node.js)
|   +-- feishu.py                 ← Canal Feishu (WebSocket)
|   +-- dingtalk.py               ← Canal DingTalk (Stream)
|
+-- providers/                    ← Conexao com modelos de IA
|   +-- base.py                   ← Interface base do provider
|   +-- registry.py               ← Registro de providers (auto-detect)
|   +-- litellm_provider.py       ← Provider principal (LiteLLM)
|   +-- transcription.py          ← Transcricao de audio (Whisper/Groq)
|
+-- config/                       ← Configuracao
|   +-- schema.py                 ← Schema Pydantic v2
|   +-- loader.py                 ← Carrega e valida config.json
|
+-- session/                      ← Historico de conversas
|   +-- manager.py                ← SessionManager (JSONL, cache LRU)
|
+-- cron/                         ← Tarefas agendadas
|   +-- service.py                ← Servico de cron (every, cron, at)
|   +-- types.py                  ← Tipos: CronJob, CronSchedule, CronPayload
|
+-- heartbeat/                    ← Wake-up periodico
|   +-- service.py                ← Heartbeat a cada 30 min
|
+-- security/                     ← Seguranca
|   +-- ratelimit.py              ← Token bucket por sessao
|   +-- validators.py             ← Valida paths, filenames, comandos
|   +-- sanitize.py               ← Sanitiza erros e resultados
|
+-- utils/
    +-- helpers.py                ← Funcoes auxiliares

Tabela resumo por componente

ComponenteArquivoResponsabilidade
CLI / Entrycli/commands.pyTodos os comandos, startup do gateway
Agent Coreagent/loop.pyProcessamento de mensagens, loop de tools
Busbus/queue.pyFilas async de mensagens
Channelschannels/base.py, telegram.py...Integracao com plataformas
LLMproviders/litellm_provider.pyChamadas API, multi-provider
Sessionssession/manager.pyPersistencia de historico
Contextagent/context.pyMontagem do system prompt
Memoryagent/memory.pyMemoria de longo prazo e notas diarias
Toolsagent/tools/*.pyCapacidades do agente (6 arquivos)
Skillsagent/skills.pyDescoberta e carregamento
Configconfig/schema.py, loader.pyGerenciamento de configuracao
Croncron/service.pyAgendamento de tarefas
Heartbeatheartbeat/service.pyWake-up periodico
Subagentsagent/subagent.pyTarefas em background
Segurancasecurity/ratelimit.py, validators.pyRate limit, validacao, sanitizacao