Przejdź do głównej zawartości

AI Query API — modele Ossie

AI Query API dla Ossie to semantyczno-YAML-owy odpowiednik AI Query API dla kostek OLAP. Ta sama dyscyplina: agenci pobierają samoopisującą się schemę, wypełniają żądanie JSON względem niej, serwer waliduje każdą nazwę, wykonuje zapytanie i zwraca typowane rekordy. Te same zabezpieczenia: agenci nigdy nie piszą SQL bezpośrednio.

Podczas gdy AI API dla OLAP działa na kostkach Mondrian, to działa na modelach zadeklarowanych w Open Semantic Interchange / Apache Ossie YAML. Wszystko, co robi workbench — filtry, sortowania, nadpisania agregacji, pivot crosstab, widok wykresu — jest dostępne programowo poprzez to API, plus warstwa /ask w języku naturalnym oraz analityka per-zapytanie (wykrywanie anomalii, prognozowanie).

Szybka orientacja

Trzy endpointy pokrywają ~90% zastosowań agentów:

EndpointCel
GET /rest/saiku/api/ai/ossie/modelsWylistuj każdy model Ossie, który wywołujący może odpytać.
GET /rest/saiku/api/ai/ossie/schema/{connection}/{model}Samoopisująca się schema — zbiory danych, pola, metryki, relacje, JSON Schema ciała żądania, gotowe przykładowe ciała.
POST /rest/saiku/api/ai/ossie/queryWykonaj typowane żądanie stanu półek (shelf-state). Domyślnie odpowiedź w formacie rekordów; macierz na ?format=matrix.

Endpointy z długiego ogona:

EndpointCel
POST /rest/saiku/api/ai/ossie/query/previewSkompiluj do SQL bez wykonywania. Ten sam kształt VALIDATION_ERROR co /query.
GET /rest/saiku/api/ai/ossie/values/searchWyszukiwanie podłańcuchowe distinct wartości w polu.
POST /rest/saiku/api/ai/ossie/query/execute-asyncZgłoś do wykonania w tle.
GET /rest/saiku/api/ai/ossie/query/status/{queryId}Odpytuj status.
GET /rest/saiku/api/ai/ossie/query/result/{queryId}Pobierz ukończony wynik asynchroniczny.
DELETE /rest/saiku/api/ai/ossie/query/{queryId}Anuluj zapytanie w trakcie.
POST /rest/saiku/api/ai/ossie/row-detailOdpowiednik drillthrough w Ossie — ponownie uruchamia półkę jako surowe wiersze.
POST /rest/saiku/api/ai/ossie/anomalyUruchamia zapytanie, po czym oznacza anomalie wzdłuż osi czasu.
POST /rest/saiku/api/ai/ossie/forecastRzutuje przyszłe punkty przy użyciu ETS / ARIMA / Prophet.
POST /rest/saiku/api/ai/ossie/askZapytanie w języku naturalnym. Wymaga klucza LLM.
GET /rest/saiku/api/ai/ossie/ask/healthCzy warstwa ask jest skonfigurowana na tej instancji.

Wszystkie endpointy wymagają uwierzytelnionej sesji; endpointy POST wymagają pary cookie/nagłówek CSRF. Ten sam model uwierzytelniania co w AI API dla OLAP.

Krok 1 — wylistuj modele

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
}
]

Para connectionName + modelName to identyfikator używany wszędzie indziej.

Krok 2 — pobierz schemę

GET /rest/saiku/api/ai/ossie/schema/unknown_TPCDS/TPCDS

Dodaj ?refresh=true, aby ominąć cache przykładowych wartości (domyślny TTL to 5 minut; nadpisz przez SAIKU_AI_OSSIE_SAMPLES_TTL_MINUTES).

Odpowiedź jest z założenia gęsta — to właśnie czyni to API samoopisującym się:

{
"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"}]
}
}
}
}

Godne uwagi udogodnienia:

  • label na każdym polu to czytelna dla człowieka nazwa ze specyfikacji Ossie — pojawia się wszędzie, gdzie renderują się kolumny. NETREVENUE pokazuje się jako „Net Revenue”.
  • sampleValues daje agentowi realne wartości do filtrowania. Żadnego halucynowania „US”, gdy faktyczną wartością jest „United States”.
  • cardinality to wskazówki (low / medium / medium-high / high) — z estimatedDistinct, gdy hurtownia obsługuje APPROX_COUNT_DISTINCT. Agenci mogą zdecydować, czy „filtruj po tej kolumnie” jest realistyczne.
  • supportedOverrides — zbiór nadpisań agregacji, które translator faktycznie przepisze. Metryki COUNT(*) akceptują tylko COUNT (ujawnione przez zestaw fuzz).
  • examples — gotowe do skopiowania ciała żądań dla typowych kształtów (simpleGroupBy, crosstab, topN).

