Aller au contenu

API AI Ask

L’API AI Ask prend une question en langage naturel, demande au fournisseur LLM configuré de remplir une requête typée à partir du schema de cube actif, exécute cette requête via le même convertisseur que l’API AI Query typée, et renvoie le résultat exécuté + le MDX généré + la requête structurée émise par le modèle.

C’est la couche en langage naturel posée sur la surface d’agent typée. Là où l’API typée suppose que l’appelant parle AiQueryRequest, l’API Ask permet à un humain de dire « montre les ventes par pays au dernier trimestre » et de recevoir en retour un cellset entièrement exécuté avec la traduction du modèle visible à côté.

C’est ce qui alimente le panneau AI Query de l’espace de travail — le bouton ⚡ de la barre d’outils ouvre un panneau de chat où les utilisateurs saisissent des questions et cliquent sur Edit in canvas pour déposer le résultat dans l’onglet de requête actif.

Quand l’utiliser

L’API Ask est la bonne surface lorsque :

  • Des utilisateurs finaux saisissent des questions — l’API AI Query typée attend un agent qui connaît déjà le schema du cube ; l’API Ask masque cette étape derrière un LLM.
  • Vous voulez un aller-retour par question/ai/ask effectue la traduction + l’exécution + la mise en forme de l’enveloppe en un seul appel, pour que le client n’ait pas à chaîner deux API.
  • Vous intégrez une surface de type chat — le champ multi-tour history permet aux questions de suivi de se résoudre par rapport aux tours précédents.

Quand ne pas l’utiliser :

  • Agents sur le serveur MCP — les agents préfèrent la surface typée car ils savent déjà bien remplir les champs structurés à partir d’un JSON Schema. La couche Ask est destinée aux humains.
  • Création de cubes à partir d’un entrepôt d’exemple — utilisez l’API d’inférence IA, conçue pour le flux à la conception.

Activation

Deux propriétés + une variable d’environnement par fournisseur. Les deux peuvent être définies sur la JVM ou dans le fichier de propriétés du déploiement consommé par 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

Valeurs par défaut des fournisseurs :

FournisseurModèle par défautEndpoint
anthropicclaude-sonnet-4-6https://api.anthropic.com/v1/messages
openaigpt-4o-minihttps://api.openai.com/v1/chat/completions

Docker

Fenêtre 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

Utilisez un Secret pour la clé, un ConfigMap pour le nom du fournisseur :

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

L’endpoint

POST /saiku/api/ai/ask

Requête

{
"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 est facultatif. Lorsqu’il est fourni, les tours précédents (user, assistant) sont renvoyés au modèle afin que les suivis comme « maintenant ventile par région » se résolvent par rapport à la question antérieure. Les prompts système sont contrôlés par le fournisseur — les appelants ne peuvent pas en injecter.

Réponse

{
"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 est la requête structurée produite par le modèle. Transmettez-la telle quelle à POST /ai/query pour la ré-exécuter, ou exposez-la à l’utilisateur sous la forme « la requête typée derrière votre question ».

response est le résultat exécuté, avec la même forme que ce que renvoie l’API AI Query typée. Si le modèle a émis une requête que le schema rejette, response.status vaut VALIDATION_ERROR et le corps porte field + les candidats available — le panneau de l’espace de travail les affiche sous forme de puces cliquables pour que l’utilisateur puisse s’auto-corriger.

generatedMdx est un miroir pratique de response.metadata.generatedMdx.

Codes de statut

HTTPQuand
200Traduction réussie ; response porte le résultat exécuté (ou VALIDATION_ERROR pour que l’utilisateur s’auto-corrige).
200 + degraded:trueLa traduction a échoué au niveau du fournisseur — erreur de transport, refus du modèle ou erreur d’analyse. reason porte l’explication.
400question ou cube manquant dans le corps.
503 + degraded:trueLe fournisseur est noop (non configuré) — reason explique comment l’activer.

Exemples

Désactivé par défaut — retour clair

Fenêtre 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..."
# }

Cas nominal — aller-retour complet

Fenêtre 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
# }

Suivi — multi-tour

Fenêtre 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

Le modèle voit le tour précédent et interprète « par trimestre » comme également croisé avec le pays — même forme que la première réponse avec une hiérarchie supplémentaire.

Ce que voit le modèle (et ce qu’il ne voit pas)

La couche Ask envoie au modèle uniquement :

  • L’AiSchema du cube actif — JSON sérialisé des mesures, dimensions, hiérarchies, niveaux et membres d’exemple.
  • Le JSON Schema d’AiQueryRequest comme input_schema de l’outil de sortie structurée.
  • La question de l’utilisateur et tout historique de conversation joint par l’appelant.

Le modèle est forcé via tool_choice à appeler l’outil emit_query avec une forme AiQueryRequest valide. Les réponses en prose brute sont rejetées comme dégradées.

Il ne voit jamais :

  • Les autres cubes auxquels l’utilisateur a accès — uniquement celui nommé dans la requête.
  • Le SQL sous-jacent ou les identifiants d’entrepôt.
  • L’historique de conversation d’autres utilisateurs.
  • La requête HTTP, les cookies de session ou les en-têtes envoyés par l’appelant.

C’est le même modèle d’isolation que le serveur MCP et l’API AI Query typée utilisent, appliqué une couche plus tôt.

Et ensuite

  • API d’inférence IA — la surface typée vers laquelle /ai/ask traduit. Utile lorsque vous voulez qu’un agent (et non un humain) compose la requête.
  • Serveur MCP — la même surface typée exposée comme outils MCP pour les agents Claude Desktop / Cursor / Cline.
  • Authentification — jetons Bearer, limites de débit, formes d’erreur.