Agent Spaces
Agent Spaces são personas nomeadas, autoradas por admin, que escopam um ask de AI. Onde Agent Skills codificam workflows individuais, um space codifica um ponto de vista: system prompt, allowlist de cubos, allowlist de skills, prompts sugeridos — aplicados server-side para que, não importa o que o chamador envie, o LLM veja a voz da persona + os cubos da persona + as skills da persona.
Persistido como JSON sob saiku-home/agent-spaces/. O launcher
escaneia preguiçosamente por assinatura de mtime (mesmo modelo das
skills — sem thread watcher).
Distribuído no saiku v4.7 como saiku#1440.
O que um space aplica
Formato de arquivo
{ "id": "foodmart-sales-analyst", "name": "FoodMart Sales Analyst", "description": "Weekly and monthly sales rollups over the FoodMart Sales cube.", "systemPrompt": "You are the FoodMart Sales Analyst. Prefer weekly and monthly time grain unless the user asks otherwise. Lead with the top three lines by absolute value. Flag any figure that swings by more than 20% versus the prior period. Be analytical, brief, and numbers-first.", "cubeAllowlist": [ {"connectionName": "unknown_foodmart", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales"} ], "skillAllowlist": ["weekly-foodmart-rollup"], "suggestedPrompts": [ "How did Store Sales track last week vs the prior week?", "Break down Store Sales by Product Family for Q4.", "/weekly-foodmart-rollup" ]}| Campo | Obrigatório | Tipo | Notas |
|---|---|---|---|
id | sim | string | kebab-case, [a-z][a-z0-9-]{0,63}. Segmento de path em /ai/spaces/{id}/ask. |
name | sim | string | Nome de exibição para o catálogo e a sidebar. |
description | não | string | Resumo de uma linha mostrado no seletor de space. |
systemPrompt | não | string | Prependado ao SYSTEM_PROMPT embutido em cada ask. |
cubeAllowlist | sim | AiCubeRef[] | Ao menos uma entrada. Refs fora desta lista retornam 403 FORBIDDEN. |
skillAllowlist | não | string[] | Filtra o slash-routing + o catálogo do LLM. Vazio = todas as skills permitidas. |
suggestedPrompts | não | string[] | Perguntas de quick-start livres que a UI pode renderizar. |
Chaves top-level desconhecidas são rejeitadas para que um typo
(sytemPrompt) apareça como UNKNOWN_FIELD em vez de ser
silenciosamente descartado.
Enforcement de cubo
-
O chamador faz POST em
/ai/spaces/{id}/askcom um fieldcubeopcional:Terminal window curl -sS -X POST -H 'Content-Type: application/json' \-u admin:admin \http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \-d '{"question": "How did Store Sales track last week?"}' -
Se
cubeé omitido, a primeira entrada em allowlist do space é usada. -
Se
cubeé fornecido, ele deve casar com uma entrada da allowlist do space em todas as quatro coordenadas (connectionName,catalog,schema,cubeName) ou a chamada retorna:HTTP 403 Forbidden{"degraded": true,"reason": "FORBIDDEN: cube OtherCube is not in space 'foodmart-sales-analyst' allowlist"}
Isso é deliberado: uma UI que entrega ao servidor uma cube ref velha deve ser corrigida, não silenciosamente reinterpretada para o default.
Injeção de system prompt
O systemPrompt do space é prependado ao SYSTEM_PROMPT embutido no
lado do provider. A mensagem de sistema completa montada que o LLM vê:
You are a Mondrian OLAP analyst assistant scoped to a single cube. …[…the built-in tool-choice rails…]
Agent space persona:You are the FoodMart Sales Analyst. Prefer weekly and monthly timegrain unless the user asks otherwise. Lead with the top three lines byabsolute value. Flag any figure that swings by more than 20% versus theprior period. Be analytical, brief, and numbers-first.
Cube schema:{ … the AiSchema JSON … }
Cube ref to echo: { … }Os fields history e question do usuário vêm abaixo de tudo isso
como mensagens comuns — nada que eles possam injetar reescreve a
persona.
Filtro de skills
O catálogo de skills é filtrado para a skillAllowlist
do space antes de alcançar o LLM:
- A
skillAllowlistestá vazia → todas as skills fluem. - A
skillAllowlistnomeia skills específicas → só essas aparecem no system prompt do LLM E só essas fazem slash-expand.
Um ask como /some-other-skill for Q4 em um space que não faz
allowlist de some-other-skill cai como um ask cru — o LLM vê a
mensagem literalmente, sem expansão. A decisão de roteamento é
unilateral (deny da allowlist), nunca parcial.
Prompts sugeridos
Todo space carrega uma lista suggestedPrompts — 3-6 perguntas de
quick-start autoradas que a UI expõe como chips:
"suggestedPrompts": [ "How did Store Sales track last week vs the prior week?", "Break down Store Sales by Product Family for Q4.", "Which Product Department is up the most month-over-month?", "/weekly-foodmart-rollup"]Entradas de slash-command são legais e encorajadas — um prompt sugerido
que começa com / invoca a skill nomeada diretamente.
Superfície REST
GET /rest/saiku/api/ai/spaces
Catálogo de resumos {id, name, description, suggestedPrompts}.
curl -sS -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces | jq{ "spaces": [ { "id": "foodmart-sales-analyst", "name": "FoodMart Sales Analyst", "description": "Weekly and monthly sales rollups over the FoodMart Sales cube.", "suggestedPrompts": ["How did Store Sales track last week?", "…"] } ]}GET /rest/saiku/api/ai/spaces?errors=true
Mesmo, mais um array errors[]. Códigos de erro estáveis:
| Código | Quando |
|---|---|
EMPTY_SPACE | O arquivo está vazio ou só whitespace. |
MALFORMED_JSON | O parser JSON rejeitou o corpo. |
MISSING_FIELD | Campo obrigatório (id, name) não presente. |
BLANK_FIELD | Campo obrigatório presente mas vazio / só-whitespace. |
TYPE_MISMATCH | Campo presente mas tipo errado. |
INVALID_ID | id não casa com [a-z][a-z0-9-]{0,63}. |
EMPTY_ALLOWLIST | cubeAllowlist presente mas vazio — o space seria inutilizável. |
INVALID_CUBE_REF | Entrada da allowlist faltando uma coordenada (connectionName, etc.). |
UNKNOWN_FIELD | Frontmatter contém um campo que não está no schema. |
DUPLICATE_ID | Dois arquivos declararam o mesmo id. |
IO_ERROR | Não foi possível ler o arquivo (nível de filesystem). |
GET /rest/saiku/api/ai/spaces/{id}
Registro completo — inclui systemPrompt e cubeAllowlist que o
resumo omite. Usado pela UI de admin ao editar uma persona.
POST /rest/saiku/api/ai/spaces/{id}/ask
Ask escopado ao space. O formato do corpo espelha
/ai/ask mas o field cube é opcional:
curl -sS -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \ -d '{"question": "Break down Store Sales by Product Family for Q4"}'O envelope de resposta é o padrão AiAskApi.AskResponse
— model, request (o AiQueryRequest que o modelo emitiu), e
degraded/reason em erro.
POST /rest/saiku/api/ai/spaces/{id}/ask/stream
Variante de streaming do ask escopado ao space. Emite o mesmo schema de
Server-Sent Events que /ai/ask/stream
(model → intent → chunk → final) com o escopo da persona
aplicado — o cliente vê eventos de wire idênticos quer bata em
/ai/ask/stream quer neste espelho escopado ao space. Resultados de
space-não-encontrado e cubo fora-da-allowlist aparecem como um evento
error seguido por um final degradado, para que o leitor trate
falhas de escopo e falhas de provider da mesma forma.
curl -sS -N -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask/stream \ -d '{"question": "Break down Store Sales by Product Family for Q4"}'POST /rest/saiku/api/ai/spaces/refresh
Força um rescan (ignora a checagem de assinatura de mtime).
Autorando spaces no painel de admin
Spaces não precisam mais ser JSON autorado à mão. Admin → Agent spaces é um editor completo: escreva o system prompt, marque os cubos na allowlist (respaldado por descoberta de cubos ao vivo, então você só pode permitir cubos que existem), liste as skills e prompts sugeridos e salve. Deletar um space remove seu arquivo JSON.
O editor é respaldado por uma superfície CRUD admin-gated. Esses
endpoints exigem ROLE_ADMIN e retornam a persona completa (system
prompt + cube allowlist incluídos, ao contrário do catálogo público
redigido acima):
| Método + path | Propósito |
|---|---|
GET /rest/saiku/admin/agent-spaces | Lista cada persona por completo, para edição. |
GET /rest/saiku/admin/agent-spaces/errors | Erros de parse para arquivos malformados. |
PUT /rest/saiku/admin/agent-spaces/{id} | Cria ou substitui uma persona. O id é validado (kebab-case, protegido contra path-traversal) antes de o arquivo JSON ser escrito. |
DELETE /rest/saiku/admin/agent-spaces/{id} | Remove uma persona e seu arquivo. |
Embutindo um space
<saiku-embed kind="ai" space="foodmart-sales-analyst"> insere um
assistente escopado à persona em qualquer página. A allowlist de cubos
e o system prompt são aplicados server-side exatamente como são para o
ask REST, então um assistente embutido não pode ser desviado da sua
persona. Veja o guia de embed.
Exemplos incluídos
Instalações Saiku novas preparam duas personas funcionais:
- FoodMart Sales Analyst — analítico, breve, numbers-first.
weekly-foodmart-rollupna sua skill allowlist para que/weekly-foodmart-rollupesteja disponível como slash command. - FoodMart Finance Ops — cauteloso, preciso, focado em margem.
skillAllowlistvazia = todas as skills permitidas.
Ambos escopam ao cubo FoodMart Sales — uma demo nova tem personas prontas para clicar sem nenhuma autoria de operador.
Não-objetivos para v1
- Escopo por-usuário ou por-role. Spaces são por-launcher em v1; workspaces multi-tenant podem camadar escopo mapeando diretórios de workspace para raízes de registro por-workspace — adiado.
- Sobrescrita de data-scope por-space. Queries embutidas já aplicam filtros row-level forçados (apply-or-fail-closed — veja o guia de embed), mas fixar filtros de RLS em asks escopados a space especificamente ainda é rastreado com o trabalho de RLS Ossie em saiku#1393.