AI Query API — Ossie-Modelle
Die AI Query API für Ossie ist das Semantik-YAML-Gegenstück zur AI Query API für OLAP-Cubes. Dieselbe Disziplin: Agenten holen ein selbstbeschreibendes Schema, füllen eine JSON-Anfrage dagegen aus, der Server validiert jeden Namen, führt aus und gibt typisierte Records zurück. Dieselben Leitplanken: Agenten schreiben nie direkt SQL.
Wo die OLAP-AI-API gegen Mondrian-Cubes arbeitet, arbeitet diese hier
gegen Modelle, die in
Open Semantic Interchange /
Apache Ossie-YAML deklariert sind.
Alles, was der Workbench tut — Filter, Sortierungen, Aggregations-Overrides,
Crosstab-Pivot, Chart-Ansicht — ist programmatisch über diese API verfügbar,
plus eine natürlichsprachliche /ask-Ebene und Analytics pro Abfrage
(Anomalieerkennung, Forecasting).
Schnelle Orientierung
Drei Endpunkte decken ~90 % der Agenten-Nutzung ab:
| Endpunkt | Zweck |
|---|---|
GET /rest/saiku/api/ai/ossie/models | Listet jedes Ossie-Modell auf, das der Aufrufer abfragen kann. |
GET /rest/saiku/api/ai/ossie/schema/{connection}/{model} | Selbstbeschreibendes Schema — Datasets, Felder, Metriken, Beziehungen, JSON Schema des Request-Bodys, fertige Beispiel-Bodys. |
POST /rest/saiku/api/ai/ossie/query | Führt eine typisierte Shelf-State-Anfrage aus. Standardmäßig Records-Format als Antwort; Matrix bei ?format=matrix. |
Long-Tail-Endpunkte:
| Endpunkt | Zweck |
|---|---|
POST /rest/saiku/api/ai/ossie/query/preview | Kompiliert zu SQL, ohne auszuführen. Gleiche VALIDATION_ERROR-Form wie /query. |
GET /rest/saiku/api/ai/ossie/values/search | Substring-Lookup unterschiedlicher Werte in einem Feld. |
POST /rest/saiku/api/ai/ossie/query/execute-async | Reicht zur Hintergrundausführung ein. |
GET /rest/saiku/api/ai/ossie/query/status/{queryId} | Fragt den Status ab. |
GET /rest/saiku/api/ai/ossie/query/result/{queryId} | Holt ein abgeschlossenes async-Ergebnis. |
DELETE /rest/saiku/api/ai/ossie/query/{queryId} | Bricht eine laufende Abfrage ab. |
POST /rest/saiku/api/ai/ossie/row-detail | Ossies Drillthrough-Analogon — führt das Shelf als Rohzeilen erneut aus. |
POST /rest/saiku/api/ai/ossie/anomaly | Führt die Abfrage aus und markiert dann Anomalien entlang einer Zeitachse. |
POST /rest/saiku/api/ai/ossie/forecast | Projiziert zukünftige Punkte mit ETS / ARIMA / Prophet. |
POST /rest/saiku/api/ai/ossie/ask | Natürlichsprachliche Anfrage. Benötigt einen LLM-Schlüssel. |
GET /rest/saiku/api/ai/ossie/ask/health | Ob die Ask-Ebene auf dieser Instanz konfiguriert ist. |
Alle Endpunkte erfordern eine authentifizierte Session; POST-Endpunkte erfordern das CSRF-Cookie/Header-Paar. Gleiches Auth-Modell wie die OLAP-AI-API.
Schritt 1 — die Modelle auflisten
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 }]Das Paar connectionName + modelName ist der Bezeichner, der überall
sonst verwendet wird.
Schritt 2 — das Schema holen
GET /rest/saiku/api/ai/ossie/schema/unknown_TPCDS/TPCDSFügen Sie ?refresh=true hinzu, um den Sample-Value-Cache zu umgehen
(Standard-TTL ist 5 Minuten; überschreibbar via
SAIKU_AI_OSSIE_SAMPLES_TTL_MINUTES).
Die Antwort ist bewusst dicht — genau das macht die API selbstbeschreibend:
{ "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"}] } } }}Bemerkenswerte Affordances:
labelan jedem Feld ist der menschenlesbare Name aus der Ossie-Spezifikation — überall dort sichtbar, wo Spalten gerendert werden.NETREVENUEerscheint als „Net Revenue”.sampleValuesgibt dem Agenten echte Werte zum Filtern. Kein Halluzinieren von „US”, wenn der tatsächliche Wert „United States” ist.cardinality-Hinweise (low/medium/medium-high/high) — mitestimatedDistinct, wenn das WarehouseAPPROX_COUNT_DISTINCTunterstützt. Agenten können entscheiden, ob „nach dieser Spalte filtern” realistisch ist.supportedOverrides— die Menge der Aggregations-Overrides, die der Übersetzer tatsächlich umschreibt.COUNT(*)-Metriken akzeptieren nur COUNT (aufgedeckt durch die Fuzz-Suite).examples— Copy-Paste-Request-Bodys für die gängigen Formen (simpleGroupBy,crosstab,topN).
Schritt 3 — eine Abfrage ausführen
POST /rest/saiku/api/ai/ossie/queryContent-Type: application/jsonRequest:
{ "connection": "unknown_TPCDS", "model": "TPCDS", "rows": [{"dataset": "customer", "field": "c_state"}], "values": [{"metric": "total_sales"}], "sorts": [{"metric": "total_sales", "direction": "DESC"}], "limit": 5}Antwort (Records-Format, der Standard):
{ "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 }}Fügen Sie ?format=matrix für positionsindizierte
cellSetHeaders- + cellSetBody-Ausgabe hinzu — die Form, die der
OLAP-Records-Endpunkt für nachgelagerte Konsumenten zurückgibt, die sie
bereits verarbeiten.
Filter-Operatoren
EQ, NEQ, LT, LTE, GT, GTE, IN, BETWEEN, IS_NULL,
IS_NOT_NULL. Single-Value-Operatoren verwenden value; IN /
BETWEEN verwenden values. Ein leeres IN synthetisiert das
trivial-falsche Prädikat (gibt null Zeilen ohne Parse-Fehler zurück).
{ "filters": [ {"dataset": "customer", "field": "c_state", "op": "IN", "values": ["CA", "NY", "TX"]}, {"dataset": "store_sales", "field": "SS_SALES_PRICE", "op": "GT", "value": "50"} ]}Aggregations-Overrides zur Laufzeit
Tauschen Sie die deklarierte äußere Aggregation einer Metrik via
values[i].aggregation. Der Server validiert gegen die
supportedOverrides der Metrik:
// 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"]}K-Anonymität-Unterdrückung
Konfigurieren Sie am Server via SAIKU_AI_KANONYMITY_K (Standard 5) und
SAIKU_AI_KANONYMITY_MASK (Standard null). Wenn aktiviert, werden bei
Zeilen, deren count-förmige Metrik unter den Schwellenwert fällt, die
Metrikzellen maskiert, und ein meta.suppressed-Block auf oberster
Ebene erfasst die Anzahl:
{ "records": [ {"customer.c_state": "MA", "transaction_count": {"formatted": "null"}} ], "meta": { "rowCount": 4, "suppressed": {"count": 4, "reason": "k-anonymity threshold k=5"} }}Gilt für Records- + Matrix-Ausgabe sowie für die abgeleiteten
/ai/anomaly- und /ai/forecast-Endpunkte — unterdrückte Zeilen werden
maskiert, bevor der Anomalie-Scorer oder Forecaster sie sieht, sodass
ein Wert aus einer kleinen Kohorte nicht über eine Annotation
zurückleckt.
PII-Redaktion
Felder, die im Ossie-YAML mit pii: true markiert sind, werden aus der
Schema-Ansicht vollständig entfernt. Aufrufer können sie nicht
referenzieren; eine Abfrage, die es doch tut, gibt einen
VALIDATION_ERROR zurück, der das Feld als „unbekannt” benennt.
Schritt 4 — wie die API den Agenten anlernt
Jeder falsche Name kommt mit einer Kandidatenliste zurück. Der nächste Versuch des Agenten wählt einen davon:
// 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"]}Dieselbe Form deckt unbekannte Felder, unbekannte Metriken, nicht
unterstützte Filter-Operatoren, BETWEEN mit weniger als zwei Werten,
Sort-Referenzen, die sowohl metric als auch field benennen, limit ≤ 0,
leere rows/columns/values und timeAxis auf /anomaly + /forecast ab,
das nicht in der Abfrage vorkommt.
Schritt 5 — Preview
POST /rest/saiku/api/ai/ossie/query/previewGleicher Body wie /query. Antwort:
{ "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\""}Verwendet denselben Übersetzer, den der Executor ausführt — Sie sehen 1:1,
was /query dispatchen würde.
Schritt 6 — Werte-Suche
GET /rest/saiku/api/ai/ossie/values/search?connection=unknown_TPCDS&dataset=customer&field=C_STATE&q=CA{ "matches": ["CA"]}Führt SELECT DISTINCT ... WHERE UPPER(CAST(... AS VARCHAR)) LIKE '%...%' LIMIT n aus. Lassen Sie q weg für die ersten N unterschiedlichen Werte.
Schritt 7 — Zeilendetail (Drillthrough)
POST /rest/saiku/api/ai/ossie/row-detail?maxrows=5Gleicher Body wie /query. Der Server führt das Shelf mit values=[]
erneut aus, sodass der Executor Rohzeilen statt eines Aggregats ausgibt.
Die Antwort ist im Records-Format mit meta.truncated: true, wenn die
Zeilenobergrenze erreicht wird (Standard 100, Maximum 10 000).
Schritt 8 — async
Für Abfragen, von denen Sie erwarten, dass sie mehr als ein paar Sekunden dauern:
-
Einreichen —
POST /query/execute-async. Gleicher Body wie/query. Antwort 202:{"queryId": "...", "status": "PENDING"}. -
Abfragen —
GET /query/status/{queryId}. Statusübergänge:PENDING→RUNNING→DONE|FAILED|CANCELLED. -
Holen —
GET /query/result/{queryId}. 202 mit{queryId, status}während der Ausführung, 200 mit der vollständigen Records-Antwort (oder?format=matrix) bei DONE. -
Abbrechen —
DELETE /query/{queryId}.
Schritt 9 — Analytics
Anomalieerkennung
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}Detektoren: zscore (klassischer Z-Score, Sigmas vom Mittelwert), mad
(Median Absolute Deviation, robust gegenüber Ausreißern), stl
(Seasonal-Trend-Dekomposition; fällt bei nicht-saisonalen Daten auf
zscore zurück).
Die Antwort ist die Records-Form mit einer anomalie-annotierten Metrikzelle dort, wo der Detektor einen Punkt markiert hat:
{ "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}}Forecast
POST /rest/saiku/api/ai/ossie/forecast{ "query": { /* ... */ }, "timeAxis": "date_dim.d_month", "method": "ets", "horizon": 3, "interval": 0.95}Forecaster: ets, arima, prophet. Historische Records bleiben
unangetastet; Projektionen landen unter einem forecast-Block auf
oberster Ebene, nach Metrik verschlüsselt:
{ "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} ] } }}Schritt 10 — natürlichsprachliche Anfrage
GET /rest/saiku/api/ai/ossie/ask/health{"configured": true, "provider": "anthropic (claude-sonnet-4-6)"}Am Server aktivieren:
saiku.ai.ask.provider=anthropic|openai- env
ANTHROPIC_API_KEY(Anthropic) oderOPENAI_API_KEY(OpenAI) - optional
saiku.ai.ask.model— Override der Modell-ID - optional
saiku.ai.ask.endpoint— benutzerdefinierte Basis-URL für OpenAI-kompatible Proxys (vLLM, Ollama, Together)
Dann:
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 ist optional — jeder Turn wird an das LLM übergeben, damit es
Folgefragen wie „what about by state?” auflösen kann. Antwort:
{ "question": "...", "connection": "unknown_TPCDS", "model": "TPCDS", "queryUsed": { /* the OssieAiQueryRequest the LLM produced */ }, "response": { /* the full records-format execution result */ }}Das LLM wird via tool_use (Anthropic) / tool_choice: function
(OpenAI) in strukturierte Ausgabe gezwungen, deren Schema
OssieAiQueryRequest spiegelt. Off-Topic-Fragen kommen als
OFF_TOPIC-Umschlag zurück, der als 400 mit angehängtem Grund
sichtbar wird.
MCP-Integration
Die fünf Ossie-Tools werden über den MCP-Endpunkt neben den sechs OLAP-Tools bereitgestellt:
| MCP-Tool | REST-Äquivalent |
|---|---|
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 — alles, was MCP spricht — sieht nach der Authentifizierung alle elf Tools.
Ein typischer Agent-Loop
list_ossie_modelsoderGET /models— ein Modell wählen.describe_ossie_modeloderGET /schema/{c}/{m}— Datasets, Felder, Metriken, Beispiele lesen.- Wenn der Nutzer einen Wert nennt, den das Schema nicht gesampelt hat —
search_field_valuesoderGET /values/search— die Schreibweise bestätigen. - Einen Query-Body aus
examples.simpleGroupBy(oder einem anderen Beispiel) als Vorlage bauen und die Dimensionen und Metriken des Nutzers einsetzen. run_ossie_queryoderPOST /query— wenn die Antwort einVALIDATION_ERRORist, ausavailablewählen und erneut versuchen.- Bei einer Chart-Anfrage → mit
format: "matrix"wiederholen. - Bei „explain the trend” →
/anomalyoder/forecast. - Bei „show me the underlying rows” →
/row-detail.
Oder für natürlichsprachliche Flows: 4–7 überspringen und /ask verwenden.
Woher die Modelle kommen
- Eigenes OSI-YAML schreiben — Datasets, Metriken, Beziehungen
deklarieren. Saiku via eine
.sds-Datasource-Datei darauf zeigen. - Auf ein bestehendes dbt-Projekt zeigen — der dbt-Anbindungsleitfaden führt durch den ~200-zeiligen Konverter, den wir mitliefern, der MetricFlow-YAML liest und OSI-konformes YAML ausgibt.
- Ein Mondrian-Schema exportieren — die
saiku ossie-export-CLI im Saiku-Launcher konvertiert ein Mondrian-XML-Schema zu OSI-YAML.
Siehe auch
- Ossie / OSI auf Apache — die Spezifikation + Beispiel-YAMLs
- MCP-Server — der Tool-Wrapper für LLM-Agenten
- dbt-Anbindung — Saiku auf Ihr bestehendes dbt-Projekt zeigen
- OLAP AI Query API — das Mondrian-Cube-Gegenstück dieser API