AI Query API — modelos Ossie
A AI Query API para Ossie é a contraparte em YAML semântico da AI Query API para cubos OLAP. Mesma disciplina: agentes buscam um schema autodescritivo, preenchem uma requisição JSON contra ele, o servidor valida cada nome, executa e retorna registros tipados. Mesmas proteções: agentes nunca escrevem SQL diretamente.
Onde a AI API OLAP trabalha contra cubos Mondrian, esta trabalha
contra modelos declarados em
Open Semantic Interchange /
Apache Ossie YAML. Tudo o que o
workbench faz — filtros, ordenações, sobrescritas de agregação, pivô
crosstab, visão de gráfico — está disponível programaticamente através
desta API, mais uma camada de linguagem natural /ask e analytics
por-consulta (detecção de anomalias, forecasting).
Orientação rápida
Três endpoints cobrem ~90% do uso por agentes:
| Endpoint | Propósito |
|---|---|
GET /rest/saiku/api/ai/ossie/models | Lista todo modelo Ossie que o chamador pode consultar. |
GET /rest/saiku/api/ai/ossie/schema/{connection}/{model} | Schema autodescritivo — datasets, fields, metrics, relationships, JSON Schema do corpo da requisição, corpos de exemplo prontos. |
POST /rest/saiku/api/ai/ossie/query | Executa uma requisição de estado de shelf tipada. Resposta em formato records por default; matrix em ?format=matrix. |
Endpoints de cauda longa:
| Endpoint | Propósito |
|---|---|
POST /rest/saiku/api/ai/ossie/query/preview | Compila para SQL sem executar. Mesmo formato VALIDATION_ERROR do /query. |
GET /rest/saiku/api/ai/ossie/values/search | Lookup por substring de valores distintos em um field. |
POST /rest/saiku/api/ai/ossie/query/execute-async | Submete para execução em background. |
GET /rest/saiku/api/ai/ossie/query/status/{queryId} | Consulta o status. |
GET /rest/saiku/api/ai/ossie/query/result/{queryId} | Busca um resultado assíncrono concluído. |
DELETE /rest/saiku/api/ai/ossie/query/{queryId} | Cancela uma query em andamento. |
POST /rest/saiku/api/ai/ossie/row-detail | Análogo do drillthrough do Ossie — re-executa o shelf como linhas cruas. |
POST /rest/saiku/api/ai/ossie/anomaly | Executa a query e então sinaliza anomalias ao longo de um eixo temporal. |
POST /rest/saiku/api/ai/ossie/forecast | Projeta pontos futuros usando ETS / ARIMA / Prophet. |
POST /rest/saiku/api/ai/ossie/ask | Pergunta em linguagem natural. Precisa de uma chave de LLM. |
GET /rest/saiku/api/ai/ossie/ask/health | Se a camada ask está configurada nesta instância. |
Todos os endpoints exigem uma sessão autenticada; endpoints POST exigem o par cookie/header de CSRF. Mesmo modelo de auth da AI API OLAP.
Passo 1 — listar os modelos
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 }]O par connectionName + modelName é o identificador usado em todos
os outros lugares.
Passo 2 — buscar o schema
GET /rest/saiku/api/ai/ossie/schema/unknown_TPCDS/TPCDSAdicione ?refresh=true para ignorar o cache de sample-value (o TTL
default é 5 minutos; sobrescreva via SAIKU_AI_OSSIE_SAMPLES_TTL_MINUTES).
A resposta é densa por design — é isso que torna a API autodescritiva:
{ "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"}] } } }}Recursos notáveis:
labelem cada field é o nome legível da spec Ossie — aparece em todos os lugares onde colunas renderizam.NETREVENUEaparece como “Net Revenue”.sampleValuesdá ao agente valores reais para filtrar. Sem alucinar “US” quando o valor real é “United States”.cardinalitydá dicas (low/medium/medium-high/high) — comestimatedDistinctquando o warehouse suportaAPPROX_COUNT_DISTINCT. Agentes podem decidir se “filtrar por esta coluna” é realista.supportedOverrides— o conjunto de sobrescritas de agregação que o tradutor realmente vai reescrever. MétricasCOUNT(*)só aceitam COUNT (revelado pela suíte de fuzz).examples— corpos de requisição copy-paste para os formatos comuns (simpleGroupBy,crosstab,topN).
Passo 3 — executar uma query
POST /rest/saiku/api/ai/ossie/queryContent-Type: application/jsonRequisição:
{ "connection": "unknown_TPCDS", "model": "TPCDS", "rows": [{"dataset": "customer", "field": "c_state"}], "values": [{"metric": "total_sales"}], "sorts": [{"metric": "total_sales", "direction": "DESC"}], "limit": 5}Resposta (formato records, o default):
{ "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 }}Adicione ?format=matrix para saída indexada por posição
cellSetHeaders + cellSetBody — o formato que o endpoint records
OLAP retorna para consumidores downstream que já o tratam.
Operadores de filtro
EQ, NEQ, LT, LTE, GT, GTE, IN, BETWEEN, IS_NULL,
IS_NOT_NULL. Operadores de valor único usam value; IN / BETWEEN
usam values. Um IN vazio sintetiza o predicado trivialmente-falso
(retorna zero linhas sem erro de parse).
{ "filters": [ {"dataset": "customer", "field": "c_state", "op": "IN", "values": ["CA", "NY", "TX"]}, {"dataset": "store_sales", "field": "SS_SALES_PRICE", "op": "GT", "value": "50"} ]}Sobrescritas de agregação na hora
Troque a agregação externa declarada de uma métrica via
values[i].aggregation. O servidor valida contra os
supportedOverrides da métrica:
// 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"]}Supressão por k-anonimato
Configure no servidor via SAIKU_AI_KANONYMITY_K (default 5) e
SAIKU_AI_KANONYMITY_MASK (default null). Quando habilitado, linhas
cuja métrica em formato de contagem cai abaixo do limiar têm suas
células de métrica mascaradas, e um bloco top-level meta.suppressed
registra a contagem:
{ "records": [ {"customer.c_state": "MA", "transaction_count": {"formatted": "null"}} ], "meta": { "rowCount": 4, "suppressed": {"count": 4, "reason": "k-anonymity threshold k=5"} }}Aplica-se à saída records + matrix, e aos endpoints derivados
/ai/anomaly e /ai/forecast — linhas suprimidas são mascaradas
antes que o scorer de anomalias ou o forecaster as veja, então um
valor de cohort pequeno não pode vazar de volta através de uma
anotação.
Redação de PII
Fields marcados pii: true no YAML Ossie são removidos inteiramente
da visão do schema. Chamadores não podem referenciá-los; uma query que
o faz retorna VALIDATION_ERROR nomeando o field como “unknown”.
Passo 4 — como a API ensina o agente
Todo nome ruim volta com uma lista de candidatos. A próxima tentativa do agente escolhe um:
// 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"]}O mesmo formato cobre fields desconhecidos, metrics desconhecidas,
operadores de filtro não suportados, BETWEEN com menos de dois
valores, refs de sort que nomeiam tanto metric quanto field, limit
≤ 0, rows/columns/values vazios, e timeAxis em /anomaly +
/forecast que não aparece na query.
Passo 5 — preview
POST /rest/saiku/api/ai/ossie/query/previewMesmo corpo do /query. Resposta:
{ "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\""}Usa o mesmo tradutor que o executor roda — você vê 1:1 o que o
/query despacharia.
Passo 6 — busca de valores
GET /rest/saiku/api/ai/ossie/values/search?connection=unknown_TPCDS&dataset=customer&field=C_STATE&q=CA{ "matches": ["CA"]}Roda SELECT DISTINCT ... WHERE UPPER(CAST(... AS VARCHAR)) LIKE '%...%' LIMIT n. Omita q para os primeiros N valores distintos.
Passo 7 — detalhe de linha (drillthrough)
POST /rest/saiku/api/ai/ossie/row-detail?maxrows=5Mesmo corpo do /query. O servidor re-executa o shelf com values=[]
para que o executor emita linhas cruas em vez de um agregado. A
resposta é em formato records com meta.truncated: true quando o
limite de linhas é atingido (default 100, máximo 10 000).
Passo 8 — assíncrono
Para queries que você espera levar mais que alguns segundos:
-
Submeta —
POST /query/execute-async. Mesmo corpo do/query. Resposta 202:{"queryId": "...", "status": "PENDING"}. -
Consulte —
GET /query/status/{queryId}. Transições de status:PENDING→RUNNING→DONE|FAILED|CANCELLED. -
Busque —
GET /query/result/{queryId}. 202 com{queryId, status}enquanto roda, 200 com a resposta records completa (ou?format=matrix) em DONE. -
Cancele —
DELETE /query/{queryId}.
Passo 9 — analytics
Detecção de anomalias
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}Detectores: zscore (z-score clássico, sigmas a partir da média),
mad (desvio absoluto mediano, robusto a outliers), stl
(decomposição sazonal-tendência; recai em zscore para dados não
sazonais).
A resposta é o formato records com uma célula de métrica anotada com anomalia onde o detector sinalizou um ponto:
{ "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}Forecasters: ets, arima, prophet. Registros históricos
intocados; projeções caem sob um bloco top-level forecast indexado
por métrica:
{ "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} ] } }}Passo 10 — pergunta em linguagem natural
GET /rest/saiku/api/ai/ossie/ask/health{"configured": true, "provider": "anthropic (claude-sonnet-4-6)"}Habilite no servidor:
saiku.ai.ask.provider=anthropic|openai- env
ANTHROPIC_API_KEY(Anthropic) ouOPENAI_API_KEY(OpenAI) - opcional
saiku.ai.ask.model— sobrescrita do model id - opcional
saiku.ai.ask.endpoint— base URL customizada para proxies compatíveis com OpenAI (vLLM, Ollama, Together)
Depois:
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 é opcional — cada turno é passado ao LLM para que ele possa
resolver follow-ups como “what about by state?”. Resposta:
{ "question": "...", "connection": "unknown_TPCDS", "model": "TPCDS", "queryUsed": { /* the OssieAiQueryRequest the LLM produced */ }, "response": { /* the full records-format execution result */ }}O LLM é forçado a produzir saída estruturada via tool_use (Anthropic)
/ tool_choice: function (OpenAI) cujo schema espelha
OssieAiQueryRequest. Perguntas fora de tópico voltam como um
envelope OFF_TOPIC exposto como 400 com o motivo anexado.
Integração MCP
As cinco ferramentas Ossie são expostas via o endpoint MCP junto às seis ferramentas OLAP:
| Ferramenta MCP | Equivalente 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 — qualquer coisa que fale MCP — vê todas as onze ferramentas assim que autenticada.
Um loop típico de agente
list_ossie_modelsouGET /models— escolha um modelo.describe_ossie_modelouGET /schema/{c}/{m}— leia datasets, fields, metrics, examples.- Se o usuário nomeia um valor que o schema não amostrou —
search_field_valuesouGET /values/search— confirme a grafia. - Construa um corpo de query a partir de
examples.simpleGroupBy(ou outro exemplo) como template, substituindo as dimensões e métricas do usuário. run_ossie_queryouPOST /query— se a resposta for umVALIDATION_ERROR, escolha deavailablee tente de novo.- Em um pedido de gráfico → repita com
format: "matrix". - Em “explain the trend” →
/anomalyou/forecast. - Em “show me the underlying rows” →
/row-detail.
Ou para fluxos de linguagem natural: pule 4–7 e use /ask.
De onde vêm os modelos
- Escreva seu próprio YAML OSI — declare datasets, metrics,
relationships. Aponte o Saiku para ele via um arquivo de datasource
.sds. - Aponte para um projeto dbt existente — o guia de hookup do dbt percorre o conversor de ~200 linhas que distribuímos, que lê YAML MetricFlow e emite YAML OSI-compliant.
- Exporte um schema Mondrian — o CLI
saiku ossie-exportno launcher do Saiku converte um schema XML Mondrian em YAML OSI.
Veja também
- Ossie / OSI no Apache — a spec + YAMLs de exemplo
- Servidor MCP — o wrapper de ferramentas para agentes LLM
- Hookup do dbt — aponte o Saiku para seu projeto dbt existente
- OLAP AI Query API — a contraparte em cubo Mondrian desta API