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
6 changes: 6 additions & 0 deletions ARQUITETURA.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ Responsavel por:
- persistir metricas em CSV;
- exibir resultado final em formato padrao.

Funcoes principais:

- `_montar_saida_resultado_terminal(resultado)`: template reutilizavel que transforma linhas brutas da query em texto formatado para o terminal, usando `tabulate` para renderizar a tabela. Suporta fallback para amostra quando resultado completo nao estiver disponivel.
- `salvar_resultado_csv(resultado, pasta)`: exporta o resultado completo em CSV com timestamp em `results/`.
- `exibir_resultado_console(resultado)`: orquestra a exibicao completa do resultado: SQL, saida da execucao, tabela formatada, feedback e resposta natural.

### 3) API publica da biblioteca

Arquivos:
Expand Down
6 changes: 6 additions & 0 deletions DESENVOLVIMENTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,14 @@ python main.py --hitl on "Quantos pedidos existem no banco?"

# biblioteca instalada (entrypoint)
text-to-insight --hitl off "Quais categorias vendem mais?"

# com modelo OpenAI
set -a && source .env && set +a
python main.py --hitl off --model gpt-4o-mini --api-key-env OPENAI_API_KEY "Quantos pedidos existem?"
```

O resultado e exibido no terminal em formato tabular sob o bloco `RESULTADO:`, junto com SQL gerada, feedback do critico e resposta natural.

## Estrutura relevante

```text
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ python main.py --hitl off "Quais categorias vendem mais?"
text-to-insight --hitl on "Quantos pedidos existem no banco?"
```

### Saida do terminal

Apos a execucao, o resultado da query e exibido no bloco `RESULTADO` em formato tabular:

```
----------------------------------------------------------------------
RESULTADO:
----------------------------------------------------------------------
+------------+
| COUNT(*) |
+============+
| 99441 |
+------------+
Total de linhas retornadas: 1
```

O template de apresentacao usa `tabulate` para montar as linhas da query:
- Ate 5 linhas: exibe a tabela completa
- Acima de 5 linhas: mostra as 3 primeiras, omite as intermediarias, exibe as 2 ultimas
- Resultado completo e exportado em CSV em `results/` automaticamente

## Testes

Camadas atuais:
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ langchain-openai>=0.1.0
pytest>=9.0.2
pytest-recording>=0.13.0
pytest-timeout>=2.3.0
numpy
numpy
pandas>=2.0.0
tabulate>=0.9.0
7 changes: 6 additions & 1 deletion text_to_insight/nodes/code_agent/code_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def validar_sql_segura(sql: str) -> tuple[bool, str]:
def executar_sql_sqlite(
db_path: str,
sql: str,
limite_preview: int = 30,
limite_preview: int = 5,
) -> dict[str, Any]:
"""
Executa SQL validada em SQLite modo read-only e retorna resultado estruturado.
Expand All @@ -66,6 +66,7 @@ def executar_sql_sqlite(
"ok": False,
"erro_execucao": erro_validacao,
"linhas_resultado_preview": [],
"linhas_resultado_completo": [],
"total_linhas_resultado": 0,
"saida_terminal": f"[SANDBOX] SQL invalida: {erro_validacao}",
}
Expand All @@ -77,6 +78,7 @@ def executar_sql_sqlite(
"ok": False,
"erro_execucao": msg,
"linhas_resultado_preview": [],
"linhas_resultado_completo": [],
"total_linhas_resultado": 0,
"saida_terminal": f"[SANDBOX] {msg}",
}
Expand All @@ -90,11 +92,13 @@ def executar_sql_sqlite(
rows = cur.fetchall()
total = len(rows)
preview_rows = [dict(r) for r in rows[:limite_preview]]
all_rows = [dict(r) for r in rows]

return {
"ok": True,
"erro_execucao": "",
"linhas_resultado_preview": preview_rows,
"linhas_resultado_completo": all_rows,
"total_linhas_resultado": total,
"saida_terminal": (
f"[SANDBOX] Execucao OK | linhas_total={total} "
Expand All @@ -108,6 +112,7 @@ def executar_sql_sqlite(
"ok": False,
"erro_execucao": f"Falha ao executar SQL: {e}",
"linhas_resultado_preview": [],
"linhas_resultado_completo": [],
"total_linhas_resultado": 0,
"saida_terminal": f"[SANDBOX] Erro de execucao: {e}",
}
2 changes: 2 additions & 0 deletions text_to_insight/nodes/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def nos_nodo_sandbox(estado: EstadoTextToInsight) -> dict:
print(f"[EXECUTOR] SQL executada com sucesso — {resultado['total_linhas_resultado']} linhas.")
return {
"linhas_resultado_preview": resultado["linhas_resultado_preview"],
"linhas_resultado_completo": resultado["linhas_resultado_completo"],
"total_linhas_resultado": resultado["total_linhas_resultado"],
"saida_terminal": resultado["saida_terminal"],
"erro_execucao": "",
Expand All @@ -44,6 +45,7 @@ def nos_nodo_sandbox(estado: EstadoTextToInsight) -> dict:
print(f"[EXECUTOR] Erro na execução: {resultado['erro_execucao']}")
return {
"linhas_resultado_preview": [],
"linhas_resultado_completo": [],
"total_linhas_resultado": 0,
"saida_terminal": resultado["saida_terminal"],
"erro_execucao": resultado["erro_execucao"],
Expand Down
74 changes: 64 additions & 10 deletions text_to_insight/runtime.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import annotations

import csv
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable

from tabulate import tabulate

from .utils import salvar_metricas_csv

HITL_AWAITING_STATUS = "AWAITING_USER"
Expand All @@ -23,10 +28,60 @@ def construir_estado_inicial(pergunta: str, db_path: str) -> dict[str, Any]:
"tentativas_loop": 0,
"db_path": db_path,
"espera_humana": False,
"linhas_resultado_completo": [],
"historico_tentativas": [],
}


def _montar_saida_resultado_terminal(resultado: dict[str, Any]) -> str:
"""Monta o texto do bloco de resultado para exibição no terminal."""
linhas = resultado.get("linhas_resultado_completo", []) or []
if not linhas:
linhas = resultado.get("linhas_resultado_preview", []) or []
total = int(resultado.get("total_linhas_resultado", 0) or 0)

if not linhas:
return "[Nenhum resultado]"

colunas = list(linhas[0].keys()) if isinstance(linhas[0], dict) else []
if not colunas:
return "[Resultado indisponivel para exibicao]"

def _formatar_tabela(amostras: list[dict[str, Any]]) -> str:
return tabulate(amostras, headers="keys", tablefmt="grid", showindex=False)

partes: list[str] = []

if len(linhas) <= 5:
partes.append(_formatar_tabela(linhas))
else:
partes.append(_formatar_tabela(linhas[:3]))
partes.append(f"... (omitted {len(linhas) - 5} rows) ...")
partes.append(_formatar_tabela(linhas[-2:]))

partes.append(f"Total de linhas retornadas: {total}")
return "\n".join(partes)


def salvar_resultado_csv(resultado: dict[str, Any], pasta_resultados: Path | None = None) -> Path | None:
"""Salva o resultado completo em CSV quando houver linhas para exportar."""
linhas = resultado.get("linhas_resultado_completo", []) or []
if not linhas:
return None

timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
resultados_dir = pasta_resultados or (Path(__file__).parent.parent / "results")
resultados_dir.mkdir(exist_ok=True)
csv_path = resultados_dir / f"query_{timestamp}.csv"

with csv_path.open("w", newline="", encoding="utf-8") as arquivo_csv:
writer = csv.DictWriter(arquivo_csv, fieldnames=list(linhas[0].keys()))
writer.writeheader()
writer.writerows(linhas)

return csv_path


def exibir_resultado_console(resultado: dict[str, Any]) -> None:
"""Exibe o resultado final de forma consistente entre CLI e engine."""
print("\n" + "=" * 70)
Expand All @@ -48,18 +103,17 @@ def exibir_resultado_console(resultado: dict[str, Any]) -> None:
saida = str(resultado.get("saida_terminal", "")).strip()
print(saida if saida else "[Nenhuma saida]")

# Nova lógica: Exibir resultado como DataFrame
print("\n" + "-" * 70)
print("RESULTADO (preview):")
print("RESULTADO:")
print("-" * 70)
preview = resultado.get("linhas_resultado_preview", []) or []
total = int(resultado.get("total_linhas_resultado", 0) or 0)
if preview:
for row in preview[:10]:
print(row)
if total > 10:
print(f"... ({total - 10} linhas omitidas)")
else:
print("[Nenhum resultado]")

print(_montar_saida_resultado_terminal(resultado))

csv_path = salvar_resultado_csv(resultado)
if csv_path is not None:
total = int(resultado.get("total_linhas_resultado", 0) or 0)
print(f"\n✓ Resultados completos salvos em: {csv_path.as_posix()} ({total} linhas)")

print("\n" + "-" * 70)
print("FEEDBACK DO CRITICO:")
Expand Down
Loading