Skip to content

Commit d61cee0

Browse files
committed
Enhance retrieval policy and documentation
- Updated the `.gitignore` to include `.pytest_cache/`. - Improved the `README.md` to clarify the strict local mode of the ACL, detailing the hard stop mechanisms and retrieval policies. - Modified `api/routes.py` to include the decision in the streaming response. - Enhanced `documentation.md` to explain the retrieval decision process and hard stop scenarios. - Updated `core/config.py` to add retrieval thresholds for better configuration management. - Refactored `engine/chat_provider.py` and `engine/context.py` to implement hard stop logic and improve context management. - Revised `engine/search.py` to separate retrieval logic and maintain backward compatibility with the old search API. - Added new evaluation questions and answers to `evaluation/questions_answers.json` for testing retrieval scenarios.
1 parent 037f636 commit d61cee0

21 files changed

Lines changed: 2915 additions & 271 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.env
22

3+
.pytest_cache/
4+
35
**/__pycache__/
46
*.py[cod]
57
*$py.class

README.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Agente de contexto local com RAG (BM25) sobre **MySQL**, interface em `templates/` e respostas em streaming via OpenRouter.
44

5+
**Modo local estrito (padrão):** desde a mitigação descrita em `.cursor/plans/rag_acl_incremental_6951b55f.plan.md`, o ACL opera por padrão em modo `strict`. A geração só acontece quando a decisão de retrieval permitir; pergunta sem base não chama LLM, `/content` não injeta mais `scope_chunks[:5]` e respostas que passam pelos gates mas falham no sanity check pós-geração são substituídas por hard stop `post_generation_misalignment`. A regra central é: na dúvida, não responder.
6+
57
## Requisitos
68

79
- Python 3.10+
@@ -85,10 +87,58 @@ O projeto usa `logging` da biblioteca padrão com loggers prefixados `kernelbots
8587
## Comandos no chat
8688

8789
- `/reload` — reconstrói o índice BM25 a partir do MySQL.
88-
- `/content …`força uso da base local (com fallback para os primeiros chunks se não houver hit BM25).
89-
- `/doc …` — injeta o conteúdo da documentação quando disponível no índice.
90+
- `/content …`busca global explícita; **sem fallback** de primeiros chunks. Se não houver hit forte o bastante, retorna hard stop com reformulação guiada.
91+
- `/doc …` — injeta o conteúdo da documentação quando disponível no índice; fluxo determinístico (não passa pela política de decisão).
9092
- `/python …`, `/visualizacao-sql …`, `/projeto-bloco …`, `/planejamento-curso-carreira …` — filtra por disciplina.
9193

94+
## Política de retrieval e hard stop
95+
96+
A camada `engine/retrieval.py` separa recuperação lexical bruta (`SearchEngine.search_candidates`) da decisão de suficiência (`build_decision`). A decisão devolve `RetrievalDecision` com `allow_generation`, `reason`, `confidence` e `trace` estruturado, exposto no SSE como `[ACL_META]`.
97+
98+
Hard stops possíveis:
99+
100+
| `reason` | Quando dispara |
101+
|---|---|
102+
| `insufficient_context` | Sem hits ou `top_score < MIN_SCORE` |
103+
| `underspecified_query` | Menos que `MIN_TERMS` termos informativos (strict) |
104+
| `vague_but_high_risk` | Termo vago (`performance`, `erro`, `timeout`) sem domínio específico |
105+
| `ambiguous_retrieval` | `top_score - second_score < MIN_SCORE_MARGIN` |
106+
| `context_misaligned` | `coverage < MIN_COVERAGE` no melhor chunk |
107+
| `low_confidence` | Coverage ponderada baixa ou termo central ausente no modo strict |
108+
| `post_generation_misalignment` | Sanity check pós-geração detectou resposta fora dos chunks |
109+
| `provider_error` | Todos os modelos OpenRouter falharam |
110+
111+
Thresholds configuráveis via `.env` (ver `core/config.py`):
112+
113+
| Variável | Default | Efeito |
114+
|---|---|---|
115+
| `ACL_RETRIEVAL_MIN_SCORE` | 1.5 | Hard stop se `top_score` bruto for menor |
116+
| `ACL_RETRIEVAL_MIN_SCORE_MARGIN` | 0.15 | Margem mínima entre top1 e top2 |
117+
| `ACL_RETRIEVAL_MIN_COVERAGE` | 0.34 | Overlap mínimo de termos informativos |
118+
| `ACL_RETRIEVAL_MIN_COVERAGE_WEIGHTED` | 0.34 | Idem, com centrais valendo 2x |
119+
| `ACL_RETRIEVAL_MIN_TERMS` | 2 | Termos informativos mínimos em `strict` |
120+
| `ACL_RETRIEVAL_CANDIDATE_K` | 8 | Quantos candidatos o retrieval devolve |
121+
| `ACL_RETRIEVAL_TOP_K` | 4 | Quantos vão para o prompt |
122+
| `ACL_RETRIEVAL_MAX_CHUNKS_PER_SOURCE` | 2 | Diversidade de fonte |
123+
124+
## Calibração e avaliação
125+
126+
A pasta `evaluation/` contém a ferramenta que gera o artefato exigido pelo plano incremental:
127+
128+
```bash
129+
python -m evaluation.calibration_runner --questions evaluation/all.md --out evaluation/calibration_traces.jsonl --limit 20
130+
python -m evaluation.calibration_summary --traces evaluation/calibration_traces.jsonl
131+
```
132+
133+
O runner produz um JSONL com trace completo por query (scores, coverage, termos, fontes, decisão) e um placeholder `manual_label` para revisão humana. O summary imprime percentis de scores e as taxas `Stop vs Answer`, `Ambiguous Retrieval`, `Underspecified Query`, `Vague But High Risk`.
134+
135+
## Limitações conhecidas
136+
137+
- **BM25-only é mitigação temporária.** O módulo `engine/retrieval.py` deixa explícito (`retrieval_mode = "bm25_lexical_temporary"`). A evolução correta é busca híbrida (BM25 + embeddings + reranking), fora do escopo inicial.
138+
- **Coverage lexical** aproxima alinhamento semântico, não resolve. Por isso a Fase 3 adiciona sanity check pós-geração como último firewall.
139+
- **Calibração manual de 20 casos é bootstrap**, não prova final. A evolução prevista é 100–200 casos semi-automatizados.
140+
- **Modo `assistive`** existe como flag, mas não é ativado por padrão. A mitigação estabiliza o modo `strict` primeiro.
141+
92142
## Fluxo de dados
93143

94144
```

api/routes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ async def _reload_stream() -> AsyncGenerator[str, None]:
105105
)
106106

107107
return StreamingResponse(
108-
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+
),
109113
media_type="text/event-stream",
110114
headers={
111115
"Cache-Control": "no-cache",

0 commit comments

Comments
 (0)