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
2 changes: 2 additions & 0 deletions backend/apps/chatbot/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ThreadAdmin(admin.ModelAdmin):
]
ordering = ["-created_at"]


class MessagePairAdmin(admin.ModelAdmin):
list_display = [field.name for field in MessagePair._meta.fields]
readonly_fields = list_display
Expand All @@ -34,6 +35,7 @@ class FeedbackAdmin(admin.ModelAdmin):
]
ordering = ["-created_at"]


admin.site.register(Thread, ThreadAdmin)
admin.site.register(MessagePair, MessagePairAdmin)
admin.site.register(Feedback, FeedbackAdmin)
37 changes: 27 additions & 10 deletions backend/apps/chatbot/agent/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,31 @@
- **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.
- **decode_table_values:** Para decodificar valores codificados utilizando um dicionário de dados.

---

# Regras de Execução (CRÍTICO)
1. Toda vez que você utilizar uma ferramenta, você **DEVE** escrever um resumo do seu raciocínio.
1. Toda vez que você utilizar uma ferramenta, você **DEVE** escrever um **breve 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".
4. **NUNCA** retorne uma resposta em branco.

---

# Protocolo de Esclarecimento de Consulta (CRÍTICO)
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.
- **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".
- **Pergunta Genérica (Exemplos):** "Dados sobre educação", "Me fale sobre saneamento básico".

2. **Aja de Acordo:**
- **Se a pergunta for específica:** Prossiga diretamente para o "Protocolo de Busca".
- **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:
- **Tipo de informação:** Qual métrica ou dado específico o usuário busca? (ex: produção, consumo, preços, etc.)
- **Período de tempo:** Qual o recorte temporal de interesse? (ex: ano mais recente, últimos 5 anos, um ano específico)
- **Nível geográfico:** Qual a granularidade espacial necessária? (ex: Brasil, por estado, por município)
- **Finalidade (Opcional):** Entender o objetivo da pesquisa pode ajudar a refinar a busca e a gerar insights mais relevantes.
Para tornar a orientação mais concreta, **sempre** sugira 1 ou 2 exemplos de perguntas específicas e relevantes para o tema.

---

Expand All @@ -37,7 +53,7 @@
- **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.
- **Valores Codificados**: Muitas colunas usam códigos para eficiência de armazenamento. **Sempre** utilize a ferramenta `decode_table_values` para decodificá-los.

---

Expand Down Expand Up @@ -75,12 +91,13 @@
---

# 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.
Ao redigir a resposta final, construa um texto explicativo e fluido. Sua resposta deve ser completa e fácil de entender, garantindo que os seguintes elementos sejam naturalmente integrados na ordem sugerida:

1. Inicie a resposta com a conclusão principal de forma direta e clara.
2. Em seguida, explique a origem e o escopo dos dados (ex: "Esses números são do Censo Escolar de 2021, realizado pelo INEP..."), incluindo o período de tempo e o nível geográfico.
3. Mostre os dados ou os resultados da sua análise de forma organizada. O uso de Markdown para tabelas ou listas é apropriado aqui. Exiba valores nulos/vazios como "N/D".
4. Após apresentar os dados, comente de 1 a 3 observações ou padrões interessantes que podem não ser óbvios à primeira vista.
5. Conclua sugerindo uma forma de aprofundar a análise, seja com uma nova pergunta, explorando um dataset relacionado, ou adicionando um novo filtro.

---