Krok 3 — wykonaj zapytanie

POST /rest/saiku/api/ai/ossie/query
Content-Type: application/json

Żądanie:

{
"connection": "unknown_TPCDS",
"model": "TPCDS",
"rows": [{"dataset": "customer", "field": "c_state"}],
"values": [{"metric": "total_sales"}],
"sorts": [{"metric": "total_sales", "direction": "DESC"}],
"limit": 5
}

Odpowiedź (format rekordów, domyślny):

{
"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
}
}

Dodaj ?format=matrix, aby otrzymać wyjście cellSetHeaders + cellSetBody indeksowane pozycyjnie — kształt, który endpoint rekordów OLAP zwraca dla konsumentów downstream już go obsługujących.

Operatory filtrów

EQ, NEQ, LT, LTE, GT, GTE, IN, BETWEEN, IS_NULL, IS_NOT_NULL. Operatory jednowartościowe używają value; IN / BETWEEN używają values. Puste IN syntetyzuje trywialnie-fałszywy predykat (zwraca zero wierszy bez błędu parsowania).

{
"filters": [
{"dataset": "customer", "field": "c_state", "op": "IN",
"values": ["CA", "NY", "TX"]},
{"dataset": "store_sales", "field": "SS_SALES_PRICE", "op": "GT",
"value": "50"}
]
}

Nadpisania agregacji w locie

Podmień zadeklarowaną zewnętrzną agregację metryki przez values[i].aggregation. Serwer waliduje względem supportedOverrides metryki:

// 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"]
}

Tłumienie k-anonimowości

Skonfiguruj na serwerze przez SAIKU_AI_KANONYMITY_K (domyślnie 5) i SAIKU_AI_KANONYMITY_MASK (domyślnie null). Gdy włączone, wiersze, których metryka o kształcie liczności spada poniżej progu, mają swoje komórki metryk zamaskowane, a blok najwyższego poziomu meta.suppressed zapisuje liczbę:

{
"records": [
{"customer.c_state": "MA", "transaction_count": {"formatted": "null"}}
],
"meta": {
"rowCount": 4,
"suppressed": {"count": 4, "reason": "k-anonymity threshold k=5"}
}
}

Dotyczy wyjścia rekordów + macierzy oraz pochodnych endpointów /ai/anomaly i /ai/forecast — wiersze stłumione są maskowane, zanim zobaczy je scorer anomalii lub prognozer, więc wartość małej kohorty nie może wyciec z powrotem przez adnotację.

Redakcja PII

Pola oznaczone pii: true w YAML-u Ossie są całkowicie usuwane z widoku schemy. Wywołujący nie mogą się do nich odwoływać; zapytanie, które to robi, zwraca VALIDATION_ERROR nazywające pole jako „nieznane”.

Krok 4 — jak API uczy agenta

Każda błędna nazwa wraca z listą kandydatów. Kolejna próba agenta wybiera jedną:

// 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"]
}

Ten sam kształt pokrywa nieznane pola, nieznane metryki, nieobsługiwane operatory filtrów, BETWEEN z mniej niż dwoma wartościami, referencje sortowania nazywające zarówno metric jak i field, limit ≤ 0, puste rows/columns/values oraz timeAxis w /anomaly + /forecast, który nie występuje w zapytaniu.

Krok 5 — podgląd

POST /rest/saiku/api/ai/ossie/query/preview

To samo ciało co /query. Odpowiedź:

{
"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\""
}

Używa tego samego translatora, który uruchamia executor — widzisz 1:1, co /query by wysłał.

Krok 6 — wyszukiwanie wartości

GET /rest/saiku/api/ai/ossie/values/search?connection=unknown_TPCDS&dataset=customer&field=C_STATE&q=CA
{
"matches": ["CA"]
}

Uruchamia SELECT DISTINCT ... WHERE UPPER(CAST(... AS VARCHAR)) LIKE '%...%' LIMIT n. Pomiń q, aby otrzymać pierwsze N distinct wartości.

Krok 7 — szczegóły wiersza (drillthrough)

POST /rest/saiku/api/ai/ossie/row-detail?maxrows=5

To samo ciało co /query. Serwer ponownie uruchamia półkę z values=[], więc executor emituje surowe wiersze zamiast agregatu. Odpowiedź jest w formacie rekordów z meta.truncated: true, gdy zostaje osiągnięty limit wierszy (domyślnie 100, maks. 10 000).

Krok 8 — asynchronicznie

