Aller au contenu

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 :

EndpointObjectif
GET /rest/saiku/api/ai/ossie/modelsListe 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/queryExé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 :

EndpointObjectif
POST /rest/saiku/api/ai/ossie/query/previewCompile en SQL sans exécuter. Même forme de VALIDATION_ERROR que /query.
GET /rest/saiku/api/ai/ossie/values/searchRecherche par sous-chaîne des valeurs distinctes d’un champ.
POST /rest/saiku/api/ai/ossie/query/execute-asyncSoumet 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-detailL’analogue du drillthrough d’Ossie — réexécute le shelf sous forme de lignes brutes.
POST /rest/saiku/api/ai/ossie/anomalyExécute la requête puis signale les anomalies le long d’un axe temporel.
POST /rest/saiku/api/ai/ossie/forecastProjette des points futurs via ETS / ARIMA / Prophet.
POST /rest/saiku/api/ai/ossie/askQuestion en langage naturel. Nécessite une clé LLM.
GET /rest/saiku/api/ai/ossie/ask/healthIndique 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/TPCDS

Ajoutez ?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 :

  • label sur chaque champ est le nom lisible par l’humain de la spec Ossie — affiché partout où les colonnes s’affichent. NETREVENUE apparaît comme « Net Revenue ».
  • sampleValues donne à l’agent de vraies valeurs pour filtrer. Pas d’hallucination de « US » quand la valeur réelle est « United States ».
  • cardinality indique (low / medium / medium-high / high) — avec estimatedDistinct quand l’entrepôt supporte APPROX_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étriques COUNT(*) 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/query
Content-Type: application/json

Requê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/preview

Mê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=5

Mê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 :

  1. SoumettrePOST /query/execute-async. Même corps que /query. Réponse 202 : {"queryId": "...", "status": "PENDING"}.

  2. InterrogerGET /query/status/{queryId}. Transitions de statut : PENDINGRUNNINGDONE | FAILED | CANCELLED.

  3. RécupérerGET /query/result/{queryId}. 202 avec {queryId, status} pendant l’exécution, 200 avec la réponse records complète (ou ?format=matrix) sur DONE.

  4. AnnulerDELETE /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) ou OPENAI_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_modelsGET /ai/ossie/models
describe_ossie_modelGET /ai/ossie/schema/{c}/{m}
search_field_valuesGET /ai/ossie/values/search
run_ossie_queryPOST /ai/ossie/query
preview_ossie_queryPOST /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

  1. list_ossie_models ou GET /models — choisir un modèle.
  2. describe_ossie_model ou GET /schema/{c}/{m} — lire les datasets, champs, métriques, exemples.
  3. Si l’utilisateur nomme une valeur que le schéma n’a pas échantillonnée — search_field_values ou GET /values/search — confirmer l’orthographe.
  4. 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.
  5. run_ossie_query ou POST /query — si la réponse est une VALIDATION_ERROR, choisir dans available et réessayer.
  6. Sur une demande de graphique → répéter avec format: "matrix".
  7. Sur « explique la tendance » → /anomaly ou /forecast.
  8. 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 existantle 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-export dans le launcher Saiku convertit un schéma XML Mondrian en YAML OSI.

Voir aussi