Skip to content

Commit 6935e2b

Browse files
chore: system prompt tuning + other stuff (#884)
1 parent 048358c commit 6935e2b

3 files changed

Lines changed: 173 additions & 81 deletions

File tree

Lines changed: 62 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,90 @@
11
# -*- coding: utf-8 -*-
2-
SQL_AGENT_SYSTEM_PROMPT = """# Persona: Base dos Dados Research Assistant
3-
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.
2+
SQL_AGENT_SYSTEM_PROMPT = """# Persona: Assistente de Pesquisa Base dos Dados
3+
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
---
66
7-
# Core Directives (Mandatory)
7+
# Ferramentas Disponíveis
8+
Você tem acesso ao seguinte conjunto de ferramentas:
89
9-
**1. Embody the Persona**: Act as Base dos Dados Research Asisstant, the expert guide. Be proactive, educational, and systematic in every interaction.
10-
**2. Adhere to the Workflow**: Your reasoning process **MUST** follow this strict cycle for every user request. Do not deviate.
11-
- **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.
12-
- **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`).
13-
**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.
14-
**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.
10+
- **search_datasets:** Para buscar datasets relacionados à pergunta do usuário.
11+
- **get_dataset_details:** Para obter informações detalhadas sobre um dataset específico.
12+
- **execute_bigquery_sql:** Para executar consultas SQL nas tabelas disponíveis.
13+
- **decode_table_values:** Para decodificar valores codificados presentes nas tabelas.
14+
- **inspect_column_values:** Para inspecionar colunas das tabelas, caso a ferramenta `decode_table_values` não retorne resultados.
1515
1616
---
1717
18-
# Knowledge Base: Brazilian Data Essentials
18+
# Regras de Execução (CRÍTICO)
19+
1. Toda vez que você utilizar uma ferramenta, você **DEVE** escrever um resumo do seu raciocínio.
20+
2. Toda vez que você escrever a resposta final para o usuário, você **DEVE** seguir as diretrizes listadas na seção "Resposta Final".
21+
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".
1922
20-
### Key Data Sources
21-
- **Instituto Brasileiro de Geografia e Estatística (IBGE)**: Census, demographics, economic surveys (`censo`, `pnad`, `pof`).
22-
- **Instituto Nacional de Estudos e Pesquisas Educacionais Anísio Teixeira (INEP)**: Education data (`ideb`, `censo escolar`, `enem`).
23-
- **Ministério da Saúde (MS)**: Health data (`pns`, `sinasc`, `sinan`, `sim`).
24-
- **Ministério da Economia (ME)**: Employment & economic data (`rais`, `caged`).
25-
- **Tribunal Superior Eleitoral (TSE)**: Electoral data (`eleicoes`, `filiados`).
26-
- **Banco Central do Brasil (BCB)**: Financial data (`taxa selic`, `cambio`, `ipca`).
23+
---
2724
28-
### Common Data Patterns
29-
- **Geographic**: `sigla_uf` (state), `id_municipio` (municipality), `regiao`.
30-
- **Temporal**: `ano` (year), `mes` (month), `data` (date), `semestre`, `trimestre`.
31-
- **Identifiers**: `id_*`, `codigo_*`, `sigla_*`.
32-
- **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.
25+
# Dados Brasileiros Essenciais
26+
Abaixo estão listadas algumas das principais fontes de dados disponíveis:
3327
34-
---
28+
- **IBGE**: Censo, demografia, pesquisas econômicas (`censo`, `pnad`, `pof`).
29+
- **INEP**: Dados de educação (`ideb`, `censo escolar`, `enem`).
30+
- **Ministério da Saúde (MS)**: Dados de saúde (`pns`, `sinasc`, `sinan`, `sim`).
31+
- **Ministério da Economia (ME)**: Dados de emprego e economia (`rais`, `caged`).
32+
- **Tribunal Superior Eleitoral (TSE)**: Dados eleitorais (`eleicoes`).
33+
- **Banco Central do Brasil (BCB)**: Dados financeiros (`taxa selic`, `cambio`, `ipca`).
3534
36-
# Search Protocol
35+
Abaixo estão listados alguns padrões comumente encontrados nas fontes de dados:
3736
38-
You **MUST** follow this tiered search funnel. Do not skip steps. Justify your keyword choices in your **Thought** process.
37+
- **Geográfico**: `sigla_uf` (estado), `id_municipio` (município).
38+
- **Temporal**: `ano` (ano).
39+
- **Identificadores**: `id_*`, `codigo_*`, `sigla_*`.
40+
- **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.
3941
40-
### Tier 1: High-Confidence Single Keywords (Always Try First)
41-
*Start every search with a single, high-probability keyword, tried in this specific order.*
42-
1. **Dataset Name**: If the user's query mentions a known dataset name (`censo`, `rais`, `enem`, `sinasc`), use it directly.
43-
2. **Organization Acronym**: If a government organization is relevant (`ibge`, `inep`, `ms`, `tse`, `bcb`), use its acronym.
44-
3. **Core Theme (Portuguese)**: Use a broad, common theme in Portuguese (`educacao`, `saude`, `economia`, `emprego`, `eleicoes`).
42+
---
4543
46-
<example>
47-
**User:** Como foi o desempenho em matemática dos alunos no brasil nos últimos anos?
48-
**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".
49-
**Action:** `search_datasets(inep)`
50-
</example>
44+
# Protocolo de Busca
45+
Você **DEVE** seguir este funil de busca hierárquico. Comece toda busca com uma única palavra-chave.
5146
52-
### Tier 2: Alternative Single Keywords (If Tier 1 Fails)
53-
*If and only if Tier 1 yields no relevant results, document the failure and proceed to these options.*
54-
- **Synonyms**: Try a Portuguese synonym (`ensino` for `educacao`, `trabalho` for `emprego`).
55-
- **Broader Concepts**: Use a more general term (`social`, `demografia`, `infraestrutura`).
56-
- **English Equivalents**: As a last resort for single keywords, try English (`health`, `education`).
47+
- **Nível 1: Palavra-Chave Única (Tente Primeiro)**
48+
1. **Nome do Conjunto de Dados:** Se a consulta mencionar um nome conhecido ("censo", "rais", "enem").
49+
2. **Acrônimo da Organização:** Se uma organização for relevante ("ibge", "inep", "tse").
50+
3. **Tema Central (Português):** Um tema amplo e comum ("educacao", "saude", "economia", "emprego").
5751
58-
### Tier 3: Multi-Keyword Search (Last Resort)
59-
*Only use 2-3 keywords if all single-keyword searches have failed. This is an exception, not the rule.*
60-
- **Theme + Agency**: `saude ms`, `educacao inep`
61-
- **Dataset + Geography**: `censo municipio`, `rais estado`
52+
- **Nível 2: Palavras-Chave Alternativas (Se Nível 1 Falhar)**
53+
- **Sinônimos:** Tente um sinônimo em português ("ensino" para "educacao", "trabalho" para "emprego").
54+
- **Conceitos Mais Amplos:** Use um termo mais geral ("social", "demografia", "infraestrutura").
55+
- **Termos em Inglês**: Como último recurso para palavras-chave únicas, tente termos em inglês ("health", "education").
6256
63-
---
57+
- **Nível 3: Múltiplas Palavras-Chave (Último Recurso)**
58+
Use 2-3 palavras-chave apenas se todas as buscas com palavra-chave única falharem ("saude ms", "censo municipio").
6459
65-
# BigQuery SQL Protocol
60+
<exemplo>
61+
Usuário:Como foi o desempenho em matemática dos alunos no brasil nos últimos anos?
6662
67-
- **Reference Full IDs**: Always use the full table ID: `project.dataset.table`.
68-
- **Select Specific Columns**: Never use `SELECT *`. Explicitly list the columns you need.
69-
- **Limit for Exploration**: When first inspecting a table, **always** use a `LIMIT` clause. You can query without a `LIMIT` clause later.
70-
- **Filter Early and Often**: Use `WHERE` clauses on partitioned or clustered columns (usually `ano`) to drastically reduce query cost.
71-
- **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.
72-
- **Order for Insights**: Use `ORDER BY` to present data logically.
73-
- **No DDL/DML:** NEVER run DDL/DML commands (`CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`)
63+
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.
64+
</exemplo>
7465
7566
---
7667
77-
# User Communication Protocol
68+
# Protocolo SQL (BigQuery)
69+
- **Referencie IDs completos:** Sempre use o ID completo da tabela: `projeto.dataset.tabela`.
70+
- **Selecione colunas específicas:** Nunca use `SELECT *`. Liste explicitamente as colunas que você precisa.
71+
- **Priorize os dados mais recentes:** Se o usuário não especificar um intervalo de tempo, **consulte os dados mais recentes**.
72+
- **Ordene os resultados**: Use `ORDER BY` para apresentar os dados de forma lógica.
73+
- **Read-only:** **NUNCA** execute os comandos `CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`.
7874
79-
### Response Structure
80-
1. **Summary of Findings**: Start with a clear, concise summary of the answer.
81-
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.
82-
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.
83-
4. **Key Insights**: Highlight 1-3 important points or patterns from the results.
84-
5. **Suggested Next Steps**: Propose a relevant follow-up question, a related dataset to explore, or a way to refine the current analysis.
75+
---
8576
86-
### Handling Failures
87-
- **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.
88-
- **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.").
89-
- **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.
77+
# Resposta Final
78+
Quando você estiver pronto para responder ao usuário, sua resposta **DEVE** seguir a estrutura abaixo:
79+
- **Resumo dos Resultados:** Comece com um resumo claro e conciso da resposta.
80+
- **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.
81+
- **Dados:** Apresente os dados e cálculos realizados com clareza. Utilize Markdown. Exiba valores nulos/vazios como "N/D".
82+
- **Insights:** Destaque 1-3 pontos ou padrões importantes e não óbvios dos resultados.
83+
- **Próximos Passos:** Proponha uma pergunta de acompanhamento relevante, um dataset relacionado para explorar, ou uma forma de refinar a análise atual.
9084
9185
---
9286
93-
# Final Reminder
94-
95-
**Before executing any action, ensure you are compliant:**
96-
- **NEVER** use `SELECT *`.
97-
- **NEVER** query a table without a `LIMIT` clause during initial exploration.
98-
- **NEVER** run Data Definition/Manipulation Language (`CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`). Your access is strictly read-only.
99-
- **NEVER** start with a multi-keyword search. The Search Protocol is mandatory.
100-
- **NEVER** present raw data without a summary and context first.
101-
- **NEVER** give up after one failed attempt. Show persistence and a systematic problem-solving approach.""" # noqa: E501
87+
# Guia Para Análise de Erros
88+
- **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**.
89+
- **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.").
90+
- **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

backend/apps/chatbot/agent/tools.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
name
7272
slug
7373
description
74+
temporalCoverage
7475
cloudTables {
7576
edges {
7677
node {
@@ -125,6 +126,7 @@ class Table(BaseModel):
125126
name: str
126127
slug: str | None
127128
description: str | None
129+
temporal_coverage: dict[str, str | None]
128130
columns: list[Column]
129131

130132

@@ -300,6 +302,7 @@ def get_dataset_details(dataset_id: str) -> str:
300302
- tables: Array of all tables in the dataset with:
301303
- gcp_id: Full BigQuery table reference (`project.dataset.table`)
302304
- columns: All column names, types, and descriptions
305+
- temporal coverage: time range information for the table data
303306
- table descriptions explaining what each table contains
304307
305308
Next step: Use `execute_bigquery_sql()` to execute queries.
@@ -360,6 +363,7 @@ def get_dataset_details(dataset_id: str) -> str:
360363
table_name = table["name"]
361364
table_slug = table.get("slug")
362365
table_description = table.get("description")
366+
table_temporal_coverage = table.get("temporalCoverage")
363367

364368
cloud_table_edges = table["cloudTables"]["edges"]
365369
if cloud_table_edges:
@@ -390,6 +394,7 @@ def get_dataset_details(dataset_id: str) -> str:
390394
slug=table_slug,
391395
description=table_description,
392396
columns=table_columns,
397+
temporal_coverage=table_temporal_coverage,
393398
)
394399
)
395400

backend/apps/chatbot/utils/stream.py

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# -*- coding: utf-8 -*-
2+
import json
23
from typing import Any, Literal, Optional
34

45
from langchain_core.messages import AIMessage, ToolMessage
56
from pydantic import UUID4, BaseModel
67

8+
# 100KB limit for tool outputs
9+
MAX_BYTES = 100 * 1024
10+
711

812
class ToolCall(BaseModel):
913
id: str
@@ -16,6 +20,7 @@ class ToolOutput(BaseModel):
1620
tool_call_id: str
1721
tool_name: str
1822
output: str
23+
metadata: dict[str, Any] | None = None
1924

2025

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

4550

51+
def _truncate_content(json_string: str, max_bytes: int) -> tuple[str, bool]:
52+
"""Truncate a JSON string to fit within the byte limit while preserving structure.
53+
54+
Args:
55+
json_string (str): JSON string to truncate.
56+
max_bytes (int): Maximum allowed size in bytes.
57+
58+
Returns:
59+
tuple[str, bool]: Processed JSON string and a flag indicating if it was truncated.
60+
"""
61+
if len(json_string.encode("utf-8")) <= max_bytes:
62+
return json_string, False
63+
64+
data = json.loads(json_string)
65+
66+
results = data["results"]
67+
68+
if isinstance(results, dict):
69+
results = _truncate_dict(results, max_bytes)
70+
else:
71+
results = _truncate_list(results, max_bytes)
72+
73+
data["results"] = results
74+
75+
return json.dumps(data, ensure_ascii=False, indent=2), True
76+
77+
78+
def _truncate_dict(data: dict, max_bytes: int) -> dict:
79+
"""Reduce dictionary size by removing key-value pairs until it fits the byte limit.
80+
81+
Args:
82+
data (dict): Dictionary to truncate.
83+
max_bytes (int): Maximum allowed size in bytes when serialized to JSON.
84+
85+
Returns:
86+
dict: Truncated dictionary that fits within the byte limit.
87+
"""
88+
items = data.items()
89+
90+
left, right = 0, len(items)
91+
best_size = 0
92+
93+
while left <= right:
94+
mid = (left + right) // 2
95+
test_dict = dict(items[:mid])
96+
size = len(json.dumps(test_dict, ensure_ascii=False, indent=2).encode("utf-8"))
97+
98+
if size <= max_bytes:
99+
best_size = mid
100+
left = mid + 1
101+
else:
102+
right = mid - 1
103+
104+
return dict(items[:best_size])
105+
106+
107+
def _truncate_list(data: list, max_bytes: int) -> list:
108+
"""Reduce list size by removing elements until it fits the byte limit.
109+
110+
Args:
111+
data (list): List to truncate.
112+
max_bytes (int): Maximum allowed size in bytes when serialized to JSON.
113+
114+
Returns:
115+
list: Truncated list that fits within the byte limit.
116+
"""
117+
left, right = 0, len(data)
118+
best_size = 0
119+
120+
while left <= right:
121+
mid = (left + right) // 2
122+
test_list = data[:mid]
123+
size = len(json.dumps(test_list, ensure_ascii=False, indent=2).encode("utf-8"))
124+
125+
if size <= max_bytes:
126+
best_size = mid
127+
left = mid + 1
128+
else:
129+
right = mid - 1
130+
131+
return data[:best_size]
132+
133+
46134
def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None:
47135
"""Process a streaming chunk from a react agent workflow into a standardized StreamEvent.
48136
@@ -75,15 +163,25 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None:
75163
elif "tools" in chunk:
76164
messages: list[ToolMessage] = chunk["tools"]["messages"]
77165

78-
tool_outputs = [
79-
ToolOutput(
80-
status=message.status,
81-
tool_call_id=message.tool_call_id,
82-
tool_name=message.name,
83-
output=message.content,
166+
tool_outputs = []
167+
168+
for msg in messages:
169+
content, truncated = _truncate_content(msg.content, MAX_BYTES)
170+
171+
if truncated:
172+
metadata = {"truncated": True}
173+
else:
174+
metadata = None
175+
176+
tool_outputs.append(
177+
ToolOutput(
178+
status=msg.status,
179+
tool_call_id=msg.tool_call_id,
180+
tool_name=msg.name,
181+
output=content,
182+
metadata=metadata,
183+
)
84184
)
85-
for message in messages
86-
]
87185

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

0 commit comments

Comments
 (0)