Pular para o conteúdo

API AI Ask

A API AI Ask recebe uma pergunta em inglês simples, pede ao provedor de LLM configurado para preencher uma consulta tipada contra o schema do cubo ao vivo, executa essa consulta pelo mesmo conversor que a API AI Query tipada usa e retorna o resultado executado + o MDX gerado + a requisição estruturada que o modelo emitiu.

É a camada de linguagem natural sobre a superfície tipada do agente. Onde a API tipada assume que o chamador fala AiQueryRequest, a API ask permite que um humano diga “mostre as vendas por país no último trimestre” e receba de volta um cellset totalmente executado com a tradução do modelo visível ao lado.

Isso alimenta o painel AI Query do workspace — o botão ⚡ na barra de ferramentas abre um painel de chat onde os usuários digitam perguntas e clicam em Editar no canvas para colocar o resultado na aba de consulta ativa.

Quando usar

A API ask é a superfície certa quando:

  • Usuários finais vão digitar perguntas — a API AI Query tipada espera um agente que já conhece o schema do cubo; a API ask esconde esse passo atrás de um LLM.
  • Você quer um round-trip por pergunta/ai/ask faz tradução + execução + montagem do envelope em uma única chamada, então o cliente não precisa encadear duas APIs.
  • Você está incorporando uma superfície estilo chat — o campo history multi-turn permite que perguntas de acompanhamento resolvam contra turns anteriores.

Quando não usar:

  • Agentes no servidor MCP — agentes preferem a superfície tipada porque já são bons em preencher campos estruturados a partir de um JSON Schema. A camada ask é para humanos.
  • Autoria de cubos a partir de um data warehouse de exemplo — use a API AI inference, que é projetada para o fluxo em tempo de design.

Ativação

Duas propriedades + uma variável de ambiente por provedor. Ambas podem ser definidas na JVM ou no arquivo de propriedades da implantação consumido pelo Spring.

# Anthropic Claude
saiku.ai.ask.provider = anthropic
# env ANTHROPIC_API_KEY = sk-ant-...
# OpenAI — ou qualquer host compatível com OpenAI (Azure, vLLM, Ollama, Together)
saiku.ai.ask.provider = openai
# env OPENAI_API_KEY = sk-...
# Opcional, ambos os provedores
saiku.ai.ask.model = claude-sonnet-4-7 | gpt-4o-mini | ...
saiku.ai.ask.endpoint = https://my.openai-compatible.host/v1/chat/completions
saiku.ai.ask.apiKey = sk-... # override explícito da variável de ambiente

Defaults do provedor:

ProvedorModelo defaultEndpoint
anthropicclaude-sonnet-4-6https://api.anthropic.com/v1/messages
openaigpt-4o-minihttps://api.openai.com/v1/chat/completions

Docker

Terminal window
docker run -d --name saiku \
-e ANTHROPIC_API_KEY=sk-ant-... \
-e JAVA_OPTS='-Dsaiku.ai.ask.provider=anthropic' \
ghcr.io/spiculedata/saiku:latest

Kubernetes

Use um Secret para a chave, um ConfigMap para o nome do provedor:

apiVersion: v1
kind: Secret
metadata:
name: saiku-ai
type: Opaque
stringData:
ANTHROPIC_API_KEY: sk-ant-...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: saiku
spec:
template:
spec:
containers:
- name: saiku
envFrom:
- secretRef:
name: saiku-ai
env:
- name: JAVA_OPTS
value: -Dsaiku.ai.ask.provider=anthropic

O endpoint

POST /saiku/api/ai/ask

Requisição

{
"question": "show sales by country last quarter",
"cube": {
"connectionName": "foodmart",
"catalog": "FoodMart",
"schema": "FoodMart",
"cubeName": "Sales"
},
"history": [
{ "role": "user", "content": "earlier question" },
{ "role": "assistant", "content": "earlier summary" }
]
}

history é opcional. Quando fornecido, turns anteriores (user, assistant) são enviados de volta ao modelo para que acompanhamentos como “agora detalhe por região” resolvam contra a pergunta anterior. Os system prompts são controlados pelo provedor — os chamadores não podem injetá-los.

Resposta

