Docker Compose

The AI layer is composed of the following profiles, to be started together with the rest of the stack (see Docker Compose — Overview):

Profile Service Purpose
neo4j-mcp neo4j-mcp MCP toolbox exposing knowledge graph tools (port 5000, internal)
crisalid-agents-owui crisalid-agents-owui Graph agent served as OpenWebUI pipelines (port 9099, internal)
crisalid-agents-chat-api crisalid-agents-chat-api Graph agent served as a streaming chat API for SoVisuPlus (port 9100, internal)
openwebui openwebui User-facing chat UI
crisalid-taxi crisalid-taxi, crisalid-taxi-opensearch OpenAlex topics classification API used by IKG (port 8000, internal) and its vector store
crisalid-taxi-dashboards crisalid-taxi-dashboards Optional OpenSearch Dashboards UI for the taxi index (port 5601)
horizon_europe_index opensearch OpenSearch index of Horizon Europe work programme topics, see Horizon Europe topics (port 9200, internal)
crisalid-agents-batch crisalid-agents-batch Batch entry points of crisalid-agents (topic index ingestion, topic-to-researcher match), run on demand with docker compose run, see Horizon Europe topics

Both agent services run the same CRISalid graph agent and require the neo4j-mcp profile (they wait for the toolbox to be healthy). Only start the wrapper(s) you actually use:

# OpenWebUI chat stack
docker compose \
  -f docker-compose.yaml \
  -f docker-compose.dev.yaml \
  --profile neo4j-mcp \
  --profile crisalid-agents-owui \
  --profile openwebui \
  up -d

# SoVisuPlus chat API
docker compose \
  -f docker-compose.yaml \
  -f docker-compose.dev.yaml \
  --profile neo4j-mcp \
  --profile crisalid-agents-chat-api \
  up -d

Configuration:

Topics classification (crisalid-taxi)

crisalid-taxi classifies document titles and abstracts against the OpenAlex taxonomy (domains, fields, subfields, topics) by semantic similarity. It embeds the whole taxonomy into its own OpenSearch instance at startup, then answers matching requests from IKG.

Before the first start, download the OpenAlex snapshot into the taxi data directory (see Download the OpenAlex Classification Tree):

bash docker/scripts/download_openalex_tree.sh crisalid-taxi

Then start the profile (add crisalid-taxi-dashboards to get the OpenSearch Dashboards UI on port 5601):

docker compose \
  -f docker-compose.yaml \
  -f docker-compose.dev.yaml \
  --profile crisalid-taxi \
  up -d

The first startup embeds the full taxonomy through the shared embedding endpoint (see Semantic Embeddings), which can take several minutes; the API only starts serving once indexing is complete. Later startups skip unchanged data.

