From 2a5be5c49f6bc72b3268ee8c6a5862a5ba021d92 Mon Sep 17 00:00:00 2001 From: JonasMelo21 Date: Sun, 3 May 2026 19:23:06 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20mostrar=20resultado=20da=20query=20na?= =?UTF-8?q?=20sa=C3=ADda=20do=20terminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARQUITETURA.md | 6 ++ DESENVOLVIMENTO.md | 6 ++ README.md | 21 ++++++ requirements.txt | 4 +- text_to_insight/nodes/code_agent/code_sql.py | 7 +- text_to_insight/nodes/sandbox.py | 2 + text_to_insight/runtime.py | 74 +++++++++++++++++--- 7 files changed, 108 insertions(+), 12 deletions(-) diff --git a/ARQUITETURA.md b/ARQUITETURA.md index 3226d33..1e9a6cc 100644 --- a/ARQUITETURA.md +++ b/ARQUITETURA.md @@ -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: diff --git a/DESENVOLVIMENTO.md b/DESENVOLVIMENTO.md index 8811495..a77d097 100644 --- a/DESENVOLVIMENTO.md +++ b/DESENVOLVIMENTO.md @@ -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 diff --git a/README.md b/README.md index 24bd3dd..d2ca441 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/requirements.txt b/requirements.txt index 76b8fa5..8664add 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,6 @@ langchain-openai>=0.1.0 pytest>=9.0.2 pytest-recording>=0.13.0 pytest-timeout>=2.3.0 -numpy \ No newline at end of file +numpy +pandas>=2.0.0 +tabulate>=0.9.0 \ No newline at end of file diff --git a/text_to_insight/nodes/code_agent/code_sql.py b/text_to_insight/nodes/code_agent/code_sql.py index 3e52869..e737e23 100644 --- a/text_to_insight/nodes/code_agent/code_sql.py +++ b/text_to_insight/nodes/code_agent/code_sql.py @@ -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. @@ -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}", } @@ -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}", } @@ -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} " @@ -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}", } \ No newline at end of file diff --git a/text_to_insight/nodes/sandbox.py b/text_to_insight/nodes/sandbox.py index 414b9ec..ed29409 100644 --- a/text_to_insight/nodes/sandbox.py +++ b/text_to_insight/nodes/sandbox.py @@ -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": "", @@ -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"], diff --git a/text_to_insight/runtime.py b/text_to_insight/runtime.py index c1a7114..d41bb78 100644 --- a/text_to_insight/runtime.py +++ b/text_to_insight/runtime.py @@ -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" @@ -23,9 +28,59 @@ 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": [], } +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) @@ -47,18 +102,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:")