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.

AI Open Framework scenario gallery


Table of contents


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.

Login screen

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.

Scenario gallery

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.

Data dashboard

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.

ML predictions with SHAP explanation

Global SHAP feature importance

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.

Settings โ€” LLM provider status

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.

Conversational RAG chat

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.

Assistant running a live prediction and rendering a SHAP chart and a data table via AG-UI

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.

Assisted form workspace โ€” the assistant fills in the Public Service Request Portal form live from conversation


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).

ScenarioKind / taskWhat it predictsSource
Customer Churn Prediction (churn)tabular_ml โ€” classificationBank customer churn riskKaggle โ€” Sonali Dasgupta
Machine Predictive Maintenance (mpm)tabular_ml โ€” classificationIndustrial machine failure riskKaggle โ€” AI4I 2020
Supply Chain Shipping ETA (supply_chain)tabular_ml โ€” regressionDays to deliveryAWS SageMaker workshop (synthetic)
Supermarket Weekly Sales (supermarket_sales)tabular_ml โ€” regressionWeekly department salesKaggle โ€” Walmart dataset
Electric Motor Speed (electric_motor)tabular_ml โ€” regressionMotor rotational speed (rpm)Kaggle โ€” Electric Motor Temperature
Building Energy Consumption (energy_building)tabular_ml โ€” regressionAppliance energy use (Wh)UCI โ€” Appliances Energy Prediction
AI Open Framework Reference Guide (ai_circus_reference)conversational_ragN/A โ€” agentic Q&A over this project's own dev/ML/GenAI reference notesOriginal content
Public Service Request Portal (service_request)assisted_formN/A โ€” the assistant fills out and classifies a service-request form live, from conversationOriginal 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:

OptionWhat 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 allRun 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.

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

http://aiopen.localhost

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-registry port-forward from step 3 above isn't running โ€” see k8s/README.md's "Design notes".

On Docker Compose, run make verify (or just make all again) to pinpoint which service isn't answering; the most common causes are: (1) you tested right after make up, before every container was actually ready โ€” make all/make verify wait for that, plain docker compose up -d doesn't; (2) postgres-data (or another) volume already existed from an earlier partial run, so its one-time init script never reran โ€” make reset-all fixes 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 than http://aiopen.localhost (e.g. plain http://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:

  1. 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/SECRET in .env) has no admin rights of its own โ€” grant its service-account user the manage-users/manage-organizations/manage-realm/manage-clients realm-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.
  2. Real users, and the scenario:<slug> realm roles derived from scenarios/*/scenario.yaml, are one command:
    make -C services/platform-registry provision-owner-user
    
    Set KEYCLOAK_OWNER_EMAIL/KEYCLOAK_OWNER_PASSWORD in .env first. Idempotent โ€” safe to re-run any time. It creates scenario:<slug> as plain realm roles for every scenario's role_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, see libs/shared's auth.py docstring), creates (or finds) an owner Organization and that Keycloak user, adds them to the Organization, assigns every scenario:* role directly to the user, and syncs the result into local entitlements โ€” so signing in through Keycloak's hosted page with that email lands on every scenario, the same as the ADMIN_API_KEY bypass. 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 the scenario:* 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.)

  1. Rotate (or blank, to disable the shortcut outright) ADMIN_API_KEY/ ENGINEERING_DEMO_API_KEY away from their shipped demo values, and confirm AUTH_DISABLED=false.
  2. 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
    
  3. Set DEPLOYMENT_TARGET=public in .env โ€” this arms every service's boot-time refusal to start if you missed step 1 (see libs/shared/src/ai_circus_shared/deployment_guard.py), so a mistake here is a startup crash with a clear message, not a silent hole.
  4. make check-public-ready sanity-checks all three steps above without starting/stopping anything, then deploy with the usual make up (or make 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.

AI Open Framework architecture diagram โ€” realistic, fully detailed view

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

AI Open Framework architecture diagram โ€” simplified view

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-registry owns a Postgres schema (tenants/scenarios/entitlements); scenarios/*.yaml is 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 a traefik.enable=true label.
  • infra/{postgres,keycloak,qdrant,seaweedfs,traefik}/: reserved per-service config directories โ€” infra/postgres/ (a multi-database init script), infra/keycloak/ (the declarative realm-export.json bootstrap), and infra/seaweedfs/ (the generated S3 gateway credentials file) have content; the others' config is inline in docker-compose.yml (command args/env/labels) until each grows enough to warrant its own files.
  • Admin credential: ADMIN_API_KEY (default ai-circus-2026 โ€” rotate before any real deployment) is a shared bearer token resolving to a fixed admin tenant, auto-granted access to every scenario platform-registry seeds โ€” a real, auditable entitlement row, not a bypass of the entitlement check. ENGINEERING_DEMO_API_KEY is the same mechanism scoped to a narrower engineering-demo tenant, 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_nameProviderKey neededNotes
gemini-flashGoogle GeminiGOOGLE_API_KEYDefault free-tier pick
gpt-4o-miniOpenAIOPENAI_API_KEY
claude-haikuAnthropicANTHROPIC_API_KEYFast/cheap Claude tier
deepseek-chatDeepSeekDEEPSEEK_API_KEY
groq-llamaGroqCloudGROQ_API_KEYFree tier, very low latency
openrouterOpenRouterOPENROUTER_API_KEYOne key, many vendors
azure-gpt4oAzure OpenAIAZURE_OPENAI_API_KEY + AZURE_OPENAI_API_BASEAlso edit the azure/<deployment> line in litellm_config.yaml
llama3Ollama (local)noneOptional, 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 from ai-circus-template, wires in libs/shared, and adapts the Dockerfile for this repo's build-context conventions. Then add it to docker-compose.yml.
  • New scenario: add scenarios/<slug>/scenario.yaml (see churn/mpm for tabular_ml, ai_circus_reference for conversational_rag, service_request for assisted_form) with a chat: block (context + sample_questions), restart platform-registry (it seeds on startup), and create the matching scenario:<slug> Keycloak realm role. No new container, no UI code โ€” the existing prediction/assistant, rag-agent, or form-agent instance picks it up automatically, and ui-react renders its form/chat generically. An assisted_form scenario additionally needs a form: block (field catalog + validation rules) and, if it's RAG-classified like service_request, a documents:/vector_store: block for etl-vectorize to 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.