Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
117 changes: 117 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: CI

on:
pull_request:
push:
branches:
- main
- dev
workflow_dispatch:
schedule:
- cron: "0 3 * * *"

jobs:
tests-vcr:
name: Tests (VCR deterministic)
runs-on: ubuntu-latest
env:
GOOGLE_API_KEY: dummy-key
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .

- name: Run deterministic test layers
run: |
pytest \
tests/test_componentes.py \
tests/test_nodes.py \
tests/test_integracao.py \
tests/test_main_engine_integracao.py \
-v -s --record-mode=none -m "not real_api"

record-vcr-cassettes:
name: Record VCR cassettes (manual)
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .

- name: Validate API key
run: |
if [[ -z "${GOOGLE_API_KEY}" ]]; then
echo "GOOGLE_API_KEY secret ausente; nao e possivel gravar cassetes."
exit 1
fi

- name: Record or update VCR cassettes
run: |
pytest \
tests/test_nodes.py \
tests/test_integracao.py \
-v -s --record-mode=new_episodes -m "not real_api"

- name: Show cassette status
run: |
git status -- tests/cassettes || true

- name: Upload recorded cassettes artifact
uses: actions/upload-artifact@v4
with:
name: vcr-cassettes-${{ github.run_id }}
path: tests/cassettes
if-no-files-found: warn

tests-real-api:
name: Tests (real API optional)
runs-on: ubuntu-latest
needs: tests-vcr
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .

- name: Run optional real API smoke
run: |
if [[ -z "${GOOGLE_API_KEY}" ]]; then
echo "GOOGLE_API_KEY secret ausente; pulando job real_api."
exit 0
fi
pytest tests/test_real_api_smoke.py -v -s -m real_api
220 changes: 121 additions & 99 deletions ARQUITETURA.md
Original file line number Diff line number Diff line change
@@ -1,137 +1,159 @@
# Arquitetura - Text-to-Insight

## Visão geral
## Visao geral

O sistema usa um grafo de agentes (LangGraph) com 5 nós e 3 roteadores condicionais. Três nós usam LLM (Gemini), um usa introspecção SQLite e um executa SQL diretamente.
O sistema combina:

```
- um grafo LangGraph com 7 nos;
- uma camada de runtime compartilhada (`text_to_insight/runtime.py`);
- duas interfaces de entrada: biblioteca (`InsightEngine`) e CLI (`main.py` -> `text_to_insight/cli.py`).

Fluxo principal:

```text
START
|
[Planejador] ---------> decide estratégia (LLM)
|
|-- schema vazio? --> [Schema] --> extrai metadados do banco
| |
| v
|-- pronto? --------> [Agente Código] --> gera SQL (LLM)
| |
| v
| [Executor] --> executa SQL no banco real
| |
| v
| [Crítico] --> avalia resultado (LLM)
| |
| aprovado? -- Sim --> END
| -- Não --> volta ao Planejador
|
|-- aprovado? ------> END
-> Planejador
-> Schema (quando necessario)
-> Agente de Codigo
-> Executor
-> Critico
-> Resposta
END
```

## Nós
Fluxo alternativo HITL:

### Planejador (`src/nodes/planner.py`)
```text
Planejador -> Espera Humana (interrupt_before) -> Planejador
```

Decide a próxima etapa do fluxo.
## Camadas

- **Sem schema**: retorna `"aguardando_schema"` (determinístico, sem API)
- **Com schema**: chama Gemini para decidir entre `"pronto_codificacao"` ou `"revisando_estrategia"`
- **Já aprovado**: mantém `"aprovado"`
### 1) Orquestracao do grafo

Entrada: `pergunta_usuario`, `contexto_schema`, `feedback_critico`, `status`
Saída: `status`
Arquivo: `text_to_insight/graph.py`

### Schema (`src/nodes/schema.py`)
Responsavel por:

Extrai metadados estruturais do banco SQLite via introspecção (PRAGMA).
- criar os nos;
- definir arestas fixas e condicionais;
- compilar com `MemorySaver`;
- interromper antes de `espera_humana` para suportar HITL.

- Lista tabelas, colunas, tipos, constraints e foreign keys
- Conexão read-only (`?mode=ro`)
- Sem chamada de API
### 2) Runtime compartilhado

Entrada: `db_path`
Saída: `contexto_schema`, `status`
Arquivo: `text_to_insight/runtime.py`

### Agente de Código (`src/nodes/code_agent/code_agent.py`)
Responsavel por:

Gera SQL a partir da pergunta + schema usando Gemini.
- construir estado inicial;
- executar loop de stream ate fim/pausa;
- tratar bloqueio HITL quando `hitl=False`;
- registrar resposta humana na thread;
- persistir metricas em CSV;
- exibir resultado final em formato padrao.

- Prompt inclui schema completo, pergunta do usuário e feedback anterior (se retry)
- Extrai SQL pura da resposta (remove markdown se presente)
- Incrementa `tentativas_loop`
### 3) API publica da biblioteca

Entrada: `pergunta_usuario`, `contexto_schema`, `feedback_critico`
Saída: `sql_gerada`, `status`, `tentativas_loop`
Arquivos:

Utilidades auxiliares em `code_sql.py`:
- `validar_sql_segura()`: bloqueia INSERT, UPDATE, DELETE, DROP, etc. Permite apenas SELECT e WITH.
- `executar_sql_sqlite()`: executa SQL em modo read-only, retorna resultado estruturado.
- `text_to_insight/InsightEngine.py`
- `text_to_insight/__init__.py`

### Executor (`src/nodes/sandbox.py`)
Contrato estavel:

Valida e executa a SQL gerada contra o banco real.
- `run(thread_id, query)`;
- `resume(thread_id, user_response)`;
- `get_insight(...)` como base de compatibilidade.

- Usa `executar_sql_sqlite()` de `code_sql.py`
- Retorna preview de até 30 linhas + total de linhas
- Sem chamada de API
API publica exportada:

Entrada: `sql_gerada`, `db_path`
Saída: `linhas_resultado_preview`, `total_linhas_resultado`, `saida_terminal`, `erro_execucao`, `status`
- `InsightEngine`
- `Graph`
- `EstadoTextToInsight`

### Crítico (`src/nodes/critic.py`)
### 4) Adaptador CLI

Avalia se o resultado responde à pergunta original usando Gemini.
Arquivos:

- Se houve erro de execução: reprova direto sem chamar API
- Se execução OK: chama Gemini com pergunta + SQL + preview dos resultados
- Retorna veredito (`"aprovado"` / `"reprovado"`) e feedback textual
- `text_to_insight/cli.py`
- `main.py`

Entrada: `pergunta_usuario`, `sql_gerada`, `linhas_resultado_preview`, `status`
Saída: `feedback_critico`, `status`
`main.py` e um wrapper fino da CLI do pacote.

## Roteadores (`src/routers/edges.py`)
## Nos do grafo

### Após Executor (`roteador_sandbox`)
- `exec_ok` → Crítico
- `exec_erro` + tentativas < 3 → Planejador
- Caso contrário → Planejador
### Planejador (`text_to_insight/nodes/planner.py`)

### Após Planejador (`roteador_planejador`)
- Schema vazio → Schema
- `pronto_codificacao` ou `revisando_estrategia` → Agente Código
- `aprovado` → END
- sem schema: `aguardando_schema` (deterministico)
- com schema: decide entre `pronto_codificacao`, `revisando_estrategia` ou `necessita_ajuda`
- opcionalmente ativa HITL (`espera_humana=True`)

### Após Crítico (`roteador_critico`, definido em `graph.py`)
- `aprovado` → END
- Caso contrário → Planejador
### Espera Humana (`text_to_insight/graph.py`)

## Estado compartilhado (`src/state.py`)
- no estrutural sem transformacao de estado
- usado como ponto de interrupcao para retomada por `thread_id`

```python
# Campos obrigatórios
pergunta_usuario: str # Pergunta em linguagem natural
db_path: str # Caminho para o SQLite
### Schema (`text_to_insight/nodes/schema.py`)

