Skip to content

Commit a8abf60

Browse files
Merge pull request #104 from lpiwowar/lpiwowar/rag
Configure OGX with RAG
2 parents d98fee9 + 2831297 commit a8abf60

40 files changed

Lines changed: 1516 additions & 674 deletions

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ $(KUTTL): $(LOCALBIN)
274274

275275
.PHONY: kuttl-test
276276
kuttl-test: kuttl ## Run kuttl tests
277+
@command -v diff >/dev/null 2>&1 || { echo "ERROR: 'diff' command is required for KUTTL tests but not found in PATH" >&2; exit 1; }
278+
@command -v oc >/dev/null 2>&1 || { echo "ERROR: 'oc' command is required for KUTTL tests but not found in PATH" >&2; exit 1; }
277279
$(LOCALBIN)/kubectl-kuttl test --config kuttl-test.yaml test/kuttl/tests $(KUTTL_ARGS)
278280

279281
.PHONY: kuttl-test-run

api/v1beta1/openstacklightspeed_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ import (
2323
)
2424

2525
const (
26+
// TODO(lpiwowar): Replace this with a stable (non-alpha) image version once
27+
// the automated pipeline for building OGX-compatible vector database images
28+
// is ready.
2629
// OpenStackLightspeedContainerImage is the fall-back container image for OpenStackLightspeed
27-
OpenStackLightspeedContainerImage = "quay.io/openstack-lightspeed/rag-content:os-docs-2025.2"
30+
OpenStackLightspeedContainerImage = "quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2"
2831

2932
// LCoreContainerImage is the fall-back container image for LCore
3033
LCoreContainerImage = "quay.io/lightspeed-core/lightspeed-stack:latest"

bundle/manifests/openstack-lightspeed-operator.clusterserviceversion.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ metadata:
2525
]
2626
capabilities: Basic Install
2727
categories: AI/Machine Learning
28-
createdAt: "2026-04-09T11:08:52Z"
28+
createdAt: "2026-05-17T15:20:36Z"
2929
description: AI-powered virtual assistant for Red Hat OpenStack Services on OpenShift
3030
features.operators.openshift.io/cnf: "false"
3131
features.operators.openshift.io/cni: "false"
@@ -255,7 +255,7 @@ spec:
255255
fieldRef:
256256
fieldPath: metadata.annotations['olm.targetNamespaces']
257257
- name: RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT
258-
value: quay.io/openstack-lightspeed/rag-content:os-docs-2025.2
258+
value: quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2
259259
- name: RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT
260260
value: quay.io/lightspeed-core/lightspeed-stack:latest
261261
- name: RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT
@@ -433,7 +433,7 @@ spec:
433433
name: Red Hat
434434
url: https://github.com/openstack-lightspeed/operator
435435
relatedImages:
436-
- image: quay.io/openstack-lightspeed/rag-content:os-docs-2025.2
436+
- image: quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2
437437
name: openstack-lightspeed-image-url-default
438438
- image: quay.io/lightspeed-core/lightspeed-stack:latest
439439
name: lcore-image-url-default

config/manager/manager.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ spec:
7070
valueFrom:
7171
fieldRef:
7272
fieldPath: metadata.namespace
73+
# TODO(lpiwowar): Replace this with a stable (non-alpha) image version once
74+
# the automated pipeline for building OGX-compatible vector database images
75+
# is ready.
7376
- name: RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT
74-
value: quay.io/openstack-lightspeed/rag-content:os-docs-2025.2
77+
value: quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2
7578
- name: RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT
7679
value: quay.io/lightspeed-core/lightspeed-stack:latest
7780
- name: RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT

