AI Open Framework
September 2, 2026 ยท View on GitHub
Formerly known as ai-circus-framework.
๐ง Work in progress. This is a personal, evolving open-source project โ architecture, scenarios, and UI are all still moving. Expect rough edges, and treat anything here as a snapshot rather than a finished product.
A scalable, multi-tenant microservices platform for building and demoing data-science and GenAI scenarios (tabular ML dashboards, agentic RAG chatbots, assisted-form intake flows, ...) behind a real login.
Table of contents
- Tour of the platform
- Scenario catalog
- Getting started
- Architecture
- LLM providers
- Adding a new scenario or service
- Testing & CI
- Reserved for later
- Why this exists
- Contributing
- Author & license
Tour of the platform
Login
A single branded entry point: Keycloak-managed sign-in for real users/organizations, or the admin key shortcut for quick local demos โ both resolve through the exact same identity path on the backend, so nothing is a security bypass, just a different way in.

Scenario gallery
Every scenario a tenant is entitled to, rendered generically from scenarios/*/scenario.yaml โ
no per-scenario UI code. Tabular ML scenarios show their task type (classification/regression);
the conversational scenario shows up alongside them.

Data
Dataset summary stats, a filterable/queryable row explorer, a build-your-own chart dashboard, and credit for the original public dataset โ all generated from the scenario's schema, not hand-built per dataset.

ML predictions & explainability
Run the live trained model on one record (or a batch), and see why it predicted what it did via a real, per-prediction SHAP breakdown โ plus global feature importance and partial-dependence sweeps computed from live API calls, not precomputed synthetic charts.


Settings & LLM providers
Every configured LLM provider's live routing status in one place, a per-provider Test button (a real completion round-trip), and instant switching of the active model โ the same screen that makes step 3 of Getting Started concrete.

Themes
The whole app is skinned from one Theme object (colors + a logo, see ui-react/src/themes/) โ
switching themes in Settings โ Appearance is instant, no rebuild. Two ship today: Tron
(the neon dark default) and White Tron, the same blue/cyan branding on flat, light,
corporate-friendly surfaces.
Conversational assistant
A real LangChain tool-calling agent, not a fixed "always retrieve" pipeline โ it decides whether a question needs retrieval at all, grounded in the scenario's own reference documents.

Inside any tabular_ml scenario, that same assistant is also wired to the live model via
AG-UI (CopilotKit) generative UI: it can call the real prediction API on your behalf and
render the result as an actual chart or sortable table in the chat โ not markdown pasted into
prose โ using the exact same Plotly/table components as the Data tab.
Assisted forms
A third scenario kind, alongside tabular_ml and conversational_rag: a generic form rendered
entirely from a scenario's form: config, paired with a chat assistant that can fill fields in
live as you describe your request in plain language โ classifying it via RAG over a small
reference catalog, and highlighting which fields it just filled in versus what's still missing.
Public Service Request Portal (service_request) is the reference example: report a
streetlight outage, request an address registration, or apply for a permit, and watch the form
fill itself in as you type.
Scenario catalog
Three kinds of scenario exist today โ adding a new one is a YAML file, never new UI or container code (see Adding a new scenario).
| Scenario | Kind / task | What it predicts | Source |
|---|---|---|---|
Customer Churn Prediction (churn) | tabular_ml โ classification | Bank customer churn risk | Kaggle โ Sonali Dasgupta |
Machine Predictive Maintenance (mpm) | tabular_ml โ classification | Industrial machine failure risk | Kaggle โ AI4I 2020 |
Supply Chain Shipping ETA (supply_chain) | tabular_ml โ regression | Days to delivery | AWS SageMaker workshop (synthetic) |
Supermarket Weekly Sales (supermarket_sales) | tabular_ml โ regression | Weekly department sales | Kaggle โ Walmart dataset |
Electric Motor Speed (electric_motor) | tabular_ml โ regression | Motor rotational speed (rpm) | Kaggle โ Electric Motor Temperature |
Building Energy Consumption (energy_building) | tabular_ml โ regression | Appliance energy use (Wh) | UCI โ Appliances Energy Prediction |
AI Open Framework Reference Guide (ai_circus_reference) | conversational_rag | N/A โ agentic Q&A over this project's own dev/ML/GenAI reference notes | Original content |
Public Service Request Portal (service_request) | assisted_form | N/A โ the assistant fills out and classifies a service-request form live, from conversation | Original content |
Every tabular_ml scenario above is ported from a real public dataset rather than original
content โ full credit/link lives in each scenarios/<slug>/scenario.yaml's credits field and is
surfaced in the Data tab.
One consolidated service instance serves every scenario of a given kind โ prediction and
assistant both load every tabular_ml scenario from the same running container, routed by a
{scenario_slug} path segment; rag-agent does the same for every conversational_rag scenario,
and form-agent does the same for every assisted_form scenario.
Getting started
Prerequisites
- Kubernetes (recommended) โ Docker,
k3d,kubectl,make. - Docker Compose (alternative) โ Docker + Docker Compose,
make. - At least one LLM provider, either way โ a free API key (Google Gemini's free tier is easiest) or the bundled local Ollama fallback. Chat features simply won't answer without one.
1. Clone and bootstrap the environment
git clone <this-repo-url> && cd ai-circus-framework
make bootstrap # copies .env.example -> .env
Open the new .env โ every setting has a comment explaining it. You don't need to touch most of
it to get a working demo; the two things that matter most (an LLM key, and which deployment path
below) are covered next.
2. Choose your LLM and set its API key โ this step is required
assistant (tabular chat) and rag-agent (document Q&A) won't answer anything until one model
is actually reachable. Pick one of these:
| Option | What to do |
|---|---|
| Cloud provider (recommended) | Get a free API key from Google AI Studio (or OpenAI/Anthropic/DeepSeek/Groq/OpenRouter/Azure), paste it into .env as GOOGLE_API_KEY=..., and set LLM_MODEL=gemini-flash. See the LLM providers table below for every option and its exact env var. |
| No API key at all | Run make ollama-up โ starts a bundled, local, free Ollama container and pulls a small model automatically. Leave LLM_MODEL=llama3 (the default). |
You can change your mind later from the app itself: Settings โ LLM Provider Settings shows
every provider's live status and lets you switch the active model instantly, without a restart
(see the Settings screenshot above) โ new keys still require editing
.env and restarting llm-gateway, though.
3. Start the platform
Two paths get you to the same app โ pick one.
Kubernetes (recommended)
A local k3d (k3s-in-Docker) cluster running the exact same stateless services,
via plain Kustomize manifests โ see k8s/README.md for the full manifest
reference and design notes.
make k3s-cluster # create the local k3d cluster (port 80, ./scenarios bind-mounted)
make k3s-build # build every service image locally
make k3s-import # import them into the cluster's containerd
make k3s-secrets # generate k8s Secrets from .env/infra
make k3s-up # kubectl apply -k k8s/base
make k3s-wait # wait for every pod to actually be Ready
make k3s-verify # curl-check the admin tenant end-to-end, same as `make verify` below
make k3s-pipeline # optional: (re)runs the ETL -> training pipeline for the tabular_ml scenarios
Or run the first six of those (cluster through wait) in one shot: make k3s-all.
Not working the demo but staying in WSL? Pause the cluster's containers (frees CPU/RAM, keeps all state) instead of tearing it down:
make k3s-pause # stop the cluster โ resume later with `make k3s-resume`
make k3s-resume # start it back up
Before opening the app in a browser, start a standing port-forward โ platform-registry's
browser-facing API isn't reachable through Traefik or k3s-verify's own (command-scoped)
port-forward:
kubectl -n ai-circus port-forward svc/platform-registry 8010:8000 &
Skipping this shows up as a client-side Failed to fetch right on the login screen even though
every other check passes โ see k8s/README.md's "Design notes" for why.
This is dev-parity, single-node only today (no registry โ images are built locally and imported
straight into the cluster; no Helm chart, no multi-node/HA) โ not yet a drop-in production
manifest set. It's still the recommended path because it's the same manifests you'd adapt for a
real cluster (remote k3s, managed cloud Kubernetes, OpenShift): kubectl apply -k k8s/base
already targets whatever kubeconfig context is active, local or not.
Docker Compose (alternative)
Simplest option for iterating on a single service without rebuilding into a cluster image each time.
make up # every backend service + both UIs
make pipeline # (re)runs the ETL -> training pipeline for the tabular_ml scenarios
docker compose up --build etl-vectorize # vectorizes every conversational_rag scenario's reference docs,
# plus any assisted_form scenario's RAG catalog (e.g. service_request)
make all (infra + services + both pipelines + an end-to-end admin-tenant check) runs this whole
compose path in the right order for you, waiting for each container to actually be ready before
moving to the next โ safe to re-run any time. If something's clearly broken (stale volumes,
half-applied .env change), make reset-all tears everything down โ including data in
postgres/keycloak/qdrant/seaweedfs โ and reruns make all from a clean slate.
4. Open the app
For a quick look without configuring an identity provider at all, use the login screen's User
dropdown: pick admin and enter the key from .env's ADMIN_API_KEY (ai-circus-2026 by
default) as the password โ it comes pre-granted access to every scenario. For real
multi-user/multi-tenant login, see "First-time Keycloak setup" further down.
The dropdown's other option, demo engineering, is the same bypass mechanism scoped to a
narrower demo tenant โ entitled to only the three engineering scenarios (Predictive Maintenance,
Electric Motor Speed, Building Energy Consumption), not every scenario. Its key/password is
.env's ENGINEERING_DEMO_API_KEY (ai-circus-engineering-2026 by default; leave it blank to
disable this login option). It's provisioned automatically wherever ADMIN_API_KEY is โ no
separate setup step โ and make verify (part of make all) checks that it's scoped correctly:
entitled to exactly those three scenarios, and rejected (403) on any other. This is meant as a
template for adding your own narrower demo tenants: pick a name, an env var, and a scenario slug
set in services/platform-registry/src/platform_registry/core/seed.py's
ENGINEERING_DEMO_SCENARIOS.
"Failed to fetch" after logging in? That's the browser's network-level error, not an application error โ it means a request never reached a server at all.
On Kubernetes, this almost always means the standing
platform-registryport-forward from step 3 above isn't running โ seek8s/README.md's "Design notes".On Docker Compose, run
make verify(or justmake allagain) to pinpoint which service isn't answering; the most common causes are: (1) you tested right aftermake up, before every container was actually ready โmake all/make verifywait for that, plaindocker compose up -ddoesn't; (2)postgres-data(or another) volume already existed from an earlier partial run, so its one-time init script never reran โmake reset-allfixes this; (3) something else on the machine is already bound to port 80 (Traefik's entrypoint), 8010 (platform-registry), 6333 (Qdrant), or 4000 (llm-gateway) โ the latter three are loopback-only, for local non-Docker dev; (4) the app was opened via an origin other thanhttp://aiopen.localhost(e.g. plainhttp://localhost) โ every backend's CORS allow-list is keyed to that exact hostname.
Local (non-Docker) development: each generated service under services/*/ has its own
make run โ run it directly with uv run from inside that service's directory while the infra
containers stay up via make up-infra.
First-time Keycloak setup
Both deployment paths bring up Keycloak already bootstrapped: infra/keycloak/realm-export.json
is loaded declaratively via start --import-realm on first boot, so the ai-circus realm,
the organization/platform-backend client scopes (Organization membership + Audience
mappers), and the ui-react SPA client all exist the moment the container is healthy โ no
manual Admin Console click-through. http://keycloak.localhost is the sign-in page,
http://admin.keycloak.localhost the Admin Console (Basic-Auth-gated, same as before).
Two things the static realm export can't do for you:
- One-time, manual, via the Admin Console (redo after any
make reset-all, since it wipes Keycloak's own data): the M2M client (KEYCLOAK_M2M_CLIENT_ID/SECRETin.env) has no admin rights of its own โ grant its service-account user themanage-users/manage-organizations/manage-realm/manage-clientsrealm-management client roles, signed in as the container's own bootstrap admin (KEYCLOAK_ADMIN_USERNAME/KEYCLOAK_ADMIN_PASSWORD). Nothing below works until this is done. - Real users, and the
scenario:<slug>realm roles derived fromscenarios/*/scenario.yaml, are one command:
Setmake -C services/platform-registry provision-owner-userKEYCLOAK_OWNER_EMAIL/KEYCLOAK_OWNER_PASSWORDin.envfirst. Idempotent โ safe to re-run any time. It createsscenario:<slug>as plain realm roles for every scenario'srole_required(Keycloak's Organizations API has no org-scoped role endpoint, so entitlement roles are assigned per-user, not per-organization-membership โ a deliberate simplification, seelibs/shared'sauth.pydocstring), creates (or finds) anownerOrganization and that Keycloak user, adds them to the Organization, assigns everyscenario:*role directly to the user, and syncs the result into localentitlementsโ so signing in through Keycloak's hosted page with that email lands on every scenario, the same as theADMIN_API_KEYbypass. For any other user/Organization you want scoped differently, do that one by hand in the Admin Console: add them to an Organization and assign only thescenario:*realm role(s) you want them entitled to โ that assignment is what grants access to a scenario.
Public deployment
Deploying this as-is to a public VM or behind a public minikube
Ingress is still just make up โ there's no separate compose file or up variant โ but it
needs a few .env values changed first, since APP_ENVIRONMENT: docker in
docker-compose.yml is identical for local dev and a real deployment and so can't be used to
tell them apart. (The same .env values apply if you adapt k8s/base/ for a real cluster โ but
see the Kubernetes step above: today's manifests are dev-parity/single-node only, not yet a
production-ready starting point on their own.)
- Rotate (or blank, to disable the shortcut outright)
ADMIN_API_KEY/ENGINEERING_DEMO_API_KEYaway from their shipped demo values, and confirmAUTH_DISABLED=false. - Regenerate the Basic Auth credential Traefik puts in front of Keycloak's Admin Console and
SeaweedFS's console โ both are otherwise purely-administrative UIs, always reachable on the
Traefik entrypoint (Keycloak's Admin Console in particular lets whoever holds the bootstrap
admin credentials fully control the identity system, so it's gated even locally, just with a
shipped demo credential you must rotate here):
make generate-console-auth # prints a one-time password โ save it, it isn't stored anywhere - Set
DEPLOYMENT_TARGET=publicin.envโ this arms every service's boot-time refusal to start if you missed step 1 (seelibs/shared/src/ai_circus_shared/deployment_guard.py), so a mistake here is a startup crash with a clear message, not a silent hole. make check-public-readysanity-checks all three steps above without starting/stopping anything, then deploy with the usualmake up(ormake all).
SeaweedFS's S3 API route (as opposed to its console) is deliberately left without Basic Auth โ see
the comment on its Traefik labels in docker-compose.yml for why.
Architecture
Runs on a local Kubernetes (k3s/k3d) cluster, namespace ai-circus โ the recommended path, see
Getting started โ and identically via docker compose up: the same stateless,
env-configured microservices either way, just a different orchestrator.
Solid arrows are primary request/data paths; dotted arrows are cross-cutting auth/admin calls or
traffic leaving the cluster. Every scenario service independently validates the caller's token
against Keycloak and re-checks the entitlement with platform-registry โ never just trusting
what the UI already filtered.
Simplified view โ grouped data flow
A tenant (Keycloak Organization, or the shared admin credential) only sees the scenarios its
members have been granted the matching scenario:<slug> role for โ enforced both in the UI (what's
shown) and at each backend service's API (what's allowed).
Foundations chosen for future SaaS scale
These are in place from day one โ not deferred โ because they're cheap to build correctly now and expensive to retrofit once single-tenant assumptions are baked in.
- Tenancy: Keycloak Organizations model tenants;
scenario:*entitlement roles are plain realm roles assigned per-user (Keycloak's Organizations API has no org-scoped role endpoint). - Object storage: all datasets/models/documents live in SeaweedFS (S3-compatible), never on a service's local disk โ keeps services stateless and horizontally scalable.
- Scenario/entitlement registry:
platform-registryowns a Postgres schema (tenants/scenarios/entitlements);scenarios/*.yamlis only the human-editable seed format, not read directly by any other service. - Ingress: Traefik is the only container reachable from outside the host โ a 1:1 mapping
onto a Kubernetes Ingress later. A few services additionally publish a loopback-only port
(
platform-registry,qdrant,llm-gateway) purely so services running outside Docker (local, non-container dev) can still reach them directly; none of those three has auth strong enough to be safe on Traefik's public entrypoint, so they must never gain atraefik.enable=truelabel. infra/{postgres,keycloak,qdrant,seaweedfs,traefik}/: reserved per-service config directories โinfra/postgres/(a multi-database init script),infra/keycloak/(the declarativerealm-export.jsonbootstrap), andinfra/seaweedfs/(the generated S3 gateway credentials file) have content; the others' config is inline indocker-compose.yml(command args/env/labels) until each grows enough to warrant its own files.- Admin credential:
ADMIN_API_KEY(defaultai-circus-2026โ rotate before any real deployment) is a shared bearer token resolving to a fixedadmintenant, auto-granted access to every scenarioplatform-registryseeds โ a real, auditable entitlement row, not a bypass of the entitlement check.ENGINEERING_DEMO_API_KEYis the same mechanism scoped to a narrowerengineering-demotenant, entitled to only the engineering scenarios โ a template for adding more scoped demo tenants without touching Keycloak.
Shared code
Every backend service is generated via real cookiecutter generation against
ai-circus-template (see
scripts/new_service.sh), so each stays an independent uv project with its own
pyproject.toml/uv.lock/Dockerfile โ no monorepo-wide uv workspace. That template is itself
built on the conventions from ai-circus, my
Python best-practices reference repo. Common code (Keycloak token validation, SeaweedFS client,
entitlement-check client, scenario schema) lives in libs/shared (ai-circus-shared), added to
each service as a local non-editable uv path dependency.
LLM providers
llm-gateway execs the real LiteLLM proxy โ every consumer (assistant, rag-agent,
ui-react) calls its OpenAI-compatible API by model_name, never a provider SDK directly (see
services/llm-gateway/litellm_config.yaml for the routing table).
model_name | Provider | Key needed | Notes |
|---|---|---|---|
gemini-flash | Google Gemini | GOOGLE_API_KEY | Default free-tier pick |
gpt-4o-mini | OpenAI | OPENAI_API_KEY | |
claude-haiku | Anthropic | ANTHROPIC_API_KEY | Fast/cheap Claude tier |
deepseek-chat | DeepSeek | DEEPSEEK_API_KEY | |
groq-llama | GroqCloud | GROQ_API_KEY | Free tier, very low latency |
openrouter | OpenRouter | OPENROUTER_API_KEY | One key, many vendors |
azure-gpt4o | Azure OpenAI | AZURE_OPENAI_API_KEY + AZURE_OPENAI_API_BASE | Also edit the azure/<deployment> line in litellm_config.yaml |
llama3 | Ollama (local) | none | Optional, off by default โ see below |
ollama is not started by make up โ it's a real container with real RAM/disk cost, gated
behind the ollama compose profile. make ollama-up starts it and pulls a small model
automatically on first run.
Runtime key rotation from the browser isn't possible (this deployment doesn't run LiteLLM's
DB-backed proxy mode) โ a new key always means edit .env, then
docker compose up -d llm-gateway. Switching which already-configured provider is active,
though, is instant from Settings.
Adding a new scenario or service
- New backend service:
make new-service NAME=my-serviceโ wraps real cookiecutter generation fromai-circus-template, wires inlibs/shared, and adapts the Dockerfile for this repo's build-context conventions. Then add it todocker-compose.yml. - New scenario: add
scenarios/<slug>/scenario.yaml(seechurn/mpmfortabular_ml,ai_circus_referenceforconversational_rag,service_requestforassisted_form) with achat:block (context+sample_questions), restartplatform-registry(it seeds on startup), and create the matchingscenario:<slug>Keycloak realm role. No new container, no UI code โ the existingprediction/assistant,rag-agent, orform-agentinstance picks it up automatically, andui-reactrenders its form/chat generically. Anassisted_formscenario additionally needs aform:block (field catalog + validation rules) and, if it's RAG-classified likeservice_request, adocuments:/vector_store:block foretl-vectorizeto index.
Testing & CI
Every backend service is an independent ai-circus-template project with its own QA stack โ from
inside services/<name>/:
make check # pre-commit (ruff, pyrefly, gitleaks, checkmake) + settings.yaml/data_model.py drift check + pytest
make check-all (from the repo root) runs this for every service in sequence. ui-react has its
own npm run build (type-checks via tsc -b then builds via Vite).
.github/workflows/ci.yml runs the same checks per service as a matrix job, builds ui-react,
and validates docker-compose.yml, on every push/PR to main/develop.
Reserved for later (documented, not built)
A Helm chart (plain YAML + Kustomize exists instead โ see
Getting started > Kubernetes and k8s/README.md โ for local
dev-parity; Helm would only matter for a real multi-environment/production rollout), a
custom in-app admin screen, a task queue for on-demand tenant-triggered jobs, distributed
tracing/OpenTelemetry, evaluation tooling (Opik/Giskard),
voice/multimodal agents (Pipecat), per-tenant billing/metering, a shared cache (e.g. Redis)
for multi-replica deployments, and (optional) extracting embedded images out of uploaded PDFs
in the chat attachment flow โ today platform_registry.core.document_extraction only pulls
text/OCR out of a PDF, so a figure or diagram embedded in an otherwise text-native page never
reaches a vision-capable model. (The AG-UI/CopilotKit runtime bridge for ui-react's chat,
previously listed here, is built โ see ChatPanel.tsx/chatGenerativeUi.tsx.)
Why this exists
I'm Angel Martinez-Tenor (github.com/angelmtenor) โ for the last decade I've worked as a tech lead on data, analytics, ETL, ML, and GenAI projects across many clients and industries. This repo is my attempt to distill that experience into something open, reusable, and free for anyone to learn from or build on โ the same way open source has given a huge amount back to me over the years.
It's also an experiment in applying vibe coding to a methodology I've been refining and teaching for a long time, not a methodology invented for this repo:
- 2017 โ my first "agnostic" data-science project: one set of ML templates, reused across a wide variety of business scenarios instead of one-off notebooks per client.
- Later โ building blocks for AI ethics: explainability (SHAP/LIME) and interval/uncertainty predictions as first-class citizens, not an afterthought bolted on at the end.
- Later โ GenAI layered on top, with interactive dashboards for exploring models and data.
- Now โ taking that same agnostic-scenario philosophy into agentic, tool-calling GenAI, with
AG-UI (via CopilotKit) now wired end to end for
ui-react's chat โ streaming replies and real generative UI (the chatbot renders live charts/tables, not just prose).
The constant across all of it: build vendor-agnostic, scenario-driven foundations, keep them
open source, and let the plumbing (auth, storage, ingress, entitlements) be boring and correct so
the interesting part โ the ML/GenAI scenario itself โ can be swapped freely. ai-circus-framework
is one concrete example of what that foundation looks like today: mostly Python on the backend,
and โ new for this project โ vibe-coded microservices for everything around it (identity
provider wiring, the React frontend, infra).
Contributing
- AGENTS.md โ mandates for AI-assisted and human contributions alike.
- styleguide.md โ commit message conventions (Conventional Commits).
Author & license
Created and maintained by Angel Martinez-Tenor โ github.com/angelmtenor.
Licensed under the MIT License.
Disclaimer: I am currently Head of Data & AI at Getronics. A separate, Getronics-branded fork of this framework is being developed there for production-grade use.