Sistema de Prompts#
O Sulfite Chat opera em dois modos com system prompts especializados. Em ambos, o prompt é montado server-side — o client nunca vê nem controla o conteúdo.
┌─────────────────────────────────────────────────────┐
│ System Prompt │
├─────────────────────────────────────────────────────┤
│ Base (regras fixas) │
│ + Data Context (dados do relatório) ← server-side │
│ + Knowledge Base (llms.txt) ← opcional │
│ + Extra Instructions ← opcional │
└─────────────────────────────────────────────────────┘
Dois modos#
| Modo | Endpoint | Classe | Função |
|---|---|---|---|
| Designer | POST /api/v1/chat/generate |
SulfiteSystemPrompts.designer() |
Gera JSON de ReportDefinition via linguagem natural |
| Consumer | POST /api/v1/consumer/chat/generate |
SulfiteSystemPrompts.consumer() |
Responde perguntas sobre dados de um relatório específico |
Modo Designer#
O agente Designer recebe uma descrição em linguagem natural e gera um JSON completo de ReportDefinition. O prompt inclui:
Estrutura do prompt#
┌─ Base ────────────────────────────────────────────┐
│ Papel: "Sulfite report generator" │
│ Output: JSON em fences ```json ... ``` │
│ │
│ REPORT STRUCTURE │
│ - Formatos de página (A4 portrait/landscape) │
│ - Tipos de elementos │
│ - Convenções de cor e coordenadas │
│ │
│ WORKFLOW (4 passos obrigatórios) │
│ 1. Gerar direto se simples │
│ 2. Chamar sulfite_get_schema se precisa │
│ 3. Produzir JSON após qualquer tool result │
│ 4. Sempre usar fences ```json │
│ │
│ ELEMENT QUICK REFERENCE │
│ - text, field, table, barcode, chart │
│ │
│ DATA SOURCES / LABEL RULES / RULES │
├─ Knowledge Base (opcional) ───────────────────────┤
│ Conteúdo do llms.txt, se configurado via │
│ CHAT_LLMS_TXT_PATH │
└───────────────────────────────────────────────────┘
Tools disponíveis#
| Tool | Descrição |
|---|---|
sulfite_get_schema | Schema campo-a-campo de um tipo de elemento |
sulfite_validate_report | Valida um JSON de ReportDefinition |
sulfite_introspect_datasource | Introspecta campos de um datasource |
sulfite_list_reports | Lista relatórios salvos |
Configuração#
O Knowledge Base é opcional e controlado pela env var CHAT_LLMS_TXT_PATH:
CHAT_LLMS_TXT_PATH=assets/llms.txt
O conteúdo do arquivo é appended ao prompt base como seção ## KNOWLEDGE BASE.
Modo Consumer#
O agente Consumer é um analista de dados que responde perguntas sobre um relatório já renderizado. O prompt é montado automaticamente com os dados reais.
Estrutura do prompt (V2)#
O formato V2 usa JSON Lines com schema e summaries per-band, otimizado para aritmética LLM:
┌─ Base ────────────────────────────────────────────┐
│ Papel: "Data analyst assistant" │
│ Idioma: responde no idioma da pergunta │
│ │
│ DATA FORMAT │
│ - Dados em JSON Lines (1 objeto por linha) │
│ - Datas compactas: "2026-04-02" (meia-noite) │
│ - Números reais: 3454.5 (não "3454.5") │
│ - Cada band tem seu próprio schema │
│ │
│ RULES │
│ - Consultar Summary ANTES de iterar linhas │
│ - Usar valores do Summary diretamente │
│ - Cada band tem campos independentes │
│ - Se REPORT AGGREGATES existir, são autoritativos│
│ - Responder no idioma da pergunta │
│ - Não sugerir mudanças no relatório │
├─ Data Context (injetado pelo servidor) ───────────┤
│ │
│ ## REPORT INFO │
│ - Name: Dashboard Financeiro │
│ - ID: fin_dashboard │
│ │
│ ## APPLIED FILTERS │
│ - **Data Início** (`dataInicio`, date): 2026-01 │
│ - **Região** (`regiao`, select): Sul │
│ │
│ ## REPORT SCHEMA │
│ │
│ ### vendas (180 rows) │
│ Fields: numero (text), data (date), │
│ status (text, values: faturado|aberto|canc), │
│ valor_total (number), margem_bruta (number) │
│ Summary: valor_total SUM=423891 AVG=2355 │
│ MIN=87.30 MAX=17125.28 | │
│ status: faturado=142, aberto=31, cancelado=7 | │
│ valor_total by status: faturado=389210, │
│ aberto=28456.22, cancelado=6225.28 │
│ │
│ ### pagamentos (205 rows) │
│ Fields: numero (text), fornecedor (text, │
│ values: Acme|Globex|...), data (date), │
│ valor (number), dias_atraso (number) │
│ Summary: valor SUM=198442 AVG=968.01 │
│ MIN=12.50 MAX=8930 | │
│ valor by fornecedor: Acme=48210, Globex=35100 │
│ │
│ ## REPORT AGGREGATES │
│ - total_vendas: 423891.50 │
│ - total_pagamentos: 198442.10 │
│ │
│ ## REPORT DATA (100 of 385 rows) │
│ --- vendas --- │
│ {"numero":"VD-048","data":"2026-04-02",...} │
│ {"numero":"VD-097","data":"2026-04-02",...} │
│ --- pagamentos --- │
│ {"numero":"PAG-084","fornecedor":"Acme",...} │
│ │
├─ Extra Instructions (opcional) ───────────────────┤
│ Instruções de deploy customizadas │
└───────────────────────────────────────────────────┘
Formato dos dados (V2)#
O formato V2 aplica várias otimizações para melhorar a precisão da LLM:
| Otimização | Antes | Depois | Por quê |
|---|---|---|---|
| Schema per-band | Schema flat, todos os campos misturados | Cada band tem schema independente com tipos | LLM não confunde campos de bands diferentes |
| Summaries pré-computados | LLM precisava somar manualmente | SUM/AVG/MIN/MAX por campo numérico + enum breakdowns + group aggregates | Elimina erros de aritmética em datasets grandes |
| Group aggregates | Sem pré-computação por grupo | total by vendedor: Ana=300, Bruno=150 |
LLM responde "quem vendeu mais?" sem somar |
| Datas compactas | "2026-04-02T00:00:00.000Z" |
"2026-04-02" (meia-noite) |
Menos tokens, mais legível |
| Coerção numérica | "3454.50" (string) |
3454.5 (number) |
LLM trata como número real, não texto |
| Formato BR | "3.782,27" ficava texto |
3782.27 (number) |
Valores em formato brasileiro são coercidos |
| Detecção de enums | Sem contexto | status (text, values: faturado|aberto|cancelado) |
LLM sabe quais valores categóricos existem |
| Report Aggregates | Engine aggregates descartados | Seção REPORT AGGREGATES com valores autoritativos |
AggregateElements do engine são fonte de verdade |
| Leading zeros | "060" → 60.0 (número) |
"060" preservado como texto |
Códigos fiscais (CST, NCM) mantêm zeros à esquerda |
| Dedup de bands | DANFE: canhoto + header com dados iguais × 2 | Mergeados em canhoto+header (1 cópia) |
Sem dados redundantes no prompt |
| Summary inteligente | Single-row band: SUM=AVG=MIN=MAX=valor | Sem summary (dados já visíveis na row) | Zero ruído para bands de contexto |
| Filtragem de constantes | cfop SUM=27025 (todas as rows iguais) |
Omitido do summary | Aggregates só para campos que variam |
| Identity enum filter | produto: A=1, B=1, C=1 (todos com count 1) |
Omitido do summary (mantém values: no schema) |
Contagens unitárias não ajudam análise |
Summary: total by vendedor: Ana=300, Bruno=150 | total by status: faturado=389210, aberto=28456
Isso permite que a LLM responda perguntas como "quem vendeu mais?" ou "qual o status com maior valor?" sem precisar somar linhas manualmente.
Filtros aplicados automaticamente:
- Campos onde
distinctCount ≥ totalRows(identity) são excluídos — produziriam uma repetição verbosa dos dados brutos - Campos numéricos constantes (MIN == MAX) não geram aggregates
Como o Data Context é construído#
O servidor executa tudo internamente antes de chamar a LLM:
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client envia│────▶│ Carrega │────▶│ Processa │────▶│ Monta │
│ reportId + │ │ Definition │ │ dados │ │ Data Context│
│ params + │ │ do repo │ │ (engine) │ │ + prompt │
│ messages │ └──────────────┘ └──────────────┘ └──────────────┘
└─────────────┘ │
▼
┌──────────────┐
│ Envia ao │
│ LLM com │
│ tools │
└──────────────┘
- Carrega a
ReportDefinitionpeloreportId - Processa os dados com os
params(filtros) viaSulfiteEngine.processReport() - Exporta os dados como JSON Lines via
ConsumerChatToolExecutor.exportJsonLines() - Computa schema per-band, summaries (SUM/AVG/MIN/MAX), enum breakdowns, group aggregates e report aggregates
- Aplica filtros inteligentes: remove constantes (MIN==MAX), identity enums, single-row band summaries e bands duplicadas
- Monta o Data Context com report info + filtros + schema + summaries + dados
- Injeta no system prompt e envia à LLM
Tools disponíveis#
| Tool | Descrição |
|---|---|
sulfite_aggregate_data | SUM, AVG, COUNT, MIN ou MAX sobre um campo, com filtro opcional |
sulfite_group_by | Agrupa por campo e agrega valor — retorna top N |
Filtros e parâmetros#
Os parâmetros do relatório (ReportParameter) são declarados no JSON do relatório:
{
"parameters": [
{
"id": "dataInicio",
"label": "Data Início",
"type": "date",
"required": true,
"defaultValue": "2026-01-01"
},
{
"id": "regiao",
"label": "Região",
"type": "select",
"options": ["Norte", "Sul", "Leste", "Oeste"]
}
]
}
O cliente envia os valores escolhidos no campo params do request:
{
"reportId": "uuid",
"params": {
"dataInicio": "2026-01-01",
"regiao": "Sul"
},
"messages": [{ "role": "user", "content": "qual o total?" }]
}
O servidor usa esses params para:
- Processar os dados — os params são passados ao engine e substituídos nos templates SQL/URL dos datasources
- Documentar no prompt — os filtros aplicados aparecem na seção
APPLIED FILTERScom label, id, tipo e valor
A LLM sabe exatamente quais filtros o usuário aplicou e pode mencioná-los nas respostas.
Tratamento de erros#
Quando a LLM chama uma tool com campo inexistente, o servidor retorna os campos válidos:
{
"verb": "SUM",
"field": "orçamento",
"result": null,
"note": "Field \"orçamento\" does not exist. Available fields: vendedor, numero, produto, valor, ativo."
}
Isso permite que a LLM se auto-corrija na próxima iteração.
Provedores LLM (server-side)#
A factory LlmClient.fromConfig() seleciona o client baseado na env var CHAT_PROVIDER:
| Provider | CHAT_PROVIDER | Autenticação | URL base |
|---|---|---|---|
| OpenAI | openai | Authorization: Bearer | api.openai.com/v1 |
| Anthropic | anthropic | x-api-key | api.anthropic.com |
| Azure OpenAI | azure | api-key header | CHAT_BASE_URL (obrigatório) |
| Cloudflare Workers AI | cloudflare | Authorization: Bearer | CHAT_BASE_URL (obrigatório) |
| Ollama | ollama | nenhuma | localhost:11434/v1 |
| GitHub Models | gh_models | Authorization: Bearer | models.github.ai/inference |
Variáveis de ambiente#
# Obrigatórias
CHAT_PROVIDER=azure # Provider (ver tabela acima)
CHAT_MODEL=gpt-4.1-mini # Modelo ou deployment name
CHAT_API_KEY=sua-chave-aqui # API key do provider
# Opcionais
CHAT_BASE_URL=https://... # URL base (obrigatório para azure/cloudflare)
CHAT_MAX_ITERATIONS=8 # Máximo de iterações do loop agentic
CHAT_LLMS_TXT_PATH=assets/llms.txt # Knowledge base para modo designer
Azure OpenAI#
Para o Azure, o CHAT_MODEL deve ser o nome do deployment (não o nome do modelo):
CHAT_PROVIDER=azure
CHAT_MODEL=gpt-4.1-mini
CHAT_API_KEY=1UqAns...
CHAT_BASE_URL=https://meu-recurso.cognitiveservices.azure.com
O client monta automaticamente:
{CHAT_BASE_URL}/openai/deployments/{CHAT_MODEL}/chat/completions?api-version=2024-10-21
Para usar uma api-version específica, inclua na URL:
CHAT_BASE_URL=https://meu-recurso.cognitiveservices.azure.com?api-version=2025-01-01-preview
Loop agentic#
Ambos os modos usam o mesmo ChatLoop que itera entre chamadas à LLM e execução de tools:
┌────────────────────────────────────────────────────────────────┐
│ ChatLoop │
│ │
│ for iteration in 1..maxIterations: │
│ 1. Envia history + system prompt + tools ao LLM │
│ 2. Se resposta contém tool_calls: │
│ - Executa cada tool │
│ - Adiciona resultados ao history │
│ - Volta ao passo 1 │
│ 3. Se resposta é texto: │
│ - Stream via SSE ao client │
│ - Emite evento 'done' │
│ │
│ Guard: após N rounds sem texto, remove tools e força geração │
└────────────────────────────────────────────────────────────────┘
Proteções#
- Anti-loop: se a LLM ficar chamando tools sem produzir texto por 2+ rounds, o loop remove as tools e injeta instrução de "gerar agora"
- Deduplicação: tool calls repetidas são detectadas e reportadas
- Validação: tool names são validados contra a whitelist do executor
- Timeout: máximo de
CHAT_MAX_ITERATIONSiterações (default: 8)
Eventos SSE#
O stream retorna eventos no formato Server-Sent Events:
| Tipo | Payload | Quando |
|---|---|---|
open | {} | Início da conexão |
text | { content: "..." } | Cada chunk de texto da LLM |
tool_call | { name, status, detail? } | Tool sendo executada |
error | { message } | Erro no loop |
done | {} | Final da resposta |