Pular para o conteúdo

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:

EndpointPropósito
GET /rest/saiku/api/ai/ossie/modelsLista 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/queryExecuta uma requisição de estado de shelf tipada. Resposta em formato records por default; matrix em ?format=matrix.

Endpoints de cauda longa:

EndpointPropósito
POST /rest/saiku/api/ai/ossie/query/previewCompila para SQL sem executar. Mesmo formato VALIDATION_ERROR do /query.
GET /rest/saiku/api/ai/ossie/values/searchLookup por substring de valores distintos em um field.
POST /rest/saiku/api/ai/ossie/query/execute-asyncSubmete 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-detailAnálogo do drillthrough do Ossie — re-executa o shelf como linhas cruas.
POST /rest/saiku/api/ai/ossie/anomalyExecuta a query e então sinaliza anomalias ao longo de um eixo temporal.
POST /rest/saiku/api/ai/ossie/forecastProjeta pontos futuros usando ETS / ARIMA / Prophet.
POST /rest/saiku/api/ai/ossie/askPergunta em linguagem natural. Precisa de uma chave de LLM.
GET /rest/saiku/api/ai/ossie/ask/healthSe 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/TPCDS

Adicione ?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:

  • label em cada field é o nome legível da spec Ossie — aparece em todos os lugares onde colunas renderizam. NETREVENUE aparece como “Net Revenue”.
  • sampleValues dá ao agente valores reais para filtrar. Sem alucinar “US” quando o valor real é “United States”.
  • cardinality dá dicas (low / medium / medium-high / high) — com estimatedDistinct quando o warehouse suporta APPROX_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étricas COUNT(*) 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/query
Content-Type: application/json

Requisiçã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/preview

Mesmo 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=5

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

  1. SubmetaPOST /query/execute-async. Mesmo corpo do /query. Resposta 202: {"queryId": "...", "status": "PENDING"}.

  2. ConsulteGET /query/status/{queryId}. Transições de status: PENDINGRUNNINGDONE | FAILED | CANCELLED.

  3. BusqueGET /query/result/{queryId}. 202 com {queryId, status} enquanto roda, 200 com a resposta records completa (ou ?format=matrix) em DONE.

  4. CanceleDELETE /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) ou OPENAI_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 MCPEquivalente 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 — qualquer coisa que fale MCP — vê todas as onze ferramentas assim que autenticada.

Um loop típico de agente

  1. list_ossie_models ou GET /models — escolha um modelo.
  2. describe_ossie_model ou GET /schema/{c}/{m} — leia datasets, fields, metrics, examples.
  3. Se o usuário nomeia um valor que o schema não amostrou — search_field_values ou GET /values/search — confirme a grafia.
  4. 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.
  5. run_ossie_query ou POST /query — se a resposta for um VALIDATION_ERROR, escolha de available e tente de novo.
  6. Em um pedido de gráfico → repita com format: "matrix".
  7. Em “explain the trend” → /anomaly ou /forecast.
  8. 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 existenteo 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-export no launcher do Saiku converte um schema XML Mondrian em YAML OSI.

Veja também