|
2 | 2 | <img src="frontend/assets/images/KernelBanner.webp" alt="KernelBot" width="100%" /> |
3 | 3 | </p> |
4 | 4 |
|
5 | | -# KernelBots (ACL) |
| 5 | +# KernelBot |
6 | 6 |
|
7 | | -Agente de contexto local com RAG (BM25) sobre Markdown em `content/`, interface em `templates/` e respostas em streaming via OpenRouter. |
| 7 | +**Chatbot RAG local-first que transforma Markdown em base de conhecimento consultável via chat.** |
8 | 8 |
|
9 | | -## Requisitos |
| 9 | +--- |
| 10 | + |
| 11 | +## O que é |
| 12 | + |
| 13 | +O KernelBot — internamente chamado **ACL (Agente de Contexto Local)** — é um sistema que indexa arquivos Markdown de um diretório local (`content/`) usando BM25, recupera trechos relevantes e os injeta como contexto para um LLM via OpenRouter. A resposta chega em streaming (SSE) com rastreabilidade: você sabe de onde veio cada trecho usado. |
| 14 | + |
| 15 | +Sem banco de dados, sem fila, sem infra pesada. Índice e estado ficam na memória do processo. |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +## Problema que resolve |
| 20 | + |
| 21 | +Alunos precisam revisar gravações de aulas de ~2h para tirar dúvidas simples. Sem o KernelBot, a alternativa é reassistir o material bruto ou esperar resposta da secretaria/professores. |
| 22 | + |
| 23 | +O sistema usa resumos estruturados das aulas como corpus RAG, permitindo perguntas diretas com respostas ancoradas no conteúdo real — não em "achismo" do modelo. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Como funciona |
| 28 | + |
| 29 | +``` |
| 30 | +content/*.md ──► SearchEngine (BM25 por silo) ──► ContextManager ──► ChatProvider ──► SSE |
| 31 | + ▲ │ |
| 32 | + │ ▼ |
| 33 | + Watchdog OpenRouter API |
| 34 | +(rebuild automático) (LLM com fallback) |
| 35 | +``` |
| 36 | + |
| 37 | +**Três etapas:** |
| 38 | + |
| 39 | +1. **Indexação** — `SearchEngine` lê `content/**/*.md`, divide por headers em chunks e cria um índice BM25 separado por silo (subpasta). O Watchdog monitora alterações e reconstrói o índice automaticamente (~1.5s de debounce). |
| 40 | + |
| 41 | +2. **Decisão de contexto** — `ContextManager.build_messages()` interpreta comandos (`/python`, `/doc`, `/content`), campo `discipline` do JSON e pinned context por sessão para decidir quais chunks injetar no system prompt. |
| 42 | + |
| 43 | +3. **Geração** — `ChatProvider` chama o LLM via OpenRouter em streaming SSE. Suporta fallback automático entre modelos. Antes dos tokens, emite metadados (`[ACL_META]`) com rótulo do silo e fontes usadas. |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## Diferenciais |
| 48 | + |
| 49 | +| Recurso | O que faz | |
| 50 | +|---------|-----------| |
| 51 | +| **Silos BM25** | Cada subpasta de `content/` é um domínio isolado com índice próprio — evita mistura de contextos entre disciplinas | |
| 52 | +| **Comandos de escopo** | `/python`, `/doc`, `/content` etc. forçam onde a busca acontece | |
| 53 | +| **Pinned context** | Mantém contexto entre mensagens da mesma sessão para follow-ups curtos não "perderem o fio" | |
| 54 | +| **File watching** | Editou um `.md` em `content/`? Em ~1.5s o índice já reflete a mudança | |
| 55 | +| **Rastreabilidade** | Respostas incluem breadcrumbs com os arquivos fonte usados (via `[ACL_META]` no SSE) | |
| 56 | +| **Zero infra** | Sem banco, sem Redis, sem Docker obrigatório — `python main.py` e pronto | |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## Estrutura de pastas |
| 61 | + |
| 62 | +``` |
| 63 | +KernelBot/ |
| 64 | +├── main.py # Orquestração: serviços, watchdog, app FastAPI |
| 65 | +├── core/ # Settings (.env), logging centralizado |
| 66 | +├── engine/ # SearchEngine, ContentWatcher, ContextManager, ChatProvider, PinnedSessionStore |
| 67 | +├── api/ # Rotas FastAPI (GET /, POST /chat) |
| 68 | +├── app/ # create_app(), estado injetado (AppServices) |
| 69 | +├── content/ # Base de conhecimento — silos por subpasta |
| 70 | +│ ├── python/ # 16 aulas |
| 71 | +│ ├── visualizacao-sql/# 17 aulas |
| 72 | +│ ├── projeto-bloco/ # 9 aulas |
| 73 | +│ ├── planejamento-curso-carreira/ # 7 aulas |
| 74 | +│ └── doc/ # Documentação interna (silo /doc) |
| 75 | +├── frontend/ # Assets CSS e módulos JS da UI |
| 76 | +├── templates/ # index.html (Jinja2) |
| 77 | +├── tests/ # Smoke tests (pytest) |
| 78 | +└── requirements.txt |
| 79 | +``` |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Como rodar |
| 84 | + |
| 85 | +### Pré-requisitos |
10 | 86 |
|
11 | 87 | - Python 3.10+ |
12 | | -- Chave `OPENROUTER_API_KEY` no arquivo `.env` na raiz do repositório |
| 88 | +- Chave de API do [OpenRouter](https://openrouter.ai/) |
13 | 89 |
|
14 | | -## Instalação |
| 90 | +### Instalação |
15 | 91 |
|
16 | 92 | ```bash |
| 93 | +git clone https://github.com/seu-usuario/KernelBot.git |
| 94 | +cd KernelBot |
17 | 95 | pip install -r requirements.txt |
18 | 96 | ``` |
19 | 97 |
|
20 | | -## Executar |
| 98 | +### Configuração |
| 99 | + |
| 100 | +Crie um arquivo `.env` na raiz: |
| 101 | + |
| 102 | +``` |
| 103 | +OPENROUTER_API_KEY=sua-chave-aqui |
| 104 | +``` |
| 105 | + |
| 106 | +Variáveis opcionais: |
| 107 | + |
| 108 | +| Variável | Default | Descrição | |
| 109 | +|----------|---------|-----------| |
| 110 | +| `ACL_GLOBAL_CONTEXT` | `geral` | Escopo BM25 sem filtro: `geral` (só silo geral) ou `all` (todos os silos) | |
| 111 | +| `ACL_PINNED_MAX_TURNS` | `5` | Turnos antes do pinned context expirar | |
| 112 | +| `ACL_PINNED_MAX_CHARS` | `24000` | Limite de caracteres no pinned context | |
| 113 | +| `ACL_PINNED_WEAK_SCORE` | `0.4` | Score BM25 abaixo do qual se reutiliza o pin | |
| 114 | + |
| 115 | +### Executar |
21 | 116 |
|
22 | 117 | ```bash |
23 | 118 | python main.py |
24 | 119 | ``` |
25 | 120 |
|
26 | | -Ou com Uvicorn: |
| 121 | +Ou com reload para desenvolvimento: |
27 | 122 |
|
28 | 123 | ```bash |
29 | | -uvicorn main:app --host 127.0.0.1 --port 8000 |
| 124 | +uvicorn main:app --host 127.0.0.1 --port 8000 --reload |
30 | 125 | ``` |
31 | 126 |
|
32 | | -Abra `http://127.0.0.1:8000`. |
| 127 | +Acesse `http://127.0.0.1:8000`. |
33 | 128 |
|
34 | | -## Estrutura |
| 129 | +--- |
35 | 130 |
|
36 | | -| Caminho | Função | |
37 | | -|--------|--------| |
38 | | -| `main.py` | Orquestração: logging, `SearchEngine`, watchdog, `create_app` | |
39 | | -| `core/` | Config (`Settings`), logging centralizado | |
40 | | -| `engine/` | BM25 (`SearchEngine`), `ContentWatcher`, `ContextManager`, `ChatProvider` | |
41 | | -| `api/` | Rotas FastAPI (`GET /`, `POST /chat`) | |
42 | | -| `app/` | `create_app()`, estado injetado em `app.state` | |
43 | | -| `content/` | Arquivos `.md` indexados | |
44 | | -| `templates/` | UI (Jinja2) | |
| 131 | +## Comandos do chat |
| 132 | + |
| 133 | +| Comando | Efeito | |
| 134 | +|---------|--------| |
| 135 | +| `/python <pergunta>` | Busca RAG restrita ao silo `python` | |
| 136 | +| `/visualizacao-sql <pergunta>` | Busca RAG restrita ao silo `visualizacao-sql` | |
| 137 | +| `/projeto-bloco <pergunta>` | Busca RAG restrita ao silo `projeto-bloco` | |
| 138 | +| `/planejamento-curso-carreira <pergunta>` | Busca RAG restrita ao silo `planejamento-curso-carreira` | |
| 139 | +| `/doc <pergunta>` | Injeta todo o conteúdo de `content/doc/` no prompt | |
| 140 | +| `/content <pergunta>` | Força RAG no escopo global (com fallback para os primeiros chunks se não houver hit) | |
| 141 | +| `/reset` ou `/limpar` | Limpa o pinned context da sessão | |
| 142 | + |
| 143 | +Sem comando, o sistema usa o campo `discipline` do JSON (se enviado) ou o escopo global configurado. |
| 144 | + |
| 145 | +--- |
45 | 146 |
|
46 | 147 | ## Testes |
47 | 148 |
|
48 | 149 | ```bash |
49 | 150 | python -m pytest tests/ -v |
50 | 151 | ``` |
51 | 152 |
|
52 | | -## Logging |
| 153 | +--- |
| 154 | + |
| 155 | +## Limitações |
| 156 | + |
| 157 | +- **BM25 é lexical, não semântico** — se o vocabulário da pergunta divergir do corpus, o retrieval pode falhar. Use `/content` para forçar contexto. |
| 158 | +- **Sem autenticação** — adequado para uso local (`127.0.0.1`). Não expor em rede sem proteção adicional. |
| 159 | +- **Pinned context em RAM** — reiniciar o servidor perde o estado de sessão. |
| 160 | +- **Comandos de silo não são automáticos** — novas subpastas em `content/` indexam automaticamente, mas o atalho `/nome` precisa ser adicionado manualmente em `_DISCIPLINE_COMMAND_PREFIXES` (`engine/context.py`). |
| 161 | +- **Versões não fixadas** — `requirements.txt` sem pins exatos; risco de drift em novas instalações. |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Por que existe |
| 166 | + |
| 167 | +Criado para resolver uma dor real: alunos do INFNET gastavam horas revisando gravações de aula para tirar dúvidas pontuais. O KernelBot transforma resumos estruturados dessas aulas em uma base pesquisável via chat — com controle de escopo, rastreabilidade e zero dependência de infra complexa. |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## Stack |
| 172 | + |
| 173 | +| Camada | Tecnologia | |
| 174 | +|--------|------------| |
| 175 | +| Servidor HTTP | FastAPI + Uvicorn | |
| 176 | +| Índice de busca | BM25Okapi (`rank-bm25`) | |
| 177 | +| File watching | Watchdog | |
| 178 | +| LLM gateway | OpenRouter API (`httpx` async) | |
| 179 | +| Frontend | HTML/CSS/JS puro (Jinja2) | |
| 180 | +| Streaming | Server-Sent Events (SSE) | |
53 | 181 |
|
54 | | -O projeto usa `logging` da biblioteca padrão com loggers prefixados `kernelbots.*` (ex.: `kernelbots.engine.search`, `kernelbots.api.chat`). Para logs estruturados em JSON no stdout, é possível estender `core/logging_config.py` com algo como `structlog` no mesmo ponto de configuração. |
| 182 | +--- |
55 | 183 |
|
56 | | -## Comandos no chat |
| 184 | +## Licença |
57 | 185 |
|
58 | | -- `/content …` — força uso da base local (com fallback para os primeiros chunks se não houver hit BM25). |
59 | | -- `/doc …` — injeta o conteúdo de `documentation.md` quando disponível no índice. |
| 186 | +Veja o arquivo [LICENSE](LICENSE). |
0 commit comments