# atrium-keyword-extract — the published environment contract (atrium-project#60). # # Copy to `.env` and edit. Do not commit `.env` (gitignored). # # This file is the COMPLETE ledger: every variable the api image reads, at the # default that is in the CODE. The operator-facing subset across all the services is # the table in the hub's docs/k8s_deployment.md; this file's layout is fixed by # docs/templates/env.example.template in ufal/atrium-project. The batch CLI's own # settings (keywords.py, llm_run.py) live in kw_config.txt and llm_config.txt and are # deliberately NOT here: a deployment of the service never configures them. # # TAG LEGEND: [required] raises at startup without it · [secret] never a literal # anywhere committed, use a k8s Secret · [operator] an ordinary deployment knob · # [algorithmic] changes what the service COMPUTES, not how it is deployed · # [build] read by compose/Dockerfile or baked at build time · [limit] a limit # (atrium-project#53): reported in GET /info, listed in service/README.md § Limits # # HOW THIS FILE REACHES THE PROCESS # docker compose → delivered. docker-compose.yaml's `kw-api` service declares # `env_file: .env`, so everything below is injected into the # container. Four names (MAX_CONCURRENT_REQUESTS, # DEFAULT_KW_METHOD, PORT, OLLAMA_HOST) are ALSO named # explicitly in the `environment:` block as `${VAR:-default}`, # because compose itself needs PORT for the `ports:` mapping and # OLLAMA_HOST's compose default deliberately differs from the # code default (see §5). # Kubernetes → NOT delivered. Use the manifest's own env: block. See # ufal/atrium-project docs/k8s_deployment.md. # python -m service.api # → NOT delivered. Nothing here calls load_dotenv(); export the # values yourself: set -a; . ./.env; set +a # # BLANK IS NOT UNIVERSALLY "UNSET". ALLOWED_ORIGINS is the trap: a blank value parses # to an EMPTY origin list and blocks every browser request, where omitting the line # gives you `*` — so it ships commented below, not blank. # ── §0 Required before first start ───────────────────────────────────────────── # None — this service starts with no variables set, and serves the statistical kind. # The controlled kind (the LLM over the AMČR and TEATER vocabularies) needs its backend: # OPENROUTER_API_KEY and OPENROUTER_MODEL for the default LLM_BACKEND=openrouter, or # OLLAMA_MODEL with LLM_BACKEND=ollama (§5). Without them `kind=controlled` answers 501, # `kind=both` reports it `skipped`, and GET /info `controlled.detail` names what is missing. # ── §1 HTTP surface ───────────────────────────────────────────────────────────── # [operator] Port the api service binds, and the port service/healthcheck.py probes # inside the container. docker-compose.yaml maps "${PORT:-8000}:${PORT:-8000}", so # changing it here moves listener, published port and HEALTHCHECK together. In # Kubernetes, change `containerPort` in the same edit. (atrium-project#58) PORT=8000 # [operator] Bind address. Leave at 0.0.0.0. service/healthcheck.py always probes # 127.0.0.1 by design and never reads HOST, so 127.0.0.1 here gives a container that # reports HEALTHY to every probe while being unreachable from outside it. This is the # one value whose failure mode is silent. # HOST=0.0.0.0 # [operator] CSV of CORS origins. Code default `*` (every origin) — # service/atrium_service.py falls back to it when unset; this repo's compose file does # not narrow it, so `docker compose --profile api up` serves every origin. Ships # commented so an operator who leaves it alone gets the real code default; uncomment # to narrow it for a real deployment. # ALLOWED_ORIGINS=* # [limit] Canonical upload limit in megabytes. 10 is THIS repo's built-in # (tool_limits.py, `upload_limit(10)`). Enforced on the uploaded record (`document_json`) # of /extract_keywords and on the whole /extract_keywords_text body. Over it → 413 # limit_exceeded. Default: 10. MAX_UPLOAD_MB=10 # [deprecated] Do not set. A byte-valued fallback kept working for one release for # callers that already exported it (strategy §4.5); MAX_UPLOAD_MB wins whenever both # are present (service/atrium_service.py:353-364). Listed commented-out so the # contract is complete and the deprecation is discoverable. # MAX_UPLOAD_BYTES= # [operator] Path the container's own HEALTHCHECK probes. Change it only alongside # the route itself. # HEALTHCHECK_PATH=/health # ── §2 Lifecycle ──────────────────────────────────────────────────────────────── # [operator] Seconds uvicorn waits for in-flight requests after SIGTERM. Raise # together with the deployment's grace period (terminationGracePeriodSeconds in k8s, # see docs/k8s_deployment.md in the hub; stop_grace_period under compose, already 60s # here). serve_lifecycle's own drain runs ON TOP of this — budget both. GRACEFUL_SHUTDOWN_S=20 # [operator] Root logger level for the `python -m service.api` start path. All five # services honour it identically (atrium-project#61). LOG_LEVEL=INFO # [operator] Filesystem auto-reload. Development only — never in a deployment. # Deliberately NOT wired into any compose or k8s template. # RELOAD=false # ── §4 Image / provenance ──────────────────────────────────────────────────────── # [build] Image tag used by docker-compose: on release, para_config.txt's [tool] version # WITHOUT the leading `v` (para_config.txt says `v1.0.0`, GHCR carries `1.0.0` -- # atrium-project#69, roadmap B3). Read only by docker-compose.yaml; no Python here reads it. ATRIUM_VERSION=dev # [build] Baked at build time; stamped into paradata as runner_ref. ATRIUM_RUNNER_REF=dev # [build] Override either only if you publish under another name. # ATRIUM_RUNNER_IMAGE=ghcr.io/ufal/atrium-keyword-extract:dev # ATRIUM_RUNNER_REPO=https://github.com/ufal/atrium-keyword-extract # [build] The uid docker-compose.yaml runs the containers as (`user: ${ATRIUM_UID:-10001}:0`); # no Python here reads it. On Linux set it to your own uid -- `echo "ATRIUM_UID=$(id -u)" >> .env` # (compose does not run `$(...)` itself) -- so the container can write ./data, which a clone # owns as you. Docker Desktop needs nothing. A named volume created by an older image is # writable by uid 10001 only: recreate it once (`docker compose down -v`) after setting # this. (atrium-project#69, roadmap B6) # ATRIUM_UID=10001 # [build] Correlation id copied verbatim into paradata `request_id`. Read ONCE from # the process environment at import (atrium_paradata.py's module-level constants), # so it identifies a batch run, not an individual HTTP request. # ATRIUM_REQUEST_ID= # [operator] The IRI (e.g. a ROR id) of the organisation that operates this deployment, # recorded as the `agent` of every run's CreateAction (atrium-project#71). Unset, no agent # is recorded; never set it to a processing account, which only reads and calls back. # ATRIUM_RUN_AGENT= # [build] Where the HuggingFace model cache lives. The Dockerfile sets this; without # a matching volume mount the cache is re-downloaded on every start. # HF_HOME=/cache/huggingface # ── §5 Service knobs — operator ────────────────────────────────────────────────── # [operator] Default statistical method used when a request does not name one: keybert, # yake or legacy. KeyBERT is the default; YAKE (AGPL-3.0) and the legacy KER are selectable. DEFAULT_KW_METHOD=keybert # # Limits (atrium-project#53) — every limit of this service except MAX_UPLOAD_MB (§1), # declared in tool_limits.py, reported with its current value in GET /info (`limits`; # `limits_meta` names the variable and whether the value came from the environment, the # config file or the default), and listed in service/README.md's `## Limits` table. A # limit that shaped a result without refusing it is returned in the response's # `limits_applied` and recorded in the paradata. A malformed value stops the service at # startup, naming the variable. # # [limit] Words one document may have (the record's lines, or the text). Over it → 413 # limit_exceeded. Default: 200000. MAX_DOCUMENT_WORDS=200000 # [limit] Keywords one request may ask for (`num_keywords`). Over it → 422 limit_exceeded. # Default: 100. MAX_KEYWORDS=100 # [limit] Extractions running at once. Each one holds the embedding model or a CPU for its # whole run; a request with every slot taken → 429 busy, with Retry-After. Default: 2. MAX_CONCURRENT_REQUESTS=2 # [limit] Words per KeyBERT chunk: a longer document is embedded in overlapping chunks # and its keywords merged, with a `split` note. A chunk longer than the encoder's token # window (/info keybert_max_seq_tokens, 128 for the default model) is embedded from its # start, with a `trimmed` note. Default: 400. KEYBERT_CHUNK_WORDS=400 # [limit] Words two consecutive KeyBERT chunks share. Default: 50. KEYBERT_CHUNK_OVERLAP=50 # # The controlled kind's LLM backend (atrium-keyword-extract#1, #2). The model runs in an # inference service, never in this image: OpenRouter, or an Ollama server. The backend is # set up once at startup; GET /info `controlled` reports what was configured and, when it # could not be, why. The prompt (prompts/system_prompt.txt and its PROMPT_* flags), # VOCAB_PATH, EMIT_CATEGORY_IDS and the line filter are read from LLM_CONFIG; the # environment wins over that file for every name in this section. # # [operator] openrouter (the default) or ollama. LLM_BACKEND=openrouter # [operator] Config file read for any value not supplied by the environment, the PROMPT_* # flags among them. Relative to the working directory, then to the repository. LLM_CONFIG=llm_config.txt # # [secret] OpenRouter backend, only when LLM_BACKEND=openrouter: required for the controlled # kind there. Never commit a real value — not to this file, not to .env. In Kubernetes do NOT # put it in the manifest's env: block as a literal either: use a Secret and a secretKeyRef. # OPENROUTER_API_KEY= # [operator] An OpenRouter model id, e.g. openai/gpt-4o-mini; required with the key above. # OPENROUTER_MODEL= # [operator] Optional attribution headers OpenRouter shows on your dashboard. Neither # affects behaviour. # OPENROUTER_SITE_URL= # OPENROUTER_APP_NAME=atrium-keyword-extract # # [operator] Ollama backend, only when LLM_BACKEND=ollama. The CODE default is # http://localhost:11434, which is wrong INSIDE a container — it points at the container's # own loopback, where no Ollama runs. docker-compose.yaml supplies # http://host.docker.internal:11434 instead, the host's Ollama, and applies ONLY under # compose. Ships COMMENTED, deliberately: an active value here would be injected via # env_file and defeat that compose default. In Kubernetes, set it to the Ollama Service's # DNS name. # OLLAMA_HOST=http://localhost:11434 # [operator] An Ollama model tag, e.g. qwen2.5:14b; required for this backend. # OLLAMA_MODEL= # # [limit] Seconds one LLM call may take; a timeout is retried. Default: 300. LLM_TIMEOUT=300 # [limit] Attempts of one call. Only a timeout, a connection error, HTTP 429 or 5xx is # retried; another 4xx is refused at once with the provider's reply. Once they run out # the line is an error. Default: 3. LLM_MAX_RETRIES=3 # [limit] Most tokens one reply may have — OpenRouter `max_tokens`, Ollama `num_predict`. # A reply cut at it is never used: the line gets no result, with a `skipped` note. # Default: 2048. LLM_MAX_NEW_TOKENS=2048 # [limit] Consecutive failed lines after which the document is given up, with a # `stopped` note (the lines before keep their results). Default: 10. LLM_MAX_CONSECUTIVE_ERRORS=10 # [limit] Context window of the selected model, in tokens. It sizes the vocabulary # prompt — terms that do not fit are left out, with a standing `trimmed` note (all 4719 # terms take about 40k tokens) — and is sent to Ollama as num_ctx. Rule 3: ships # COMMENTED, because its default depends on the backend — 128000 for openrouter, 32000 for # ollama (each batch client's default) — after CONTEXT_WINDOW in LLM_CONFIG, when it sets one. # LLM_CONTEXT_WINDOW=128000 # ── §6 Completeness ledger — algorithmic, deprecated, unreachable ──────────────── # [algorithmic] The built vocabulary the controlled kind's prompt is rendered from; the flat # files beside it (amcr_flat.json, teater_flat.json) resolve entities[].pid. Falls back to # VOCAB_PATH in LLM_CONFIG, then data_samples/vocab/union_nested.json, which the images carry. # VOCAB_PATH=data_samples/vocab/union_nested.json