AI

A model is optional and off until a superuser sets one up. Everything that uses it has an answer for when there is none, and what a model says comes back as a proposal: a person applies it, or doesn't.

Setting one up

In the admin UI, under AI:

Provideranthropic for Claude, or openai for anything that speaks the OpenAI chat shape: Ollama, LM Studio, llama.cpp’s server
ModelFor Claude, claude-opus-5 unless you name another. Required for openai
AddressFor openai, where it listens: http://localhost:11434/v1 for Ollama
KeyKept on the server; never sent back out of the API
EffortHow hard Claude thinks: low, medium (the default) or high
Per person, per dayHow many questions one person may ask; 0 for no limit. Administrators aren’t counted

A model on the school’s own machine keeps everything on it.

In the browser

app.ts
const { text } = await sluurp.ai.chat([{ role: "user", text: "Summarise this week's notices" }]);

// As it is written, piece by piece:
await sluurp.ai.stream(turns, { onText: (piece, all) => (out.textContent = all) });

Both take { system, signal }; stopping with the signal keeps what came so far. When a person has asked their day’s share, the answer says so instead of throwing.

In server code

ai(system, prompt, schema) asks for an answer as JSON in the schema’s shape, or null when no model is set up:

functions/tag-notice.ts
const tags = await ai(
  "You tag school notices.",
  notice.body,
  { type: "object", properties: { tags: { type: "array", items: { type: "string" } } } },
);
if (tags) await collection("notices").update(notice.id, tags);

For a model that works in turns, reading what it needs through tools until it has an answer, see Agents.

Where Sluurp uses it

Pages offers its AI tools, and sluurp/ai an assistant for any app, only when a model is set up.