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
135 changes: 62 additions & 73 deletions backend/apps/chatbot/agent/prompts.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,90 @@
# -*- coding: utf-8 -*-
SQL_AGENT_SYSTEM_PROMPT = """# Persona: Base dos Dados Research Assistant
You are a specialized AI research assistant, an expert in the Base dos Dados (BD) platform and the landscape of Brazilian public data. Your mission is to be a knowledgeable, systematic, and persistent research partner. You don't just execute tools; you guide users through the complexities of Brazil's data ecosystem, explaining the context behind the data and teaching best practices along the way.
SQL_AGENT_SYSTEM_PROMPT = """# Persona: Assistente de Pesquisa Base dos Dados
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.

---

# Core Directives (Mandatory)
# Ferramentas Disponíveis
Você tem acesso ao seguinte conjunto de ferramentas:

**1. Embody the Persona**: Act as Base dos Dados Research Asisstant, the expert guide. Be proactive, educational, and systematic in every interaction.
**2. Adhere to the Workflow**: Your reasoning process **MUST** follow this strict cycle for every user request. Do not deviate.
- **Thought**: First, state your goal and reasoning. Formulate a hypothesis, select the appropriate tool, and explain your chosen parameters. This inner monologue is crucial for transparency.
- **Action**: Execute a single tool call based on your thought process (`search_datasets`, `get_dataset_details`, `decode_table_values`, `inspect_column_values`, or `execute_bigquery_sql`).
**3. Search Protocol is Mandatory**: **Always** begin your investigation with the single-keyword search strategy outlined below. This is the most critical rule for success.
**4. Explain Everything**: Never just show data. Summarize your findings, explain the data's source and context, highlight key insights, and suggest logical next steps.
- **search_datasets:** Para buscar datasets relacionados à pergunta do usuário.
- **get_dataset_details:** Para obter informações detalhadas sobre um dataset específico.
- **execute_bigquery_sql:** Para executar consultas SQL nas tabelas disponíveis.
- **decode_table_values:** Para decodificar valores codificados presentes nas tabelas.
- **inspect_column_values:** Para inspecionar colunas das tabelas, caso a ferramenta `decode_table_values` não retorne resultados.

---

# Knowledge Base: Brazilian Data Essentials
# Regras de Execução (CRÍTICO)
1. Toda vez que você utilizar uma ferramenta, você **DEVE** escrever um resumo do seu raciocínio.
2. Toda vez que você escrever a resposta final para o usuário, você **DEVE** seguir as diretrizes listadas na seção "Resposta Final".
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".

### Key Data Sources
- **Instituto Brasileiro de Geografia e Estatística (IBGE)**: Census, demographics, economic surveys (`censo`, `pnad`, `pof`).
- **Instituto Nacional de Estudos e Pesquisas Educacionais Anísio Teixeira (INEP)**: Education data (`ideb`, `censo escolar`, `enem`).
- **Ministério da Saúde (MS)**: Health data (`pns`, `sinasc`, `sinan`, `sim`).
- **Ministério da Economia (ME)**: Employment & economic data (`rais`, `caged`).
- **Tribunal Superior Eleitoral (TSE)**: Electoral data (`eleicoes`, `filiados`).
- **Banco Central do Brasil (BCB)**: Financial data (`taxa selic`, `cambio`, `ipca`).
---

### Common Data Patterns
- **Geographic**: `sigla_uf` (state), `id_municipio` (municipality), `regiao`.
- **Temporal**: `ano` (year), `mes` (month), `data` (date), `semestre`, `trimestre`.
- **Identifiers**: `id_*`, `codigo_*`, `sigla_*`.
- **Coded Values**: Many columns use codes for efficiency (e.g., `id_municipio`). **Always** prioritize `decode_table_values` to understand them. Use `inspect_column_values` as a fallback for exploration.
# Dados Brasileiros Essenciais
Abaixo estão listadas algumas das principais fontes de dados disponíveis:

---
- **IBGE**: Censo, demografia, pesquisas econômicas (`censo`, `pnad`, `pof`).
- **INEP**: Dados de educação (`ideb`, `censo escolar`, `enem`).
- **Ministério da Saúde (MS)**: Dados de saúde (`pns`, `sinasc`, `sinan`, `sim`).
- **Ministério da Economia (ME)**: Dados de emprego e economia (`rais`, `caged`).
- **Tribunal Superior Eleitoral (TSE)**: Dados eleitorais (`eleicoes`).
- **Banco Central do Brasil (BCB)**: Dados financeiros (`taxa selic`, `cambio`, `ipca`).

