A domain-agnostic simulation and impact-reasoning platform built on:
| Concern | Technology |
|---|---|
| Inference & embeddings | Llama Stack (OpenAI-compatible /v1); modes: OpenAI upstream or in-cluster vLLM |
| Vector / RAG | pgvector (queried directly via asyncpg) |
| Dependency graph | Neo4j (native async driver, Cypher queries) |
| Live / geo snapshot | PostGIS, queried directly |
| API | FastAPI |
| Admin API | JSON endpoints at /admin/* (automation / scripts) |
| Dependency management | uv |
Core design rule: Live ground-truth data is never mutated by a simulation. Simulations are overlays applied at query time.
- What this platform does
- System at a glance
- The components and how they relate
- How a query flows through the system
- How the simulation actually works
- Why the same design serves multiple domains
- Key decisions and risks to watch
- Repository layout
- Quickstart (local dev)
- Running without hardware (CI / dev laptops)
- LLM backend configuration
- OpenShift Deployment
- Adding a domain · Cursor prompts
This system answers impact and response questions about a live operational environment when a disruptive event is layered on top of it. In plain terms: it takes a real-time picture of what is happening, overlays a hypothetical or unfolding disruption, and reasons over the combination to answer questions like “how does this event affect the current situation?” and “how should things be rerouted or rescheduled in response?”
The design is deliberately domain-agnostic. The original framing is a supply-chain scenario — a port closure or volcanic ash disrupting flights — but the same machinery applies unchanged to a manufacturing plant, where the disruption is a machine breakdown or material shortage. The core abstraction is the same in both: live data + a dependency graph + a simulation-event overlay + staged reasoning.
The whole platform is built to run on OpenShift, which is a hard constraint that shapes every technology choice below.
The one-sentence model: A simulation event is an overlay; the LLM agent investigates it by calling graph-traversal, solver, vector-search, and ingestion tools in whatever order the question demands — then explains the result grounded in the numbers those tools returned.
The platform is a small number of cooperating layers running inside one OpenShift cluster. The diagram below shows how they stack: an API and an orchestrator at the top, the three reasoning stages beneath, the LLM client as the inference/vector backend, and separate Neo4j and Postgres instances for graph and geo/vector state. Two things are worth noticing immediately — the reasoning stages are colour-coded by whether they use the LLM, and both the graph (Neo4j) and geo (PostGIS) paths bypass the LLM client to be queried directly.
Figure 1 — Layered system overview. Everything runs inside OpenShift. The LLM client fronts inference and vector/RAG; graph (Neo4j) and live/geo (PostGIS) are queried directly.
The remaining sections walk through each component: what it is, why it is there, and how it relates to its neighbours.
OpenShift is the deployment substrate and a fixed requirement, not an interchangeable choice. Every other component is selected partly because it runs cleanly on OpenShift: Postgres via an operator, vLLM via OpenShift AI / KServe, and the application services as ordinary Deployments and CronJobs. Treating OpenShift as the constant is what lets the rest of the stack stay portable across domains.
On OpenShift, the API and ingestion CronJob always call Llama Stack (http://llamastack:8321/v1). Stack fronts three modes:
| Mode | Upstream | When to use |
|---|---|---|
maas (default) |
LiteMaaS (external-model/llama-scout-17b) |
Workshop / shared MaaS endpoint |
openai |
OpenAI API | No GPU / quickest path |
local |
In-cluster llm-service (vLLM on OpenShift AI) |
Keep weights inside the cluster |
The app talks to any OpenAI-compatible inference endpoint through lib/llm/openai_client.py. Cluster defaults point at Stack; local-dev can still point at OpenAI or a laptop vLLM:
| Provider | LLM_BASE_URL |
LLM_BACKEND |
|---|---|---|
| Llama Stack (OpenShift default) | http://llamastack:8321/v1 |
openai |
| OpenAI (local dev) | https://api.openai.com/v1 |
openai |
| vLLM (local laptop) | http://localhost:8080/v1 |
openai |
| Llama Stack SDK | http://llamastack:8321 |
llamastack |
| Tests / no GPU | (any) | fake |
Vector/RAG operations (embed, ingest, search) go directly to pgvector via asyncpg — no intermediate server required. A single llm_embeddings table in Postgres stores all collections.
Design rule: Application code goes through
LLMClientBase(lib/llm/) for anything involving the model, embeddings, or vector search — never calling any inference API or pgvector SQL directly. The sole exceptions are graph (Neo4j) and live/geo (PostGIS), which are queried directly.
Neo4j holds the dependency graph and simulation-event overlays. It is queried directly via the official async Python driver using native Cypher. Entities and their dependency edges live here; SimulationEvent nodes are injected as overlays and can be removed without touching any other data. Neo4j runs as a StatefulSet in the cluster, deployed via the official neo4j/neo4j Helm chart.
Postgres carries the remaining two persistence concerns. The dependency graph has moved to Neo4j, so the custom Postgres image only needs pgvector + PostGIS — no AGE extension required.
| Extension | Role | Accessed via |
|---|---|---|
| pgvector | Embeddings and RAG. Stores simulation-event narratives, playbooks, and precedent for retrieval. | Directly via asyncpg |
| PostGIS | The live “current situation” snapshot — entity positions, states, geospatial data — written by ingestion. | Directly via asyncpg |
Ingestion adapters live under domain/<name>/adapters/. Each pulls from one
external source and normalises into a single canonical schema (id, type,
optional geometry, timestamp, status, and a free-form attributes field). The
shared runner in lib/ingestion/ upserts into PostGIS only — ground truth,
never the simulation overlay. Which domain packages load is controlled by
ENABLED_DOMAINS; which adapter a CronJob runs is --adapter / Helm
adapterId. Details: ADD_DOMAIN.md. Cursor paste-prompts:
docs/prompts/add-domain/README.md.
Each adapter runs two ways: as a scheduled OpenShift CronJob for steady polling, and as an on-demand callable that the reasoning agent can trigger mid-query when it needs current data.
REST endpoints under /admin support automation: ingestion runs, graph browsing, scenario injection, and imports. Use seed-demo and POST /query for post-deploy smoke tests (see Demo against a live deployment).
| Route | Description |
|---|---|
GET /admin/stats |
Aggregate counts from Postgres and Neo4j |
GET /admin/entity-types |
Distinct entity types in the live store |
GET /admin/entities |
Paginated entity list with search/filter |
GET /admin/entities/geojson |
GeoJSON FeatureCollection for entities with geometry |
GET /admin/entities/{id} |
Entity detail and state history |
GET /admin/entities/in-bbox |
Entity IDs inside a WGS84 bounding box |
GET /admin/data/sync-status |
Postgres vs Neo4j entity drift |
GET /admin/ingestion/adapters |
List enabled ingestion adapters |
POST /admin/ingestion/run |
On-demand adapter run |
GET /admin/platform/config |
Read-only platform settings |
POST /admin/platform/bootstrap |
Idempotent schema bootstrap |
GET /admin/imports/formats |
Supported import formats and edge types |
POST /admin/imports/preview |
Validate an import file |
POST /admin/imports/commit |
Commit entities + dependency edges |
GET /admin/graph/nodes |
Entity nodes from Neo4j |
GET /admin/graph/scenarios |
Distinct scenario IDs |
GET /admin/graph/events |
SimulationEvent nodes (optional scenario filter) |
GET /admin/graph/edges |
All dependency / AFFECTED_BY edges |
POST /admin/graph/events |
Inject a new simulation event overlay |
POST /admin/graph/scenarios/{id}/sync-spatial |
Refresh AFFECTED_BY edges from PostGIS bbox |
POST /admin/graph/dependency-edges |
Create or merge a dependency edge |
DELETE /admin/graph/dependency-edges |
Remove a dependency edge |
DELETE /admin/graph/scenarios/{id} |
Remove a scenario from the graph and vector store |
A web UI is not shipped in the current release; a separate UI will be added in a future release (exact form TBD).
The pipeline (lib/reasoning/pipeline.py) is a ReAct (Reason + Act) agent loop: the LLM is the top-level orchestrator. It decides which tools to call, in what order, and when it has gathered enough information to answer. A safety cap of six rounds prevents unbounded loops.
The LLM has four tools:
| Tool | Module | What it does |
|---|---|---|
get_affected_subgraph |
lib/graph/tool.py |
Neo4j Cypher traversal — finds every entity reachable from the simulation event via dependency edges, plus entity attributes (callsign, route, etc.) |
solve_impact |
lib/reasoning/pipeline.py |
Runs the Stage-2 solver on the affected subgraph — returns impact score, chain length, value at risk, ranked response options, and recommended reroutes |
search_scenario_context |
lib/reasoning/search_tool.py |
pgvector semantic search over the scenario's event-narrative collection |
run_ingestion_pull |
lib/ingestion/tool.py |
On-demand live data refresh from a registered adapter |
The agent typically calls tools in the order above, but nothing enforces that sequence: a question about current positions may start with run_ingestion_pull; a simple clarification may skip the solver entirely. The tool_call_trace field in every POST /query response exposes each call the agent made and what it returned, making the reasoning fully auditable.
The three underlying data-access functions (Neo4j traversal, PostGIS + solver, pgvector search) are kept as independent modules (stage1.py, stage2.py, stage3.py). Each is independently testable and can be called directly — they are the tools' implementation, not the orchestration logic.
A POST /query request kicks off the ReAct agent loop. The diagram below captures the most common investigation path; the actual sequence depends on what the LLM decides to call.
Figure 2 — The ReAct query lifecycle. The LLM decides which tools to call; every tool invocation and its result appear in the tool_call_trace field of the response.
Typical agent sequence for an impact question:
POST /query { question, scenario_id }
│
│ Round 1 — LLM calls get_affected_subgraph(scenario_id)
├────── Neo4j traversal → entity IDs + dependency edges + attributes
│
│ Round 2 — LLM calls solve_impact(scenario_id)
├────── PostGIS live state read + StubSolver → impact score + response options
│
│ Round 3 (optional) — LLM calls search_scenario_context(query, scenario_id)
├────── pgvector search → event narrative chunks
│
└────── LLM produces final answer grounded in tool outputs
↓
QueryResponse { answer, affected_entities, solver, tool_call_trace }
The tool_call_trace in the response is the audit trail — it shows what the agent investigated and what each data source returned before the LLM wrote its answer.
The most important architectural decision is that a simulation never mutates live data. A simulation event is an overlay applied at query time: it is a node injected into the graph, connected by AFFECTED_BY edges to the entities it perturbs, with its narrative embedded separately in the vector store. The live snapshot is read through the lens of that event, but is left untouched.
This is what makes multiple concurrent what-if scenarios trivial — each is an independent overlay tagged by its own scenario id — and what makes them fully reversible: removing the event node resets everything in a single operation.
Figure 3 — The overlay mechanism. Ground truth (left) is read-only at query time. The event and its affected-entity references (right) are injected and removable, leaving the base graph intact.
What kind of simulation this is (and isn’t): This is a dependency-and-impact reasoning engine: it propagates effects through a known graph and applies solver logic on top. It is not a tick-by-tick discrete-event physics simulation (e.g. AnyLogic). That is an intentional trade: you gain explainability, speed, and concurrent what-if scenarios; you give up stochastic second-by-second temporal dynamics. Because the Stage 2 solver is pluggable, a full discrete-event engine can be dropped into that slot later without changing anything else.
The platform is best understood as a domain-agnostic skeleton with well-defined
swap points. The skeleton — OpenShift, vLLM (optional), Postgres, Neo4j, the
ReAct pipeline, and the overlay mechanism — stays identical. Domain-specific
code lives under top-level domain/<name>/ packages (adapters, optional
solvers). Which packages load is controlled by ENABLED_DOMAINS
(see ADD_DOMAIN.md; Cursor:
docs/prompts/add-domain/README.md).
Figure 4 — The fixed core (left) versus the per-domain swap seams (right). Domain adaptation touches only the right-hand column.
The reason this works is that impact propagation is graph traversal in every domain. A port closure cascading through dependent routes and a stopped machine cascading through dependent cells are the same Cypher traversal over a different schema. The table below makes the mapping concrete.
| Layer | Supply chain | Manufacturing plant |
|---|---|---|
| Ingestion | Flight / AIS / freight APIs (domain/…/adapters) |
OPC-UA, MQTT, SCADA, historian |
| Graph schema | Port, Route, Region | ISA-95: Site → Area → Work Cell → Equipment |
| Simulation event | Port closure, volcanic ash | Machine breakdown, material shortage |
| Solver (Stage 2) | Route pathfinding (domain/…/solver.py) |
Production rescheduling / line balancing |
| RAG context | Logistics precedent | SOPs, maintenance manuals, playbooks |
A manufacturing note worth flagging: plant sensor data is far higher-frequency than logistics data, so that domain leans harder on the historian/time-series side and may add a time-series extension or a downsampling step in ingestion. That is an ingestion-layer concern — it does not disturb the core.
Shipped today:
| Domain id | Package | Adapters |
|---|---|---|
aviation |
domain/aviation/ |
opensky_flights (live OpenSky) |
shipping |
domain/shipping/ |
shipping_demo (synthetic fixture; swap fetch for a live API) |
Disruptions (port closures, airspace shutdowns, etc.) are simulation event
overlays, not separate domains — see scripts/seed_demo.py (aviation) and
scripts/seed_shipping.py (LA port strike).
ENABLED_DOMAINS=aviation,shipping # default
uv run ingest-run --adapter opensky_flights
uv run ingest-run --adapter shipping_demo
uv run python scripts/seed_shipping.py # ingest + graph + LA closure scenario- Two separate graph stores (Neo4j for the dependency graph, Postgres for live data and embeddings) keep concerns cleanly separated; validate Neo4j connectivity and the Helm chart deployment early.
- The vLLM model must support structured tool calling, with tool-calling enabled at serve time (
--enable-auto-tool-choice) — the ReAct pipeline depends on the model correctly emitting tool calls and text completions. - The agent loop owns orchestration; the data-access modules stay pure.
stage1.py,stage2.py, andstage3.pycontain no agent logic — they are called by the pipeline dispatcher and remain independently testable. - Graph stays in Neo4j, geo stays in Postgres. The LLM client owns inference, embeddings, and vector search only — it is not a front door for all state.
- Live data and simulation knowledge stay separate. The overlay must never mutate ground truth; this is what enables concurrent, reversible what-if scenarios.
- The
tool_call_traceis the reasoning audit trail. EveryQueryResponseincludes the ordered list of tool calls the agent made — use this to debug or explain any answer.
In short: a fixed OpenShift-native skeleton handles platform, inference, storage, and agentic reasoning identically across domains, while domain packages under
domain/— adapters, optional solvers, and related wiring — are all that change to retarget it from supply chains to manufacturing plants. See ADD_DOMAIN.md.
domain/ # Domain packages (adapters, optional solvers)
aviation/
adapters/ # e.g. opensky_flights
shipping/
adapters/ # e.g. shipping_demo (synthetic → live API)
bootstrap_graph.py # Neo4j edges + scenario overlay
lib/
core/ # Domain-agnostic abstractions, interfaces, and Settings
ingestion/
registry.py # ENABLED_DOMAINS catalog + adapter/solver resolution
runner.py # Canonical ingest loop
tool.py # run_ingestion_pull tool schema + callable
graph/
bootstrap.py # Idempotent DDL for Postgres + Neo4j
nodes.py # Entity CRUD + dependency edge helpers
events.py # SimulationEvent overlay inject / remove
cypher.py # neo4j_session() context manager
tool.py # get_affected_subgraph tool schema + callable
reasoning/
pipeline.py # ReAct agent loop — top-level orchestrator
stage1.py # Neo4j graph traversal (called by pipeline dispatcher)
stage2.py # PostGIS live state read + solver (called by pipeline dispatcher)
stage3.py # Standalone synthesis helper (vector search + single generate())
search_tool.py # search_scenario_context tool schema + callable
types.py # QueryRequest / QueryResponse / ToolCallRecord
solver/
stub.py # StubSolver (fallback; domain solvers live under domain/)
tool.py # solve_impact tool schema + legacy callable
llm/
base.py # LLMClientBase protocol
openai_client.py # OpenAI-compatible inference + pgvector RAG
fake.py # FakeLLMClient for tests (supports response_sequence)
types.py # Message / ToolCall / GenerateResult / Chunk
apps/
api/ # FastAPI entrypoint + JSON admin API (/admin/*)
deploy/ # Containerfiles, Helm chart, local dev compose
local/
composefile.yml # Postgres + Neo4j for local development
container_files/
api/ # FastAPI app Containerfile
postgres/ # Custom Postgres image (pgvector + PostGIS)
helm/ # Umbrella Helm chart (authoritative deploy path)
templates/
api/ # API Deployment, Service, Route
bootstrap/ # Schema bootstrap Job
ingestion/ # CronJob + hook Job
neo4j/ # neo4j-auth Secret, SA, SCC binding
postgres/ # Postgres StatefulSet + Services
tests/
uv sync --all-extrascp .env.example .env
# Defaults match compose (Postgres on 5433 — not 5432, to avoid Postgres.app clashes):
# POSTGRES_DSN=postgresql://sim:sim@localhost:5433/sim
# NEO4J_URI=bolt://localhost:7687
# NEO4J_USER=neo4j
# NEO4J_PASSWORD=simsimsim
# ENABLED_DOMAINS=aviation,shippingmake local-up
# or: podman compose -f deploy/local/composefile.yml up -d --waitThis starts Postgres (pgvector + PostGIS) on port 5433 and Neo4j on ports
7474 (Browser UI) and 7687 (Bolt). --wait blocks until healthchecks pass.
make local-bootstrap
# or: uv run python -m lib.graph.bootstrapuv run python -m apps.api.main
# or:
uv run uvicorn apps.api.app:app --reloadVisit http://localhost:8000/health — returns {"status": "ok", "db": "reachable"} when Postgres is reachable.
make test-unit
# or: uv run pytestCLI helpers for smoke-testing a running cluster (UK airspace closure scenario):
# Seed demo entities + UK closure overlay, then POST /query
make smoke-test
# or with in-cluster seeding after deploy:
SEED_MODE=cluster NAMESPACE=general-simulation make smoke-test
# Seed only (local or in-cluster)
uv run seed-demo
# Query only against a running API (defaults to UK airspace closure)
./scripts/smoke-uk-closure.sh # set SEED_MODE=skip to skip seeding
# Shipping LA-closure demo (fixture ingest + graph + overlay)
uv run python scripts/seed_shipping.pyseed-demo / scripts/seed_demo.py create sample aircraft, dependency edges, and the opensky-uk-closure-001 simulation event so the full pipeline can be exercised end to end.
Set LLM_BACKEND=fake in .env (or the environment). This swaps in
FakeLLMClient which returns canned completions, embeddings, and vector
search hits, so the full reasoning pipeline can be exercised in tests without a
GPU or a running Llama Stack server.
The app uses any OpenAI-compatible inference endpoint. The backend is selected
by the LLM_BACKEND environment variable.
- Start vLLM locally with tool-calling enabled:
export HF_TOKEN=<your-hf-token> # Llama-derived weights are gated
vllm serve RedHatAI/Meta-Llama-3.1-8B-Instruct-FP8 \
--enable-auto-tool-choice \
--tool-call-parser llama3_json \
--max-model-len 8192- Set in
.env:
LLM_BASE_URL=http://localhost:8080/v1
OPENAI_API_KEY=unused
LLM_BACKEND=openai
GENERATION_MODEL_ID=RedHatAI/Meta-Llama-3.1-8B-Instruct-FP8
EMBEDDING_MODEL_ID=all-MiniLM-L6-v2
EMBEDDING_DIMENSION=384OpenShift umbrella deploys set this automatically. For local overrides:
LLM_BASE_URL=http://llamastack:8321/v1
OPENAI_API_KEY=unused
LLM_BACKEND=openai
GENERATION_MODEL_ID=openai/gpt-4o-mini # or llama-3-2-3b-instruct/meta-llama/Llama-3.2-3B-InstructDeployment is driven by a Makefile that wraps podman build/push for
images and Helm for all Kubernetes resources. Each component has its own
Helm chart under deploy/helm/ so components can be upgraded independently.
| Requirement | Notes |
|---|---|
| OpenShift 4.13+ | Tested against OCP 4.14/4.15 |
oc CLI logged in |
oc login ... — needs cluster-admin (or a role covering Deployments, StatefulSets, Services, Routes, Jobs, CronJobs, Secrets, ConfigMaps, ServiceAccounts, and ClusterRoleBindings) |
helm 3.x |
Install Helm |
podman |
To build and push images |
| GPU nodes | Required only when enabling llm-service on GPU |
| NVIDIA GPU Operator | Required for GPU llm-service device profile |
| Red Hat OpenShift AI | Required for in-cluster llm-service (KServe ServingRuntime / InferenceService) |
Core platform components (Postgres, Neo4j, API, ingestion) need no extra operators. Postgres runs as a plain StatefulSet. In-cluster vLLM uses the shared llm-service chart on OpenShift AI.
# 1. Log in to quay.io so podman can push images
podman login quay.io
# 2. Configure secrets (required before deploy)
cp deploy/helm/values-secrets.yaml.example deploy/helm/values-secrets.yaml
# edit deploy/helm/values-secrets.yaml — passwords, API tokens, hf_token
# 3. Build and push container images
make build
# 4. One umbrella release (Postgres + Neo4j + Llama Stack + API + ingestion)
make deployDefault toggles in deploy/helm/values.yaml: ingestion.enabled: false
(CronJob off until you turn it on), llm-service.enabled: true (requires GPU +
OpenShift AI unless you disable it).
make deploy installs the umbrella chart as a single Helm release, creates
templates/neo4j/ resources (neo4j-sa, anyuid SCC, Secret neo4j-auth when
openshift.neo4j.scc.enabled), and wires Llama Stack to enabled global.models
providers.
| Chart | Path | Key resources |
|---|---|---|
general-simulation |
deploy/helm/ |
Inline templates: postgres/, neo4j/, bootstrap/, api/, ingestion/ |
neo4j |
external dep | Official Neo4j StatefulSet (reads Secret neo4j-auth) |
llama-stack |
external dep | Inference gateway (llamastack:8321) |
llm-service |
external dep | In-cluster vLLM; enable via llm-service.enabled |
# Default: push to quay.io/rh-ai-quickstart/
make build
# Local testing — your Quay org (match APP_IMAGE_NAME to your repo names):
make build REGISTRY=quay.io/robertsandoval APP_IMAGE_NAME=general-sim-api
# Or build individual images:
make build-postgres
make build-appOverride the registry or tag if needed:
make build REGISTRY=quay.io/myorg TAG=v1.2.3Use the single umbrella release — Postgres, Neo4j, bootstrap, Llama Stack, API,
and ingestion are all in deploy/helm/:
make deploySecrets must be in deploy/helm/values-secrets.yaml (see deploy/helm/values-secrets.yaml.example).
Component toggles live in deploy/helm/values.yaml under postgres.enabled, api.enabled, etc.
Neo4j is deployed by the umbrella chart (neo4j.enabled in deploy/helm/values.yaml).
OpenShift wiring (neo4j-sa, SCC, neo4j-auth) is rendered from
deploy/helm/templates/neo4j/ when openshift.neo4j.scc.enabled is true.
For local Browser + Bolt access (Bolt cannot be proxied through a Route):
make neo4j-connectSet llm-service.enabled: true in deploy/helm/values.yaml and enable
the in-cluster model under global.models. Llama Stack points at the
InferenceService (<model-key>-vllm). Set llm-service.secret.hf_token in
deploy/helm/values-secrets.yaml.
Requires Red Hat OpenShift AI (KServe). First start downloads model weights and can take several minutes. Do not point the API at vLLM directly — Stack is the only client of that Service.
After changing code, rebuild and redeploy the umbrella chart:
make build-app
make deployTo change config, edit deploy/helm/values.yaml and/or
deploy/helm/values-secrets.yaml, then run make deploy.
make undeploy
# PVCs are NOT deleted automatically — remove manually if needed:
# oc delete pvc -n general-simulation --allmake help
make build
make deploy
make neo4j-connect
make status
make lint-charts
make undeploy| Variable | Default | Description |
|---|---|---|
REGISTRY |
quay.io/rh-ai-quickstart |
Image registry root |
APP_IMAGE_NAME |
general-sim-api |
App image name under REGISTRY |
NAMESPACE |
general-simulation |
Target OpenShift namespace |
TAG |
latest |
Image tag |
DEPLOY_TIMEOUT |
25m |
Helm --wait timeout |
Short names resolve inside the release namespace (standalone or when this chart is a subchart). Cross-namespace clients should use <service>.<namespace>.svc.
| Service | Same namespace | Cross-namespace example |
|---|---|---|
| Postgres | postgres:5432 |
postgres.general-simulation.svc:5432 |
| Neo4j Bolt | bolt://neo4j:7687 |
bolt://neo4j.general-simulation.svc:7687 |
| Neo4j HTTP | http://neo4j:7474 |
http://neo4j.general-simulation.svc:7474 |
| Llama Stack | http://llamastack:8321 |
http://llamastack.<ns>.svc:8321 |
vLLM (llm-service) |
http://<model-key>-vllm |
http://<model-key>-vllm.<ns>.svc |
| API | http://general-sim-api:8000 |
http://general-sim-api.general-simulation.svc:8000 |
The umbrella chart under deploy/helm/ is the primary install path (make deploy).
See deploy/helm/README.md.
Pre-Helm Kubernetes manifests are preserved under deploy/archived/openshift/
for reference. The Helm chart under deploy/helm/ is the authoritative deployment path.



