Skip to content

Commit 21bd161

Browse files
authored
Merge pull request frommysql-refactor
Mysql refactor
2 parents 6a1df1b + 0ba0ad8 commit 21bd161

37 files changed

Lines changed: 4893 additions & 1173 deletions

.env.example

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# =============================================================================
2+
# ACL (KernelBot) — variáveis de ambiente
3+
# Copie para .env e preencha os valores reais:
4+
# copy .env.example .env (Windows PowerShell: Copy-Item .env.example .env)
5+
# O ficheiro .env não deve ir para o Git (.gitignore).
6+
# =============================================================================
7+
8+
# --- OpenRouter (obrigatório) ---
9+
# Chave da API em https://openrouter.ai/keys — usada em todas as chamadas ao LLM.
10+
OPENROUTER_API_KEY=
11+
12+
# --- MySQL (obrigatório para o índice BM25 e ingestão) ---
13+
# Host do servidor MySQL (ex.: 127.0.0.1 ou localhost). Nao use 127.0.0.0 — costuma dar timeout.
14+
DB_HOST=
15+
# Porta TCP (padrão MySQL: 3306).
16+
DB_PORT=3306
17+
# Nome da base de dados onde está a tabela `knowledge`.
18+
DB_NAME=
19+
# Utilizador com permissão de leitura (e escrita se correres ingest).
20+
DB_USER=
21+
DB_PASSWORD=
22+
23+
# --- Contexto global (/content sem disciplina) ---
24+
# geral = BM25 em todos os silos e merge por score.
25+
# all = mesmo modo de nome alternativo aceite pelo código.
26+
ACL_GLOBAL_CONTEXT=geral
27+
28+
# --- Contexto fixado (pin na sessão) ---
29+
# Quantos turnos manter chunks fixados antes de voltar a buscar forte.
30+
ACL_PINNED_MAX_TURNS=5
31+
# Tamanho máximo em caracteres do texto fixado no pin.
32+
ACL_PINNED_MAX_CHARS=24000
33+
# Score normalizado abaixo do qual o pin é considerado "fraco" (legado / heurísticas de pin).
34+
ACL_PINNED_WEAK_SCORE=0.4
35+
36+
# --- Política de retrieval (modo strict — ver engine/retrieval.py) ---
37+
# Score BM25 bruto mínimo do melhor candidato; abaixo → hard stop (contexto insuficiente).
38+
ACL_RETRIEVAL_MIN_SCORE=1.5
39+
# Margem mínima entre 1.º e 2.º score; evita ambos muito próximos (ambiguidade).
40+
ACL_RETRIEVAL_MIN_SCORE_MARGIN=0.15
41+
# Cobertura mínima (0–1) dos termos informativos da query nos chunks escolhidos.
42+
ACL_RETRIEVAL_MIN_COVERAGE=0.34
43+
# Igual, mas termos "centrais" da query contam com peso 2× na métrica.
44+
ACL_RETRIEVAL_MIN_COVERAGE_WEIGHTED=0.34
45+
# Número mínimo de termos informativos na pergunta (strict).
46+
ACL_RETRIEVAL_MIN_TERMS=2
47+
# Quantos candidatos o SearchEngine devolve antes de build_decision (1–50).
48+
ACL_RETRIEVAL_CANDIDATE_K=8
49+
# Quantos chunks entram no prompt após passar os gates (1–20).
50+
ACL_RETRIEVAL_TOP_K=4
51+
# Máximo de chunks por fonte (slug) entre os selecionados — diversidade (1–10).
52+
ACL_RETRIEVAL_MAX_CHUNKS_PER_SOURCE=2
53+
54+
# --- Logging ---
55+
# text = linha legivel; json = uma linha JSON por evento ACL (grep/agregadores).
56+
# ACL_LOG_FORMAT=text
57+
# ACL_LOG_LEVEL=INFO
58+
# ACL_LOG_LEVEL=DEBUG # inclui evento retrieval_gates_inputs (metricas antes dos cortes)
59+
60+
# --- Não configurável por .env (definido em core/config.py) ---
61+
# Lista de modelos OpenRouter, URL base, timeout HTTP, textos de system/sticky vêm de ficheiros
62+
# em core/systemPrompt/ — ajusta lá ou no código se precisares de outro stack de modelos.

.gitignore

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
.env
2-
3-
**/__pycache__/
4-
*.py[cod]
5-
*$py.class
6-
*.so
7-
8-
cursor/
1+
.env
2+
3+
.pytest_cache/
4+
5+
**/__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
*.so
9+
10+
cursor/

README.md

