A self-sufficient, local AI workspace for orchestrating autonomous agents and multi-model workflows.
230
A local AI workspace for running autonomous agents, MCP servers, and multi-model workflows.
Calypso runs on your own machine. You bring the model (a local Ollama instance, or an API key for OpenAI, Anthropic, Google, or Azure) and Calypso handles the rest: agents with their own tools and knowledge, multi-agent workflows you build visually, and a chat interface to run them.
https://github.com/nipun-dezoysa/calypso
The recommended way to run Calypso is the published image. It bundles the UI and the API in one container.
docker run -d \
--name calypso \
-p 8000:8000 \
-v calypso-data:/app/data \
--add-host=host.docker.internal:host-gateway \
--restart unless-stopped \
nipundezoysa/calypso:latest
Open http://localhost:8000.
Two flags matter:
-v calypso-data:/app/data - everything you create (database, agents, workflows, chat history, uploaded documents, vector index) lives here. Skip it and your work disappears when the container is removed.--add-host=host.docker.internal:host-gateway - lets the container reach services on your host, which you need if you run Ollama locally. Harmless otherwise.To upgrade, pull the new image and recreate the container. The volume carries your data across:
docker pull nipundezoysa/calypso:latest
docker rm -f calypso
# then re-run the command above
Pin a version instead of latest if you'd rather upgrade deliberately - see the available tags.
http://host.docker.internal:11434 as the URL (not localhost, which points at the container itself).stdio, streamable_http, sse, or websocket. Calypso discovers each server's tools and exposes them to the agents you attach them to.The API is self-documenting at http://localhost:8000/docs.
git clone https://github.com/nipun-dezoysa/calypso.git
cd calypso
docker build -t calypso:local .
docker run -d -p 8000:8000 -v calypso-data:/app/data calypso:local
Frontend and backend run separately here, with vite proxying /api/v1 to the API. Requires Python 3.13+, uv, and Node 20+.
# ── terminal 1: API on :8000 ──
cd server
uv sync
uv run fastapi dev main.py
# ── terminal 2: UI on :5173 ──
cd client
npm install
npm run dev
Open http://localhost:5173. State is written to server/calypso.db, server/chroma_db/, and server/kb_files/.
In this mode the API doesn't serve the frontend - it only mounts a bundle when a static/ directory sits next to it, which is how the Docker image works. Nothing to configure either way.
Every setting is optional; the defaults below are what the image uses. Providers, keys, and knowledge base settings are configured in the UI, not here.
| Variable | Default (in image) | Purpose |
|---|---|---|
DATABASE_URL | sqlite+aiosqlite:////app/data/calypso.db | SQLAlchemy async database URL |
KB_UPLOAD_DIR | /app/data/kb_files | Where uploaded documents are stored |
CHROMA_PERSIST_DIR | /app/data/chroma_db | Local ChromaDB persistence directory |
FASTEMBED_CACHE_PATH | /app/data/fastembed_cache | Cache for downloaded embedding models |
STATIC_DIR | /app/static | Built frontend bundle; unset in local dev |
CORS_ORIGINS | http://localhost:5173,http://127.0.0.1:5173 | Comma-separated allowed origins |
CORS_ORIGIN_REGEX | localhost/127.0.0.1, any port | Regex alternative to the list above |
DEFAULT_USERNAME | admin | Username seeded on first boot |
DEFAULT_PASSWORD | admin | Password seeded on first boot |
JWT_SECRET | generated once, stored in the database | Token signing key |
JWT_EXPIRE_DAYS | 365 | Access token lifetime |
JWT_ALGORITHM | HS256 | Token signing algorithm |
HISTORY_TOKEN_BUDGET | 6000 | History ceiling for models with no context window set; 0 sends the whole thread |
The UI and API share an origin in the image, so CORS only matters if you call the API from somewhere else.
Langfuse tracing is off until you turn it on. Everything it needs - host, public key, secret key, environment label, sampling rate - is set under Settings → Observability in the UI and stored in the database; there is nothing to configure on the server. Test connection checks the keys against your Langfuse server before you save them, and changes take effect on the next message - no restart.
Each chat turn is one trace: a root span for the agent (or the workflow, with a span per node underneath), the model generations below it, and a span per MCP tool call. Threads become Langfuse sessions, so a whole conversation reads as one group. The workflow designer is traced too.
| Endpoint | Purpose |
|---|---|
GET /api/v1/langfuse/settings | Current settings; the secret key comes back as a boolean |
PUT /api/v1/langfuse/settings | Update; only the fields sent are changed |
POST /api/v1/langfuse/settings/test | Check credentials against the server before saving |
Nothing here can break a chat: if Langfuse is unreachable or the keys are wrong, the run is answered as usual and the failure is logged.
The API is behind a JWT bearer token. On first boot the server seeds a single account from DEFAULT_USERNAME / DEFAULT_PASSWORD and flags it as unchanged, so the UI sends you straight to a choose your credentials screen after the first sign-in. The defaults only apply to an empty database - changing those variables later does nothing.
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/v1/auth/login | none | Exchange username + password for a token |
GET /api/v1/auth/me | bearer | The signed-in user, including must_change_credentials |
PUT /api/v1/auth/credentials | bearer | Change username and/or password; returns a fresh token |
POST /api/v1/chat/{id}/ask | none | Public, so other applications can call your agents |
POST /api/v1/chat/{id}/ask/stream | none | The same, streamed back as server-sent events |
| everything else | bearer |
/ask is deliberately left open: it returns answer text only and never exposes provider secret keys, which is exactly why the rest of the API is closed. /ask/stream is open for the same reason - it is the same capability over a different transport.
Changing a password invalidates every token issued before the change - tokens carry a fingerprint of the stored password hash. If JWT_SECRET is not set the server generates one on first boot and keeps it in the database, so tokens survive restarts.
POST /api/v1/chat/{id}/ask/stream takes the same body as /ask and answers
with text/event-stream. Each frame is one JSON object:
| Event | Payload | Meaning |
|---|---|---|
start | thread_id | The thread this answer will be saved to, sent before the model runs |
token | text | A piece of the answer. Append them in order |
node | name | A workflow step has begun. Its tokens are its own, so clear what the previous step wrote |
tool | name | An MCP tool is being called |
done | thread_id, answer, stopped | The finished answer, as saved |
error | detail | The run failed. Nothing is saved |
Closing the connection stops the generation. Whatever had streamed by then is still written to the thread, so stopping mid-answer keeps the part you got instead of throwing the turn away - which is what the UI's stop button does. The generation itself outlives the request that started it just long enough to save; it is not tied to the connection.
/ask is unchanged and still returns the whole answer in one response, for
callers that would rather not parse a stream.
Every turn replays the thread so far, which is what gives an agent its memory - but a thread left to grow will eventually overrun the model's context window, and on a metered provider it pays for the same old turns again with each new message. So the replay is capped: turns are kept newest first until the budget is used up, and what falls outside is left out, with a note telling the model the conversation did not start where it appears to. Attachment text and images count against the budget too, since a PDF costs far more than anything typed alongside it.
The budget comes from the model. Each model can be given a context window
when you add or edit its provider, and the history is allowed half of it, the
rest has to hold the system prompt, the knowledge-base chunks, the question with
its attachments, and the reply. So a 200k Claude replays far more of a
conversation than a 4k local model, instead of both being held to one figure
picked to be safe on the smaller one. The field suggests a size for models it
recognises, including Ollama tags like llama3.1:8b, and you can override it.
Models with no window set fall back to HISTORY_TOKEN_BUDGET, which is also the
global off switch: set it to 0 and the whole thread is sent, whatever any model
says. In a workflow each node is sized by its own model, so a step on a large
model is not cut down to fit a step on a small one.
Turns are dropped rather than summarised on purpose. A summary means another full generation before the real answer can start, which on a local model is minutes of waiting for something the user did not ask for.
One caveat for Ollama: the sizes suggested are the model's limit, but Ollama
serves a smaller num_ctx unless the Modelfile raises it. If answers start
failing on a local model, lower its window here to match what Ollama actually
serves.
Backend - FastAPI, SQLAlchemy (async, SQLite), LangChain and LangGraph, ChromaDB / Qdrant, FastEmbed, Neo4j.
Frontend - React 19, TypeScript, Vite, Tailwind CSS, Zustand, React Flow.
Releases are built and pushed to Docker Hub automatically by .github/workflows/docker-publish.yml when a GitHub release is published.
Contributions are welcome - issues, feature ideas, and pull requests alike.
If you're picking something up:
main.npm run lint, and match the style of the code around you.docker build -t calypso:test . still succeeds if you touched dependencies, the Dockerfile, or anything under server/.Not sure where to start, or unsure whether an idea fits? Open an issue first and let's talk it through - cheaper than building the wrong thing.
Content type
Image
Digest
sha256:ca2b5e261…
Size
383.7 MB
Last updated
15 days ago
docker pull nipundezoysa/calypso