API AI Query — modèles Ossie
L’API AI Query pour Ossie est le pendant en YAML sémantique de l’API AI Query pour les cubes OLAP. Même discipline : les agents récupèrent un schéma auto-descriptif, remplissent une requête JSON à partir de celui-ci, le serveur valide chaque nom, exécute et retourne des records typés. Mêmes garde-fous : les agents n’écrivent jamais de SQL directement.
Là où l’API AI OLAP travaille contre des cubes Mondrian, celle-ci travaille
contre des modèles déclarés en YAML
Open Semantic Interchange /
Apache Ossie. Tout ce que fait le
workbench — filtres, tris, surcharges d’agrégation, pivot crosstab,
vue graphique — est disponible programmatiquement via cette API,
plus une couche /ask en langage naturel et des analyses par requête
(détection d’anomalies, prévision).
Orientation rapide
Trois endpoints couvrent ~90 % de l’usage des agents :
| Endpoint | Objectif |
|---|---|
GET /rest/saiku/api/ai/ossie/models | Liste chaque modèle Ossie que l’appelant peut requêter. |
GET /rest/saiku/api/ai/ossie/schema/{connection}/{model} | Schéma auto-descriptif — datasets, champs, métriques, relations, JSON Schema du corps de requête, corps d’exemple prêts à l’emploi. |
POST /rest/saiku/api/ai/ossie/query | Exécute une requête d’état de shelf typée. Réponse au format records par défaut ; matrix avec ?format=matrix. |
Endpoints de longue traîne :
| Endpoint | Objectif |
|---|---|
POST /rest/saiku/api/ai/ossie/query/preview | Compile en SQL sans exécuter. Même forme de VALIDATION_ERROR que /query. |
GET /rest/saiku/api/ai/ossie/values/search | Recherche par sous-chaîne des valeurs distinctes d’un champ. |
POST /rest/saiku/api/ai/ossie/query/execute-async | Soumet pour exécution en arrière-plan. |
GET /rest/saiku/api/ai/ossie/query/status/{queryId} | Interroge le statut. |
GET /rest/saiku/api/ai/ossie/query/result/{queryId} | Récupère un résultat asynchrone terminé. |
DELETE /rest/saiku/api/ai/ossie/query/{queryId} | Annule une requête en cours. |
POST /rest/saiku/api/ai/ossie/row-detail | L’analogue du drillthrough d’Ossie — réexécute le shelf sous forme de lignes brutes. |
POST /rest/saiku/api/ai/ossie/anomaly | Exécute la requête puis signale les anomalies le long d’un axe temporel. |
POST /rest/saiku/api/ai/ossie/forecast | Projette des points futurs via ETS / ARIMA / Prophet. |
POST /rest/saiku/api/ai/ossie/ask | Question en langage naturel. Nécessite une clé LLM. |
GET /rest/saiku/api/ai/ossie/ask/health | Indique si la couche ask est configurée sur cette instance. |
Tous les endpoints nécessitent une session authentifiée ; les endpoints POST nécessitent la paire cookie/en-tête CSRF. Même modèle d’authentification que l’API AI OLAP.
Étape 1 — lister les modèles
GET /rest/saiku/api/ai/ossie/models[ { "connectionName": "unknown_TPCDS", "modelName": "TPCDS", "description": "TPC-DS retail — sales, customers, products, stores.", "factDataset": "store_sales", "datasetCount": 5, "metricCount": 5 }]La paire connectionName + modelName est l’identifiant utilisé
partout ailleurs.
Étape 2 — récupérer le schéma
GET /rest/saiku/api/ai/ossie/schema/unknown_TPCDS/TPCDSAjoutez ?refresh=true pour contourner le cache de valeurs d’échantillon
(le TTL par défaut est de 5 minutes ; surchargez via
SAIKU_AI_OSSIE_SAMPLES_TTL_MINUTES).
La réponse est dense par conception — c’est ce qui rend l’API auto-descriptive :
{ "modelId": "unknown_TPCDS/TPCDS", "connectionName": "unknown_TPCDS", "modelName": "TPCDS", "factDataset": "store_sales",
"datasets": { "item": { "name": "item", "source": "ITEM", "primaryKey": ["I_ITEM_SK"], "fields": { "i_brand": { "name": "i_brand", "label": "Brand", "type": "VARCHAR", "cardinality": "low", "sampleValues": ["AudioLine", "BookHouse", "CasualCo", "DeskPro"] }, "i_category": { "name": "i_category", "label": "Category", "type": "VARCHAR", "cardinality": "low", "sampleValues": ["Apparel", "Books", "Electronics", "Furniture"] } } } },
"metrics": { "total_sales": { "name": "total_sales", "expression": "SUM(\"store_sales\".\"SS_QUANTITY\" * \"store_sales\".\"SS_SALES_PRICE\")", "aggregationKind": "sum", "supportedOverrides": ["SUM", "AVG", "MIN", "MAX", "COUNT"] }, "transaction_count": { "name": "transaction_count", "expression": "COUNT(*)", "aggregationKind": "count", "supportedOverrides": ["COUNT"] } },
"relationships": [ { "name": "store_sales_to_item", "from": "store_sales", "to": "item", "fromColumns": ["SS_ITEM_SK"], "toColumns": ["I_ITEM_SK"] } ],
"requestSchema": { /* JSON Schema for the POST /query body */ },
"examples": { "simpleGroupBy": { "description": "Total sales grouped by item brand", "body": { "model": "TPCDS", "rows": [{"dataset": "item", "field": "i_brand"}], "values": [{"metric": "total_sales"}] } } }}Affordances notables :
labelsur chaque champ est le nom lisible par l’humain de la spec Ossie — affiché partout où les colonnes s’affichent.NETREVENUEapparaît comme « Net Revenue ».sampleValuesdonne à l’agent de vraies valeurs pour filtrer. Pas d’hallucination de « US » quand la valeur réelle est « United States ».cardinalityindique (low/medium/medium-high/high) — avecestimatedDistinctquand l’entrepôt supporteAPPROX_COUNT_DISTINCT. Les agents peuvent décider si « filtrer par cette colonne » est réaliste.supportedOverrides— l’ensemble des surcharges d’agrégation que le traducteur réécrira réellement. Les métriquesCOUNT(*)n’acceptent que COUNT (mis en évidence par la suite fuzz).examples— corps de requête à copier-coller pour les formes courantes (simpleGroupBy,crosstab,topN).
Étape 3 — exécuter une requête
POST /rest/saiku/api/ai/ossie/queryContent-Type: application/jsonRequête :
{ "connection": "unknown_TPCDS", "model": "TPCDS", "rows": [{"dataset": "customer", "field": "c_state"}], "values": [{"metric": "total_sales"}], "sorts": [{"metric": "total_sales", "direction": "DESC"}], "limit": 5}Réponse (format records, le défaut) :
{ "queryId": "ossie-ai-a3f81", "runtime": 210, "columns": [ {"key": "customer.c_state", "label": "State", "type": "dimension"}, {"key": "total_sales", "label": "total_sales", "type": "metric", "aggregationKind": "sum"} ], "records": [ {"customer.c_state": "CA", "total_sales": {"value": 1835.0, "formatted": "1835.00"}}, {"customer.c_state": "NY", "total_sales": {"value": 1281.8, "formatted": "1281.80"}} ], "meta": { "rowCount": 2, "truncated": false }}Ajoutez ?format=matrix pour une sortie cellSetHeaders + cellSetBody
indexée par position — la forme que retourne l’endpoint records OLAP pour
les consommateurs en aval qui la gèrent déjà.
Opérateurs de filtre
EQ, NEQ, LT, LTE, GT, GTE, IN, BETWEEN, IS_NULL,
IS_NOT_NULL. Les opérateurs à valeur unique utilisent value ; IN /
BETWEEN utilisent values. Un IN vide synthétise le prédicat
trivialement faux (retourne zéro ligne sans erreur d’analyse).
{ "filters": [ {"dataset": "customer", "field": "c_state", "op": "IN", "values": ["CA", "NY", "TX"]}, {"dataset": "store_sales", "field": "SS_SALES_PRICE", "op": "GT", "value": "50"} ]}Surcharges d’agrégation à la volée
Remplacez l’agrégation externe déclarée d’une métrique via values[i].aggregation.
Le serveur valide contre le supportedOverrides de la métrique :
// Bad: SUM on a COUNT(*) metric{"metric": "transaction_count", "aggregation": "SUM"}
// 400 Response{ "error": "VALIDATION_ERROR", "field": "values[0].aggregation", "message": "aggregation 'SUM' not supported for metric 'transaction_count'", "available": ["COUNT"]}Suppression par k-anonymat
Configurez au niveau du serveur via SAIKU_AI_KANONYMITY_K (défaut 5) et
SAIKU_AI_KANONYMITY_MASK (défaut null). Quand c’est activé, les lignes dont
la métrique de type comptage tombe sous le seuil voient leurs cellules de
métrique masquées, et un bloc meta.suppressed de premier niveau enregistre
le compte :
{ "records": [ {"customer.c_state": "MA", "transaction_count": {"formatted": "null"}} ], "meta": { "rowCount": 4, "suppressed": {"count": 4, "reason": "k-anonymity threshold k=5"} }}S’applique aux sorties records + matrix, et aux endpoints dérivés /ai/anomaly
et /ai/forecast — les lignes supprimées sont masquées avant que le scoreur
d’anomalies ou le prévisionniste ne les voie, pour qu’une valeur de petite
cohorte ne puisse pas refuir via une annotation.
Rédaction PII
Les champs marqués pii: true dans le YAML Ossie sont entièrement retirés de
la vue du schéma. Les appelants ne peuvent pas les référencer ; une requête qui
le fait retourne VALIDATION_ERROR nommant le champ comme « unknown ».
Étape 4 — comment l’API enseigne à l’agent
Chaque nom incorrect revient avec une liste de candidats. La tentative suivante de l’agent en choisit un :
// Request with a typo{"rows": [{"dataset": "geographi", "field": "region"}], "values": [{"metric": "net_revenue"}]}// 400{ "error": "VALIDATION_ERROR", "field": "rows[0].dataset", "message": "unknown dataset 'geographi'", "available": ["fact_pharma", "geography", "payer", "product"]}La même forme couvre les champs inconnus, les métriques inconnues, les
opérateurs de filtre non supportés, BETWEEN avec moins de deux valeurs, les
références de tri qui nomment à la fois metric et field, une limite ≤ 0, des
rows/columns/values vides, et un timeAxis sur /anomaly + /forecast qui
n’apparaît pas dans la requête.
Étape 5 — preview
POST /rest/saiku/api/ai/ossie/query/previewMême corps que /query. Réponse :
{ "queryId": "ossie-ai-preview-9fe75acf", "status": "PREVIEW", "generatedSql": "SELECT \"customer\".\"C_STATE\" AS \"customer.c_state\", SUM(\"store_sales\".\"SS_QUANTITY\" * \"store_sales\".\"SS_SALES_PRICE\") AS \"total_sales\" FROM \"store_sales\", \"customer\" GROUP BY \"customer\".\"C_STATE\""}Utilise le même traducteur que celui exécuté par l’exécuteur — vous voyez au
1:1 ce que /query dispatcherait.
Étape 6 — recherche de valeurs
GET /rest/saiku/api/ai/ossie/values/search?connection=unknown_TPCDS&dataset=customer&field=C_STATE&q=CA{ "matches": ["CA"]}Exécute SELECT DISTINCT ... WHERE UPPER(CAST(... AS VARCHAR)) LIKE '%...%' LIMIT n. Omettez q pour les N premières valeurs distinctes.
Étape 7 — détail de ligne (drillthrough)
POST /rest/saiku/api/ai/ossie/row-detail?maxrows=5Même corps que /query. Le serveur réexécute le shelf avec values=[]
pour que l’exécuteur émette des lignes brutes au lieu d’un agrégat. La réponse
est au format records avec meta.truncated: true quand le plafond de lignes est
atteint (défaut 100, max 10 000).
Étape 8 — asynchrone
Pour les requêtes que vous attendez à prendre plus de quelques secondes :
-
Soumettre —
POST /query/execute-async. Même corps que/query. Réponse 202 :{"queryId": "...", "status": "PENDING"}. -
Interroger —
GET /query/status/{queryId}. Transitions de statut :PENDING→RUNNING→DONE|FAILED|CANCELLED. -
Récupérer —
GET /query/result/{queryId}. 202 avec{queryId, status}pendant l’exécution, 200 avec la réponse records complète (ou?format=matrix) sur DONE. -
Annuler —
DELETE /query/{queryId}.
Étape 9 — analyses
Détection d’anomalies
POST /rest/saiku/api/ai/ossie/anomaly{ "query": { "connection": "unknown_TPCDS", "model": "TPCDS", "rows": [{"dataset": "date_dim", "field": "d_month"}], "values": [{"metric": "total_sales"}] }, "timeAxis": "date_dim.d_month", "method": "zscore", "threshold": 1.5}Détecteurs : zscore (z-score classique, écarts-types par rapport à la
moyenne), mad (déviation absolue médiane, robuste aux valeurs aberrantes),
stl (décomposition saisonnière-tendance ; retombe sur zscore sur des données
non saisonnières).
La réponse est au format records avec une cellule de métrique annotée d’anomalie là où le détecteur a signalé un point :
{ "records": [ { "date_dim.d_month": "December", "total_sales": { "value": 4235.75, "formatted": "4235.75", "anomaly": {"score": 1.85, "expected": 2900.12, "direction": "high"} } } ], "anomaly": {"method": "zscore", "threshold": 1.5, "anomalyCount": 1}}Prévision
POST /rest/saiku/api/ai/ossie/forecast{ "query": { /* ... */ }, "timeAxis": "date_dim.d_month", "method": "ets", "horizon": 3, "interval": 0.95}Prévisionnistes : ets, arima, prophet. Records historiques intacts ;
les projections atterrissent sous un bloc forecast de premier niveau clé par
métrique :
{ "records": [ /* historical rows */ ], "forecast": { "total_sales": { "method": "ets", "horizon": 3, "confidence": 0.95, "points": [ {"index": 4, "value": 573.41, "lower": 417.73, "upper": 729.08}, {"index": 5, "value": 598.97, "lower": 378.81, "upper": 819.14} ] } }}Étape 10 — question en langage naturel
GET /rest/saiku/api/ai/ossie/ask/health{"configured": true, "provider": "anthropic (claude-sonnet-4-6)"}Activez au niveau du serveur :
saiku.ai.ask.provider=anthropic|openai- env
ANTHROPIC_API_KEY(Anthropic) ouOPENAI_API_KEY(OpenAI) - optionnel
saiku.ai.ask.model— surcharge de l’id de modèle - optionnel
saiku.ai.ask.endpoint— URL de base personnalisée pour les proxies compatibles OpenAI (vLLM, Ollama, Together)
Puis :
POST /rest/saiku/api/ai/ossie/ask{ "connection": "unknown_TPCDS", "model": "TPCDS", "question": "What's total revenue per state for the CA and NY brands?", "history": [ {"role": "user", "content": "show me sales by product"}, {"role": "assistant", "content": "here's revenue by brand..."} ]}history est optionnel — chaque tour est passé au LLM pour qu’il puisse
résoudre les relances comme « what about by state? ». Réponse :
{ "question": "...", "connection": "unknown_TPCDS", "model": "TPCDS", "queryUsed": { /* the OssieAiQueryRequest the LLM produced */ }, "response": { /* the full records-format execution result */ }}Le LLM est forcé en sortie structurée via tool_use (Anthropic)
/ tool_choice: function (OpenAI) dont le schéma reflète
OssieAiQueryRequest. Les questions hors sujet reviennent comme une
enveloppe OFF_TOPIC exposée en 400 avec la raison attachée.
Intégration MCP
Les cinq outils Ossie sont exposés via l’endpoint MCP aux côtés des six outils OLAP :
| Outil MCP | Équivalent REST |
|---|---|
list_ossie_models | GET /ai/ossie/models |
describe_ossie_model | GET /ai/ossie/schema/{c}/{m} |
search_field_values | GET /ai/ossie/values/search |
run_ossie_query | POST /ai/ossie/query |
preview_ossie_query | POST /ai/ossie/query/preview |
Claude Desktop, Cursor, Cline — tout ce qui parle MCP — voit les onze outils une fois authentifié.
Une boucle d’agent typique
list_ossie_modelsouGET /models— choisir un modèle.describe_ossie_modelouGET /schema/{c}/{m}— lire les datasets, champs, métriques, exemples.- Si l’utilisateur nomme une valeur que le schéma n’a pas échantillonnée —
search_field_valuesouGET /values/search— confirmer l’orthographe. - Construire un corps de requête à partir de
examples.simpleGroupBy(ou d’un autre exemple) comme gabarit, en substituant les dimensions et métriques de l’utilisateur. run_ossie_queryouPOST /query— si la réponse est uneVALIDATION_ERROR, choisir dansavailableet réessayer.- Sur une demande de graphique → répéter avec
format: "matrix". - Sur « explique la tendance » →
/anomalyou/forecast. - Sur « montre-moi les lignes sous-jacentes » →
/row-detail.
Ou pour les flux en langage naturel : sautez les étapes 4 à 7 et utilisez /ask.
D’où viennent les modèles
- Écrivez votre propre YAML OSI — déclarez datasets, métriques,
relations. Pointez Saiku dessus via un fichier de datasource
.sds. - Pointez vers un projet dbt existant — le guide de branchement dbt parcourt le convertisseur de ~200 lignes que nous livrons, qui lit le YAML MetricFlow et émet du YAML conforme à OSI.
- Exportez un schéma Mondrian — la CLI
saiku ossie-exportdans le launcher Saiku convertit un schéma XML Mondrian en YAML OSI.
Voir aussi
- Ossie / OSI sur Apache — la spec + les YAMLs d’exemple
- Serveur MCP — le wrapper d’outils pour les agents LLM
- Branchement dbt — pointez Saiku vers votre projet dbt existant
- API AI Query OLAP — le pendant cube Mondrian de cette API