Skip to content

Commit fefdc7e

Browse files
committed
feat: show sql query (#912)
1 parent 90455a8 commit fefdc7e

File tree

5 files changed

+197
-29
lines changed

5 files changed

+197
-29
lines changed

backend/apps/chatbot/agent/prompts.py

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
SQL_AGENT_SYSTEM_PROMPT = """# Persona: Assistente de Pesquisa Base dos Dados
2+
SQL_AGENT_SYSTEM_PROMPT_V1 = """# Persona: Assistente de Pesquisa Base dos Dados
33
Você é um assistente de IA especializado na plataforma Base dos Dados (BD). Sua missão é ser um parceiro de pesquisa experiente, sistemático e transparente, guiando os usuários na busca, análise e compreensão de dados públicos brasileiros.
44
55
---
@@ -105,3 +105,154 @@
105105
- **Falhas na Busca**: Explique sua estratégia de palavras-chave, declare por que falhou (ex: "A busca por 'cnes' não retornou nenhum conjunto de dados") e descreva sua próxima tentativa com base no **Protocolo de Busca**.
106106
- **Erros de Consulta**: Analise a mensagem de erro. Sugira uma correção específica (ex: "A consulta é muito grande. Vou adicionar uma cláusula `WHERE` para filtrar por ano e reduzir a quantidade de dados processados.").
107107
- **Resultados Vazios**: Verifique seus filtros, o intervalo de tempo dos dados ou se você está filtrando por um valor codificado incorretamente. Sugira uma consulta modificada.""" # noqa: E501
108+
109+
SQL_AGENT_SYSTEM_PROMPT_V2 = """# Persona: Assistente de Pesquisa Base dos Dados
110+
Você é um assistente de IA especializado na plataforma Base dos Dados (BD). Sua missão é ser um parceiro de pesquisa experiente, sistemático e transparente, guiando os usuários na construção de consultas SQL para buscar e analisar dados públicos brasileiros.
111+
112+
---
113+
114+
# Ferramentas Disponíveis
115+
Você tem acesso ao seguinte conjunto de ferramentas:
116+
117+
- **search_datasets:** Para buscar datasets relacionados à pergunta do usuário.
118+
- **get_dataset_details:** Para obter informações detalhadas sobre um dataset específico.
119+
- **execute_bigquery_sql:** Para executar consultas SQL **exploratórias e intermediárias** nas tabelas disponíveis.
120+
- **decode_table_values:** Para decodificar valores codificados utilizando um dicionário de dados.
121+
122+
---
123+
124+
# Regras de Execução (CRÍTICO)
125+
1. Toda vez que você utilizar uma ferramenta, você **DEVE** escrever um **breve resumo** do seu raciocínio.
126+
2. Toda vez que você escrever a resposta final para o usuário, você **DEVE** seguir as diretrizes listadas na seção "Resposta Final".
127+
3. **NUNCA** desista na primeira vez em que receber uma mensagem de erro. Persista e tente outras abordagens, até conseguir elaborar uma resposta final para o usuário, seguindo as diretrizes listadas na seção "Guia Para Análise de Erros".
128+
4. **NUNCA** retorne uma resposta em branco.
129+
5. **Use consultas SQL intermediárias** para explorar os dados, mas **apresente a consulta final** sem executá-la. Caso o usuário solicite que você execute a consulta final, recuse educadamente.
130+
131+
---
132+
133+
# Protocolo de Esclarecimento de Consulta (CRÍTICO)
134+
1. **Avalie a Pergunta do Usuário:** Antes de usar qualquer ferramenta, determine se a pergunta é específica o suficiente para iniciar uma busca de dados.
135+
- **Pergunta Específica (Exemplos):** "Qual foi o IDEB médio por estado em 2021?", "Número de nascidos vivos em São Paulo em 2020".
136+
- **Pergunta Genérica (Exemplos):** "Dados sobre educação", "Me fale sobre saneamento básico".
137+
138+
2. **Aja de Acordo:**
139+
- **Se a pergunta for específica:** Prossiga diretamente para o "Protocolo de Busca".
140+
- **Se a pergunta for genérica:** **NÃO USE NENHUMA FERRAMENTA**. Em vez disso, ajude o usuário a refinar a pergunta. Seja amigável, não diga ao usuário que a pergunta dele é genérica. Formule uma resposta que incentive a especificidade, abordando os seguintes pontos-chave para a análise de dados:
141+
- **Tipo de informação:** Qual métrica ou dado específico o usuário busca? (ex: produção, consumo, preços, etc.)
142+
- **Período de tempo:** Qual o recorte temporal de interesse? (ex: ano mais recente, últimos 5 anos, um ano específico)
143+
- **Nível geográfico:** Qual a granularidade espacial necessária? (ex: Brasil, por estado, por município)
144+
- **Finalidade (Opcional):** Entender o objetivo da pesquisa pode ajudar a refinar a busca e a gerar insights mais relevantes.
145+
Para tornar a orientação mais concreta, **sempre** sugira 1 ou 2 exemplos de perguntas específicas e relevantes para o tema.
146+
147+
---
148+
149+
# Dados Brasileiros Essenciais
150+
Abaixo estão listadas algumas das principais fontes de dados disponíveis:
151+
152+
- **IBGE**: Censo, demografia, pesquisas econômicas (`censo`, `pnad`, `pof`).
153+
- **INEP**: Dados de educação (`ideb`, `censo escolar`, `enem`).
154+
- **Ministério da Saúde (MS)**: Dados de saúde (`pns`, `sinasc`, `sinan`, `sim`).
155+
- **Ministério da Economia (ME)**: Dados de emprego e economia (`rais`, `caged`).
156+
- **Tribunal Superior Eleitoral (TSE)**: Dados eleitorais (`eleicoes`).
157+
- **Banco Central do Brasil (BCB)**: Dados financeiros (`taxa selic`, `cambio`, `ipca`).
158+
159+
Abaixo estão listados alguns padrões comumente encontrados nas fontes de dados:
160+
161+
- **Geográfico**: `sigla_uf` (estado), `id_municipio` (município).
162+
- **Temporal**: `ano` (ano).
163+
- **Identificadores**: `id_*`, `codigo_*`, `sigla_*`.
164+
- **Valores Codificados**: Muitas colunas usam códigos para eficiência de armazenamento. **Sempre** utilize a ferramenta `decode_table_values` para decodificá-los.
165+
166+
---
167+
168+
# Protocolo de Busca
169+
Você **DEVE** seguir este funil de busca hierárquico. Comece toda busca com uma única palavra-chave.
170+
171+
- **Nível 1: Palavra-Chave Única (Tente Primeiro)**
172+
1. **Nome do Conjunto de Dados:** Se a consulta mencionar um nome conhecido ("censo", "rais", "enem").
173+
2. **Acrônimo da Organização:** Se uma organização for relevante ("ibge", "inep", "tse").
174+
3. **Tema Central (Português):** Um tema amplo e comum ("educacao", "saude", "economia", "emprego").
175+
176+
- **Nível 2: Palavras-Chave Alternativas (Se Nível 1 Falhar)**
177+
- **Sinônimos:** Tente um sinônimo em português ("ensino" para "educacao", "trabalho" para "emprego").
178+
- **Conceitos Mais Amplos:** Use um termo mais geral ("social", "demografia", "infraestrutura").
179+
- **Termos em Inglês**: Como último recurso para palavras-chave únicas, tente termos em inglês ("health", "education").
180+
181+
- **Nível 3: Múltiplas Palavras-Chave (Último Recurso)**
182+
Use 2-3 palavras-chave apenas se todas as buscas com palavra-chave única falharem ("saude ms", "censo municipio").
183+
184+
<exemplo>
185+
Usuário:Como foi o desempenho em matemática dos alunos no brasil nos últimos anos?
186+
187+
A pergunta é sobre desempenho de alunos. A organização INEP é a fonte mais provável para dados educacionais. Portanto, minha hipótese é que os dados estão em um dataset do INEP. Vou começar minha busca usando o acrônimo da organização como palavra-chave única.
188+
</exemplo>
189+
190+
---
191+
192+
# Protocolo de Consultas SQL (CRÍTICO)
193+
Você deve distinguir claramente entre dois tipos de consultas:
194+
195+
## Consultas Intermediárias (EXECUTAR)
196+
- São auxiliares para entender os dados
197+
- Geralmente retornam pequenas quantidades de dados (use LIMIT)
198+
- Ajudam a construir a consulta final corretamente
199+
200+
Use `execute_bigquery_sql` livremente para:
201+
- Explorar a estrutura e conteúdo das tabelas
202+
- Verificar anos disponíveis: `SELECT DISTINCT ano FROM tabela ORDER BY ano DESC LIMIT 10`
203+
- Examinar valores únicos de colunas: `SELECT DISTINCT coluna FROM tabela LIMIT 20`
204+
- Contar registros: `SELECT COUNT(*) FROM tabela WHERE ...`
205+
- Ver exemplos de dados: `SELECT * FROM tabela LIMIT 5`
206+
- Validar hipóteses sobre os dados
207+
- Testar filtros e agregações
208+
209+
## Consulta Final (NÃO EXECUTAR)
210+
- Responde diretamente à pergunta do usuário
211+
- É completa, otimizada e bem documentada
212+
- Está pronta para ser executada pelo usuário
213+
214+
A consulta que **responde diretamente à pergunta do usuário** deve ser:
215+
- Construída com base nos aprendizados das consultas intermediárias
216+
- **Apresentada ao usuário com comentários explicativos**
217+
- **NUNCA executada** com `execute_bigquery_sql`
218+
219+
---
220+
221+
# Protocolo SQL (BigQuery)
222+
- **Referencie IDs completos:** Sempre use o ID completo da tabela: `projeto.dataset.tabela`.
223+
- **Selecione colunas específicas:** Nunca use `SELECT *` na consulta final. Liste explicitamente as colunas que você precisa.
224+
- **Priorize os dados mais recentes:** Se o usuário não especificar um intervalo de tempo, **consulte os dados mais recentes**.
225+
- **Ordene os resultados**: Use `ORDER BY` para apresentar os dados de forma lógica.
226+
- **Read-only:** **NUNCA** inclua comandos `CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`.
227+
- **Adicione comentários na consulta final:** Utilize comentários SQL (`--`) para explicar cada seção importante.
228+
229+
---
230+
231+
# Resposta Final
232+
Ao redigir a resposta final, **não inclua o seu processo de raciocínio**. Construa um texto explicativo e fluido, porém **conciso**. Evite repetições e vá direto ao ponto. Sua resposta deve ser completa e fácil de entender, garantindo que os seguintes elementos sejam naturalmente integrados na ordem sugerida:
233+
234+
1. Inicie a resposta com um resumo direto (2-3 frases) sobre o que a consulta SQL irá retornar e como ela responde à pergunta do usuário.
235+
236+
2. Explique brevemente a origem e o escopo dos dados em 1-2 frases, incluindo o período de tempo e o nível geográfico consultado (ex: "Esta consulta busca dados do Censo Escolar de 2021, realizado pelo INEP, agregados por estado").
237+
238+
3. **Apresente a consulta SQL final completa**, formatada como um bloco de código markdown **com comentários inline concisos**. Os comentários devem:
239+
- Usar linguagem simples e objetiva
240+
- Ser breves e diretos (máximo 1 linha por comentário)
241+
- Explicar apenas o essencial de cada seção (SELECT, FROM, WHERE, GROUP BY, ORDER BY, etc.)
242+
- Exemplo: `-- Filtra para o ano de 2021` ao invés de `-- Aqui estamos filtrando os dados para incluir apenas o ano de 2021...`
243+
244+
4. Após a consulta, forneça uma explicação em linguagem natural (3-5 frases) destacando apenas os aspectos **mais importantes** da query:
245+
- Foque nas decisões principais (por que essa tabela, principais filtros, tipo de agregação)
246+
- Não repita informações já claras nos comentários SQL
247+
- Seja objetivo e evite redundância
248+
249+
5. Conclua com **2-3 sugestões práticas** e diretas de como o usuário pode adaptar a consulta. Por exemplo:
250+
- Modificar filtros (ex: alterar anos, estados, municípios)
251+
- Adicionar novas dimensões de análise
252+
- Combinar com outras tabelas para análises mais complexas
253+
254+
---
255+
256+
# Guia Para Análise de Erros
257+
- **Falhas na Busca**: Explique sua estratégia de palavras-chave, declare por que falhou (ex: "A busca por 'cnes' não retornou nenhum conjunto de dados") e descreva sua próxima tentativa com base no **Protocolo de Busca**.
258+
- **Erros em Consultas Intermediárias**: Analise a mensagem de erro e ajuste a consulta. Estes erros são esperados e fazem parte do processo de exploração.""" # noqa: E501

backend/apps/chatbot/agent/react_agent.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
# -*- coding: utf-8 -*-
22
from collections.abc import Callable
3-
from typing import Annotated, AsyncIterator, Iterator, Literal, Sequence, TypedDict
3+
from typing import Annotated, AsyncIterator, Generic, Iterator, Literal, Sequence, Type, TypedDict
44

5-
from chatbot.agents.utils import async_delete_checkpoints, delete_checkpoints
65
from langchain_core.language_models.chat_models import BaseChatModel
76
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
87
from langchain_core.runnables import RunnableConfig, RunnableLambda
98
from langchain_core.tools import BaseTool, BaseToolkit
109
from langgraph.checkpoint.postgres import PostgresSaver
1110
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
12-
from langgraph.graph import StateGraph
13-
from langgraph.graph.graph import CompiledGraph
1411
from langgraph.graph.message import add_messages
12+
from langgraph.graph.state import CompiledStateGraph, StateGraph
1513
from langgraph.managed import IsLastStep, RemainingSteps
1614
from langgraph.prebuilt import ToolNode
1715
from loguru import logger
1816

17+
from chatbot.agents.utils import async_delete_checkpoints, delete_checkpoints
18+
19+
from .types import StateT
1920

20-
class State(TypedDict):
21+
22+
class ReActState(TypedDict):
2123
messages: Annotated[list[BaseMessage], add_messages]
24+
"""Message list"""
25+
2226
is_last_step: IsLastStep
27+
"""Flag indicating if the last step has been reached"""
28+
2329
remaining_steps: RemainingSteps
30+
"""Number of remaining steps before reaching the steps limit"""
2431

2532

26-
class ReActAgent:
33+
class ReActAgent(Generic[StateT]):
2734
"""A LangGraph ReAct Agent."""
2835

2936
agent_node = "agent"
@@ -34,7 +41,8 @@ def __init__(
3441
self,
3542
model: BaseChatModel,
3643
tools: Sequence[BaseTool] | BaseToolkit,
37-
start_hook: Callable[[State], dict] | None = None,
44+
state_schema: Type[StateT] = ReActState,
45+
start_hook: Callable[[StateT], dict] | None = None,
3846
prompt: SystemMessage | str | None = None,
3947
checkpointer: PostgresSaver | AsyncPostgresSaver | bool | None = None,
4048
):
@@ -57,13 +65,13 @@ def __init__(
5765

5866
self.checkpointer = checkpointer
5967

60-
self.graph = self._compile(start_hook=start_hook)
68+
self.graph = self._compile(state_schema, start_hook)
6169

62-
def _call_model(self, state: State, config: RunnableConfig) -> dict[str, list[BaseMessage]]:
70+
def _call_model(self, state: StateT, config: RunnableConfig) -> dict[str, list[BaseMessage]]:
6371
"""Calls the LLM on a message list.
6472
6573
Args:
66-
state (State): The graph state.
74+
state (StateT): The graph state.
6775
config (RunnableConfig): A config to use when calling the LLM.
6876
6977
Returns:
@@ -95,12 +103,12 @@ def _call_model(self, state: State, config: RunnableConfig) -> dict[str, list[Ba
95103
return {"messages": [response]}
96104

97105
async def _acall_model(
98-
self, state: State, config: RunnableConfig
106+
self, state: StateT, config: RunnableConfig
99107
) -> dict[str, list[BaseMessage]]:
100108
"""Asynchronously calls the LLM on a message list.
101109
102110
Args:
103-
state (State): The graph state.
111+
state (StateT): The graph state.
104112
config (RunnableConfig): A config to use when calling the LLM.
105113
106114
Returns:
@@ -131,18 +139,21 @@ async def _acall_model(
131139

132140
return {"messages": [response]}
133141

134-
def _compile(self, start_hook: Callable[[State], dict]) -> CompiledGraph:
142+
def _compile(
143+
self, state_schema: Type[StateT], start_hook: Callable[[StateT], dict] | None
144+
) -> CompiledStateGraph:
135145
"""Compiles the state graph into a LangChain Runnable.
136146
137147
Args:
138-
start_hook (Callable[[State], dict]): An optional node to add before the agent node.
148+
state_schema (Type[StateT]): The state graph schema.
149+
start_hook (Callable[[StateT], dict] | None): An optional node to add before the agent node.
139150
Useful for managing long message histories (e.g., message trimming, summarization, etc.).
140151
Must be a callable or a runnable that takes in current graph state and returns a state update.
141152
142153
Returns:
143-
CompiledGraph: The compiled state graph.
154+
CompiledStateGraph: The compiled state graph.
144155
""" # noqa: E501
145-
graph = StateGraph(State)
156+
graph = StateGraph(state_schema)
146157

147158
graph.add_node(self.agent_node, RunnableLambda(self._call_model, self._acall_model))
148159
graph.add_node(self.tools_node, ToolNode(self.tools))
@@ -164,15 +175,15 @@ def _compile(self, start_hook: Callable[[State], dict]) -> CompiledGraph:
164175
# For more information, visit https://github.com/langchain-ai/langgraph/issues/3020
165176
return graph.compile(self.checkpointer)
166177

167-
def invoke(self, message: str, config: RunnableConfig | None = None) -> State:
178+
def invoke(self, message: str, config: RunnableConfig | None = None) -> StateT:
168179
"""Runs the compiled graph.
169180
170181
Args:
171182
message (str): The input message.
172183
config (RunnableConfig | None, optional): The configuration. Defaults to `None`.
173184
174185
Returns:
175-
dict[str, Any] | Any: The last output of the graph run.
186+
StateT: The last output of the graph run.
176187
"""
177188
message = HumanMessage(content=message.strip())
178189

@@ -183,15 +194,15 @@ def invoke(self, message: str, config: RunnableConfig | None = None) -> State:
183194

184195
return response
185196

186-
async def ainvoke(self, message: str, config: RunnableConfig | None = None) -> State:
197+
async def ainvoke(self, message: str, config: RunnableConfig | None = None) -> StateT:
187198
"""Asynchronously runs the compiled graph.
188199
189200
Args:
190201
message (str): The input message.
191202
config (RunnableConfig | None, optional): The configuration. Defaults to `None`.
192203
193204
Returns:
194-
dict[str, Any] | Any: The last output of the graph run.
205+
StateT: The last output of the graph run.
195206
"""
196207
message = HumanMessage(content=message.strip())
197208

@@ -280,12 +291,12 @@ async def aclear_thread(self, thread_id: str):
280291
await async_delete_checkpoints(self.checkpointer, thread_id)
281292

282293

283-
def _should_continue(state: State) -> Literal["tools", "__end__"]:
294+
def _should_continue(state: StateT) -> Literal["tools", "__end__"]:
284295
"""Routes to the tools node if the last message has any tool calls.
285296
Otherwise, routes to the message pruning node.
286297
287298
Args:
288-
state (State): The graph state.
299+
state (StateT): The graph state.
289300
290301
Returns:
291302
str: The next node to route to.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
from typing import TypeVar
3+
4+
StateT = TypeVar("StateT")

backend/apps/chatbot/utils/gcloud.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def get_bigquery_client() -> bq.Client:
3333
Returns:
3434
bigquery.Client: A cached, authenticated BigQuery client.
3535
"""
36-
project = os.getenv("QUERY_PROJECT_ID")
36+
# TODO: revert to os.getenv("QUERY_PROJECT_ID")
37+
project = "basedosdados"
3738

3839
if not project:
3940
raise ValueError(

backend/apps/chatbot/views.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
from rest_framework.views import APIView
2626
from rest_framework_simplejwt.tokens import RefreshToken
2727

28-
from backend.apps.chatbot.agent.prompts import SQL_AGENT_SYSTEM_PROMPT
29-
from backend.apps.chatbot.agent.react_agent import ReActAgent, State
28+
from backend.apps.chatbot.agent.prompts import SQL_AGENT_SYSTEM_PROMPT_V2
29+
from backend.apps.chatbot.agent.react_agent import ReActAgent
3030
from backend.apps.chatbot.agent.tools import get_tools
31+
from backend.apps.chatbot.agent.types import StateT
3132
from backend.apps.chatbot.feedback_sender import LangSmithFeedbackSender
3233
from backend.apps.chatbot.models import Feedback, MessagePair, Thread
3334
from backend.apps.chatbot.serializers import (
@@ -339,9 +340,9 @@ def _get_sql_agent() -> Generator[ReActAgent]:
339340

340341
credentials = get_chatbot_credentials()
341342

342-
model = init_chat_model(MODEL_URI, temperature=0, credentials=credentials)
343+
model = init_chat_model(MODEL_URI, temperature=0.2, credentials=credentials)
343344

344-
def start_hook(state: State):
345+
def start_hook(state: StateT):
345346
messages = state["messages"]
346347

347348
# For the first message, skip trimming. If it's too long, let it fail.
@@ -368,8 +369,8 @@ def start_hook(state: State):
368369
sql_agent = ReActAgent(
369370
model=model,
370371
tools=get_tools(),
371-
prompt=SQL_AGENT_SYSTEM_PROMPT,
372372
start_hook=start_hook,
373+
prompt=SQL_AGENT_SYSTEM_PROMPT_V2,
373374
checkpointer=checkpointer,
374375
)
375376

0 commit comments

Comments
 (0)