Saltearse al contenido

API AI Ask

La API AI Ask toma una pregunta en lenguaje natural, le pide al proveedor LLM configurado que complete una consulta tipada contra el schema del cubo en vivo, ejecuta esa consulta a través del mismo convertidor que usa la API AI Query tipada y devuelve el resultado ejecutado más el MDX generado más la petición estructurada que emitió el modelo.

Es la capa de lenguaje natural que se asienta sobre la superficie tipada del agente. Donde la API tipada asume que el llamador habla AiQueryRequest, la API ask permite que una persona diga “muestra las ventas por país del último trimestre” y reciba de vuelta un cellset completamente ejecutado con la traducción del modelo visible junto a él.

Esto impulsa el panel AI Query del workspace — el botón ⚡ en la barra de herramientas abre un panel de chat donde los usuarios escriben preguntas y hacen clic en Editar en lienzo para insertar el resultado en la pestaña de consulta activa.

Cuándo usarla

La API ask es la superficie adecuada cuando:

  • Los usuarios finales escribirán preguntas — la API AI Query tipada espera un agente que ya conoce el schema del cubo; la API ask oculta ese paso detrás de un LLM.
  • Quiere una sola ida y vuelta por pregunta/ai/ask hace traducción + ejecución + modelado del envoltorio en una sola llamada, por lo que el cliente no tiene que encadenar dos APIs.
  • Está integrando una superficie tipo chat — el campo history multi-turno permite que las preguntas de seguimiento se resuelvan contra turnos anteriores.

Cuándo no usarla:

  • Agentes en el servidor MCP — los agentes prefieren la superficie tipada porque ya son buenos completando campos estructurados a partir de un JSON Schema. La capa ask es para personas.
  • Crear cubos a partir de un data warehouse de muestra — use la API de inferencia de IA, que está diseñada para el flujo de diseño.

Activación

Dos propiedades + una variable de entorno por proveedor. Ambas pueden definirse en la JVM o en el archivo de propiedades del despliegue consumido por Spring.

# Anthropic Claude
saiku.ai.ask.provider = anthropic
# env ANTHROPIC_API_KEY = sk-ant-...
# OpenAI — or any OpenAI-compatible host (Azure, vLLM, Ollama, Together)
saiku.ai.ask.provider = openai
# env OPENAI_API_KEY = sk-...
# Optional, both providers
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-... # explicit override of the env var

Valores por defecto del proveedor:

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

Docker

Ventana de terminal
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 un Secret para la clave y un ConfigMap para el nombre del proveedor:

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

El endpoint

POST /saiku/api/ai/ask

Petición

{
"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 es opcional. Cuando se proporciona, los turnos (user, assistant) previos se envían de nuevo al modelo para que las preguntas de seguimiento como “ahora desglósalo por región” se resuelvan contra la pregunta anterior. Los prompts de sistema están controlados por el proveedor — los llamadores no pueden inyectarlos.

Respuesta

{
"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 es la consulta estructurada que produjo el modelo. Entréguela a POST /ai/query literalmente para reejecutarla, o expóngala al usuario como “la consulta tipada detrás de su pregunta”.

response es el resultado ejecutado, con la misma forma que devuelve la API AI Query tipada. Si el modelo emitió una petición que el schema rechaza, response.status es VALIDATION_ERROR y el cuerpo lleva field + candidatos available — el panel del workspace los representa como chips clicables para que el usuario pueda autocorregirse.

generatedMdx es una réplica de conveniencia de response.metadata.generatedMdx.

Códigos de estado

HTTPCuándo
200La traducción tuvo éxito; response lleva el resultado ejecutado (o VALIDATION_ERROR para que el usuario lo corrija).
200 + degraded:trueLa traducción falló en la capa del proveedor — error de transporte, el modelo se negó o error de parseo. reason lleva la explicación.
400Falta question o cube en el cuerpo.
503 + degraded:trueEl proveedor es noop (no configurado) — reason explica cómo activarlo.

Ejemplos

Desactivado por defecto — feedback claro

Ventana de terminal
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..."
# }

Ruta feliz — ida y vuelta completa

Ventana de terminal
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
# }

Seguimiento — multi-turno

Ventana de terminal
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

El modelo ve el turno anterior y resuelve “por trimestre” como también cruzado contra el país — misma forma que la primera respuesta con una jerarquía adicional.

Qué ve el modelo y qué no

La capa ask envía al modelo solo:

  • El AiSchema del cubo activo — JSON serializado de medidas, dimensiones, jerarquías, niveles y miembros de muestra.
  • El JSON Schema de AiQueryRequest como input_schema de la herramienta de salida estructurada.
  • La pregunta del usuario y cualquier historial de conversación que haya adjuntado el llamador.

El modelo está forzado mediante tool_choice a llamar a la herramienta emit_query con una forma AiQueryRequest válida. Las respuestas de prosa cruda se rechazan como degradadas.

Nunca ve:

  • Otros cubos a los que el usuario tiene acceso — solo el nombrado en la petición.
  • El SQL subyacente ni las credenciales del data warehouse.
  • Historial de conversación de otros usuarios.
  • La petición HTTP, las cookies de sesión ni ningún encabezado que haya enviado el llamador.

Este es el mismo modelo de aislamiento que usan el servidor MCP y la API AI Query tipada, aplicado una capa antes.

Próximos pasos

  • API de inferencia de IA — la superficie tipada a la que /ai/ask traduce. Útil cuando quiere que un agente (no una persona) componga la petición.
  • Servidor MCP — la misma superficie tipada expuesta como herramientas MCP para agentes de Claude Desktop / Cursor / Cline.
  • Autenticación — tokens Bearer, límites de velocidad, forma de los errores.