Monorepo for the Alquist Insight platform
19
stars
17
commits
Python
primary language
Sep 7, 2026
updated
Alquist Insight is an open-source platform designed for building and deploying production-ready RAG (Retrieval-Augmented Generation) chatbots on private or cloud infrastructure.
Developed by the Alquist Research team (winners of the Amazon Alexa Prize Socialbot Grand Challenge), it provides a complete monorepo for managing knowledge bases, local LLM deployment, and conversational interfaces. It is specifically built to allow organizations to automate administrative tasks and customer support using their own data without relying on external cloud-based AI providers.
Key technical specs:
Alquist Insight consists of four FastAPI backend services, a shared Python module, and two frontend client apps that are developed in separate repositories and fetched as prebuilt tarballs at runtime.
| Component | Port | Description |
|---|---|---|
| alchemist | 9642 | Document conversion (Docling PDF to Markdown); stateless, called by Kronos |
| kronos | 9625 | Projects, knowledge bases, sessions/turns, resources; owns Mongo & storage |
| maestro | 8020 | Chatbot interaction, FSM dialogue, analytics; serves the client apps |
| ragnarok | 9696 | The RAG engine: chunking, embeddings, search, reranking, generation; owns the ES index |
| common | – | Shared module imported by every app: config, models, logging, API calls |
| admin | – | Admin console client app, served by Maestro at /admin/ |
| interactor | – | Chatbot client app, served by Maestro at / and /interactor/ |
browser ──► maestro ──► kronos ──► ragnarok ──► Elasticsearch / embedding & LLM models
│ │ │
│ │ └──► MongoDB (projects, sessions, turns, KB metadata)
│ │ └──► MinIO / Azure Blob (source documents, resources)
│ │ └──► alchemist (advanced PDF conversion)
│ └──► Elasticsearch (analytics, logs)
└──► keycloak (Admin console login)
Maestro never calls Ragnarok directly for RAG (only for search-result highlights); it goes through Kronos, which fetches the conversation history and project settings and forwards the request to Ragnarok. Ragnarok calls back into Kronos for one thing only: fetching the project's LLM prompts.
Inter-service calls use httpx/requests against the *_URL config values and authenticate with an X-Api-Key header
(Kronos, Alchemist) or an Authorization header (Ragnarok). All of these API call functions are shared and live in
common/common/services/{alchemist,kronos,ragnarok}.py.
The pipeline lives in ragnarok/ragnarok/rag.py:
Provider-specific implementations (embeddings, generation, reranking) are selected at runtime via Singleton-backed
factories (*Factory.get_model(provider=...)) keyed on ModelProvider. Supported providers: Cohere, JinaAI,
OpenAI (incl. Azure OpenAI), Triton, vLLM.
LLM prompts are not hardcoded — they live in a prompts.md resource file stored per project by Kronos, with a fallback
to the default file in resources/prompts.md.
| Service | Container ports | Purpose |
|---|---|---|
| Elasticsearch | 9200, 9300 | Vector index, BM25 index, highlight chunks, application logs |
| MongoDB | 27017 | Projects, knowledge base metadata, sessions, turns, migration locks |
| MinIO | 9000, 9001 | Object storage for source documents and resources (S3-compatible) |
| Keycloak | 8080 | Identity provider for the Admin console |
| PostgreSQL | 5432 | Keycloak's database |
| vLLM (embedding) | 8000 | Local embedding model server |
| vLLM (generation) | 8000 | Local LLM server |
| Triton | 8000, 8001, 8002 | Alternative local model server (disabled by default) |
Azure Blob Storage can be used instead of MinIO (STORAGE_TYPE=AZURE_BLOB_STORAGE).
alchemist/ # Alchemist app (Dockerfile, Pipfile, run.py, start.sh)
common/common/ # Shared module imported by all apps
kronos/ # Kronos app
maestro/ # Maestro app
ragnarok/ # Ragnarok app
resources/ # Default resource files: FSM dialogues, images, prompts.md
scripts/ # Deployment and maintenance scripts
config.env # Committed config template with safe defaults (NO SECRETS)
config.local.env # Local secrets & overrides (gitignored, not committed)
docker-compose.yaml # Full local deployment
Pipfile # Convenience union of all app dependencies for local development
Prerequisites:
Note for servers without an Nvidia GPU: Several services request GPU access in the
docker-compose.yamlfile (gpus: alland thedeploy.resources.reservations.devicesblock) — the vLLM services, which require a GPU, and Alchemist, which uses one only if available. Docker Compose cannot make a GPU reservation conditional, so these containers fail to start with a "could not select device driver" error on a machine with no Nvidia GPU (or without the Nvidia Container Toolkit installed). Comment out the GPU access lines of the affected services in thedocker-compose.yamlfile before deploying. Alchemist then automatically falls back to the CPU; the vLLM services should be disabled entirely and replaced with cloud models (see step 5 below).
Clone the repository:
git clone https://github.com/AlquistAI/insight.git
cd insight
Prepare a config.local.env file in the project root with the server configuration and secrets. The default/sample
values are provided in the config.env file. The servers will run even with the default configuration, but setting up
secrets etc. manually is strongly recommended. See Configuration for details.
Run the deployment script from the repository root:
./scripts/deployment-full.sh
The script will:
config.env & config.local.env files.
config.local.env file is created if it doesn't exist.config.local.env file.docker commands in case the current user doesn't have access to the Docker socket.docker compose services.docker-compose.yaml file (i.e. comment out the "vllm..." lines in the "services" section).data folder. Any subsequent start of the vLLM services
should only take a few minutes.docker ps command.The script is idempotent — every step checks whether it has already been completed and is skipped if so.
After the script finishes execution, you should be able to open the chatbot with the default "test" project in your
browser at http://localhost:8020/. Note that the generation model can still take some time to load.
Useful endpoints of a running deployment:
| URL | Description |
|---|---|
http://localhost:8020/ | Chatbot UI (default project) |
http://localhost:8020/admin/ | Admin console |
http://localhost:9642/docs | Alchemist Swagger UI |
http://localhost:9625/docs | Kronos Swagger UI |
http://localhost:8020/docs | Maestro Swagger UI |
http://localhost:9696/docs | Ragnarok Swagger UI |
http://localhost:8080/ | Keycloak admin console |
http://localhost:<port>/health | Healthcheck of any of the four apps |
# The env files are needed for the ${...} interpolation in docker-compose.yaml (see Configuration below)
DC="docker compose --env-file config.env --env-file config.local.env"
$DC down # stop the deployment (data is preserved)
./scripts/deployment-full.sh # start it again; skips already-finished steps
$DC logs -f kronos # follow the logs of a single service
$DC up -d --build kronos # rebuild & restart a single app after a code change
$DC restart maestro # restart without rebuilding (e.g. after a config change)
All state is persisted in the data folder on the host:
| Path | Content |
|---|---|
data/alchemist | Docling & HuggingFace model caches |
data/elasticsearch | Elasticsearch indices (vectors, highlights, logs) |
data/keycloak | Keycloak data directory |
data/maestro/frontend | Extracted frontend client apps |
data/minio | Uploaded source documents and resources |
data/mongo | MongoDB database files |
data/postgres | Keycloak's PostgreSQL database |
data/ragnarok/models | HuggingFace model cache |
data/vllm-*/models | vLLM model weights (large!) |
To reset a specific part of the deployment, stop the containers, delete the corresponding folder and start again. The
deployment script recreates the folders with the correct ownership. To wipe everything (including the downloaded
models), remove the whole data folder.
All config is read by common/common/config.py into two pydantic-settings singletons that are imported everywhere:
CONFIG (class Config) — runtime config required at startup: service URLs, secrets, Elastic/Mongo/MinIO connection
settings, feature flags. Fails fast on a missing required value.DF (class Defaults) — default RAG/model settings, env prefix DEFAULT_. These can be overridden per project.Env files are layered, with later files overriding earlier ones:
/config/config.local.env — used in Kubernetes deployments (mounted secret).config.env — committed to git, must never contain secrets. Holds the defaults for a local deployment.config.local.env — gitignored; put your real secrets and overrides here.Both config.env and config.local.env are passed to the app containers via env_file in docker-compose.yaml.
When adding a new setting, add it to the appropriate class in config.py and document its default in config.env.
Running
docker composedirectly:docker-compose.yamlalso interpolates a few variables (${...}) for the infrastructure services — the Keycloak and MinIO credentials. Docker Compose does not readconfig.env/config.local.envfor those on its own (it only looks at.env), so a baredocker compose up -dwould recreate Keycloak, PostgreSQL and MinIO with blank passwords.scripts/deployment-full.shexports the variables before calling compose; when running compose yourself, pass both files explicitly:docker compose --env-file config.env --env-file config.local.env up -d
Backend services — internal URLs (on the compose network) and API keys used for inter-service authentication:
ALCHEMIST_URL=http://alchemist:9642
ALCHEMIST_API_KEY=<secret>
KRONOS_URL=http://kronos:9625
KRONOS_API_KEY=<secret>
MAESTRO_URL=http://maestro:8020
MAESTRO_API_KEY=<secret>
RAGNAROK_URL=http://ragnarok:9696
RAGNAROK_API_KEY=<secret>
External URLs — the addresses the browser uses. They are baked into the frontend client config at Maestro startup, so they must be reachable from the client machine (see External Access):
KEYCLOAK_URL_EXTERNAL=http://localhost:8080
KRONOS_URL_EXTERNAL=http://localhost:9625
MAESTRO_URL_EXTERNAL=http://localhost:8020
Infrastructure services and their credentials:
ES_URL=http://elasticsearch:9200
# security is disabled in the local Elasticsearch container
ES_PASSWORD=NOT_USED
# internal URL, used by Kronos to fetch the realm public key
KEYCLOAK_URL=http://keycloak:8080
KEYCLOAK_REALM=alquist
KEYCLOAK_CLIENT_ID=alquist-insight-development
KEYCLOAK_ADMIN_PASSWORD=<secret>
KEYCLOAK_DB_PASSWORD=<secret>
MINIO_URL=http://minio:9000
MINIO_ROOT_PASSWORD=<secret>
MINIO_SECRET_KEY="${MINIO_ROOT_PASSWORD}"
MONGO_CONN_STR=mongodb://mongo:27017
Feature flags and default models:
# send the conversation history to the LLM
CONTEXT_ENABLED=false
# number of latest turns used as context (0 = unlimited)
CONTEXT_WINDOW_SIZE=5
# ship logs of all backend services to Elasticsearch
ES_LOGGING_ENABLED=false
DEFAULT_LANG=en-US
DEFAULT_PROVIDER_EMB=vLLM
DEFAULT_MODEL_EMB=Qwen/Qwen3-Embedding-0.6B
DEFAULT_BASE_URL_EMB=http://vllm-embedding:8000/v1
DEFAULT_PROVIDER_LLM=vLLM
DEFAULT_MODEL_LLM=Qwen/Qwen3-30B-A3B
DEFAULT_BASE_URL_LLM=http://vllm-generation:8000/v1
The following secrets have no usable default and should always be set in config.local.env:
ALCHEMIST_API_KEY, KRONOS_API_KEY, MAESTRO_API_KEY, RAGNAROK_API_KEY, KEYCLOAK_ADMIN_PASSWORD,
KEYCLOAK_DB_PASSWORD, MINIO_ROOT_PASSWORD.
Comment out the vllm-embedding and vllm-generation entries in the services section of docker-compose.yaml.
Set the provider credentials and defaults in config.local.env, e.g. for OpenAI:
# or AzureOpenAI (then OPENAI_ENDPOINT is also required)
OPENAI_TYPE=OpenAI
OPENAI_KEY=<secret>
DEFAULT_PROVIDER_EMB=OpenAI
DEFAULT_MODEL_EMB=text-embedding-3-large
DEFAULT_BASE_URL_EMB=None
DEFAULT_PROVIDER_LLM=OpenAI
DEFAULT_MODEL_LLM=gpt-4o
DEFAULT_BASE_URL_LLM=None
Optionally enable reranking with COHERE_KEY (DEFAULT_PROVIDER_RERANK=Cohere) or JINAAI_KEY
(DEFAULT_PROVIDER_RERANK=JinaAI).
Restart the deployment. Note that changing the embedding model invalidates existing vector indices — the knowledge
base has to be re-uploaded (or a new ES_INDEX_EMBEDDINGS used).
If some of the default ports are already taken on your server, you have to change the published (host) ports in
docker-compose.yaml and then update the matching *_URL_EXTERNAL values in config.local.env.
Check which ports are already in use:
ss -tulpn | grep -E ':(8020|8080|9625|9642|9696)\b'
Change the host port in docker-compose.yaml. The service definitions live in the x-services section as YAML
anchors and are only referenced from the services section, so this is where the ports: blocks are. Every
published port is written as "127.0.0.1:<host_port>:<container_port>" — only ever change the middle (host)
value:
x-services:
kronos:
ports:
- "127.0.0.1:19625:9625" # host port 19625 -> container port 9625
Update the external URLs in config.local.env so that the frontend clients (and the deployment script) use the
new ports:
KRONOS_URL_EXTERNAL=http://localhost:19625
MAESTRO_URL_EXTERNAL=http://localhost:18020
KEYCLOAK_URL_EXTERNAL=http://localhost:18080
Do not change the internal *_URL values (KRONOS_URL, ES_URL, KEYCLOAK_URL, MINIO_URL,
MONGO_CONN_STR, DEFAULT_BASE_URL_*, …). Those resolve service names on the compose network and always use the
container ports, which are unaffected by the host-side mapping.
Restart the deployment (docker compose down && ./scripts/deployment-full.sh). Maestro regenerates the frontend
client config from the *_URL_EXTERNAL values on every start.
Notes:
ports: blocks
commented out — they are only reachable from within the compose network. Uncomment a mapping (and pick a free host
port) if you need direct access from the host, e.g. for debugging.127.0.0.1, so they are not reachable from outside the server. That is intentional —
external access should go through a reverse proxy (see Reverse Proxy & HTTPS).<APP>_CONTAINER_PORT variable in config.local.env (read by the app's start.sh and by config.py as
<APP>_PORT), and update the container side of the ports: mapping, the healthcheck URL and the internal
<APP>_URL in docker-compose.yaml/config.local.env to match.The Admin console authenticates users against Keycloak using the OIDC authorization code flow; Kronos then validates the
resulting JWT (common/common/api/security_jwt.py). A fresh Keycloak container has no realm/client/user configured, so
this has to be done once manually before the first Admin console login.
Three config values have to match the Keycloak configuration exactly (they are read by both the backend and the frontend clients):
| Config variable | Keycloak field | Default value |
|---|---|---|
KEYCLOAK_REALM | Realm → Realm name | alquist |
KEYCLOAK_CLIENT_ID | Client → Client ID | alquist-insight-development |
KEYCLOAK_URL_EXTERNAL | – (URL the browser uses) | http://localhost:8080 |
KEYCLOAK_URL | – (URL the backend services use) | http://keycloak:8080 |
The Keycloak UI distinguishes between an object's ID/name (used in URLs and tokens — this is what the config variables refer to) and its display name (a cosmetic label). Always fill in the Realm name and Client ID fields; the Display name / Name fields can be left empty or set to anything.
Open http://localhost:8080/ and click Administration console. Log in with the credentials from your config:
KEYCLOAK_ADMIN_USER (default admin)KEYCLOAK_ADMIN_PASSWORD (sample value admin123 — change it in config.local.env)alquist (must equal KEYCLOAK_REALM; it appears in the OIDC URLs and is case-sensitive).Make sure the newly created realm (not master) is selected for the following steps.
OpenID Connectalquist-insight-development (must equal KEYCLOAK_CLIENT_ID)Off — the Admin console is a browser SPA, so it must be a public client
(authorization code flow with PKCE, no client secret)http://localhost:8020 with your MAESTRO_URL_EXTERNAL value:
http://localhost:8020 (optional)/admin/ (optional)http://localhost:8020/admin/*http://localhost:8020/admin/** (allow all origins; alternatively set the exact http://localhost:8020 origin)Leave the default client scopes (acr, basic, email, profile, roles, web-origins) assigned. The roles
scope adds the account audience to issued access tokens, which is what Kronos expects when validating the JWT.
Off (otherwise
the user is forced into a password-change screen that the Admin console cannot render natively).You should now be able to log in to the Admin console at http://localhost:8020/admin/ using these credentials.
ToDo: Do this setup as part of the deployment script.
If you used different values than the defaults, set KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID in config.local.env and
restart Maestro so it regenerates the frontend client config:
docker compose restart maestro
Keycloak derives the absolute URLs it publishes (the OIDC issuer, the authorization/token endpoints in its discovery
document, the admin console asset URLs, and the redirect it sends the browser to) from the incoming request. Behind a
reverse proxy the request it receives is the internal, plain-HTTP one. Without extra configuration it advertises
http:// URLs built from whatever host name the proxy forwards — which the browser either cannot reach or refuses to
use — and it may reject the request outright because it considers the connection insecure.
To fix that, add these two environment variables to the keycloak service in docker-compose.yaml:
x-services:
keycloak:
environment:
# ... existing variables ...
KC_HOSTNAME: "keycloak.example.com" # public base URL of Keycloak
KC_PROXY_HEADERS: "xforwarded" # trust X-Forwarded-* headers from the proxy
KC_HOSTNAME pins the public base URL used in all generated URLs and in the token issuer.KC_PROXY_HEADERS=xforwarded makes Keycloak trust the X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-For
headers, so it knows the original request arrived over HTTPS. Your proxy must actually set those headers (the
nginx config below does) — only enable this when Keycloak is not directly reachable from outside.Then set the matching external URL in config.local.env and recreate the containers:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
docker compose --env-file config.env --env-file config.local.env up -d
Keep KEYCLOAK_URL (the internal URL used by Kronos to fetch the realm's public key) pointing at
http://keycloak:8080 — it does not go through the proxy.
Remember to update the client's Valid redirect URIs, Valid post logout redirect URIs and Web origins in
Keycloak to the public Maestro URL (e.g. https://insight.example.com/admin/*).
The compose file runs Keycloak with the
start-devcommand, which enables plain HTTP and relaxes hostname checks. This is convenient for a self-hosted deployment behind a TLS-terminating proxy, but it is not a hardened production setup — for production, switch tostartand configure TLS,KC_HOSTNAME_STRICTand a proper admin account.
The Admin console and the chatbot Interactor are built in separate repositories and published as dist.tar.gz generic
packages. At startup, Maestro (maestro/maestro/utils/frontend.py):
PACKAGE_REGISTRY_TOKEN) and extracts it into /home/app/frontend, which is
bind-mounted to data/maestro/frontend on the host — but only if the client directory does not exist yet.dist/config.json with the current external URLs, Keycloak realm/client ID and default
project — this happens on every start.Relevant config:
ADMIN_CONSOLE_PACKAGE_NAME=admin
ADMIN_CONSOLE_VERSION=latest
INTERACTOR_PACKAGE_NAME=chatbot_js
INTERACTOR_VERSION=latest
PROJECT_ID=test
PROJECT_TITLE="Test Project"
Because the download is skipped when the extracted files are already present, a new client build is not picked up automatically on restart. To force a re-fetch, delete the frontend data folder:
docker compose down
sudo rm -rf data/maestro/frontend # the folder is owned by the container user (uid 999)
./scripts/deployment-full.sh # recreates the folder with the right ownership and starts everything
If you prefer not to run the full deployment script:
docker compose stop maestro
sudo rm -rf data/maestro/frontend
mkdir -p data/maestro/frontend
sudo chown -R 999:999 data/maestro
docker compose --env-file config.env --env-file config.local.env up -d maestro
Notes:
ADMIN_CONSOLE_VERSION / INTERACTOR_VERSION are left at latest (the default), this pulls the newest
published build. If a specific version is pinned, that exact version is downloaded again instead.data/maestro/frontend/admin or
data/maestro/frontend/interactor).docker compose restart maestro is enough, since config.json is rewritten on every start.docker compose logs -f maestro) to confirm the download succeeded; a failed fetch prevents
the app from starting.For creating your own projects and uploading knowledge base documents, you can either use the Kronos API or the Admin console UI.
You can open the chatbot for a custom project at http://localhost:8020/?project_id=<project_id>.
The Admin console is available at http://localhost:8020/admin/ and requires a Keycloak login (see
Keycloak Setup). It allows creating new projects and managing the related knowledge base in a similar
fashion as a regular file explorer, as well as editing project settings, prompts and the dialogue definition.
The easiest way to use the API is through the Swagger UI at http://localhost:9625/docs, which also serves as the API
documentation. Authenticate with the X-Api-Key header using your KRONOS_API_KEY value (a Keycloak JWT in the
Authorization header works as well).
Use POST /projects/ to create a project, then one of the knowledge base endpoints to add documents:
| Endpoint | Description |
|---|---|
POST /knowledge_base/file/ | Upload a single file (pdf, docx, pptx, xlsx, md, txt, html) |
POST /knowledge_base/file/bulk | Upload multiple files at once |
POST /knowledge_base/pdf/advanced | PDF upload with Docling conversion via Alchemist (streamed progress) |
POST /knowledge_base/file/marker | Upload a pre-converted paginated Markdown + its source PDF |
POST /knowledge_base/url/ | Fetch and index the content of a single URL |
POST /knowledge_base/url/bulk | Fetch and index multiple URLs |
POST /knowledge_base/url/crawl | Crawl a seed URL and index all discovered pages/files |
Common query parameters: project_id (required), kb_id, name, description, language, custom_metadata (JSON
string) and enable_highlights (build the extra chunk index needed for highlighting answers in the source document).
See the CREATE EXAMPLE PROJECT IF MISSING section of scripts/deployment-full.sh for a working curl example.
POST /projects/{project_id}/nlp/rag/ (Kronos) — full RAG answer for a query.POST /projects/{project_id}/nlp/rag/stream (Kronos) — the same, streamed token by token.POST /projects/{project_id}/query/rag (Maestro) — the endpoint used by the chatbot client.If you want external users to be able to access the Admin/Chatbot UI, you will need to expose the following services to them:
8020) — serves both client apps and the chatbot API.9625) — required for Admin console access only.8080) — required for Admin console access only.Ragnarok and Alchemist are internal-only and should not be exposed.
The public URLs then need to be set in config.local.env using the <NAME>_URL_EXTERNAL variables, because they are
baked into the frontend client configuration:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
KRONOS_URL_EXTERNAL=https://kronos.example.com
MAESTRO_URL_EXTERNAL=https://insight.example.com
The Admin console uses several browser APIs that are only available in a secure context, meaning the services have to
be served over https. For quick testing this requirement can be bypassed by telling your browser to treat the origins
as secure (in Chrome: chrome://flags/#unsafely-treat-insecure-origin-as-secure), but the proper solution is a
TLS-terminating reverse proxy as described below.
This section sets up nginx as a TLS-terminating reverse proxy in front of Maestro, Kronos and Keycloak, with free Let's Encrypt certificates issued and auto-renewed by certbot. The example uses three subdomains:
| Public URL | Upstream |
|---|---|
https://insight.example.com | 127.0.0.1:8020 (Maestro) |
https://kronos.example.com | 127.0.0.1:9625 (Kronos) |
https://keycloak.example.com | 127.0.0.1:8080 (Keycloak) |
Prerequisites:
A/AAAA records for all three subdomains pointing at the server's public IP.80 and 443 open in the firewall / cloud security group.127.0.0.1 (the default). If nginx runs on a different host,
change the bind address of the published ports in docker-compose.yaml from 127.0.0.1 to the interface reachable by
the proxy, and firewall them accordingly.sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
First create the shared proxy header snippet /etc/nginx/snippets/insight-proxy.conf. The X-Forwarded-* headers are
what makes KC_PROXY_HEADERS=xforwarded work for Keycloak; keeping them in a snippet lets all three server blocks stay
identical:
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Then create the site itself, /etc/nginx/sites-available/insight.conf:
server {
server_name keycloak.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
include snippets/insight-proxy.conf;
}
}
server {
server_name kronos.example.com;
listen 80;
# Uploaded documents can be large; RAG streaming responses can take minutes.
client_max_body_size 256m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:9625;
proxy_buffering off; # do not buffer server-sent events (streamed chatbot answers)
include snippets/insight-proxy.conf;
}
}
server {
server_name insight.example.com;
listen 80;
# Uploaded documents can be large; RAG streaming responses can take minutes.
client_max_body_size 256m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:8020;
proxy_buffering off; # do not buffer server-sent events (streamed chatbot answers)
include snippets/insight-proxy.conf;
}
}
Enable the site and reload nginx:
sudo ln -s /etc/nginx/sites-available/insight.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Certbot's nginx plugin validates the domains over HTTP, obtains the certificates and rewrites the site config to redirect HTTP to HTTPS and serve TLS on port 443:
sudo certbot --nginx \
--cert-name insight \
-d keycloak.example.com \
-d kronos.example.com \
-d insight.example.com
Renewal is handled automatically by the certbot.timer systemd unit installed with the package. Verify it:
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
In config.local.env:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
KRONOS_URL_EXTERNAL=https://kronos.example.com
MAESTRO_URL_EXTERNAL=https://insight.example.com
In docker-compose.yaml, add the proxy-related variables to the keycloak service (see
Keycloak Behind a Reverse Proxy):
KC_HOSTNAME: "keycloak.example.com"
KC_PROXY_HEADERS: "xforwarded"
In the Keycloak admin console, update the client's Valid redirect URIs and Valid post logout redirect URIs to
https://insight.example.com/admin/*.
Finally recreate the containers so the new configuration is applied:
docker compose --env-file config.env --env-file config.local.env up -d
The Admin console should now be reachable at https://insight.example.com/admin/ and the chatbot at
https://insight.example.com/.
Prerequisites:
pyenv for managing multiple Python versions on your machine).pipenv Python package manager.You can install the Python requirements for all apps using pipenv (omit the dev flag for purely runtime dependencies):
pipenv install --dev
The shared common module is not a Python package installed by pipenv — it is a plain module directory that every app
imports directly. Its dependencies are listed in the ## Common module dependencies ## section of each Pipfile. To
make it importable, the /common folder has to be on the Python path:
export PYTHONPATH="$PWD/common"
Prepare a config.local.env configuration file (see Configuration for details). Note that when
running an app directly on the host, the service URLs must point at localhost and the published host ports instead of
the compose service names, e.g. MONGO_CONN_STR=mongodb://localhost:27017 — which also means the corresponding ports:
mappings have to be uncommented in docker-compose.yaml.
You can run the individual apps using the provided run.py scripts, e.g.:
PYTHONPATH="$PWD/common" pipenv run python kronos/run.py
There are no automated tests in this repository. Validate changes by running the relevant service and exercising its
Swagger UI at /docs.
Each app has its own Pipfile, used to build its Docker image; the root Pipfile is a convenience union of all
dependencies for local development only. Every Pipfile splits [packages] into a ## Common module dependencies ##
group (what common/common imports, limited to the parts the app actually uses) and a ## Component dependencies ##
group. When adding an import to common, add the dependency to the common group of every Pipfile that needs it
(sort_pipfile is disabled to keep the grouping intact) and re-run pipenv lock there. The common module itself is
copied into the image in the Dockerfile's runtime stage, so changing it does not rebuild the dependency layers.
This section describes the recommended way how to work with this monorepo in the PyCharm IDE.
/common folder and other app folders (/alchemist, /kronos, etc.) as Sources Root. This makes both the
common module and the app modules importable without setting PYTHONPATH manually.cd kronos/ && pipenv install --dev.Settings -> Python -> Interpreter.Add Interpreter -> Add Local Interpreter... -> Select existing.~/.local/share/virtualenvs/<name>-<hash>/bin/python.# -*- coding: utf-8 -*- and an rST-style module docstring: the dotted module path, a
~~~ underline of matching length, then a one-line summary.:param x:, :return:) and are parsed by
common/common/utils/swagger.py to populate the Swagger UI descriptions — keep the format intact. Mark
logging-only/unused endpoint params with # noqa.@error_handler / @error_handler_async (from common.utils.api), which
maps domain exceptions (common/common/utils/exceptions.py) to HTTP status codes via EXC_TO_STATUS. Raise those
exceptions rather than HTTPException for domain errors. Routers are assembled in each app's api/router.py.common.models.base.CustomBaseModel (alias-aware, validate-on-assign). Cross-service
request/response models live in common/common/models/api_{kronos,maestro,ragnarok}.py.model_version (VER_* constant).
Bumping a model means adding a migration branch in kronos/kronos/prestart.py, which runs under a Mongo lock before
Kronos starts.common.core.get_component_logger(); never instantiate one. Structured fields go in
extra={...}. Each app sets up its component logger in its __init__.py.Singleton / SingletonABC metaclass from common.utils.singleton (identity
keyed on init args).prompts resource file (prompts.md), stored per project by
Kronos with a fallback to the default file (resources/prompts.md). Ragnarok fetches them via
common/common/services/kronos.py (TTL cache keyed on project & session ID) and parses them with
common.utils.prompts.parse_prompts; Kronos validates uploads the same way. Adding a prompt means adding a field to
common.models.prompts.Prompts, its runtime variables to PROMPT_VARIABLES, and a ## <prompt_name> section to
resources/prompts.md.| Script | Description |
|---|---|
scripts/deployment-full.sh | Full local Docker deployment (build, start, seed the test project) |
scripts/find_orphaned_resources.py | Report resources not belonging to any project / knowledge base |
scripts/migrate_es_index.py | Copy an Elasticsearch index to another (already created) index |
scripts/marker_pdf/ | Standalone marker-pdf PDF pipeline (see its own README.md) |
could not select device driver ... with capabilities: [[gpu]] — the host has no Nvidia GPU or no Nvidia
Container Toolkit. Comment out the GPU access lines as described in
Local Docker Deployment.data subfolder ownership is
wrong. Re-run ./scripts/deployment-full.sh, or fix it manually:
sudo chown -R 1000:1000 data/{elasticsearch,keycloak} and sudo chown -R 999:999 data/{alchemist,maestro,ragnarok}.ValidationError — a required config value is missing or invalid.
CONFIG fails fast; the error message names the offending variable.docker compose logs -f vllm-generation and docker ps (health status).MAESTRO_URL_EXTERNAL, or KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID do not match the
Keycloak configuration. See Keycloak Setup.KEYCLOAK_URL
is reachable from the Kronos container and that the client's default scopes (incl. roles) are still assigned.16 commits
1 commits
Python
97.6%
Shell
1.5%
Monorepo for the Alquist Insight platform
19
stars
17
commits
Python
primary language
Sep 7, 2026
updated
Alquist Insight is an open-source platform designed for building and deploying production-ready RAG (Retrieval-Augmented Generation) chatbots on private or cloud infrastructure.
Developed by the Alquist Research team (winners of the Amazon Alexa Prize Socialbot Grand Challenge), it provides a complete monorepo for managing knowledge bases, local LLM deployment, and conversational interfaces. It is specifically built to allow organizations to automate administrative tasks and customer support using their own data without relying on external cloud-based AI providers.
Key technical specs:
Alquist Insight consists of four FastAPI backend services, a shared Python module, and two frontend client apps that are developed in separate repositories and fetched as prebuilt tarballs at runtime.
| Component | Port | Description |
|---|---|---|
| alchemist | 9642 | Document conversion (Docling PDF to Markdown); stateless, called by Kronos |
| kronos | 9625 | Projects, knowledge bases, sessions/turns, resources; owns Mongo & storage |
| maestro | 8020 | Chatbot interaction, FSM dialogue, analytics; serves the client apps |
| ragnarok | 9696 | The RAG engine: chunking, embeddings, search, reranking, generation; owns the ES index |
| common | – | Shared module imported by every app: config, models, logging, API calls |
| admin | – | Admin console client app, served by Maestro at /admin/ |
| interactor | – | Chatbot client app, served by Maestro at / and /interactor/ |
browser ──► maestro ──► kronos ──► ragnarok ──► Elasticsearch / embedding & LLM models
│ │ │
│ │ └──► MongoDB (projects, sessions, turns, KB metadata)
│ │ └──► MinIO / Azure Blob (source documents, resources)
│ │ └──► alchemist (advanced PDF conversion)
│ └──► Elasticsearch (analytics, logs)
└──► keycloak (Admin console login)
Maestro never calls Ragnarok directly for RAG (only for search-result highlights); it goes through Kronos, which fetches the conversation history and project settings and forwards the request to Ragnarok. Ragnarok calls back into Kronos for one thing only: fetching the project's LLM prompts.
Inter-service calls use httpx/requests against the *_URL config values and authenticate with an X-Api-Key header
(Kronos, Alchemist) or an Authorization header (Ragnarok). All of these API call functions are shared and live in
common/common/services/{alchemist,kronos,ragnarok}.py.
The pipeline lives in ragnarok/ragnarok/rag.py:
Provider-specific implementations (embeddings, generation, reranking) are selected at runtime via Singleton-backed
factories (*Factory.get_model(provider=...)) keyed on ModelProvider. Supported providers: Cohere, JinaAI,
OpenAI (incl. Azure OpenAI), Triton, vLLM.
LLM prompts are not hardcoded — they live in a prompts.md resource file stored per project by Kronos, with a fallback
to the default file in resources/prompts.md.
| Service | Container ports | Purpose |
|---|---|---|
| Elasticsearch | 9200, 9300 | Vector index, BM25 index, highlight chunks, application logs |
| MongoDB | 27017 | Projects, knowledge base metadata, sessions, turns, migration locks |
| MinIO | 9000, 9001 | Object storage for source documents and resources (S3-compatible) |
| Keycloak | 8080 | Identity provider for the Admin console |
| PostgreSQL | 5432 | Keycloak's database |
| vLLM (embedding) | 8000 | Local embedding model server |
| vLLM (generation) | 8000 | Local LLM server |
| Triton | 8000, 8001, 8002 | Alternative local model server (disabled by default) |
Azure Blob Storage can be used instead of MinIO (STORAGE_TYPE=AZURE_BLOB_STORAGE).
alchemist/ # Alchemist app (Dockerfile, Pipfile, run.py, start.sh)
common/common/ # Shared module imported by all apps
kronos/ # Kronos app
maestro/ # Maestro app
ragnarok/ # Ragnarok app
resources/ # Default resource files: FSM dialogues, images, prompts.md
scripts/ # Deployment and maintenance scripts
config.env # Committed config template with safe defaults (NO SECRETS)
config.local.env # Local secrets & overrides (gitignored, not committed)
docker-compose.yaml # Full local deployment
Pipfile # Convenience union of all app dependencies for local development
Prerequisites:
Note for servers without an Nvidia GPU: Several services request GPU access in the
docker-compose.yamlfile (gpus: alland thedeploy.resources.reservations.devicesblock) — the vLLM services, which require a GPU, and Alchemist, which uses one only if available. Docker Compose cannot make a GPU reservation conditional, so these containers fail to start with a "could not select device driver" error on a machine with no Nvidia GPU (or without the Nvidia Container Toolkit installed). Comment out the GPU access lines of the affected services in thedocker-compose.yamlfile before deploying. Alchemist then automatically falls back to the CPU; the vLLM services should be disabled entirely and replaced with cloud models (see step 5 below).
Clone the repository:
git clone https://github.com/AlquistAI/insight.git
cd insight
Prepare a config.local.env file in the project root with the server configuration and secrets. The default/sample
values are provided in the config.env file. The servers will run even with the default configuration, but setting up
secrets etc. manually is strongly recommended. See Configuration for details.
Run the deployment script from the repository root:
./scripts/deployment-full.sh
The script will:
config.env & config.local.env files.
config.local.env file is created if it doesn't exist.config.local.env file.docker commands in case the current user doesn't have access to the Docker socket.docker compose services.docker-compose.yaml file (i.e. comment out the "vllm..." lines in the "services" section).data folder. Any subsequent start of the vLLM services
should only take a few minutes.docker ps command.The script is idempotent — every step checks whether it has already been completed and is skipped if so.
After the script finishes execution, you should be able to open the chatbot with the default "test" project in your
browser at http://localhost:8020/. Note that the generation model can still take some time to load.
Useful endpoints of a running deployment:
| URL | Description |
|---|---|
http://localhost:8020/ | Chatbot UI (default project) |
http://localhost:8020/admin/ | Admin console |
http://localhost:9642/docs | Alchemist Swagger UI |
http://localhost:9625/docs | Kronos Swagger UI |
http://localhost:8020/docs | Maestro Swagger UI |
http://localhost:9696/docs | Ragnarok Swagger UI |
http://localhost:8080/ | Keycloak admin console |
http://localhost:<port>/health | Healthcheck of any of the four apps |
# The env files are needed for the ${...} interpolation in docker-compose.yaml (see Configuration below)
DC="docker compose --env-file config.env --env-file config.local.env"
$DC down # stop the deployment (data is preserved)
./scripts/deployment-full.sh # start it again; skips already-finished steps
$DC logs -f kronos # follow the logs of a single service
$DC up -d --build kronos # rebuild & restart a single app after a code change
$DC restart maestro # restart without rebuilding (e.g. after a config change)
All state is persisted in the data folder on the host:
| Path | Content |
|---|---|
data/alchemist | Docling & HuggingFace model caches |
data/elasticsearch | Elasticsearch indices (vectors, highlights, logs) |
data/keycloak | Keycloak data directory |
data/maestro/frontend | Extracted frontend client apps |
data/minio | Uploaded source documents and resources |
data/mongo | MongoDB database files |
data/postgres | Keycloak's PostgreSQL database |
data/ragnarok/models | HuggingFace model cache |
data/vllm-*/models | vLLM model weights (large!) |
To reset a specific part of the deployment, stop the containers, delete the corresponding folder and start again. The
deployment script recreates the folders with the correct ownership. To wipe everything (including the downloaded
models), remove the whole data folder.
All config is read by common/common/config.py into two pydantic-settings singletons that are imported everywhere:
CONFIG (class Config) — runtime config required at startup: service URLs, secrets, Elastic/Mongo/MinIO connection
settings, feature flags. Fails fast on a missing required value.DF (class Defaults) — default RAG/model settings, env prefix DEFAULT_. These can be overridden per project.Env files are layered, with later files overriding earlier ones:
/config/config.local.env — used in Kubernetes deployments (mounted secret).config.env — committed to git, must never contain secrets. Holds the defaults for a local deployment.config.local.env — gitignored; put your real secrets and overrides here.Both config.env and config.local.env are passed to the app containers via env_file in docker-compose.yaml.
When adding a new setting, add it to the appropriate class in config.py and document its default in config.env.
Running
docker composedirectly:docker-compose.yamlalso interpolates a few variables (${...}) for the infrastructure services — the Keycloak and MinIO credentials. Docker Compose does not readconfig.env/config.local.envfor those on its own (it only looks at.env), so a baredocker compose up -dwould recreate Keycloak, PostgreSQL and MinIO with blank passwords.scripts/deployment-full.shexports the variables before calling compose; when running compose yourself, pass both files explicitly:docker compose --env-file config.env --env-file config.local.env up -d
Backend services — internal URLs (on the compose network) and API keys used for inter-service authentication:
ALCHEMIST_URL=http://alchemist:9642
ALCHEMIST_API_KEY=<secret>
KRONOS_URL=http://kronos:9625
KRONOS_API_KEY=<secret>
MAESTRO_URL=http://maestro:8020
MAESTRO_API_KEY=<secret>
RAGNAROK_URL=http://ragnarok:9696
RAGNAROK_API_KEY=<secret>
External URLs — the addresses the browser uses. They are baked into the frontend client config at Maestro startup, so they must be reachable from the client machine (see External Access):
KEYCLOAK_URL_EXTERNAL=http://localhost:8080
KRONOS_URL_EXTERNAL=http://localhost:9625
MAESTRO_URL_EXTERNAL=http://localhost:8020
Infrastructure services and their credentials:
ES_URL=http://elasticsearch:9200
# security is disabled in the local Elasticsearch container
ES_PASSWORD=NOT_USED
# internal URL, used by Kronos to fetch the realm public key
KEYCLOAK_URL=http://keycloak:8080
KEYCLOAK_REALM=alquist
KEYCLOAK_CLIENT_ID=alquist-insight-development
KEYCLOAK_ADMIN_PASSWORD=<secret>
KEYCLOAK_DB_PASSWORD=<secret>
MINIO_URL=http://minio:9000
MINIO_ROOT_PASSWORD=<secret>
MINIO_SECRET_KEY="${MINIO_ROOT_PASSWORD}"
MONGO_CONN_STR=mongodb://mongo:27017
Feature flags and default models:
# send the conversation history to the LLM
CONTEXT_ENABLED=false
# number of latest turns used as context (0 = unlimited)
CONTEXT_WINDOW_SIZE=5
# ship logs of all backend services to Elasticsearch
ES_LOGGING_ENABLED=false
DEFAULT_LANG=en-US
DEFAULT_PROVIDER_EMB=vLLM
DEFAULT_MODEL_EMB=Qwen/Qwen3-Embedding-0.6B
DEFAULT_BASE_URL_EMB=http://vllm-embedding:8000/v1
DEFAULT_PROVIDER_LLM=vLLM
DEFAULT_MODEL_LLM=Qwen/Qwen3-30B-A3B
DEFAULT_BASE_URL_LLM=http://vllm-generation:8000/v1
The following secrets have no usable default and should always be set in config.local.env:
ALCHEMIST_API_KEY, KRONOS_API_KEY, MAESTRO_API_KEY, RAGNAROK_API_KEY, KEYCLOAK_ADMIN_PASSWORD,
KEYCLOAK_DB_PASSWORD, MINIO_ROOT_PASSWORD.
Comment out the vllm-embedding and vllm-generation entries in the services section of docker-compose.yaml.
Set the provider credentials and defaults in config.local.env, e.g. for OpenAI:
# or AzureOpenAI (then OPENAI_ENDPOINT is also required)
OPENAI_TYPE=OpenAI
OPENAI_KEY=<secret>
DEFAULT_PROVIDER_EMB=OpenAI
DEFAULT_MODEL_EMB=text-embedding-3-large
DEFAULT_BASE_URL_EMB=None
DEFAULT_PROVIDER_LLM=OpenAI
DEFAULT_MODEL_LLM=gpt-4o
DEFAULT_BASE_URL_LLM=None
Optionally enable reranking with COHERE_KEY (DEFAULT_PROVIDER_RERANK=Cohere) or JINAAI_KEY
(DEFAULT_PROVIDER_RERANK=JinaAI).
Restart the deployment. Note that changing the embedding model invalidates existing vector indices — the knowledge
base has to be re-uploaded (or a new ES_INDEX_EMBEDDINGS used).
If some of the default ports are already taken on your server, you have to change the published (host) ports in
docker-compose.yaml and then update the matching *_URL_EXTERNAL values in config.local.env.
Check which ports are already in use:
ss -tulpn | grep -E ':(8020|8080|9625|9642|9696)\b'
Change the host port in docker-compose.yaml. The service definitions live in the x-services section as YAML
anchors and are only referenced from the services section, so this is where the ports: blocks are. Every
published port is written as "127.0.0.1:<host_port>:<container_port>" — only ever change the middle (host)
value:
x-services:
kronos:
ports:
- "127.0.0.1:19625:9625" # host port 19625 -> container port 9625
Update the external URLs in config.local.env so that the frontend clients (and the deployment script) use the
new ports:
KRONOS_URL_EXTERNAL=http://localhost:19625
MAESTRO_URL_EXTERNAL=http://localhost:18020
KEYCLOAK_URL_EXTERNAL=http://localhost:18080
Do not change the internal *_URL values (KRONOS_URL, ES_URL, KEYCLOAK_URL, MINIO_URL,
MONGO_CONN_STR, DEFAULT_BASE_URL_*, …). Those resolve service names on the compose network and always use the
container ports, which are unaffected by the host-side mapping.
Restart the deployment (docker compose down && ./scripts/deployment-full.sh). Maestro regenerates the frontend
client config from the *_URL_EXTERNAL values on every start.
Notes:
ports: blocks
commented out — they are only reachable from within the compose network. Uncomment a mapping (and pick a free host
port) if you need direct access from the host, e.g. for debugging.127.0.0.1, so they are not reachable from outside the server. That is intentional —
external access should go through a reverse proxy (see Reverse Proxy & HTTPS).<APP>_CONTAINER_PORT variable in config.local.env (read by the app's start.sh and by config.py as
<APP>_PORT), and update the container side of the ports: mapping, the healthcheck URL and the internal
<APP>_URL in docker-compose.yaml/config.local.env to match.The Admin console authenticates users against Keycloak using the OIDC authorization code flow; Kronos then validates the
resulting JWT (common/common/api/security_jwt.py). A fresh Keycloak container has no realm/client/user configured, so
this has to be done once manually before the first Admin console login.
Three config values have to match the Keycloak configuration exactly (they are read by both the backend and the frontend clients):
| Config variable | Keycloak field | Default value |
|---|---|---|
KEYCLOAK_REALM | Realm → Realm name | alquist |
KEYCLOAK_CLIENT_ID | Client → Client ID | alquist-insight-development |
KEYCLOAK_URL_EXTERNAL | – (URL the browser uses) | http://localhost:8080 |
KEYCLOAK_URL | – (URL the backend services use) | http://keycloak:8080 |
The Keycloak UI distinguishes between an object's ID/name (used in URLs and tokens — this is what the config variables refer to) and its display name (a cosmetic label). Always fill in the Realm name and Client ID fields; the Display name / Name fields can be left empty or set to anything.
Open http://localhost:8080/ and click Administration console. Log in with the credentials from your config:
KEYCLOAK_ADMIN_USER (default admin)KEYCLOAK_ADMIN_PASSWORD (sample value admin123 — change it in config.local.env)alquist (must equal KEYCLOAK_REALM; it appears in the OIDC URLs and is case-sensitive).Make sure the newly created realm (not master) is selected for the following steps.
OpenID Connectalquist-insight-development (must equal KEYCLOAK_CLIENT_ID)Off — the Admin console is a browser SPA, so it must be a public client
(authorization code flow with PKCE, no client secret)http://localhost:8020 with your MAESTRO_URL_EXTERNAL value:
http://localhost:8020 (optional)/admin/ (optional)http://localhost:8020/admin/*http://localhost:8020/admin/** (allow all origins; alternatively set the exact http://localhost:8020 origin)Leave the default client scopes (acr, basic, email, profile, roles, web-origins) assigned. The roles
scope adds the account audience to issued access tokens, which is what Kronos expects when validating the JWT.
Off (otherwise
the user is forced into a password-change screen that the Admin console cannot render natively).You should now be able to log in to the Admin console at http://localhost:8020/admin/ using these credentials.
ToDo: Do this setup as part of the deployment script.
If you used different values than the defaults, set KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID in config.local.env and
restart Maestro so it regenerates the frontend client config:
docker compose restart maestro
Keycloak derives the absolute URLs it publishes (the OIDC issuer, the authorization/token endpoints in its discovery
document, the admin console asset URLs, and the redirect it sends the browser to) from the incoming request. Behind a
reverse proxy the request it receives is the internal, plain-HTTP one. Without extra configuration it advertises
http:// URLs built from whatever host name the proxy forwards — which the browser either cannot reach or refuses to
use — and it may reject the request outright because it considers the connection insecure.
To fix that, add these two environment variables to the keycloak service in docker-compose.yaml:
x-services:
keycloak:
environment:
# ... existing variables ...
KC_HOSTNAME: "keycloak.example.com" # public base URL of Keycloak
KC_PROXY_HEADERS: "xforwarded" # trust X-Forwarded-* headers from the proxy
KC_HOSTNAME pins the public base URL used in all generated URLs and in the token issuer.KC_PROXY_HEADERS=xforwarded makes Keycloak trust the X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-For
headers, so it knows the original request arrived over HTTPS. Your proxy must actually set those headers (the
nginx config below does) — only enable this when Keycloak is not directly reachable from outside.Then set the matching external URL in config.local.env and recreate the containers:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
docker compose --env-file config.env --env-file config.local.env up -d
Keep KEYCLOAK_URL (the internal URL used by Kronos to fetch the realm's public key) pointing at
http://keycloak:8080 — it does not go through the proxy.
Remember to update the client's Valid redirect URIs, Valid post logout redirect URIs and Web origins in
Keycloak to the public Maestro URL (e.g. https://insight.example.com/admin/*).
The compose file runs Keycloak with the
start-devcommand, which enables plain HTTP and relaxes hostname checks. This is convenient for a self-hosted deployment behind a TLS-terminating proxy, but it is not a hardened production setup — for production, switch tostartand configure TLS,KC_HOSTNAME_STRICTand a proper admin account.
The Admin console and the chatbot Interactor are built in separate repositories and published as dist.tar.gz generic
packages. At startup, Maestro (maestro/maestro/utils/frontend.py):
PACKAGE_REGISTRY_TOKEN) and extracts it into /home/app/frontend, which is
bind-mounted to data/maestro/frontend on the host — but only if the client directory does not exist yet.dist/config.json with the current external URLs, Keycloak realm/client ID and default
project — this happens on every start.Relevant config:
ADMIN_CONSOLE_PACKAGE_NAME=admin
ADMIN_CONSOLE_VERSION=latest
INTERACTOR_PACKAGE_NAME=chatbot_js
INTERACTOR_VERSION=latest
PROJECT_ID=test
PROJECT_TITLE="Test Project"
Because the download is skipped when the extracted files are already present, a new client build is not picked up automatically on restart. To force a re-fetch, delete the frontend data folder:
docker compose down
sudo rm -rf data/maestro/frontend # the folder is owned by the container user (uid 999)
./scripts/deployment-full.sh # recreates the folder with the right ownership and starts everything
If you prefer not to run the full deployment script:
docker compose stop maestro
sudo rm -rf data/maestro/frontend
mkdir -p data/maestro/frontend
sudo chown -R 999:999 data/maestro
docker compose --env-file config.env --env-file config.local.env up -d maestro
Notes:
ADMIN_CONSOLE_VERSION / INTERACTOR_VERSION are left at latest (the default), this pulls the newest
published build. If a specific version is pinned, that exact version is downloaded again instead.data/maestro/frontend/admin or
data/maestro/frontend/interactor).docker compose restart maestro is enough, since config.json is rewritten on every start.docker compose logs -f maestro) to confirm the download succeeded; a failed fetch prevents
the app from starting.For creating your own projects and uploading knowledge base documents, you can either use the Kronos API or the Admin console UI.
You can open the chatbot for a custom project at http://localhost:8020/?project_id=<project_id>.
The Admin console is available at http://localhost:8020/admin/ and requires a Keycloak login (see
Keycloak Setup). It allows creating new projects and managing the related knowledge base in a similar
fashion as a regular file explorer, as well as editing project settings, prompts and the dialogue definition.
The easiest way to use the API is through the Swagger UI at http://localhost:9625/docs, which also serves as the API
documentation. Authenticate with the X-Api-Key header using your KRONOS_API_KEY value (a Keycloak JWT in the
Authorization header works as well).
Use POST /projects/ to create a project, then one of the knowledge base endpoints to add documents:
| Endpoint | Description |
|---|---|
POST /knowledge_base/file/ | Upload a single file (pdf, docx, pptx, xlsx, md, txt, html) |
POST /knowledge_base/file/bulk | Upload multiple files at once |
POST /knowledge_base/pdf/advanced | PDF upload with Docling conversion via Alchemist (streamed progress) |
POST /knowledge_base/file/marker | Upload a pre-converted paginated Markdown + its source PDF |
POST /knowledge_base/url/ | Fetch and index the content of a single URL |
POST /knowledge_base/url/bulk | Fetch and index multiple URLs |
POST /knowledge_base/url/crawl | Crawl a seed URL and index all discovered pages/files |
Common query parameters: project_id (required), kb_id, name, description, language, custom_metadata (JSON
string) and enable_highlights (build the extra chunk index needed for highlighting answers in the source document).
See the CREATE EXAMPLE PROJECT IF MISSING section of scripts/deployment-full.sh for a working curl example.
POST /projects/{project_id}/nlp/rag/ (Kronos) — full RAG answer for a query.POST /projects/{project_id}/nlp/rag/stream (Kronos) — the same, streamed token by token.POST /projects/{project_id}/query/rag (Maestro) — the endpoint used by the chatbot client.If you want external users to be able to access the Admin/Chatbot UI, you will need to expose the following services to them:
8020) — serves both client apps and the chatbot API.9625) — required for Admin console access only.8080) — required for Admin console access only.Ragnarok and Alchemist are internal-only and should not be exposed.
The public URLs then need to be set in config.local.env using the <NAME>_URL_EXTERNAL variables, because they are
baked into the frontend client configuration:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
KRONOS_URL_EXTERNAL=https://kronos.example.com
MAESTRO_URL_EXTERNAL=https://insight.example.com
The Admin console uses several browser APIs that are only available in a secure context, meaning the services have to
be served over https. For quick testing this requirement can be bypassed by telling your browser to treat the origins
as secure (in Chrome: chrome://flags/#unsafely-treat-insecure-origin-as-secure), but the proper solution is a
TLS-terminating reverse proxy as described below.
This section sets up nginx as a TLS-terminating reverse proxy in front of Maestro, Kronos and Keycloak, with free Let's Encrypt certificates issued and auto-renewed by certbot. The example uses three subdomains:
| Public URL | Upstream |
|---|---|
https://insight.example.com | 127.0.0.1:8020 (Maestro) |
https://kronos.example.com | 127.0.0.1:9625 (Kronos) |
https://keycloak.example.com | 127.0.0.1:8080 (Keycloak) |
Prerequisites:
A/AAAA records for all three subdomains pointing at the server's public IP.80 and 443 open in the firewall / cloud security group.127.0.0.1 (the default). If nginx runs on a different host,
change the bind address of the published ports in docker-compose.yaml from 127.0.0.1 to the interface reachable by
the proxy, and firewall them accordingly.sudo apt update
sudo apt install nginx certbot python3-certbot-nginx
First create the shared proxy header snippet /etc/nginx/snippets/insight-proxy.conf. The X-Forwarded-* headers are
what makes KC_PROXY_HEADERS=xforwarded work for Keycloak; keeping them in a snippet lets all three server blocks stay
identical:
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Then create the site itself, /etc/nginx/sites-available/insight.conf:
server {
server_name keycloak.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
include snippets/insight-proxy.conf;
}
}
server {
server_name kronos.example.com;
listen 80;
# Uploaded documents can be large; RAG streaming responses can take minutes.
client_max_body_size 256m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:9625;
proxy_buffering off; # do not buffer server-sent events (streamed chatbot answers)
include snippets/insight-proxy.conf;
}
}
server {
server_name insight.example.com;
listen 80;
# Uploaded documents can be large; RAG streaming responses can take minutes.
client_max_body_size 256m;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:8020;
proxy_buffering off; # do not buffer server-sent events (streamed chatbot answers)
include snippets/insight-proxy.conf;
}
}
Enable the site and reload nginx:
sudo ln -s /etc/nginx/sites-available/insight.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Certbot's nginx plugin validates the domains over HTTP, obtains the certificates and rewrites the site config to redirect HTTP to HTTPS and serve TLS on port 443:
sudo certbot --nginx \
--cert-name insight \
-d keycloak.example.com \
-d kronos.example.com \
-d insight.example.com
Renewal is handled automatically by the certbot.timer systemd unit installed with the package. Verify it:
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
In config.local.env:
KEYCLOAK_URL_EXTERNAL=https://keycloak.example.com
KRONOS_URL_EXTERNAL=https://kronos.example.com
MAESTRO_URL_EXTERNAL=https://insight.example.com
In docker-compose.yaml, add the proxy-related variables to the keycloak service (see
Keycloak Behind a Reverse Proxy):
KC_HOSTNAME: "keycloak.example.com"
KC_PROXY_HEADERS: "xforwarded"
In the Keycloak admin console, update the client's Valid redirect URIs and Valid post logout redirect URIs to
https://insight.example.com/admin/*.
Finally recreate the containers so the new configuration is applied:
docker compose --env-file config.env --env-file config.local.env up -d
The Admin console should now be reachable at https://insight.example.com/admin/ and the chatbot at
https://insight.example.com/.
Prerequisites:
pyenv for managing multiple Python versions on your machine).pipenv Python package manager.You can install the Python requirements for all apps using pipenv (omit the dev flag for purely runtime dependencies):
pipenv install --dev
The shared common module is not a Python package installed by pipenv — it is a plain module directory that every app
imports directly. Its dependencies are listed in the ## Common module dependencies ## section of each Pipfile. To
make it importable, the /common folder has to be on the Python path:
export PYTHONPATH="$PWD/common"
Prepare a config.local.env configuration file (see Configuration for details). Note that when
running an app directly on the host, the service URLs must point at localhost and the published host ports instead of
the compose service names, e.g. MONGO_CONN_STR=mongodb://localhost:27017 — which also means the corresponding ports:
mappings have to be uncommented in docker-compose.yaml.
You can run the individual apps using the provided run.py scripts, e.g.:
PYTHONPATH="$PWD/common" pipenv run python kronos/run.py
There are no automated tests in this repository. Validate changes by running the relevant service and exercising its
Swagger UI at /docs.
Each app has its own Pipfile, used to build its Docker image; the root Pipfile is a convenience union of all
dependencies for local development only. Every Pipfile splits [packages] into a ## Common module dependencies ##
group (what common/common imports, limited to the parts the app actually uses) and a ## Component dependencies ##
group. When adding an import to common, add the dependency to the common group of every Pipfile that needs it
(sort_pipfile is disabled to keep the grouping intact) and re-run pipenv lock there. The common module itself is
copied into the image in the Dockerfile's runtime stage, so changing it does not rebuild the dependency layers.
This section describes the recommended way how to work with this monorepo in the PyCharm IDE.
/common folder and other app folders (/alchemist, /kronos, etc.) as Sources Root. This makes both the
common module and the app modules importable without setting PYTHONPATH manually.cd kronos/ && pipenv install --dev.Settings -> Python -> Interpreter.Add Interpreter -> Add Local Interpreter... -> Select existing.~/.local/share/virtualenvs/<name>-<hash>/bin/python.# -*- coding: utf-8 -*- and an rST-style module docstring: the dotted module path, a
~~~ underline of matching length, then a one-line summary.:param x:, :return:) and are parsed by
common/common/utils/swagger.py to populate the Swagger UI descriptions — keep the format intact. Mark
logging-only/unused endpoint params with # noqa.@error_handler / @error_handler_async (from common.utils.api), which
maps domain exceptions (common/common/utils/exceptions.py) to HTTP status codes via EXC_TO_STATUS. Raise those
exceptions rather than HTTPException for domain errors. Routers are assembled in each app's api/router.py.common.models.base.CustomBaseModel (alias-aware, validate-on-assign). Cross-service
request/response models live in common/common/models/api_{kronos,maestro,ragnarok}.py.model_version (VER_* constant).
Bumping a model means adding a migration branch in kronos/kronos/prestart.py, which runs under a Mongo lock before
Kronos starts.common.core.get_component_logger(); never instantiate one. Structured fields go in
extra={...}. Each app sets up its component logger in its __init__.py.Singleton / SingletonABC metaclass from common.utils.singleton (identity
keyed on init args).prompts resource file (prompts.md), stored per project by
Kronos with a fallback to the default file (resources/prompts.md). Ragnarok fetches them via
common/common/services/kronos.py (TTL cache keyed on project & session ID) and parses them with
common.utils.prompts.parse_prompts; Kronos validates uploads the same way. Adding a prompt means adding a field to
common.models.prompts.Prompts, its runtime variables to PROMPT_VARIABLES, and a ## <prompt_name> section to
resources/prompts.md.| Script | Description |
|---|---|
scripts/deployment-full.sh | Full local Docker deployment (build, start, seed the test project) |
scripts/find_orphaned_resources.py | Report resources not belonging to any project / knowledge base |
scripts/migrate_es_index.py | Copy an Elasticsearch index to another (already created) index |
scripts/marker_pdf/ | Standalone marker-pdf PDF pipeline (see its own README.md) |
could not select device driver ... with capabilities: [[gpu]] — the host has no Nvidia GPU or no Nvidia
Container Toolkit. Comment out the GPU access lines as described in
Local Docker Deployment.data subfolder ownership is
wrong. Re-run ./scripts/deployment-full.sh, or fix it manually:
sudo chown -R 1000:1000 data/{elasticsearch,keycloak} and sudo chown -R 999:999 data/{alchemist,maestro,ragnarok}.ValidationError — a required config value is missing or invalid.
CONFIG fails fast; the error message names the offending variable.docker compose logs -f vllm-generation and docker ps (health status).MAESTRO_URL_EXTERNAL, or KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID do not match the
Keycloak configuration. See Keycloak Setup.KEYCLOAK_URL
is reachable from the Kronos container and that the client's default scopes (incl. roles) are still assigned.16 commits
1 commits
Python
97.6%
Shell
1.5%