Lines changed: 0 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,186 +0,0 @@
1-
<p align="center">
2-
<img src="frontend/assets/images/KernelBanner.webp" alt="KernelBot" width="100%" />
3-
</p>
4-
5-
# KernelBot
6-
7-
**Chatbot RAG local-first que transforma Markdown em base de conhecimento consultável via chat.**
8-
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``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
86-
87-
- Python 3.10+
88-
- Chave de API do [OpenRouter](https://openrouter.ai/)
89-
90-
### Instalação
91-
92-
```bash
93-
git clone https://github.com/seu-usuario/KernelBot.git
94-
cd KernelBot
95-
pip install -r requirements.txt
96-
```
97-
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
116-
117-
```bash
118-
python main.py
119-
```
120-
121-
Ou com reload para desenvolvimento:
122-
123-
```bash
124-
uvicorn main:app --host 127.0.0.1 --port 8000 --reload
125-
```
126-
127-
Acesse `http://127.0.0.1:8000`.
128-
129-
---
130-
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-
---
146-
147-
## Testes
148-
149-
```bash
150-
python -m pytest tests/ -v
151-
```
152-
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) |
181-
182-
---
183-
184-
## Licença
185-
186-
Veja o arquivo [LICENSE](LICENSE).

SQL/pybot.sql

Lines changed: 87 additions & 87 deletions
Large diffs are not rendered by default.

SQL/schema.sql

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
CREATE TABLE IF NOT EXISTS knowledge (
2-
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
3-
title VARCHAR(255) NOT NULL,
4-
content TEXT NOT NULL,
5-
category VARCHAR(100) NOT NULL DEFAULT 'geral',
6-
active TINYINT(1) NOT NULL DEFAULT 1,
7-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
8-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
9-
INDEX idx_active_category (active, category)
10-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
2+
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
3+
slug VARCHAR(255) NOT NULL,
4+
discipline VARCHAR(70) NOT NULL,
5+
title VARCHAR(255) NOT NULL,
6+
`order` INT NOT NULL DEFAULT 0,
7+
content MEDIUMTEXT NOT NULL,
8+
payload JSON NOT NULL,
9+
payload_version SMALLINT NOT NULL DEFAULT 1,
10+
source_checksum CHAR(64) NOT NULL,
11+
active TINYINT(1) NOT NULL DEFAULT 1,
12+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
13+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
14+
UNIQUE KEY uk_slug (slug),
15+
KEY idx_active_discipline (active, discipline),
16+
KEY idx_discipline_order (discipline, `order`),
17+
CONSTRAINT chk_payload_json CHECK (JSON_VALID(payload))
18+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

api/routes.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ async def chat(request: Request) -> StreamingResponse:
8181
log.info("🔄 Comando /reload recebido — reconstruindo índice BM25...")
8282
services.search_engine.rebuild()
8383
chunk_count = len(services.search_engine.chunks)
84-
db_count = sum(1 for c in services.search_engine.chunks if c.get("source", "").startswith("db:"))
85-
md_count = chunk_count - db_count
84+
silo_count = len(services.search_engine.discipline_ids)
8685
status = (
8786
f"Índice reconstruído: {chunk_count} chunk(s) total "
88-
f"({md_count} de arquivos .md + {db_count} do MySQL)."
87+
f"({silo_count} silo(s) do MySQL)."
8988
)
9089
log.info("✅ /reload concluído — %s", status)
9190

@@ -106,7 +105,11 @@ async def _reload_stream() -> AsyncGenerator[str, None]:
106105
)
107106

108107
return StreamingResponse(
109-
services.chat_provider.stream_response(built.messages, trace=built.trace),
108+
services.chat_provider.stream_response(
109+
built.messages,
110+
trace=built.trace,
111+
decision=built.decision,
112+
),
110113
media_type="text/event-stream",
111114
headers={
112115
"Cache-Control": "no-cache",

app/factory.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ def create_app(services: AppServices) -> FastAPI:
2424
async def lifespan(app: FastAPI):
2525
log.info("🚀 ACL iniciado e pronto para receber requisições.")
2626
yield
27-
services.observer.stop()
28-
services.observer.join()
29-
log.info("🛑 Watchdog encerrado. Servidor finalizado.")
27+
log.info("🛑 Servidor finalizado.")
3028

3129
app = FastAPI(title="ACL — Agente de Contexto Local", lifespan=lifespan)
3230
app.state.services = services
@@ -41,4 +39,5 @@ async def lifespan(app: FastAPI):
4139
app.mount("/src", StaticFiles(directory=str(src_dir)), name="src")
4240

4341
app.include_router(router)
42+
4443
return app

app/state.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
from dataclasses import dataclass
66

7-
from watchdog.observers import Observer
8-
97
from engine.chat_provider import ChatProvider
108
from engine.context import ContextManager
119
from engine.pinned_store import PinnedSessionStore
@@ -17,5 +15,4 @@ class AppServices:
1715
search_engine: SearchEngine
1816
context_manager: ContextManager
1917
chat_provider: ChatProvider
20-
observer: Observer
2118
pinned_store: PinnedSessionStore

0 commit comments

Comments
 (0)