hack/env.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
export RELATED_IMAGE_LCORE_IMAGE_URL_DEFAULT="quay.io/lightspeed-core/lightspeed-stack:latest"
33
export RELATED_IMAGE_EXPORTER_IMAGE_URL_DEFAULT="quay.io/lightspeed-core/lightspeed-to-dataverse-exporter:latest"
44
export RELATED_IMAGE_POSTGRES_IMAGE_URL_DEFAULT="registry.redhat.io/rhel9/postgresql-16:latest"
5-
export RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT=quay.io/openstack-lightspeed/rag-content:os-docs-2025.2
5+
# TODO(lpiwowar): Replace this with a stable (non-alpha) image version once
6+
# the automated pipeline for building OGX-compatible vector database images
7+
# is ready.
8+
export RELATED_IMAGE_OPENSTACK_LIGHTSPEED_IMAGE_URL_DEFAULT="quay.io/openstack-lightspeed/rag-content:alpha-ogx-os-docs-2025.2"
69
export WATCH_NAMESPACE="openstack-lightspeed"
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright 2026.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
"""Vector database configuration builder for the OpenStack Lightspeed operator.
19+
20+
Runs as the second init container (`vector-database-config-build`), after
21+
`vector_database_collect.sh`. It loads operator-provided base configs, walks every
22+
vector DB directory left by the collect step, and writes merged configs back to
23+
the shared volume.
24+
25+
Input layout (under --vector-db-path, produced by vector_database_collect.sh):
26+
{vector-db-path}/
27+
└── <image_uuid_dir>/ (random directory name from collect script)
28+
├── vector_db/
29+
│ ├── <vector-db-name>/
30+
│ │ ├── llama-stack.yaml
31+
│ │ └── faiss_store.db
32+
│ └── ocp_X.YZ/ (optional, when OCP RAG is enabled)
33+
│ ├── llama-stack.yaml
34+
│ └── faiss_store.db
35+
└── embeddings_model/
36+
37+
Output (written to --vector-db-path, same basenames as the base configs):
38+
{vector-db-path}/
39+
├── ogx_config.yaml
40+
├── lightspeed-stack.yaml
41+
└── <collect-dir>/ (collected data preserved)
42+
43+
Processing:
44+
1. For each subdirectory of */vector_db/, read its llama-stack.yaml.
45+
2. For each detected llama-stack.yaml file, extract its data and inject
46+
the relevant entries into the output ogx_config.yaml and lightspeed-stack.yaml
47+
files.
48+
3. Write the merged YAML next to the collected data.
49+
50+
Warning: This script only injects values into existing config structures.
51+
Base configs MUST contain otherwise this script will fail:
52+
- OGX config: registered_resources.{models,vector_stores}, storage.backends,
53+
providers.{inference,vector_io}
54+
- Lightspeed Stack config: byok_rag, rag.inline
55+
56+
Arguments:
57+
--vector-db-path Shared volume path (input collected data, output configs)
58+
--ogx-config-path Path to the base OGX configuration file
59+
--lightspeed-stack-path Path to the base Lightspeed Stack configuration file
60+
"""
61+
62+
import argparse
63+
from pathlib import Path
64+
from typing import Any, Iterable, Optional, Callable
65+
import logging
66+
import sys
67+
68+
import yaml
69+
70+
# Template for the directory path where data for a single vector database
71+
# instance resides. In the configuration, VECTOR_DB_DATA_PATH will typically
72+
# be substituted by the operator as an environment variable.
73+
VECTOR_DB_DIR_TEMPLATE = (
74+
"${{env.VECTOR_DB_DATA_PATH}}/{uuid}/vector_db/{vector_db_name}"
75+
)
76+
77+
# Template for the directory path where data for an embedding model resides. In
78+
# the configuration, VECTOR_DB_DATA_PATH will be substituted by OGX using
79+
# an environment variable.
80+
EMBEDDING_MODEL_DIR_TEMPLATE = "${{env.VECTOR_DB_DATA_PATH}}/{uuid}/embeddings_model"
81+
82+
# Template for a file path where vector db data are stored.
83+
VECTOR_DB_DATA_PATH_TEMPLATE = f"{VECTOR_DB_DIR_TEMPLATE}/faiss_store.db"
84+
85+
# The original configuration file name for OGX in the mounted vector database data.
86+
# Update later: The file is still named 'llama-stack.yaml' for backward compatibility,
87+
# since the Llama Stack project was renamed to OGX recently.
88+
OGX_CONFIG_SOURCE_FILE_NAME = "llama-stack.yaml"
89+
90+
91+
# -- Shared functions --------------------------------------------------------
92+
def load_yaml_file(yaml_file_path: Path) -> dict[str, Any]:
93+
"""Load YAML file"""
94+
try:
95+
with open(yaml_file_path, "r", encoding="utf-8") as f:
96+
return yaml.safe_load(f) or {}
97+
except FileNotFoundError:
98+
logging.error("YAML file not found: %s", yaml_file_path)
99+
sys.exit(1)
100+
101+
102+
def add_unique(lst: list, item: Any, key: Optional[str] = None) -> None:
103+
"""Add item to list if not already present.
104+
105+
:param lst: List to modify in-place
106+
:param item: Item to add
107+
:param key: If provided, check uniqueness by comparing item[key] values.
108+
If None, check direct item equality.
109+
"""
110+
if key:
111+
if any(existing.get(key) == item.get(key) for existing in lst):
112+
return
113+
elif item in lst:
114+
return
115+
lst.append(item)
116+
117+
118+
def write_yaml_file(yaml_data: dict[str, Any], dest_path: Path) -> None:
119+
"""Write YAML data to the specified file path."""
120+
try:
121+
dest_path.parent.mkdir(parents=True, exist_ok=True)
122+
with open(dest_path, "w", encoding="utf-8") as f:
123+
yaml.dump(yaml_data, f, default_flow_style=False, sort_keys=False)
124+
except (OSError, yaml.YAMLError) as e:
125+
logging.error("Failed to write YAML to %s: %s", dest_path, e)
126+
sys.exit(1)
127+
128+
129+
def iterate_vector_db_data_dir(vector_db_data_dir_path: Path) -> Iterable[Path]:
130+
"""Return all folders inside any vector_db/ subfolder, one per yield."""
131+
for image_uuid_dir in vector_db_data_dir_path.iterdir():
132+
vector_db_path = image_uuid_dir.joinpath("vector_db")
133+
134+
if not vector_db_path.is_dir():
135+
continue
136+
137+
for folder in vector_db_path.iterdir():
138+
if folder.is_dir():
139+
yield folder
140+
141+
142+
def config_build(
143+
vector_db_parent_dir: Path,
144+
config_target_path: Path,
145+
config_populate_fn: Callable[[Path, dict[str, Any]], dict[str, Any]],
146+
) -> None:
147+
config_target = load_yaml_file(config_target_path)
148+
for vector_db_dir in iterate_vector_db_data_dir(vector_db_parent_dir):
149+
ogx_config_source_path = vector_db_dir.joinpath(OGX_CONFIG_SOURCE_FILE_NAME)
150+
try:
151+
config_target = config_populate_fn(ogx_config_source_path, config_target)
152+
except (KeyError, IndexError) as e:
153+
logging.error(
154+
"Error processing config: missing required section in source "
155+
"or target file (%s)",
156+
e,
157+
)
158+
sys.exit(1)
159+
160+
config_product_path = vector_db_parent_dir.joinpath(config_target_path.name)
161+
write_yaml_file(config_target, config_product_path)
162+
163+
164+
# ----------------------------------------------------------------------------
165+
166+
167+
# -- OGX functions -----------------------------------------------------------
168+
def ogx_process(ogx_config_source_path: Path, ogx_config_target: dict[str, Any]):
169+
"""Populate the target OGX config with vector DB data from source OGX config"""
170+
ogx_config_source = load_yaml_file(ogx_config_source_path)
171+
172+
# E.g.: /data/<uuid>/vector_db/os_product_docs/llama-stack.yaml -> <uuid>
173+
image_uuid = ogx_config_source_path.parts[-4]
174+
175+
# E.g.: /data/<uuid>/vector_db/os_product_docs/llama-stack.yaml -> os_product_docs
176+
vector_db_name = ogx_config_source_path.parts[-2]
177+
178+
vector_db_file = VECTOR_DB_DATA_PATH_TEMPLATE.format(
179+
uuid=image_uuid, vector_db_name=vector_db_name
180+
)
181+
embedding_model_dir = EMBEDDING_MODEL_DIR_TEMPLATE.format(uuid=image_uuid)
182+
183+
# Populate registered_resources.models
184+
src_model = ogx_config_source["registered_resources"]["models"][0].copy()
185+
src_model["provider_model_id"] = embedding_model_dir
186+
tgt_models = ogx_config_target["registered_resources"]["models"]
187+
add_unique(tgt_models, src_model, "model_id")
188+
189+
# Populate registered_resources.vector_stores
190+
embedding_model = f"{src_model['provider_id']}/{embedding_model_dir}"
191+
src_vstore = ogx_config_source["registered_resources"]["vector_stores"][0].copy()
192+
src_vstore["embedding_model"] = embedding_model
193+
tgt_vstores = ogx_config_target["registered_resources"]["vector_stores"]
194+
add_unique(tgt_vstores, src_vstore)
195+
196+
# Populate storage.backends
197+
storage_backend_key = f"kv_rag_{image_uuid}_{vector_db_name}"
198+
storage_backend = ogx_config_source["storage"]["backends"]["kv_rag"].copy()
199+
storage_backend["db_path"] = vector_db_file
200+
ogx_config_target["storage"]["backends"][storage_backend_key] = storage_backend
201+
202+
# Populate providers.inference
203+
src_inference = ogx_config_source["providers"]["inference"][0]
204+
tgt_inferences = ogx_config_target["providers"]["inference"]
205+
add_unique(tgt_inferences, src_inference)
206+
207+
# Populate providers.vector_io
208+
src_vector_io = ogx_config_source["providers"]["vector_io"][0].copy()
209+
src_vector_io["config"]["persistence"]["backend"] = storage_backend_key
210+
tgt_vector_ios = ogx_config_target["providers"]["vector_io"]
211+
add_unique(tgt_vector_ios, src_vector_io)
212+
213+
return ogx_config_target
214+
215+
216+
# ----------------------------------------------------------------------------
217+
218+
219+
# -- Lightspeed Stack functions ----------------------------------------------
220+
def lstack_process(
221+
ogx_config_source_path: Path, lstack_config_target: dict[str, Any]
222+
) -> dict[str, Any]:
223+
"""Update Lightspeed stack config with RAG entries from OGX config source."""
224+
ogx_config_source = load_yaml_file(ogx_config_source_path)
225+
226+
src_vstores = ogx_config_source["registered_resources"]["vector_stores"]
227+
vector_store_id = src_vstores[0]["vector_store_id"]
228+
229+
add_unique(
230+
lstack_config_target["byok_rag"],
231+
{
232+
"rag_id": vector_store_id,
233+
"vector_db_id": vector_store_id,
234+
# The score multiplier is set to 1.0 so all BYOK sources have
235+
# equal weighting.
236+
"score_multiplier": 1.0,
237+
# The Lightspeed Stack currently requires a "db_path" value even
238+
# when OGX operates in server mode. This placeholder value ("NONE")
239+
# is provided solely to satisfy this requirement and should be
240+
# removed once the Lightspeed Stack no longer mandates it for
241+
# server mode.
242+
"db_path": "NONE",
243+
},
244+
)
245+
246+
add_unique(lstack_config_target["rag"]["inline"], vector_store_id)
247+
return lstack_config_target
248+
249+
250+
# ----------------------------------------------------------------------------
251+
252+
253+
def parse_arguments() -> argparse.Namespace:
254+
"""Parse command-line arguments and return parsed namespace."""
255+
parser = argparse.ArgumentParser(
256+
description=(
257+
"Build vector database configuration files by merging collected "
258+
"vector DB data with base configs"
259+
)
260+
)
261+
parser.add_argument(
262+
"--vector-db-path",
263+
type=Path,
264+
required=True,
265+
help="Path (as pathlib.Path) to the mounted vector DB data volume and output destination",
266+
)
267+
parser.add_argument(
268+
"--ogx-config-path",
269+
type=Path,
270+
required=True,
271+
help="Path (as pathlib.Path) to the base OGX configuration file",
272+
)
273+
parser.add_argument(
274+
"--lightspeed-stack-path",
275+
type=Path,
276+
required=True,
277+
help="Path (as pathlib.Path) to the base Lightspeed Stack configuration file",
278+
)
279+
280+
return parser.parse_args()
281+
282+
283+
def main() -> None:
284+
"""main"""
285+
args = parse_arguments()
286+
config_build(args.vector_db_path, args.ogx_config_path, ogx_process)
287+
config_build(args.vector_db_path, args.lightspeed_stack_path, lstack_process)
288+
289+
290+
if __name__ == "__main__":
291+
main()

0 commit comments

Comments
 (0)