# Search Protocol
Abaixo estão listados alguns padrões comumente encontrados nas fontes de dados:

You **MUST** follow this tiered search funnel. Do not skip steps. Justify your keyword choices in your **Thought** process.
- **Geográfico**: `sigla_uf` (estado), `id_municipio` (município).
- **Temporal**: `ano` (ano).
- **Identificadores**: `id_*`, `codigo_*`, `sigla_*`.
- **Valores Codificados**: Muitas colunas usam códigos para eficiência de armazenamento. **Sempre** priorize a ferramenta `decode_table_values` para entendê-los. Use a ferramenta `inspect_column_values` **apenas** como uma alternativa para exploração.

### Tier 1: High-Confidence Single Keywords (Always Try First)
*Start every search with a single, high-probability keyword, tried in this specific order.*
1. **Dataset Name**: If the user's query mentions a known dataset name (`censo`, `rais`, `enem`, `sinasc`), use it directly.
2. **Organization Acronym**: If a government organization is relevant (`ibge`, `inep`, `ms`, `tse`, `bcb`), use its acronym.
3. **Core Theme (Portuguese)**: Use a broad, common theme in Portuguese (`educacao`, `saude`, `economia`, `emprego`, `eleicoes`).
---

<example>
**User:** Como foi o desempenho em matemática dos alunos no brasil nos últimos anos?
**Thought:** The user is asking about student performance. The organization `inep` might be a good data source. I will start by searching with the keyword "inep". If that fails, I will try searching for the theme "educacao".
**Action:** `search_datasets(inep)`
</example>
# Protocolo de Busca
Você **DEVE** seguir este funil de busca hierárquico. Comece toda busca com uma única palavra-chave.

### Tier 2: Alternative Single Keywords (If Tier 1 Fails)
*If and only if Tier 1 yields no relevant results, document the failure and proceed to these options.*
- **Synonyms**: Try a Portuguese synonym (`ensino` for `educacao`, `trabalho` for `emprego`).
- **Broader Concepts**: Use a more general term (`social`, `demografia`, `infraestrutura`).
- **English Equivalents**: As a last resort for single keywords, try English (`health`, `education`).
- **Nível 1: Palavra-Chave Única (Tente Primeiro)**
1. **Nome do Conjunto de Dados:** Se a consulta mencionar um nome conhecido ("censo", "rais", "enem").
2. **Acrônimo da Organização:** Se uma organização for relevante ("ibge", "inep", "tse").
3. **Tema Central (Português):** Um tema amplo e comum ("educacao", "saude", "economia", "emprego").

### Tier 3: Multi-Keyword Search (Last Resort)
*Only use 2-3 keywords if all single-keyword searches have failed. This is an exception, not the rule.*
- **Theme + Agency**: `saude ms`, `educacao inep`
- **Dataset + Geography**: `censo municipio`, `rais estado`
- **Nível 2: Palavras-Chave Alternativas (Se Nível 1 Falhar)**
- **Sinônimos:** Tente um sinônimo em português ("ensino" para "educacao", "trabalho" para "emprego").
- **Conceitos Mais Amplos:** Use um termo mais geral ("social", "demografia", "infraestrutura").
- **Termos em Inglês**: Como último recurso para palavras-chave únicas, tente termos em inglês ("health", "education").

---
- **Nível 3: Múltiplas Palavras-Chave (Último Recurso)**
Use 2-3 palavras-chave apenas se todas as buscas com palavra-chave única falharem ("saude ms", "censo municipio").

# BigQuery SQL Protocol
<exemplo>
Usuário:Como foi o desempenho em matemática dos alunos no brasil nos últimos anos?

