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 UI | Built-in SPA at /admin/ |
| 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 exactly two modes:
| Mode | Upstream | When to use |
|---|---|---|
openai (default) |
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 src/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(src/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 src/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.
A built-in single-page application is served at GET /admin/. It provides a read/write view over both stores without any extra tooling:
| Route | Description |
|---|---|
GET /admin/ |
Admin SPA (HTML) |
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/{id} |
Entity detail and state history |
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 |
DELETE /admin/graph/scenarios/{id} |
Remove a scenario from the graph and vector store |
The pipeline (src/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 |
src/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 |
src/reasoning/pipeline.py |
Runs the Stage-2 solver on the affected subgraph — returns impact score, chain length, and ranked response options |
search_scenario_context |
src/reasoning/search_tool.py |
pgvector semantic search over the scenario's event-narrative collection |
run_ingestion_pull |
src/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
src/
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
api/ # FastAPI entrypoint + admin SPA
deploy/ # Containerfiles, Helm charts, OpenShift manifests
tests/
uv sync --all-extrasdocker compose up -dThis starts Postgres (pgvector + PostGIS) on port 5432 and Neo4j on ports 7474 (Browser UI) and 7687 (Bolt). Wait for both healthchecks to pass, then run the schema bootstrap:
uv run python -m src.graph.bootstrapcp .env.example .env
# Edit .env: set POSTGRES_DSN, NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD,
# LLM_* settings, and optionally ENABLED_DOMAINS (default: aviation,shipping).
#
# With compose defaults:
# POSTGRES_DSN=postgresql://sim:sim@localhost:5432/sim
# NEO4J_URI=bolt://localhost:7687
# NEO4J_USER=neo4j
# NEO4J_PASSWORD=sim
# ENABLED_DOMAINS=aviation,shippinguv run python -m src.api.main
# or:
uv run uvicorn src.api.app:app --reloadVisit http://localhost:8000/health — returns {"status": "ok", "db": "reachable"} when Postgres is reachable.
Visit http://localhost:8000/admin/ for the admin SPA (requires both Postgres and Neo4j).
uv run pytestTwo helpers are included for smoke-testing a running cluster:
# Run a canned query against the deployed API
./demo.sh [scenario_id] [question]
# Seed aviation UK-closure demo (Neo4j + Postgres)
uv run python scripts/seed_demo.py
# Seed shipping LA-closure demo (fixture ingest + graph + overlay)
uv run python scripts/seed_shipping.pydemo.sh defaults to the shipping LA port-closure scenario.
seed_demo.py / seed_shipping.py create sample entities, dependency edges, and a 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-InstructSet LLM_BACKEND=fake in .env. FakeLLMClient provides:
- Deterministic embeddings (hash-seeded unit vectors, correct dimension)
- In-memory vector store (ingest then search, cosine similarity)
canned_tool_calls— emitted once then cleared, for single-round tool testsresponse_sequence— an ordered queue ofGenerateResultobjects popped on eachgenerate()call; use this to simulate a full multi-step ReAct trace in tests without a real model
LLM_BACKEND=fakeDeployment 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. Build and push container images
make build
# 3. One umbrella release (Postgres + Neo4j + Llama Stack + API + ingestion)
# Secrets via --set only — never committed to values files.
# Default: Llama Stack → OpenAI
make deploy \
PG_PASSWORD=<pw> NEO4J_PASSWORD=<pw> \
OPENAI_API_KEY=<key>
# Or: Llama Stack → in-cluster vLLM (needs OpenShift AI + GPU)
make deploy LLM_MODE=local \
PG_PASSWORD=<pw> NEO4J_PASSWORD=<pw> \
HF_TOKEN=<hf-token>make deploy installs the umbrella chart as a single Helm release, creates
make deploy applies the umbrella Helm chart, which creates neo4j-sa / anyuid SCC (when openshift.neo4j.scc.enabled) and Secret neo4j-auth, and wires Llama Stack for the chosen
LLM_MODE (openai or local).
| Chart | Path | Key resources |
|---|---|---|
general-simulation (umbrella) |
deploy/helm/general-simulation |
Single release; pulls subcharts below |
postgres |
deploy/helm/postgres |
StatefulSet, Services, anyuid SCC, Secret, init SQL |
neo4j |
neo4j/neo4j (official) |
StatefulSet; neo4j-sa + anyuid for UID 7474 |
bootstrap |
deploy/helm/bootstrap |
Schema Job (Helm hook) |
llama-stack |
ai-architecture-charts | Inference gateway (llamastack:8321) |
llm-service |
same repo | In-cluster vLLM; enabled only for LLM_MODE=local |
api |
deploy/helm/api |
Deployment, Service, Route |
ingestion |
deploy/helm/ingestion |
CronJob |
# 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.3make deploy-postgres PG_PASSWORD=<your-password>This installs the postgres Helm chart which:
- Creates the
general-simulationnamespace (idempotent) - Applies a
ClusterRoleBindinggrantinganyuidSCC to thepostgres-saServiceAccount (so the container can run as UID 999) - Creates the
postgres-credentialsSecret from--set postgres.password=... - Mounts an init-SQL ConfigMap that enables the
vectorandpostgisextensions on first startup - Deploys a StatefulSet with a 10 Gi PVC and readiness/liveness probes
Wait for Postgres to be ready:
oc rollout status statefulset/postgres -n general-simulation --timeout=300smake deploy-neo4j NEO4J_PASSWORD=<your-password>This installs the official neo4j/neo4j Helm chart (advanced per-component target). The umbrella chart (make deploy) creates neo4j-sa, anyuid SCC, and neo4j-auth automatically when openshift.neo4j.scc.enabled is true.
The standalone deploy-neo4j target still:
- Creates a
neo4j-saServiceAccount and grants it theanyuidSCC (Neo4j runs as UID/GID 7474, whichrestricted-v2rejects) - Creates a
neo4j-authSecret withNEO4J_AUTH=neo4j/<password>(pass the password only — do not include aneo4j/prefix inNEO4J_PASSWORD) - Deploys a StatefulSet with Bolt (7687) and HTTP Browser (7474) services
- Creates an edge-terminated HTTPS Route for Neo4j Browser
Pass the same NEO4J_PASSWORD to later bootstrap/API/ingestion targets so they
can authenticate against this instance.
For local Browser + Bolt access (Bolt cannot be proxied through the Route):
make neo4j-connectmake deploy-bootstrap PG_PASSWORD=<your-password> NEO4J_PASSWORD=<your-password>The bootstrap chart deploys a Job as a Helm post-install,post-upgrade hook.
Helm waits for the Job to complete before marking the release successful
(--atomic --timeout 3m). The Job is deleted automatically on success.
Re-running make deploy-bootstrap is fully idempotent.
Prefer make deploy LLM_MODE=local. That enables llm-service inside the
umbrella and points Llama Stack at the in-cluster InferenceService
(<model-key>-vllm).
Standalone / debug:
make deploy-llm-service HF_TOKEN=<your-hf-token>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.
Legacy plain Deployment manifests remain under deploy/openshift/vllm/ and
deploy/archived/vllm-helm/ for reference only.
make deploy-api PG_PASSWORD=<your-password> NEO4J_PASSWORD=<your-password> OPENAI_API_KEY=<your-key>
make deploy-ingestion PG_PASSWORD=<your-password> NEO4J_PASSWORD=<your-password> OPENAI_API_KEY=<your-key>The api chart creates 2 replicas with topology spread across nodes and an
OpenShift Route with TLS edge termination.
Smoke test after deploy:
ROUTE=$(oc get route general-sim-api -n general-simulation -o jsonpath='{.spec.host}')
curl -s https://$ROUTE/health | jq .
# Expected: {"status": "ok", "db": "reachable"}Trigger the ingestion job immediately to verify end-to-end:
oc create job ingestion-manual \
--from=cronjob/general-sim-ingestion \
-n general-simulation
oc wait job/ingestion-manual \
-n general-simulation --for=condition=complete --timeout=120sAfter changing code or config, rebuild the affected image and upgrade only that chart — no need to re-deploy everything:
make build-app
make deploy-api PG_PASSWORD=<your-password> NEO4J_PASSWORD=<your-password>To upgrade a chart's non-secret values, edit deploy/helm/<chart>/values.yaml
and re-run the make deploy-<chart> target. Secrets are always supplied via
--set and are never stored in values files.
make undeploy
# PVCs are NOT deleted automatically — remove manually if needed:
# oc delete pvc -n general-simulation --allmake help
make build
make deploy PG_PASSWORD=<pw> NEO4J_PASSWORD=<pw> OPENAI_API_KEY=<key>
make deploy LLM_MODE=local PG_PASSWORD=<pw> NEO4J_PASSWORD=<pw> HF_TOKEN=<tok>
make neo4j-connect
make status
make lint-charts
make undeploy
# Advanced per-component: deploy-postgres, deploy-neo4j, deploy-bootstrap,
# deploy-api, deploy-ingestion, deploy-llm-service| Variable | Default | Description |
|---|---|---|
LLM_MODE |
openai |
openai or local |
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 |
PG_PASSWORD |
(none) | Required |
NEO4J_PASSWORD |
(none) | Required |
OPENAI_API_KEY |
(none) | Required when LLM_MODE=openai |
HF_TOKEN |
(none) | Required when LLM_MODE=local |
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, local mode) |
http://llama-3-2-3b-instruct-vllm |
http://llama-3-2-3b-instruct-vllm.<ns>.svc |
| API | http://general-sim-api:8000 |
http://general-sim-api.general-simulation.svc:8000 |
The umbrella chart under deploy/helm/general-simulation is the primary
install path (make deploy). See
deploy/helm/general-simulation/README.md.
Raw Kubernetes manifests (pre-Helm) are preserved under deploy/openshift/ for
reference. The Helm charts under deploy/helm/ are the authoritative
deployment path going forward.



