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/askfaz 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
historymulti-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 Claudesaiku.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 provedoressaiku.ai.ask.model = claude-sonnet-4-7 | gpt-4o-mini | ...saiku.ai.ask.endpoint = https://my.openai-compatible.host/v1/chat/completionssaiku.ai.ask.apiKey = sk-... # override explícito da variável de ambienteDefaults do provedor:
| Provedor | Modelo default | Endpoint |
|---|---|---|
anthropic | claude-sonnet-4-6 | https://api.anthropic.com/v1/messages |
openai | gpt-4o-mini | https://api.openai.com/v1/chat/completions |
Docker
docker run -d --name saiku \ -e ANTHROPIC_API_KEY=sk-ant-... \ -e JAVA_OPTS='-Dsaiku.ai.ask.provider=anthropic' \ ghcr.io/spiculedata/saiku:latestKubernetes
Use um Secret para a chave, um ConfigMap para o nome do provedor:
apiVersion: v1kind: Secretmetadata: name: saiku-aitype: OpaquestringData: ANTHROPIC_API_KEY: sk-ant-...---apiVersion: apps/v1kind: Deploymentmetadata: name: saikuspec: template: spec: containers: - name: saiku envFrom: - secretRef: name: saiku-ai env: - name: JAVA_OPTS value: -Dsaiku.ai.ask.provider=anthropicO endpoint
POST /saiku/api/ai/askRequisiçã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
| HTTP | Quando |
|---|---|
200 | Tradução com sucesso; response carrega o resultado executado (ou VALIDATION_ERROR para o usuário se autocorrigir). |
200 + degraded:true | Tradução falhou na camada do provedor — erro de transporte, modelo recusou ou erro de parse. reason traz a explicação. |
400 | question ou cube ausentes no body. |
503 + degraded:true | Provedor é noop (não configurado) — reason explica como habilitar. |
Exemplos
Desativado por padrão — feedback claro
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
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
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."} ]}EOFO 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
AiSchemado cubo ativo — JSON serializado de medidas, dimensões, hierarquias, levels e membros de exemplo. - O JSON Schema de
AiQueryRequestcomoinput_schemada 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/asktraduz. Ú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.