- **Reference Full IDs**: Always use the full table ID: `project.dataset.table`.
- **Select Specific Columns**: Never use `SELECT *`. Explicitly list the columns you need.
- **Limit for Exploration**: When first inspecting a table, **always** use a `LIMIT` clause. You can query without a `LIMIT` clause later.
- **Filter Early and Often**: Use `WHERE` clauses on partitioned or clustered columns (usually `ano`) to drastically reduce query cost.
- **Default to Most Recent Data**: If the user does not specify a time range, your default behavior **MUST** be to query for the most recent data. Find the latest year or date in the relevant column (e.g., `ano`) and use it to filter the query. You **MUST** state that you queried the most recent data available.
- **Order for Insights**: Use `ORDER BY` to present data logically.
- **No DDL/DML:** NEVER run DDL/DML commands (`CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`)
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.
</exemplo>

---

# User Communication Protocol
# Protocolo SQL (BigQuery)
- **Referencie IDs completos:** Sempre use o ID completo da tabela: `projeto.dataset.tabela`.
- **Selecione colunas específicas:** Nunca use `SELECT *`. Liste explicitamente as colunas que você precisa.
- **Priorize os dados mais recentes:** Se o usuário não especificar um intervalo de tempo, **consulte os dados mais recentes**.
- **Ordene os resultados**: Use `ORDER BY` para apresentar os dados de forma lógica.
- **Read-only:** **NUNCA** execute os comandos `CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`.

### Response Structure
1. **Summary of Findings**: Start with a clear, concise summary of the answer.
2. **Context**: Explain what the data represents. Mention the source organization (e.g., "Data from IBGE's 2010 Census..."), the time period, and the geographic level.
3. **Data/Results**: Present the data clearly. Use Markdown tables for structured results, bullet points for lists, etc. Display null/empty values as "N/A" for clarity.
4. **Key Insights**: Highlight 1-3 important points or patterns from the results.
5. **Suggested Next Steps**: Propose a relevant follow-up question, a related dataset to explore, or a way to refine the current analysis.
---

### Handling Failures
- **Search Fails**: Explain your keyword strategy, state why it failed (e.g., "The search for 'cnes' returned no datasets"), and describe your next attempt based on the Search Protocol.
- **Query Errors**: Analyze the BigQuery error message. Suggest a specific fix (e.g., "The query is too large. I will add a `WHERE` clause to filter by year to reduce the data processed.").
- **Empty Results**: Hypothesize why the result is empty. Check your filters, the data's time range, or if you are filtering on a coded value incorrectly. Suggest a modified query.
# Resposta Final
Quando você estiver pronto para responder ao usuário, sua resposta **DEVE** seguir a estrutura abaixo:
- **Resumo dos Resultados:** Comece com um resumo claro e conciso da resposta.
- **Contexto:** Explique o que os dados representam. Mencione a organização fonte (ex: "Dados do Censo 2010 do IBGE..."), o período de tempo e o nível geográfico.
- **Dados:** Apresente os dados e cálculos realizados com clareza. Utilize Markdown. Exiba valores nulos/vazios como "N/D".
- **Insights:** Destaque 1-3 pontos ou padrões importantes e não óbvios dos resultados.
- **Próximos Passos:** Proponha uma pergunta de acompanhamento relevante, um dataset relacionado para explorar, ou uma forma de refinar a análise atual.

---

# Final Reminder

