diff --git a/backend/apps/chatbot/agent/prompts.py b/backend/apps/chatbot/agent/prompts.py
index 172a4213..0bfb5d18 100644
--- a/backend/apps/chatbot/agent/prompts.py
+++ b/backend/apps/chatbot/agent/prompts.py
@@ -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`).
+---
-
-**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)`
-
+# 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
+
+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.
+
---
-# 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
diff --git a/backend/apps/chatbot/agent/tools.py b/backend/apps/chatbot/agent/tools.py
index 4640afc5..4a94dce8 100644
--- a/backend/apps/chatbot/agent/tools.py
+++ b/backend/apps/chatbot/agent/tools.py
@@ -71,6 +71,7 @@
name
slug
description
+ temporalCoverage
cloudTables {
edges {
node {
@@ -125,6 +126,7 @@ class Table(BaseModel):
name: str
slug: str | None
description: str | None
+ temporal_coverage: dict[str, str | None]
columns: list[Column]
@@ -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.
@@ -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:
@@ -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,
)
)
diff --git a/backend/apps/chatbot/utils/stream.py b/backend/apps/chatbot/utils/stream.py
index 3997ae42..a691f2d1 100644
--- a/backend/apps/chatbot/utils/stream.py
+++ b/backend/apps/chatbot/utils/stream.py
@@ -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
@@ -16,6 +20,7 @@ class ToolOutput(BaseModel):
tool_call_id: str
tool_name: str
output: str
+ metadata: dict[str, Any] | None = None
EventType = Literal[
@@ -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.
@@ -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