Zum Inhalt springen

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:

EndpunktZweck
GET /rest/saiku/api/ai/ossie/modelsListet 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/queryFührt eine typisierte Shelf-State-Anfrage aus. Standardmäßig Records-Format als Antwort; Matrix bei ?format=matrix.

Long-Tail-Endpunkte:

EndpunktZweck
POST /rest/saiku/api/ai/ossie/query/previewKompiliert zu SQL, ohne auszuführen. Gleiche VALIDATION_ERROR-Form wie /query.
GET /rest/saiku/api/ai/ossie/values/searchSubstring-Lookup unterschiedlicher Werte in einem Feld.
POST /rest/saiku/api/ai/ossie/query/execute-asyncReicht 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-detailOssies Drillthrough-Analogon — führt das Shelf als Rohzeilen erneut aus.
POST /rest/saiku/api/ai/ossie/anomalyFührt die Abfrage aus und markiert dann Anomalien entlang einer Zeitachse.
POST /rest/saiku/api/ai/ossie/forecastProjiziert zukünftige Punkte mit ETS / ARIMA / Prophet.
POST /rest/saiku/api/ai/ossie/askNatürlichsprachliche Anfrage. Benötigt einen LLM-Schlüssel.
GET /rest/saiku/api/ai/ossie/ask/healthOb 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/TPCDS

Fü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:

  • label an jedem Feld ist der menschenlesbare Name aus der Ossie-Spezifikation — überall dort sichtbar, wo Spalten gerendert werden. NETREVENUE erscheint als „Net Revenue”.
  • sampleValues gibt 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) — mit estimatedDistinct, wenn das Warehouse APPROX_COUNT_DISTINCT unterstü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/query
Content-Type: application/json

Request:

{
"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/preview

Gleicher 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=5

Gleicher 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:

  1. EinreichenPOST /query/execute-async. Gleicher Body wie /query. Antwort 202: {"queryId": "...", "status": "PENDING"}.

  2. AbfragenGET /query/status/{queryId}. Statusübergänge: PENDINGRUNNINGDONE | FAILED | CANCELLED.

  3. HolenGET /query/result/{queryId}. 202 mit {queryId, status} während der Ausführung, 200 mit der vollständigen Records-Antwort (oder ?format=matrix) bei DONE.

  4. AbbrechenDELETE /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) oder OPENAI_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-ToolREST-Äquivalent
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 — alles, was MCP spricht — sieht nach der Authentifizierung alle elf Tools.

Ein typischer Agent-Loop

  1. list_ossie_models oder GET /models — ein Modell wählen.
  2. describe_ossie_model oder GET /schema/{c}/{m} — Datasets, Felder, Metriken, Beispiele lesen.
  3. Wenn der Nutzer einen Wert nennt, den das Schema nicht gesampelt hat — search_field_values oder GET /values/search — die Schreibweise bestätigen.
  4. Einen Query-Body aus examples.simpleGroupBy (oder einem anderen Beispiel) als Vorlage bauen und die Dimensionen und Metriken des Nutzers einsetzen.
  5. run_ossie_query oder POST /query — wenn die Antwort ein VALIDATION_ERROR ist, aus available wählen und erneut versuchen.
  6. Bei einer Chart-Anfrage → mit format: "matrix" wiederholen.
  7. Bei „explain the trend” → /anomaly oder /forecast.
  8. 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 zeigender 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