API AI Ask
API AI Ask przyjmuje pytanie po ludzku, prosi skonfigurowanego dostawcę LLM o uzupełnienie typowanego zapytania względem aktualnej schemy kostki, przepuszcza je przez ten sam konwerter, którego używa typowane API AI Query, i zwraca wykonany wynik plus wygenerowany MDX plus strukturalne żądanie, które model wyemitował.
To warstwa języka naturalnego osadzona na typowanej powierzchni dla
agentów. Tam gdzie typowane API zakłada, że wywołujący posługuje się
strukturą AiQueryRequest, API ask pozwala człowiekowi powiedzieć
„pokaż sprzedaż wg krajów w zeszłym kwartale” i dostać w pełni
wykonany cellset razem z widoczną obok translacją modelu.
To napędza w workspace panel AI Query — przycisk ⚡ na pasku narzędzi otwiera panel czatu, gdzie użytkownicy wpisują pytania i klikają Edytuj na płótnie, by wrzucić wynik do aktywnej karty zapytania.
Kiedy używać
API ask to właściwa powierzchnia, gdy:
- Pytania będą zadawać użytkownicy końcowi — typowane API AI Query oczekuje agenta, który już zna schemę kostki; API ask chowa ten krok za LLM.
- Chcesz jeden round-trip na pytanie —
/ai/askrobi translację + wykonanie + ukształtowanie odpowiedzi w jednym wywołaniu, więc klient nie musi łączyć dwóch API. - Wbudowujesz powierzchnię typu czat — wielokrokowe pole
historypozwala pytaniom uzupełniającym rozwiązywać się względem wcześniejszych tur.
Kiedy nie używać:
- Agenci na serwerze MCP — agenci wolą typowaną powierzchnię, bo są już dobrzy w uzupełnianiu pól strukturalnych z JSON Schema. Warstwa ask jest dla ludzi.
- Autorstwo kostek z przykładowej hurtowni — użyj API inferencji AI, zaprojektowanego do przepływu na etapie projektowania.
Aktywacja
Dwie właściwości + jedna zmienna środowiskowa na dostawcę. Obie można ustawić na JVM lub w pliku properties wdrożenia konsumowanym przez Spring.
# Anthropic Claudesaiku.ai.ask.provider = anthropic# env ANTHROPIC_API_KEY = sk-ant-...
# OpenAI — lub dowolny host kompatybilny z OpenAI (Azure, vLLM, Ollama, Together)saiku.ai.ask.provider = openai# env OPENAI_API_KEY = sk-...
# Opcjonalne, obaj dostawcysaiku.ai.ask.model = claude-sonnet-4-7 | gpt-4o-mini | ...saiku.ai.ask.endpoint = https://my.openai-compatible.host/v1/chat/completionssaiku.ai.ask.apiKey = sk-... # jawne nadpisanie zmiennej środowiskowejWartości domyślne dostawców:
| Dostawca | Domyślny model | Endpoint |
|---|---|---|
anthropic | claude-sonnet-4-6 | https://api.anthropic.com/v1/messages |
openai | gpt-4o-mini | https://api.openai.com/v1/chat/completions |
Docker
docker run -d --name saiku \ -e ANTHROPIC_API_KEY=sk-ant-... \ -e JAVA_OPTS='-Dsaiku.ai.ask.provider=anthropic' \ ghcr.io/spiculedata/saiku:latestKubernetes
Użyj Secret do klucza, ConfigMap do nazwy dostawcy:
apiVersion: v1kind: Secretmetadata: name: saiku-aitype: OpaquestringData: ANTHROPIC_API_KEY: sk-ant-...---apiVersion: apps/v1kind: Deploymentmetadata: name: saikuspec: template: spec: containers: - name: saiku envFrom: - secretRef: name: saiku-ai env: - name: JAVA_OPTS value: -Dsaiku.ai.ask.provider=anthropicEndpoint
POST /saiku/api/ai/askŻądanie
{ "question": "show sales by country last quarter", "cube": { "connectionName": "foodmart", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales" }, "history": [ { "role": "user", "content": "earlier question" }, { "role": "assistant", "content": "earlier summary" } ]}history jest opcjonalne. Gdy je podasz, wcześniejsze tury
(user, assistant) są wysyłane z powrotem do modelu, dzięki czemu
pytania uzupełniające typu „a teraz rozbij to po regionach”
rozwiązują się względem wcześniejszego pytania. Prompty systemowe
są kontrolowane przez dostawcę — wywołujący nie mogą ich wstrzykiwać.
Odpowiedź
{ "degraded": false, "model": "claude-sonnet-4-6", "request": { /* strukturalne AiQueryRequest, które wyemitował model */ }, "response": { /* pełne AiQueryResponse — ten sam kształt co /ai/query */ }, "generatedMdx": "SELECT NON EMPTY ... FROM [Sales]"}request to strukturalne zapytanie, które wyprodukował model. Przekaż
je do POST /ai/query dosłownie, żeby je ponownie
wykonać, albo pokaż użytkownikowi jako „typowane zapytanie stojące
za Twoim pytaniem”.
response to wykonany wynik, ten sam kształt, który zwraca typowane
API AI Query. Jeśli model wyemitował żądanie, które schema odrzuca,
response.status jest VALIDATION_ERROR, a treść niesie pola field
availablez kandydatami — panel workspace renderuje je jako klikalne chipy, by użytkownik mógł samodzielnie skorygować.
generatedMdx to wygodne lustro response.metadata.generatedMdx.
Kody statusu
| HTTP | Kiedy |
|---|---|
200 | Translacja się udała; response niesie wykonany wynik (lub VALIDATION_ERROR do samokorekcji przez użytkownika). |
200 + degraded:true | Translacja zawiodła na warstwie dostawcy — błąd transportu, model odmówił lub błąd parsowania. reason niesie wyjaśnienie. |
400 | Brakuje question lub cube w treści. |
503 + degraded:true | Dostawca to noop (nieskonfigurowany) — reason wyjaśnia, jak włączyć. |
Przykłady
Domyślnie wyłączone — jasne sprzężenie zwrotne
curl -sS -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/ask \ -d '{"question":"show sales by country","cube":{"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"}}'
# {# "degraded": true,# "reason": "AI ask is not configured. Set saiku.ai.ask.provider..."# }Ścieżka szczęśliwa — pełny round-trip
curl -sS -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/ask \ -d '{"question":"show sales by country","cube":{"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"}}' \ | jq '{model, generatedMdx, rows: .response.totalRows}'
# {# "model": "claude-sonnet-4-6",# "generatedMdx": "SELECT NON EMPTY {[Measures].[Store Sales]} ON COLUMNS, NON EMPTY {[Customers].[Country].Members} ON ROWS FROM [Sales]",# "rows": 3# }Uzupełnienie — wiele tur
curl -sS -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/ask \ -d @- <<'EOF'{ "question": "now break it down by quarter", "cube": {"connectionName":"foodmart","catalog":"FoodMart","schema":"FoodMart","cubeName":"Sales"}, "history": [ {"role":"user","content":"show sales by country"}, {"role":"assistant","content":"3 rows returned."} ]}EOFModel widzi poprzednią turę i rozwiązuje „by quarter” jako także skrzyżowane z krajem — ten sam kształt co pierwsza odpowiedź, z jedną dodatkową hierarchią.
Co model widzi, a czego nie
Warstwa ask wysyła do modelu tylko:
AiSchemaaktywnej kostki — zserializowany JSON miar, wymiarów, hierarchii, poziomów i przykładowych członków.- JSON Schema
AiQueryRequestjakoinput_schemanarzędzia strukturalnego wyjścia. - Pytanie użytkownika i ewentualną historię rozmowy, którą dołączył wywołujący.
Model jest zmuszany przez tool_choice do wywołania narzędzia
emit_query z prawidłowym kształtem AiQueryRequest. Surowe odpowiedzi
prozą są odrzucane jako zdegradowane.
Nigdy nie widzi:
- Innych kostek, do których ma dostęp użytkownik — tylko tej nazwanej w żądaniu.
- Bazowego SQL ani poświadczeń hurtowni.
- Historii rozmów innych użytkowników.
- Żądania HTTP, ciasteczek sesyjnych ani żadnego nagłówka, który wysłał wywołujący.
To ten sam model izolacji, którego używają serwer MCP i typowane API AI Query, zastosowany jedną warstwę wcześniej.
Co dalej
- API inferencji AI — typowana powierzchnia,
w którą
/ai/asktłumaczy. Przydatna, gdy chcesz, by agent (a nie człowiek) autorsował żądanie. - Serwer MCP — ta sama typowana powierzchnia wystawiona jako narzędzia MCP dla agentów Claude Desktop / Cursor / Cline.
- Uwierzytelnianie — tokeny Bearer, limity szybkości, kształty błędów.