Skip to content

Repository files navigation

General Simulation & Impact-Reasoning Platform (MVP)

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.


Table of contents


What this platform does

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.


System at a glance

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.

Layered system overview

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.


The components and how they relate

OpenShift — the platform

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.

Llama Stack — inference gateway

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

LLM client — the inference and RAG backend

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 — the property graph

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.

PostgreSQL — two jobs, two extensions

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 — getting live data in

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.

Admin UI — browse and manage 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 ReAct agent pipeline

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.


How a query flows through the system

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.

The query lifecycle

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.


How the simulation actually works

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.

The overlay mechanism

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.


Why the same design serves multiple domains

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).

Fixed core vs. swap seams

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

Key decisions and risks to watch

  1. 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.
  2. 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.
  3. The agent loop owns orchestration; the data-access modules stay pure. stage1.py, stage2.py, and stage3.py contain no agent logic — they are called by the pipeline dispatcher and remain independently testable.
  4. 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.
  5. Live data and simulation knowledge stay separate. The overlay must never mutate ground truth; this is what enables concurrent, reversible what-if scenarios.
  6. The tool_call_trace is the reasoning audit trail. Every QueryResponse includes 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.


Repository layout

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/

Quickstart (local dev)

1. Install dependencies

uv sync --all-extras

2. Start local services (Postgres + Neo4j)

docker compose up -d

This 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.bootstrap

3. Configure environment

cp .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,shipping

4. Run the API

uv run python -m src.api.main
# or:
uv run uvicorn src.api.app:app --reload

Visit 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).

5. Run tests (no GPU or live Llama Stack required)

uv run pytest

6. Demo against a live deployment

Two 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.py

demo.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.


Running without hardware (CI / dev laptops)

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.


LLM backend configuration

The app uses any OpenAI-compatible inference endpoint. The backend is selected by the LLM_BACKEND environment variable.

Pointing at a self-hosted vLLM for local dev

  1. 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
  1. 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=384

Pointing at Llama Stack

OpenShift 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-Instruct

Running without a GPU (CI / dev laptops)

Set 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 tests
  • response_sequence — an ordered queue of GenerateResult objects popped on each generate() call; use this to simulate a full multi-step ReAct trace in tests without a real model
LLM_BACKEND=fake

OpenShift Deployment

Deployment 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.

Prerequisites

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.


Quick start — full deploy

# 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).


Helm chart overview

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

Step 1 — Build and push container images

# 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-app

Override the registry or tag if needed:

make build REGISTRY=quay.io/myorg TAG=v1.2.3

Step 2 — Deploy Postgres

make deploy-postgres PG_PASSWORD=<your-password>

This installs the postgres Helm chart which:

  • Creates the general-simulation namespace (idempotent)
  • Applies a ClusterRoleBinding granting anyuid SCC to the postgres-sa ServiceAccount (so the container can run as UID 999)
  • Creates the postgres-credentials Secret from --set postgres.password=...
  • Mounts an init-SQL ConfigMap that enables the vector and postgis extensions 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=300s

Step 3 — Deploy Neo4j

make 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-sa ServiceAccount and grants it the anyuid SCC (Neo4j runs as UID/GID 7474, which restricted-v2 rejects)
  • Creates a neo4j-auth Secret with NEO4J_AUTH=neo4j/<password> (pass the password only — do not include a neo4j/ prefix in NEO4J_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-connect

Step 4 — Run the schema bootstrap Job

make 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.


Step 4 — In-cluster vLLM (local mode only)

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.


Step 6 — Deploy the API and ingestion CronJob

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=120s

Per-component upgrades

After 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.


Tear-down

make undeploy
# PVCs are NOT deleted automatically — remove manually if needed:
# oc delete pvc -n general-simulation --all

Makefile reference

make 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

In-cluster service names

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.

About

General simulation is a neo4j backed simulation framework.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages