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
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ dependencies = [
"networkx>=3.0",
"chromadb>=1.5.9",
"tabulate>=0.10.0",
"psycopg2-binary>=2.9.0",
"pymysql>=1.1.0",
"sqlalchemy>=2.0.0",
]

[project.scripts]
Expand Down
30 changes: 27 additions & 3 deletions text_to_insight/InsightEngine.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,25 @@ class InsightEngine:
- `resume(...)` continua uma consulta que ficou pausada em HITL.
"""

def __init__(self, api_key: str, model: str, db_path: str, hitl: bool = False, show_output: bool = False, enable_graphs: bool = True, use_cot: bool = True, use_data_exploration: bool = True, use_exploration_selector: str = "off", use_rag: bool = True, enrich_rag: bool = False, inferir_fks_virtuais: bool = False, usar_schemacrawler: bool = True):
def __init__(
self,
api_key: str,
model: str,
db_path: str,
hitl: bool = False,
show_output: bool = False,
enable_graphs: bool = True,
use_cot: bool = True,
use_data_exploration: bool = True,
use_exploration_selector: str = "off",
use_rag: bool = True,
enrich_rag: bool = False,
inferir_fks_virtuais: bool = False,
usar_schemacrawler: bool = True,
db_dialeto: str = "",
db_config: dict | None = None,
db_url: str = "",
):
self._hitl_ativado = hitl
# `show_output` controla se a engine imprime o resultado final no terminal.
# Em cenários com CLI, normalmente deixamos False para evitar saída duplicada.
Expand All @@ -25,6 +43,9 @@ def __init__(self, api_key: str, model: str, db_path: str, hitl: bool = False, s
self._enrich_rag = enrich_rag
self._inferir_fks_virtuais = inferir_fks_virtuais
self._usar_schemacrawler = usar_schemacrawler
self._db_dialeto = db_dialeto
self._db_config = db_config
self._db_url = db_url
self._model = model
self._db_path = db_path
self._use_cot = use_cot
Expand All @@ -41,7 +62,7 @@ def __init__(self, api_key: str, model: str, db_path: str, hitl: bool = False, s
use_data_exploration=self._use_data_exploration,
use_exploration_selector=self._use_exploration_selector,
use_rag=self._use_rag,
enrich_rag=self._enrich_rag
enrich_rag=self._enrich_rag,
)

print(f"[CONFIG] HITL: {'ATIVADO' if self._hitl_ativado else 'DESATIVADO'}")
Expand Down Expand Up @@ -130,7 +151,10 @@ def get_insight(
)
# Caso 2: chamada nova (primeira execução para essa pergunta).
elif query:
estado_execucao = construir_estado_inicial(query, self._db_path, self._inferir_fks_virtuais, self._usar_schemacrawler)
estado_execucao = construir_estado_inicial(
query, self._db_path, self._inferir_fks_virtuais, self._usar_schemacrawler,
db_dialeto=self._db_dialeto, db_config=self._db_config, db_url=self._db_url,
)
pergunta_exibicao = query
# Caso 3: a thread já está pausada, mas ainda sem resposta do usuário.
elif snapshot.next:
Expand Down
64 changes: 63 additions & 1 deletion text_to_insight/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,57 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
default="data/olist_relational.db",
help="Caminho para o banco SQLite.",
)
parser.add_argument(
"--db-dialeto",
choices=["sqlite", "postgresql", "mysql"],
default="",
help=(
"Dialeto do banco: sqlite, postgresql ou mysql. Se omitido, e detectado "
"automaticamente pela extensao de --db-path (.duckdb, .sqlite, etc), "
"com sqlite como fallback."
),
)
parser.add_argument(
"--db-host",
default=None,
help="Host do banco (obrigatório se --db-dialeto != sqlite).",
)
parser.add_argument(
"--db-port",
type=int,
default=None,
help="Porta do banco (padrão: 5432 para postgresql, 3306 para mysql).",
)
parser.add_argument(
"--db-name",
default=None,
help="Nome do banco de dados remoto.",
)
parser.add_argument(
"--db-user",
default=None,
help="Usuário do banco remoto.",
)
parser.add_argument(
"--db-password",
default=os.getenv("DB_PASSWORD"),
help=(
"Senha do banco remoto. EVITE usar esta flag diretamente: qualquer "
"argumento de linha de comando fica visivel no historico do shell e "
"na lista de processos do sistema. Prefira definir a variavel de "
"ambiente DB_PASSWORD (ex: no .env)."
),
)
parser.add_argument(
"--db-url",
default=os.getenv("DATABASE_URL"),
help=(
"URL unica de conexao (ex: postgresql://usuario:senha@host:5432/banco). "
"Se informado, o dialeto e detectado automaticamente pelo SQLAlchemy e as "
"demais flags --db-dialeto/--db-host/--db-port/--db-name/--db-user/--db-password "
"sao ignoradas. Tambem pode vir da variavel de ambiente DATABASE_URL."
),
)
parser.add_argument(
"--model",
default="gpt-5-mini",
Expand Down Expand Up @@ -122,11 +173,22 @@ def main(argv: list[str] | None = None) -> dict[str, Any]:
f"Variável de ambiente '{args.api_key_env}' não encontrada. "
"Configure a chave da API antes de executar."
)

db_config = None
if args.db_dialeto in ("postgresql", "mysql"):
db_config = {
"host": args.db_host,
"port": args.db_port,
"database": args.db_name,
"user": args.db_user,
"password": args.db_password,
}
engine = InsightEngine(
api_key=api_key,
model=args.model,
db_path=args.db_path,
db_dialeto=args.db_dialeto,
db_config=db_config,
db_url=args.db_url or "",
hitl=hitl_ativado,
enrich_rag=enrich_rag_ativado,
inferir_fks_virtuais=inferir_fks_ativado,
Expand Down
65 changes: 63 additions & 2 deletions text_to_insight/nodes/code_agent/code_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,63 @@
from ...state import EstadoTextToInsight
from ...utils import extrair_tokens

PROMPT_TEMPLATE_COT = """Você é um especialista em SQL para bancos SQLite.
# Nome de exibição e observações de sintaxe por dialeto, injetados no prompt do
# LLM para que a SQL gerada use a sintaxe correta do banco alvo (não apenas SQLite).
_NOME_DIALETO = {
"sqlite": "SQLite",
"postgresql": "PostgreSQL",
"mysql": "MySQL",
}

