Real-time collaborative AI streaming — multiple clients in the same room see the AI response stream text-by-text, simultaneously.
This sample demonstrates what makes Atmosphere unique: broadcasting streamed LLM texts to multiple connected clients using a single @AiEndpoint annotation.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Student A│ │ Student B│ │ Student C│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ WebSocket │ WebSocket │ WebSocket
│ │ │
└──────────────┼──────────────┘
│
┌────────▼────────┐
│ Atmosphere │
│ Broadcaster │ ← All connected clients share this
└────────┬────────┘
│
┌────────▼────────┐
│ AiClassroom │ @AiEndpoint + @Prompt
│ + Interceptor │ RoomContextInterceptor sets persona
└────────┬────────┘
│
┌────────▼────────┐
│ AgentRuntime │ Pluggable backend (Built-in, Spring AI,
│ (auto-detect) │ LangChain4j, ADK, Koog, Semantic Kernel,
│ │ AgentScope; Embabel/Spring AI Alibaba on SB3)
└─────────────────┘
The endpoint (6 lines of meaningful code):
@AiEndpoint(path = "/atmosphere/classroom/{room}",
systemPromptResource = "skill:classroom",
interceptors = { RoomContextInterceptor.class })
public class AiClassroom {
@Prompt
public void onPrompt(String message, StreamingSession session, AtmosphereResource resource) {
session.stream(message); // Works with ANY AgentRuntime backend
}
}The {room} path segment is extracted by AiEndpointHandler and each unique room path gets its own Atmosphere broadcaster, so messages in the math room are isolated from the code and science rooms. The skill:classroom prefix loads the system prompt from a skill file (classpath or ~/.atmosphere/skills/).
This sample is unique in the JVM AI space: ONE @AiEndpoint serves
four DIFFERENT scopes selected per-request from the {room} path param.
No other framework (MS Agent Framework, Spring AI, LangChain4j) has
per-request scope install — they'd need four separate endpoints for this.
atmosphere-classroom-scopes.yamldeclares four rooms. Each room has its own system prompt, purpose, forbidden topics, redirect message, and scope tier. Edit YAML, restart, rooms change — zero Java edits.RoomScopesConfigloads the YAML at boot, publishes aRoomsbean, and hands the registry toRoomContextInterceptorvia a static installer.- On every incoming request,
RoomContextInterceptor.preProcessreads the{room}path param, picks the matchingScopeConfig, and places it onAiRequest.metadata()underScopePolicy.REQUEST_SCOPE_METADATA_KEY. AiStreamingSession.streampops that key, builds a transientScopePolicyfor this request, runs pre-admission + system-prompt hardening, and wraps the streamed response with a matching post-response check.
The math room rejects "write me python code"; the code room rejects "medical advice"; the science room rejects both. One endpoint, four scopes, all swappable from YAML.
# atmosphere-classroom-scopes.yaml (excerpt)
rooms:
math:
purpose: Mathematics tutoring — algebra, calculus, geometry, statistics
forbiddenTopics:
- writing source code
redirectMessage: "This is the math room — ask me about a mathematics topic."
tier: RULE_BASED
code:
purpose: Software engineering mentoring — languages, debugging, algorithms
forbiddenTopics:
- medical advice
- legal advice
redirectMessage: "This is the code room — ask me about a programming topic."
tier: RULE_BASEDatmosphere-policies.yaml (loaded by PoliciesConfig) layers cross-cutting
policies that apply to all rooms: classroom-pii-guard (PII redaction) and
classroom-drift-watcher (response-length z-score). Built-in YAML policy
types: pii-redaction, cost-ceiling, output-length-zscore, deny-list,
allow-list, message-length, rate-limit, concurrency-limit,
time-window, metadata-presence, authorization.
The easiest way to run with a real AI model is via Embacle, which turns your existing Claude Code, Copilot, Cursor, or Gemini CLI license into an OpenAI-compatible LLM provider — no separate API key required.
# 1. Start Embacle (see https://github.com/dravr-ai/dravr-embacle)
# It runs on http://localhost:3000/v1
# 2. Start the classroom with Embacle as the backend
LLM_BASE_URL=http://localhost:3000/v1 LLM_API_KEY=embacle LLM_MODEL=copilot:claude-sonnet-4.6 \
./mvnw spring-boot:run -pl samples/spring-boot-ai-classroom
# Open http://localhost:8080 in MULTIPLE browser tabs
# Join the same room, send a question — all tabs stream simultaneously# Gemini
export LLM_API_KEY=AIza...
export LLM_MODEL=gemini-2.5-flash
# OpenAI
export LLM_API_KEY=sk-...
export LLM_MODEL=gpt-4o-mini
export LLM_BASE_URL=https://api.openai.com/v1
# Local Ollama
export LLM_MODE=local
export LLM_MODEL=llama3.2Without any API key or Embacle, the sample runs in demo mode with simulated streaming responses.
Each room is a path segment — connect to a different URL to join a different room. Each path also gets its own Atmosphere broadcaster so messages stay isolated per room.
| Room | Persona | URL |
|---|---|---|
| Math | Mathematics tutor | /atmosphere/classroom/math |
| Code | Programming mentor | /atmosphere/classroom/code |
| Science | Science educator | /atmosphere/classroom/science |
| General | General assistant | /atmosphere/classroom/general |
The session.stream(message) call is framework-agnostic. To switch AI backends:
| Backend | What to do |
|---|---|
| Built-in (OpenAI-compatible) | Default — just set LLM_API_KEY |
| Spring AI | Add atmosphere-spring-ai dependency |
| LangChain4j | Add atmosphere-langchain4j dependency |
| Google ADK | Add atmosphere-adk dependency |
| JetBrains Koog | Add atmosphere-koog dependency |
| Microsoft Semantic Kernel | Add atmosphere-semantic-kernel dependency |
| Alibaba AgentScope | Add atmosphere-agentscope dependency |
| Embabel | Use the Spring Boot 3.5 profile/sample path and add atmosphere-embabel |
| Spring AI Alibaba | Use the Spring Boot 3.5 profile/sample path and add atmosphere-spring-ai-alibaba |
Zero code changes. The AgentRuntime SPI auto-detects the best available backend via ServiceLoader.
A React Native / Expo client is available at expo-client. It connects to this backend via WebSocket, streams AI responses text-by-text with markdown rendering, and includes AppState/NetInfo lifecycle integration. See the React Native client docs for details.