Dla zapytań, po których spodziewasz się więcej niż kilku sekund:

  1. ZgłośPOST /query/execute-async. To samo ciało co /query. Odpowiedź 202: {"queryId": "...", "status": "PENDING"}.

  2. OdpytujGET /query/status/{queryId}. Przejścia statusu: PENDINGRUNNINGDONE | FAILED | CANCELLED.

  3. PobierzGET /query/result/{queryId}. 202 z {queryId, status} w trakcie działania, 200 z pełną odpowiedzią rekordów (lub ?format=matrix) przy DONE.

  4. AnulujDELETE /query/{queryId}.

Krok 9 — analityka

Wykrywanie anomalii

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
}

Detektory: zscore (klasyczny z-score, sigmy od średniej), mad (mediana odchyleń bezwzględnych, odporna na wartości odstające), stl (dekompozycja sezonowo-trendowa; wraca do zscore dla danych niesezonowych).

Odpowiedź jest w kształcie rekordów z komórką metryki adnotowaną anomalią tam, gdzie detektor oznaczył punkt:

{
"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}
}

Prognoza

POST /rest/saiku/api/ai/ossie/forecast
{
"query": { /* ... */ },
"timeAxis": "date_dim.d_month",
"method": "ets",
"horizon": 3,
"interval": 0.95
}

Prognozery: ets, arima, prophet. Historyczne rekordy nietknięte; projekcje lądują pod blokiem najwyższego poziomu forecast z kluczem metryki:

{
"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}
]
}
}
}

Krok 10 — zapytanie w języku naturalnym

GET /rest/saiku/api/ai/ossie/ask/health
{"configured": true, "provider": "anthropic (claude-sonnet-4-6)"}

Włącz na serwerze:

  • saiku.ai.ask.provider = anthropic | openai
  • env ANTHROPIC_API_KEY (Anthropic) lub OPENAI_API_KEY (OpenAI)
  • opcjonalnie saiku.ai.ask.model — nadpisanie id modelu
  • opcjonalnie saiku.ai.ask.endpoint — niestandardowy bazowy URL dla proxy zgodnych z OpenAI (vLLM, Ollama, Together)

Następnie:

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 jest opcjonalne — każda tura jest przekazywana do LLM, aby mógł rozwiązać kontynuacje w rodzaju „a co według stanu?”. Odpowiedź:

{
"question": "...",
"connection": "unknown_TPCDS",
"model": "TPCDS",
"queryUsed": { /* the OssieAiQueryRequest the LLM produced */ },
"response": { /* the full records-format execution result */ }
}

LLM jest zmuszany do ustrukturyzowanego wyjścia przez tool_use (Anthropic) / tool_choice: function (OpenAI), którego schema odzwierciedla OssieAiQueryRequest. Pytania nie na temat wracają jako koperta OFF_TOPIC ujawniona jako 400 z dołączonym powodem.

Integracja MCP

Pięć narzędzi Ossie jest udostępnianych przez endpoint MCP obok sześciu narzędzi OLAP:

Narzędzie MCPOdpowiednik 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 — cokolwiek mówi w MCP — widzi wszystkie jedenaście narzędzi po uwierzytelnieniu.

Typowa pętla agenta

  1. list_ossie_models lub GET /models — wybierz model.
  2. describe_ossie_model lub GET /schema/{c}/{m} — odczytaj zbiory danych, pola, metryki, przykłady.
  3. Jeśli użytkownik nazywa wartość, której schema nie spróbkowała — search_field_values lub GET /values/search — potwierdź pisownię.
  4. Zbuduj ciało zapytania z examples.simpleGroupBy (lub innego przykładu) jako szablonu, podstawiając wymiary i metryki użytkownika.
  5. run_ossie_query lub POST /query — jeśli odpowiedź to VALIDATION_ERROR, wybierz z available i spróbuj ponownie.
  6. Na żądanie wykresu → powtórz z format: "matrix".
  7. Na „wyjaśnij trend” → /anomaly lub /forecast.
  8. Na „pokaż mi wiersze źródłowe” → /row-detail.

Lub dla przepływów w języku naturalnym: pomiń 4–7 i użyj /ask.

Skąd biorą się modele

  • Napisz własny OSI YAML — zadeklaruj zbiory danych, metryki, relacje. Wskaż Saiku na niego przez plik datasource .sds.
  • Wskaż na istniejący projekt dbtprzewodnik po podpięciu dbt przeprowadza przez ~200-liniowy konwerter, który dostarczamy, czytający MetricFlow YAML i emitujący YAML zgodny z OSI.
  • Wyeksportuj schemę Mondrian — CLI saiku ossie-export w launcherze Saiku konwertuje schemę Mondrian XML do OSI YAML.

Zobacz też