{
"degraded": false,
"model": "claude-sonnet-4-6",
"request": { /* the structured AiQueryRequest the model emitted */ },
"response": { /* the full AiQueryResponse — same shape as /ai/query */ },
"generatedMdx": "SELECT NON EMPTY ... FROM [Sales]"
}

request é a consulta estruturada que o modelo produziu. Entregue-a a POST /ai/query literalmente para reexecutar, ou exiba para o usuário como “a consulta tipada por trás da sua pergunta.”

response é o resultado executado, com o mesmo formato que a API AI Query tipada retorna. Se o modelo emitir uma requisição que o schema rejeita, response.status é VALIDATION_ERROR e o corpo carrega field + candidatos available — o painel do workspace renderiza esses como chips clicáveis para que o usuário se autocorrija.

generatedMdx é um espelho de conveniência de response.metadata.generatedMdx.

Códigos de status

HTTPQuando
200Tradução com sucesso; response carrega o resultado executado (ou VALIDATION_ERROR para o usuário se autocorrigir).
200 + degraded:trueTradução falhou na camada do provedor — erro de transporte, modelo recusou ou erro de parse. reason traz a explicação.
400question ou cube ausentes no body.
503 + degraded:trueProvedor é noop (não configurado) — reason explica como habilitar.

Exemplos

Desativado por padrão — feedback claro

Terminal window
curl -sS -X POST -H 'Content-Type: application/json' \
-u admin:admin \
http://localhost:8080/saiku/api/ai/ask \
-d '{"question":"show sales by country","cube":{"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"}}'
# {
# "degraded": true,
# "reason": "AI ask is not configured. Set saiku.ai.ask.provider..."
# }

Caminho feliz — round-trip completo

Terminal window
curl -sS -X POST -H 'Content-Type: application/json' \
-u admin:admin \
http://localhost:8080/saiku/api/ai/ask \
-d '{"question":"show sales by country","cube":{"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"}}' \
| jq '{model, generatedMdx, rows: .response.totalRows}'
# {
# "model": "claude-sonnet-4-6",
# "generatedMdx": "SELECT NON EMPTY {[Measures].[Store Sales]} ON COLUMNS, NON EMPTY {[Customers].[Country].Members} ON ROWS FROM [Sales]",
# "rows": 3
# }

Acompanhamento — multi-turn

Terminal window
curl -sS -X POST -H 'Content-Type: application/json' \
-u admin:admin \
http://localhost:8080/saiku/api/ai/ask \
-d @- <<'EOF'
{
"question": "now break it down by quarter",
"cube": {"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"},
"history": [
{"role":"user","content":"show sales by country"},
{"role":"assistant","content":"3 rows returned."}
]
}
EOF

O modelo vê o turn anterior e resolve “por trimestre” como também cruzado contra país — mesmo formato da primeira resposta com uma hierarquia extra.

O que o modelo vê vs. não vê

A camada ask envia ao modelo apenas:

  • O AiSchema do cubo ativo — JSON serializado de medidas, dimensões, hierarquias, levels e membros de exemplo.
  • O JSON Schema de AiQueryRequest como input_schema da ferramenta de saída estruturada.
  • A pergunta do usuário e qualquer histórico de conversa que o chamador tenha anexado.

O modelo é forçado via tool_choice a chamar a ferramenta emit_query com um formato AiQueryRequest válido. Respostas em prosa pura são rejeitadas como degraded.

Nunca vê:

  • Outros cubos aos quais o usuário tem acesso — apenas o nomeado na requisição.
  • O SQL subjacente ou as credenciais do data warehouse.
  • O histórico de conversa de outros usuários.
  • A requisição HTTP, cookies de sessão ou qualquer header que o chamador enviou.

Este é o mesmo modelo de isolamento que o servidor MCP e a API AI Query tipada usam, aplicado uma camada antes.

Para onde ir agora

  • API AI inference — a superfície tipada para a qual /ai/ask traduz. Útil quando você quer que um agente (não um humano) faça a autoria da requisição.
  • Servidor MCP — a mesma superfície tipada exposta como ferramentas MCP para agentes Claude Desktop / Cursor / Cline.
  • Autenticação — Bearer tokens, limites de taxa, formatos de erro.