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/askhace 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
historymulti-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 Claudesaiku.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 providerssaiku.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-... # explicit override of the env varValores por defecto del proveedor:
| Proveedor | Modelo por defecto | 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 un Secret para la clave y un ConfigMap para el nombre del
proveedor:
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=anthropicEl endpoint
POST /saiku/api/ai/askPetició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
| HTTP | Cuándo |
|---|---|
200 | La traducción tuvo éxito; response lleva el resultado ejecutado (o VALIDATION_ERROR para que el usuario lo corrija). |
200 + degraded:true | La traducción falló en la capa del proveedor — error de transporte, el modelo se negó o error de parseo. reason lleva la explicación. |
400 | Falta question o cube en el cuerpo. |
503 + degraded:true | El proveedor es noop (no configurado) — reason explica cómo activarlo. |
Ejemplos
Desactivado por defecto — 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..."# }Ruta feliz — ida y vuelta completa
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
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."} ]}EOFEl 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
AiSchemadel cubo activo — JSON serializado de medidas, dimensiones, jerarquías, niveles y miembros de muestra. - El JSON Schema de
AiQueryRequestcomoinput_schemade 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/asktraduce. Ú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.