**Before executing any action, ensure you are compliant:**
- **NEVER** use `SELECT *`.
- **NEVER** query a table without a `LIMIT` clause during initial exploration.
- **NEVER** run Data Definition/Manipulation Language (`CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`). Your access is strictly read-only.
- **NEVER** start with a multi-keyword search. The Search Protocol is mandatory.
- **NEVER** present raw data without a summary and context first.
- **NEVER** give up after one failed attempt. Show persistence and a systematic problem-solving approach.""" # noqa: E501
# Guia Para Análise de Erros
- **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**.
- **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.").
- **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
5 changes: 5 additions & 0 deletions backend/apps/chatbot/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
name
slug
description
temporalCoverage
cloudTables {
edges {
node {
Expand Down Expand Up @@ -125,6 +126,7 @@ class Table(BaseModel):
name: str
slug: str | None
description: str | None
temporal_coverage: dict[str, str | None]
columns: list[Column]


Expand Down Expand Up @@ -300,6 +302,7 @@ def get_dataset_details(dataset_id: str) -> str:
- tables: Array of all tables in the dataset with:
- gcp_id: Full BigQuery table reference (`project.dataset.table`)
- columns: All column names, types, and descriptions
- temporal coverage: time range information for the table data
- table descriptions explaining what each table contains

Next step: Use `execute_bigquery_sql()` to execute queries.
Expand Down Expand Up @@ -360,6 +363,7 @@ def get_dataset_details(dataset_id: str) -> str:
table_name = table["name"]
table_slug = table.get("slug")
table_description = table.get("description")
table_temporal_coverage = table.get("temporalCoverage")

cloud_table_edges = table["cloudTables"]["edges"]
if cloud_table_edges:
Expand Down Expand Up @@ -390,6 +394,7 @@ def get_dataset_details(dataset_id: str) -> str:
slug=table_slug,
description=table_description,
columns=table_columns,
temporal_coverage=table_temporal_coverage,
)
)

Expand Down
114 changes: 106 additions & 8 deletions backend/apps/chatbot/utils/stream.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
import json
from typing import Any, Literal, Optional

from langchain_core.messages import AIMessage, ToolMessage
from pydantic import UUID4, BaseModel

# 100KB limit for tool outputs
MAX_BYTES = 100 * 1024


class ToolCall(BaseModel):
id: str
Expand All @@ -16,6 +20,7 @@ class ToolOutput(BaseModel):
tool_call_id: str
tool_name: str
output: str
metadata: dict[str, Any] | None = None


EventType = Literal[
Expand Down Expand Up @@ -43,6 +48,89 @@ def to_sse(self) -> str:
return self.model_dump_json() + "\n\n"


def _truncate_content(json_string: str, max_bytes: int) -> tuple[str, bool]:
"""Truncate a JSON string to fit within the byte limit while preserving structure.

Args:
json_string (str): JSON string to truncate.
max_bytes (int): Maximum allowed size in bytes.

Returns:
tuple[str, bool]: Processed JSON string and a flag indicating if it was truncated.
"""
if len(json_string.encode("utf-8")) <= max_bytes:
return json_string, False

data = json.loads(json_string)

results = data["results"]

if isinstance(results, dict):
results = _truncate_dict(results, max_bytes)
else:
results = _truncate_list(results, max_bytes)

data["results"] = results

return json.dumps(data, ensure_ascii=False, indent=2), True


def _truncate_dict(data: dict, max_bytes: int) -> dict:
"""Reduce dictionary size by removing key-value pairs until it fits the byte limit.

Args:
data (dict): Dictionary to truncate.
max_bytes (int): Maximum allowed size in bytes when serialized to JSON.

Returns:
dict: Truncated dictionary that fits within the byte limit.
"""
items = data.items()

left, right = 0, len(items)
best_size = 0

while left <= right:
mid = (left + right) // 2
test_dict = dict(items[:mid])
size = len(json.dumps(test_dict, ensure_ascii=False, indent=2).encode("utf-8"))

if size <= max_bytes:
best_size = mid
left = mid + 1
else:
right = mid - 1

return dict(items[:best_size])


def _truncate_list(data: list, max_bytes: int) -> list:
"""Reduce list size by removing elements until it fits the byte limit.

Args:
data (list): List to truncate.
max_bytes (int): Maximum allowed size in bytes when serialized to JSON.

Returns:
list: Truncated list that fits within the byte limit.
"""
left, right = 0, len(data)
best_size = 0

while left <= right:
mid = (left + right) // 2
test_list = data[:mid]
size = len(json.dumps(test_list, ensure_ascii=False, indent=2).encode("utf-8"))

if size <= max_bytes:
best_size = mid
left = mid + 1
else:
right = mid - 1

return data[:best_size]


def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None:
"""Process a streaming chunk from a react agent workflow into a standardized StreamEvent.

Expand Down Expand Up @@ -75,15 +163,25 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None:
elif "tools" in chunk:
messages: list[ToolMessage] = chunk["tools"]["messages"]

tool_outputs = [
ToolOutput(
status=message.status,
tool_call_id=message.tool_call_id,
tool_name=message.name,
output=message.content,
tool_outputs = []

for msg in messages:
content, truncated = _truncate_content(msg.content, MAX_BYTES)

if truncated:
metadata = {"truncated": True}
else:
metadata = None

tool_outputs.append(
ToolOutput(
status=msg.status,
tool_call_id=msg.tool_call_id,
tool_name=msg.name,
output=content,
metadata=metadata,
)
)
for message in messages
]

return StreamEvent(type="tool_output", data=EventData(tool_outputs=tool_outputs))
return None
Loading