_NOTA_DIALETO = {
"sqlite": (
"Sintaxe SQLite: use || para concatenar strings, strftime() para "
"datas/horas, e LIMIT/OFFSET para paginação. Identificadores não "
"precisam de aspas."
),
"postgresql": (
"Sintaxe PostgreSQL: use || para concatenar strings, EXTRACT()/"
"TO_CHAR() para datas/horas, e LIMIT/OFFSET para paginação. "
"Identificadores com maiúsculas ou caracteres especiais exigem "
"aspas duplas (ex: \"NomeColuna\"); NÃO use crases (`)."
),
"mysql": (
"Sintaxe MySQL: use CONCAT() para concatenar strings (NÃO use ||), "
"DATE_FORMAT()/EXTRACT() para datas/horas, e LIMIT/OFFSET para "
"paginação. Identificadores podem usar crases (`nome_coluna`); "
"NÃO use aspas duplas para identificadores."
),
}


def _resolver_info_dialeto(estado: EstadoTextToInsight) -> tuple[str, str]:
"""Resolve (nome_exibicao, nota_sintaxe) do dialeto configurado no estado.

Ordem de resolução:
1. `db_dialeto`, se informado explicitamente.
2. `db_url`, cujo dialeto é lido do prefixo da URL (sem conectar).
3. 'sqlite' como default, preservando o comportamento anterior quando
nada é informado (compatibilidade retroativa).
"""
dialeto = (estado.get("db_dialeto") or "").strip().lower()

if not dialeto:
db_url = (estado.get("db_url") or "").strip()
if db_url:
try:
from sqlalchemy.engine import make_url

dialeto = make_url(db_url).get_backend_name()
except Exception:
dialeto = ""

dialeto = dialeto or "sqlite"
nome = _NOME_DIALETO.get(dialeto, _NOME_DIALETO["sqlite"])
nota = _NOTA_DIALETO.get(dialeto, _NOTA_DIALETO["sqlite"])
return nome, nota


PROMPT_TEMPLATE_COT = """Você é um especialista em SQL para bancos {dialeto}.

Sua tarefa: gerar UMA única consulta SQL SELECT que responda à pergunta do usuário,
usando o schema do banco de dados fornecido abaixo.
Expand All @@ -23,6 +79,7 @@
- Use nomes de tabelas e colunas EXATAMENTE como aparecem no schema.
- Se a pergunta for ambígua, faça a interpretação mais razoável.
- Use as estatísticas de dados (DATA EXPLORATION) para entender distribuições, formatos e valores reais das colunas.
- {nota_dialeto}

=== SCHEMA DO BANCO ===
{schema}
Expand All @@ -47,7 +104,7 @@
Sua consulta SQL aqui
```"""

PROMPT_TEMPLATE_NO_COT = """Você é um especialista em SQL para bancos SQLite.
PROMPT_TEMPLATE_NO_COT = """Você é um especialista em SQL para bancos {dialeto}.

Sua tarefa: gerar UMA única consulta SQL SELECT que responda à pergunta do usuário,
usando o schema do banco de dados fornecido abaixo.
Expand All @@ -59,6 +116,7 @@
- Use nomes de tabelas e colunas EXATAMENTE como aparecem no schema.
- Se a pergunta for ambígua, faça a interpretação mais razoável.
- Use as estatísticas de dados (DATA EXPLORATION) para entender distribuições, formatos e valores reais das colunas e raciocinar sobre a natureza do D.
- {nota_dialeto}

=== SCHEMA DO BANCO ===
{schema}
Expand Down Expand Up @@ -124,10 +182,13 @@ def nos_nodo_agente_codigo(estado: EstadoTextToInsight, llm: ChatGoogleGenerativ
print(f"[AGENTE_CODIGO] Gerando SQL (tentativa {tentativas + 1})...")

historico_section = _formatar_historico_tentativas(historico)
dialeto_nome, nota_dialeto = _resolver_info_dialeto(estado)

template = PROMPT_TEMPLATE_COT if use_cot else PROMPT_TEMPLATE_NO_COT

prompt = template.format(
dialeto=dialeto_nome,
nota_dialeto=nota_dialeto,
schema=schema_rag if schema_rag else schema,
data_exploration=data_exploration if data_exploration else "Não disponível.",
pergunta=pergunta,
Expand Down
Loading
Loading