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:
| Endpoint | Cel |
|---|---|
GET /rest/saiku/api/ai/ossie/models | Wylistuj 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/query | Wykonaj typowane żądanie stanu półek (shelf-state). Domyślnie odpowiedź w formacie rekordów; macierz na ?format=matrix. |
Endpointy z długiego ogona:
| Endpoint | Cel |
|---|---|
POST /rest/saiku/api/ai/ossie/query/preview | Skompiluj do SQL bez wykonywania. Ten sam kształt VALIDATION_ERROR co /query. |
GET /rest/saiku/api/ai/ossie/values/search | Wyszukiwanie podłańcuchowe distinct wartości w polu. |
POST /rest/saiku/api/ai/ossie/query/execute-async | Zgł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-detail | Odpowiednik drillthrough w Ossie — ponownie uruchamia półkę jako surowe wiersze. |
POST /rest/saiku/api/ai/ossie/anomaly | Uruchamia zapytanie, po czym oznacza anomalie wzdłuż osi czasu. |
POST /rest/saiku/api/ai/ossie/forecast | Rzutuje przyszłe punkty przy użyciu ETS / ARIMA / Prophet. |
POST /rest/saiku/api/ai/ossie/ask | Zapytanie w języku naturalnym. Wymaga klucza LLM. |
GET /rest/saiku/api/ai/ossie/ask/health | Czy 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/TPCDSDodaj ?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:
labelna każdym polu to czytelna dla człowieka nazwa ze specyfikacji Ossie — pojawia się wszędzie, gdzie renderują się kolumny.NETREVENUEpokazuje się jako „Net Revenue”.sampleValuesdaje agentowi realne wartości do filtrowania. Żadnego halucynowania „US”, gdy faktyczną wartością jest „United States”.cardinalityto wskazówki (low/medium/medium-high/high) — zestimatedDistinct, gdy hurtownia obsługujeAPPROX_COUNT_DISTINCT. Agenci mogą zdecydować, czy „filtruj po tej kolumnie” jest realistyczne.supportedOverrides— zbiór nadpisań agregacji, które translator faktycznie przepisze. MetrykiCOUNT(*)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/queryContent-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/previewTo 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=5To 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:
-
Zgłoś —
POST /query/execute-async. To samo ciało co/query. Odpowiedź 202:{"queryId": "...", "status": "PENDING"}. -
Odpytuj —
GET /query/status/{queryId}. Przejścia statusu:PENDING→RUNNING→DONE|FAILED|CANCELLED. -
Pobierz —
GET /query/result/{queryId}. 202 z{queryId, status}w trakcie działania, 200 z pełną odpowiedzią rekordów (lub?format=matrix) przy DONE. -
Anuluj —
DELETE /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) lubOPENAI_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 MCP | Odpowiednik 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 — cokolwiek mówi w MCP — widzi wszystkie jedenaście narzędzi po uwierzytelnieniu.
Typowa pętla agenta
list_ossie_modelslubGET /models— wybierz model.describe_ossie_modellubGET /schema/{c}/{m}— odczytaj zbiory danych, pola, metryki, przykłady.- Jeśli użytkownik nazywa wartość, której schema nie spróbkowała —
search_field_valueslubGET /values/search— potwierdź pisownię. - Zbuduj ciało zapytania z
examples.simpleGroupBy(lub innego przykładu) jako szablonu, podstawiając wymiary i metryki użytkownika. run_ossie_querylubPOST /query— jeśli odpowiedź toVALIDATION_ERROR, wybierz zavailablei spróbuj ponownie.- Na żądanie wykresu → powtórz z
format: "matrix". - Na „wyjaśnij trend” →
/anomalylub/forecast. - 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 dbt — przewodnik 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-exportw launcherze Saiku konwertuje schemę Mondrian XML do OSI YAML.
Zobacz też
- Ossie / OSI na Apache — specyfikacja + przykładowe YAML-e
- Serwer MCP — wrapper narzędzi dla agentów LLM
- Podpięcie dbt — wskaż Saiku na swój istniejący projekt dbt
- OLAP AI Query API — odpowiednik tego API dla kostek Mondrian