A browser-based developer workbench that puts a multi-provider LLM chat interface, a sandboxed terminal, a file manager, and a configurable multi-agent pipeline all in one window.
UDI is a Flask application with a dark-themed single-page frontend. You type goals or questions into the Interface tab and an LLM responds; you switch to the Pipeline tab to run multi-agent workflows that can write files, lint code, run tests, and execute shell commands—all sandboxed to a local project/ directory.
- Multi-provider LLM chat — supports Google Gemini, Anthropic Claude, and OpenAI GPT models, configurable via an environment variable.
- Multi-agent pipeline — define sequences of executor agents and an optional critic agent that iteratively refines output until the critic passes or a maximum iteration count is reached.
- Sandboxed code execution — agents can write files (
[CODE]), run shell commands ([RUN]), lint ([LINT]), test ([TEST]), read files back ([READ]), and perform string replacements ([STR_RPL]), all confined toproject/. - Interactive
[QUERY]pauses — a running pipeline can pause and ask the user a question, then resume with the answer incorporated. - File API integration — files can be uploaded to the Gemini File API and referenced in chat.
- Retry logic — every LLM call retries up to 5 times with exponential backoff; Gemini calls run in a thread with a 45-second hard timeout.
- Live log panel — warnings and per-iteration agent logs stream to the UI.
- Python 3.9+
- API key(s) for at least one provider (Gemini, Anthropic, or OpenAI)
flask
flask-cors
google-genai
anthropic
openai
Install with:
pip install flask flask-cors google-genai anthropic openaiAll configuration is done via environment variables.
| Variable | Description | Default |
|---|---|---|
NARRATOR_MODEL |
The model string to use for chat and pipeline agents | gemini-3.1-pro-preview |
GEMINI_API_KEY |
Google Gemini API key | (none) |
ANTHROPIC_API_KEY |
Anthropic API key | (none) |
OPENAI_API_KEY |
OpenAI API key | (none) |
The provider is inferred automatically from the model name:
- Model names containing
gemini→ Gemini - Model names containing
claude→ Anthropic - Model names containing
gpt→ OpenAI
Example — use Claude:
export ANTHROPIC_API_KEY=sk-ant-...
export NARRATOR_MODEL=claude-sonnet-4-6Example — use GPT:
export OPENAI_API_KEY=sk-...
export NARRATOR_MODEL=gpt-4opython app.pyThe server starts on http://127.0.0.1:5000. Open that URL in a browser.
.
├── app.py # Flask app, LLM handlers, /api/chat and /api/upload routes
├── pipeline.py # Multi-agent pipeline engine, sandboxed execution, pipeline routes
├── templates/
│ └── index.html # Single-page frontend (IBM Plex Mono, dark theme)
└── project/ # Sandboxed working directory for all agent file I/O (auto-created)
POST /api/chat
Send a message to the configured LLM.
{
"system_prompt": "You are a helpful assistant.",
"context": "Optional background state.",
"player_action": "The user's message.",
"file_uri": "(optional) Gemini File API URI",
"file_mime": "(optional) MIME type for the file"
}Returns { "text": "...", "warnings": [] }.
POST /api/upload
Upload a file to the Gemini File API (multipart/form-data, field name file).
Returns { "uri": "...", "mime_type": "...", "name": "..." }.
GET /api/config
Returns the active model name and provider: { "narrator_model": "...", "narrator_provider": "..." }.
POST /api/pipeline/start
Start a pipeline job.
{
"goal": "Build a web scraper for Hacker News.",
"agents": [
{
"name": "Coder",
"prompt": "Write the code to accomplish the goal.",
"is_critic": false,
"model": "gemini-3-flash-preview",
"provider": "gemini",
"temperature": 0.3
},
{
"name": "Critic",
"prompt": "Review the output and return JSON: { \"pass\": bool, \"feedback\": str }",
"is_critic": true,
"model": "gemini-3-flash-preview",
"provider": "gemini"
}
],
"max_iterations": 3,
"udi_context": "(optional) snapshot of the current UDI workspace state"
}Returns { "job_id": "a1b2c3d4" }.
GET /api/pipeline/status/<job_id>
Poll for job state. Returns the full job object including status, logs, outputs, tags, run_outputs, feedback, and any pending query.
Status values: queued → running → completed / error / cancelled / paused
POST /api/pipeline/answer/<job_id>
Resume a paused job by answering a [QUERY].
{ "answer": "Use PostgreSQL." }POST /api/pipeline/cancel/<job_id>
Cancel a running job.
GET /api/pipeline/jobs
List all jobs (summary).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/files |
List all files in project/ |
POST |
/api/files/read |
Read a file: { "path": "..." } |
POST |
/api/files/write |
Write a file: { "path": "...", "content": "..." } |
POST |
/api/files/delete |
Delete a file: { "path": "..." } |
POST |
/api/run |
Run a shell command: { "cmd": "..." } |
POST |
/api/lint |
Lint a file: { "args": "filename" } or `"filename |
POST |
/api/test |
Run tests: { "args": "filename" } or `"filename |
POST |
/api/read |
Read a project file: { "filename": "..." } |
POST |
/api/str_replace |
String replace: `{ "args": "filename |
Executor agents communicate intent through structured tags in their output. The pipeline engine parses and executes these automatically.
| Tag | Syntax | Effect |
|---|---|---|
[CODE] |
[CODE: filename] ```langcode```[/CODE] |
Writes code to project/filename |
[RUN] |
[RUN: command] |
Executes a shell command in project/ |
[LINT] |
[LINT: filename] or [LINT: filename | tool] |
Lints a file (flake8 or eslint) |
[TEST] |
[TEST: filename] or [TEST: filename | type] |
Runs pytest or jest |
[READ] |
[READ: filename] |
Reads a file and injects its content into the next agent's context |
[STR_RPL] |
[STR_RPL: filename | old | new] |
Performs a unique string replacement in a file |
[QUERY] |
[QUERY: question] |
Pauses the pipeline and asks the user a question |
[CONTEXT] |
[CONTEXT: note] |
Logs a reasoning annotation to the pipeline log |
All file operations and shell commands are sandboxed to the project/ directory. The following commands are always blocked regardless of context:
sudo, su, chmod, chown, dd, mkfs, fdisk, kill, pkill, reboot, shutdown, halt, poweroff, curl, wget, nc, ncat, netcat
Path traversal (..) and absolute paths outside project/ are also rejected.