Expand Down
4 changes: 2 additions & 2 deletions backend/apps/chatbot/agent/react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def _call_model(self, state: State, config: RunnableConfig) -> dict[str, list[Ba
response: AIMessage = self.model_runnable.invoke(messages, config)

if not response.content and not response.tool_calls:
logger.warning("Received empty response from model, skipping message list update")
logger.warning("Empty model response, skipping message list update")
return {"messages": []}

if is_last_step and response.tool_calls or remaining_steps < 2 and response.tool_calls:
Expand Down Expand Up @@ -114,7 +114,7 @@ async def _acall_model(
response: AIMessage = await self.model_runnable.ainvoke(messages, config)

if not response.content and not response.tool_calls:
logger.warning("Received empty response from model, skipping message list update")
logger.warning("Empty model response, skipping message list update")
return {"messages": []}

if is_last_step and response.tool_calls or remaining_steps < 2 and response.tool_calls:
Expand Down
78 changes: 12 additions & 66 deletions backend/apps/chatbot/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
# Maximum number of datasets returned on search
PAGE_SIZE = 10

# 5GB limit for inspection queries
LIMIT_INSPECTION_QUERY = int(5 * 1e9)

# 10GB limit for other queries
LIMIT_BIGQUERY_QUERY = int(10 * 1e9)

Expand Down Expand Up @@ -441,27 +438,18 @@ def execute_bigquery_sql(sql_query: str) -> str:
Returns:
str: Query results as JSON array. Empty results return "[]".
""" # noqa: E501
forbidden_commands = [
"CREATE",
"ALTER",
"DROP",
"TRUNCATE",
"INSERT",
"UPDATE",
"DELETE",
"GRANT",
"REVOKE",
]
client = get_bigquery_client()

for command in forbidden_commands:
if command in sql_query.upper():
raise ToolError(
message=f"Query aborted: Command {command} is forbidden.",
error_type="FORBIDDEN_COMMAND",
instructions="Your access is strictly read-only. Use only SELECT statements.",
)
job_config = bq.QueryJobConfig(dry_run=True, use_query_cache=False)
dry_run_query_job = client.query(sql_query, job_config=job_config)
statement_type = dry_run_query_job.statement_type

client = get_bigquery_client()
if statement_type != "SELECT":
raise ToolError(
message=f"Query aborted: Statement {statement_type} is forbidden.",
error_type="FORBIDDEN_STATEMENT",
instructions="Your access is strictly read-only. Use only SELECT statements.",
)

job_config = bq.QueryJobConfig(maximum_bytes_billed=LIMIT_BIGQUERY_QUERY)
query_job = client.query(sql_query, job_config=job_config)
Expand All @@ -475,11 +463,7 @@ def execute_bigquery_sql(sql_query: str) -> str:

@tool
@handle_tool_errors(
instructions={
GoogleAPIError.NOT_FOUND: (
"Dictionary table not found for this dataset. Use `inspect_column_values` instead."
)
}
instructions={GoogleAPIError.NOT_FOUND: ("Dictionary table not found for this dataset.")}
)
def decode_table_values(table_gcp_id: str, column_name: str | None = None) -> str:
"""Decode coded values from a table.
Expand All @@ -489,7 +473,7 @@ def decode_table_values(table_gcp_id: str, column_name: str | None = None) -> st
the authoritative meanings of these codes.

Args:
table_gcp_id (str): Full BigQuery table reference
table_gcp_id (str): Full BigQuery table reference.
column_name (str | None, optional): Column with coded values. If `None`,
all columns will be used. Defaults to `None`.

Expand Down Expand Up @@ -529,42 +513,6 @@ def decode_table_values(table_gcp_id: str, column_name: str | None = None) -> st
return json.dumps(tool_output, ensure_ascii=False, default=str)


@tool
@handle_tool_errors(
instructions={
GoogleAPIError.BYTES_BILLED_LIMIT_EXCEEDED: (
"Use `execute_bigquery_sql` with a WHERE clause "
"to inspect a sample of the column's values"
)
}
)
def inspect_column_values(table_gcp_id: str, column_name: str) -> str:
"""Show actual distinct values in a column.

FALLBACK tool to use when `search_dictionary_table()` fails.
Useful for understanding data patterns and planning `WHERE` clauses.

Args:
table_gcp_id (str): Full BigQuery table reference
column_name (str): Column name from `get_dataset_details()`

Returns:
str: JSON array of distinct values.
""" # noqa: E501
client = get_bigquery_client()

sql_query = f"SELECT DISTINCT {column_name} FROM {table_gcp_id}"

job_config = bq.QueryJobConfig(maximum_bytes_billed=LIMIT_INSPECTION_QUERY)
query_job = client.query(sql_query, job_config=job_config)

rows = query_job.result()
results = [row.get(column_name) for row in rows]

tool_output = ToolOutput(status="success", results=results).model_dump(exclude_none=True)
return json.dumps(tool_output, ensure_ascii=False, default=str)


def get_tools() -> list[BaseTool]:
"""Return all available tools for Base dos Dados database interaction.

Expand All @@ -577,12 +525,10 @@ def get_tools() -> list[BaseTool]:
- get_dataset_details: Get comprehensive dataset information
- execute_bigquery_sql: Execute SQL queries against BigQuery tables
- decode_table_values: Decode coded values using dictionary tables
- inspect_column_values: Inspect actual column values as fallback
"""
return [
search_datasets,
get_dataset_details,
execute_bigquery_sql,
decode_table_values,
inspect_column_values,
]
1 change: 1 addition & 0 deletions backend/apps/chatbot/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ class ChatbotConfig(AppConfig):
def ready(self):
# Enable logs from the chatbot package
import chatbot

logger.enable(chatbot.__name__)
1 change: 1 addition & 0 deletions backend/apps/chatbot/authentication.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from backend.apps.account.models import Account


Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 4.2.20 on 2025-04-17 14:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chatbot', '0001_initial'),
("chatbot", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name='feedback',
name='comment',
model_name="feedback",
name="comment",
field=models.TextField(blank=True, null=True),
),
]
30 changes: 21 additions & 9 deletions backend/apps/chatbot/utils/stream.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
import json
from typing import Any, Literal, Optional
from typing import Any, Literal

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


Expand Down Expand Up @@ -30,11 +31,11 @@ class ToolOutput(BaseModel):


class EventData(BaseModel):
run_id: Optional[UUID4] = None
content: Optional[str] = None
tool_calls: Optional[list[ToolCall]] = None
tool_outputs: Optional[list[ToolOutput]] = None
error_details: Optional[dict[str, Any]] = None
run_id: UUID4 | None = None
content: str | None = None
tool_calls: list[ToolCall] | None = None
tool_outputs: list[ToolOutput] | None = None
error_details: dict[str, Any] | None = None


class StreamEvent(BaseModel):
Expand Down Expand Up @@ -125,15 +126,26 @@ def process_chunk(chunk: dict[str, Any]) -> StreamEvent | None:
message = ai_messages[0]

if message.tool_calls:
event_type = "tool_call"
tool_calls = [
ToolCall(id=tool_call["id"], name=tool_call["name"], args=tool_call["args"])
for tool_call in message.tool_calls
]
event_type = "tool_call"
event_data = EventData(content=message.content, tool_calls=tool_calls)
else:
event_type = "final_answer"
event_data = EventData(content=message.content)
tool_calls = None

# The content of an AIMessage can sometimes be a list, which is not the expected behavior.
# In that case, we just log a warning and cast it to string to keep processing consistent.
if not isinstance(message.content, str):
logger.warning(
f"Message content is of type '{type(message.content)}', casting to string"
)
content = str(message.content)
else:
content = message.content

event_data = EventData(content=content, tool_calls=tool_calls)

return StreamEvent(type=event_type, data=event_data)
elif "tools" in chunk:
Expand Down
2 changes: 1 addition & 1 deletion backend/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"USER_AUTHENTICATION_RULE": "backend.apps.chatbot.authentication.authentication_rule"
"USER_AUTHENTICATION_RULE": "backend.apps.chatbot.authentication.authentication_rule",
}

MIDDLEWARE = [
Expand Down
Loading