Przejdź do głównej zawartości

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/ask robi translację + wykonanie + ukształtowanie odpowiedzi w jednym wywołaniu, więc klient nie musi łączyć dwóch API.
  • Wbudowujesz powierzchnię typu czat — wielokrokowe pole history pozwala 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 Claude
saiku.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 dostawcy
saiku.ai.ask.model = claude-sonnet-4-7 | gpt-4o-mini | ...
saiku.ai.ask.endpoint = https://my.openai-compatible.host/v1/chat/completions
saiku.ai.ask.apiKey = sk-... # jawne nadpisanie zmiennej środowiskowej

Wartości domyślne dostawców:

DostawcaDomyślny modelEndpoint
anthropicclaude-sonnet-4-6https://api.anthropic.com/v1/messages
openaigpt-4o-minihttps://api.openai.com/v1/chat/completions

Docker

Okno terminala
docker run -d --name saiku \
-e ANTHROPIC_API_KEY=sk-ant-... \
-e JAVA_OPTS='-Dsaiku.ai.ask.provider=anthropic' \
ghcr.io/spiculedata/saiku:latest

Kubernetes

Użyj Secret do klucza, ConfigMap do nazwy dostawcy:

apiVersion: v1
kind: Secret
metadata:
name: saiku-ai
type: Opaque
stringData:
ANTHROPIC_API_KEY: sk-ant-...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: saiku
spec:
template:
spec:
containers:
- name: saiku
envFrom:
- secretRef:
name: saiku-ai
env:
- name: JAVA_OPTS
value: -Dsaiku.ai.ask.provider=anthropic

Endpoint

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

  • available z kandydatami — panel workspace renderuje je jako klikalne chipy, by użytkownik mógł samodzielnie skorygować.

generatedMdx to wygodne lustro response.metadata.generatedMdx.

Kody statusu

HTTPKiedy
200Translacja się udała; response niesie wykonany wynik (lub VALIDATION_ERROR do samokorekcji przez użytkownika).
200 + degraded:trueTranslacja zawiodła na warstwie dostawcy — błąd transportu, model odmówił lub błąd parsowania. reason niesie wyjaśnienie.
400Brakuje question lub cube w treści.
503 + degraded:trueDostawca to noop (nieskonfigurowany) — reason wyjaśnia, jak włączyć.

Przykłady

Domyślnie wyłączone — jasne sprzężenie zwrotne

Okno terminala
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

Okno terminala
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

Okno terminala
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."}
]
}
EOF

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

  • AiSchema aktywnej kostki — zserializowany JSON miar, wymiarów, hierarchii, poziomów i przykładowych członków.
  • JSON Schema AiQueryRequest jako input_schema narzę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/ask tł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.