Configuration:

  • Matching thresholds, retries and OpenSearch settings: docker/crisalid-taxi/.env.
  • Embedding endpoint: the shared EMBEDDING_* variables in docker/.env.
  • IKG side: set TAXI_ENABLED=true in docker/ikg/.env so IKG calls the taxi service (http://crisalid-taxi:8000) when documents are written. Disabled by default.

Horizon Europe topic index

The horizon_europe_index and crisalid-agents-batch profiles (OpenSearch index of the Horizon Europe work programme topics, its ingestion from the work programme PDFs, the Horizon topic finder agent and the Topic-to-Researcher Match batch) are documented on their own page: Horizon Europe topics.

SoVisuPlus assistant configuration

The assistant is configured by a JSON file located in the docker/sovisuplus/config directory, named chat.json. It lets you set a system prompt, seed the chat’s conversation list (with a welcome message, for example) and provide query suggestions. To set it up, copy the chat.sample.json located in the same directory and fill in the fields.

cp docker/sovisuplus/config/chat.sample.json docker/sovisuplus/config/chat.json
$EDITOR docker/sovisuplus/config/chat.json

Schema

{
  "systemPrompt": "Instructions that shape the agent's behaviour, for {{institutionName}}. For example: 'If at any point you are unable to answer the user, suggest contacting {{supportEmail}}.'",
  "variables": { "supportEmail": "svp-support@example.edu" },
  "locales": {
    "en": {
      "welcome": { "title": "Welcome", "message": "Hi! I'm your SoVisu+ assistant…" },
      "suggestions": [{ "label": "Open access rate", "value": "How many publications in my organization are open access?" }]
    },
    "fr": {
      "welcome": { "title": "Bienvenue", "message": "Bonjour ! Je suis votre assistant SoVisu+…" },
      "suggestions": [{ "label": "Taux de science ouverte", "value": "Combien de publications sont en science ouverte dans mon organisation ?" }]
    }
  }
}
Field Purpose
systemPrompt Sent to the agent as a role:"system" message by the /api/chat proxy. The field is required (see below).
variables Optional map of custom {key} values for systemPrompt, so the prompt can be parametrised by editing this file alone. Merged over the app-provided defaults, and able to override them.
locales.<lang>.welcome Seeds the first conversation and assistant message when the widget opens; title names the conversation.
locales.<lang>.suggestions Starter question suggestions, rendered as chips. value is the text submitted by clicking on the chip, label is the chip’s label — it should be shorter than value (label falls back to value when omitted).

Locales are keyed by language, and should be filled in for every locale the app supports. An unknown locale falls back to en if it exists, or to empty values if not.

systemPrompt is the only mandatory field — its presence is what marks the file as a real chat config rather than a stray JSON file. Everything else is optional, so a config containing only the pieces you care about is valid.

Note

systemPrompt field value may be empty but in that case you’re relying on the agent’s own built-in prompt that doesn’t contain any specific instructions.

When the widget appears

SoVisuPlus renders the assistant only if SVP_CHAT_ENABLED is not false and CRISALID_AGENTS_API_URL is set (it defaults to http://crisalid-agents-chat-api:9100 in sovisuplus.yaml) and a config file resolves. In Docker the last condition is effectively always met — the image ships a generic chat.sample.json as a last-resort fallback, and it is only skipped if it too fails to load.

When any condition is not met, the widget is hidden, /api/chat returns 404, and SoVisuPlus logs the reason once ([ChatConfigService] AI chat is disabled: …).

Disabling the assistant

The assistant is enabled by default. To turn it off, set in docker/sovisuplus/.env:

SVP_CHAT_ENABLED=false

then recreate the sovisuplus container (docker compose … up -d). The crisalid-agents-chat-api profile (and neo4j-mcp, if nothing else uses it) can then be left out.

Note

Emptying SVP_CRISALID_CHAT_API_URL does not disable the chat: an empty value falls back to the default URL.

Fallback

The file is read at docker/sovisuplus/config/chat.json, as docker/sovisuplus/config is bind-mounted read-only at /config. If you forgot to create it, or if it is malformed, SoVisuPlus falls back to /app/chat.sample.json, the generic configuration baked into the image.

The system prompt is server-only, the rest is public

systemPrompt is read by the server and injected into the upstream request. It is never sent to the browser, so it is a reasonable place for instructions you would not want users to read or tamper with — but it is not a security boundary: it shapes agent behaviour and nothing more. Access control lives in Security, and the welcome/suggestions strings are shipped to the browser.

Note that the proxy also injects the signed-in user’s identity as a second system message, from the trusted session, so the agent can resolve “me”/“my” and scope graph queries to that person.

The institution name fills itself in

systemPrompt supports {variable} placeholders, resolved server-side before the prompt is sent. The sample uses {institutionName}, which SoVisuPlus fills from INSTITUTION_NAME in docker/.env (passed through as NEXT_PUBLIC_INSTITUTION_NAME in sovisuplus.yaml) — so there is nothing to keep in sync by hand.

Any other key you need goes in the file’s variables map. A placeholder that matches neither an app-provided default nor a variables entry resolves to an empty string, so a typo silently drops the text rather than leaking {...} to the agent.

Applying changes

The file is read once per process and cached, so a restart is required to pick up edits:

docker compose \
  -f docker-compose.yaml \
  -f docker-compose.dev.yaml \
  --profile sovisuplus \
  up -d sovisuplus