# Campos preenchidos pelos nós
contexto_schema: str # Schema textual extraído
sql_gerada: str # SQL produzida pelo agente
linhas_resultado_preview: list[dict] # Amostra de até 30 linhas
total_linhas_resultado: int # Total de linhas retornadas
erro_execucao: str # Mensagem de erro (se houver)
saida_terminal: str # Saída resumida da execução
feedback_critico: str # Feedback do crítico
status: StatusExecucao # Estágio atual do fluxo
tentativas_loop: int # Contador de tentativas
```
- introspeccao SQLite real em modo read-only
- extrai tabelas, colunas e relacionamentos

Status possíveis: `iniciado`, `aguardando_schema`, `schema_obtido`, `pronto_codificacao`, `sql_gerada`, `exec_ok`, `exec_erro`, `revisando_estrategia`, `aprovado`, `reprovado`.
### Agente de Codigo (`text_to_insight/nodes/code_agent/code_agent.py`)

## Fluxo típico
- gera SQL com LLM a partir de pergunta + schema + feedback

```
1. START → Planejador (schema vazio → "aguardando_schema")
2. → Schema (extrai metadados do SQLite)
3. → Agente Código (gera SQL com Gemini)
4. → Executor (executa SQL no banco)
5. → Crítico (avalia resultado com Gemini)
6. → Se aprovado: END
Se reprovado: volta ao passo 1 com feedback
```
### Executor (`text_to_insight/nodes/sandbox.py`)

- valida SQL e executa via `code_sql.py`
- devolve preview + total de linhas

### Critico (`text_to_insight/nodes/critic.py`)

- valida se resultado responde a pergunta
- reprova automaticamente em erro de execucao

### Resposta (`text_to_insight/nodes/response.py`)

- gera resposta natural final quando status aprovado

## Roteadores

Arquivo: `text_to_insight/routers/edges.py`

- `roteador_sandbox`: controla retry apos execucao
- `roteador_planejador`: decide schema, codificacao, HITL ou fim
- `roteador_critico` (interno em `graph.py`): aprovado -> resposta; senao -> planejador

## Estado compartilhado

Arquivo: `text_to_insight/state.py`

Campos obrigatorios:

- `pergunta_usuario`
- `db_path`

Campos principais do fluxo:

- `contexto_schema`, `sql_gerada`, `linhas_resultado_preview`, `total_linhas_resultado`
- `erro_execucao`, `saida_terminal`, `feedback_critico`, `resposta_natural`
- `status`, `tentativas_loop`, `historico_conversa`, `espera_humana`, `pergunta_ao_usuario`
- telemetria: `tokens_input`, `tokens_output`, `tokens_total`

## Status operacionais

No runtime/engine podem aparecer:

- `AWAITING_USER` (pausa HITL aguardando resposta);
- `bloqueado_hitl` (quando HITL esta desligado e o planejamento pede input humano).

## Politica VCR

Os testes marcados com `@pytest.mark.vcr` usam cassetes em `tests/cassettes/`.

- no fluxo padrao de PR/CI: `--record-mode=none` (somente replay deterministico);
- para gravar ou atualizar cassetes: `--record-mode=new_episodes`;
- apos gravacao: execute novamente com `--record-mode=none` para validar reproducibilidade.

Na CI existe um job manual `record-vcr-cassettes` (workflow_dispatch) para gravacao/atualizacao controlada.
Loading
Loading