diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 4c390d4a..2247a6a7 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -39,7 +39,10 @@ const ( OpenStackLightspeedAppServerNetworkPolicyName = "lightspeed-app-server" OpenStackLightspeedCertsSecretName = "lightspeed-tls" OpenStackLightspeedDefaultProvider = "openstack-lightspeed-provider" - OpenStackLightspeedVectorDBPath = "/rag/vector_db/os_product_docs" + RAGInitCopySubPath = "rag-0" + RAGInitCopyDestinationPath = "/rag-data/rag-0" + RAGRuntimeContentPath = "/rag/rag-0" + OpenStackLightspeedVectorDBPath = RAGRuntimeContentPath + "/vector_db/os_product_docs" ServingCertSecretAnnotationKey = "service.beta.openshift.io/serving-cert-secret-name" @@ -93,8 +96,13 @@ const ( LCoreConfigFilename = "lightspeed-stack.yaml" LCoreConfigMapResourceVersionAnnotation = "ols.openshift.io/lcore-configmap-version" LlamaStackConfigMapResourceVersionAnnotation = "ols.openshift.io/llamastack-configmap-version" + RAGImageAnnotation = "ols.openshift.io/rag-image" + ActiveOCPRAGVersionAnnotation = "ols.openshift.io/active-ocp-rag-version" LCoreUserDataMountPath = "/tmp/data" ForceReloadAnnotationKey = "ols.openshift.io/force-reload" + RAGVolumeName = "rag-data" + RAGVolumeMountPath = "/rag" + RAGInitVolumeMountPath = "/rag-data" // Data Exporter ExporterConfigVolumeName = "exporter-config" diff --git a/internal/controller/lcore_config.go b/internal/controller/lcore_config.go index 42bbe43f..6b0f5df4 100644 --- a/internal/controller/lcore_config.go +++ b/internal/controller/lcore_config.go @@ -19,6 +19,7 @@ package controller import ( _ "embed" "fmt" + "os" common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" @@ -197,8 +198,48 @@ func buildLCoreConversationCacheConfig(h *common_helper.Helper, _ *apiv1beta1.Op } } -// buildLCoreConfigYAML assembles the complete Lightspeed Core Service configuration and converts to YAML. -// NOTE: MCP servers, quota handlers, and tools approval features are disabled for OpenStack Lightspeed. +func buildLCoreRAGConfig(instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { + ragIDs := []interface{}{} + for _, rag := range buildLCoreRAGConfigs(instance, instance.Status.ActiveOCPRAGVersion) { + ragID := rag.IndexID + if ragID == "" { + ragID = "rag_" + sanitizeID(rag.Image) + } + ragIDs = append(ragIDs, ragID) + } + + return map[string]interface{}{ + "inline": []string{ + getVectorStoreID(), + }, + // NOTE(lpiwowar): RAG will be enabled as a tool once the full migration + // to lightspeed-stack is complete and feature parity has been achieved. + // "tool": ragIDs, + } +} + +func buildLCoreBYOKRAGConfig(instance *apiv1beta1.OpenStackLightspeed) []interface{} { + byokRAG := []interface{}{} + for _, rag := range buildLCoreRAGConfigs(instance, instance.Status.ActiveOCPRAGVersion) { + vectorDBID := rag.IndexID + if vectorDBID == "" { + vectorDBID = "rag_" + sanitizeID(rag.Image) + } + + byokRAG = append(byokRAG, map[string]interface{}{ + "rag_id": getVectorStoreID(), + "rag_type": "inline::faiss", + "embedding_model": "sentence-transformers/all-mpnet-base-v2", + "embedding_dimension": 768, + "vector_db_id": getVectorStoreID(), + "db_path": rag.IndexPath, + "score_multiplier": 1.0, + }) + } + + return byokRAG +} + func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) (string, error) { // Build the complete config as a map config := map[string]interface{}{ @@ -211,6 +252,8 @@ func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStac "database": buildLCoreDatabaseConfig(h, instance), "customization": buildLCoreCustomizationConfig(), "conversation_cache": buildLCoreConversationCacheConfig(h, instance), + "byok_rag": buildLCoreBYOKRAGConfig(instance), + "rag": buildLCoreRAGConfig(instance), } // Convert to YAML @@ -221,3 +264,11 @@ func buildLCoreConfigYAML(h *common_helper.Helper, instance *apiv1beta1.OpenStac return string(yamlBytes), nil } + +func getVectorStoreID() string { + id := os.Getenv("VECTOR_STORE_ID") + if id == "" { + panic("VECTOR_STORE_ID environment variable is not set") + } + return id +} diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index 069003d1..5b94c77d 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -54,6 +54,10 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins llamaCacheMounts := []corev1.VolumeMount{} addLlamaCacheVolumesAndMounts(&volumes, &llamaCacheMounts) + // Shared RAG content volume + ragMounts := []corev1.VolumeMount{} + addRAGVolumesAndMounts(&volumes, &ragMounts) + // Build env vars llamaEnvVars, err := buildLlamaStackEnvVars(h, ctx, instance) if err != nil { @@ -65,6 +69,7 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins llamaStackMounts := []corev1.VolumeMount{llamaMount} llamaStackMounts = append(llamaStackMounts, sharedMounts...) llamaStackMounts = append(llamaStackMounts, llamaCacheMounts...) + llamaStackMounts = append(llamaStackMounts, ragMounts...) llamaStackContainer := corev1.Container{ Name: "llama-stack", @@ -112,6 +117,8 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins if err != nil { return corev1.PodTemplateSpec{}, err } + annotations[RAGImageAnnotation] = instance.Spec.RAGImage + annotations[ActiveOCPRAGVersionAnnotation] = instance.Status.ActiveOCPRAGVersion return corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -120,6 +127,7 @@ func buildLCorePodTemplateSpec(h *common_helper.Helper, ctx context.Context, ins }, Spec: corev1.PodSpec{ ServiceAccountName: OpenStackLightspeedAppServerServiceAccountName, + InitContainers: []corev1.Container{buildRAGInitContainer(instance)}, Containers: containers, Volumes: volumes, }, @@ -248,6 +256,44 @@ func addLlamaCacheVolumesAndMounts(volumes *[]corev1.Volume, mounts *[]corev1.Vo }) } +// addRAGVolumesAndMounts adds an emptyDir volume shared by init and app containers for RAG content. +func addRAGVolumesAndMounts(volumes *[]corev1.Volume, mounts *[]corev1.VolumeMount) { + *volumes = append(*volumes, corev1.Volume{ + Name: RAGVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + *mounts = append(*mounts, corev1.VolumeMount{ + Name: RAGVolumeName, + MountPath: RAGVolumeMountPath, + }) +} + +// buildRAGInitContainer returns an init container that copies vector DB content from the RAG image. +func buildRAGInitContainer(instance *apiv1beta1.OpenStackLightspeed) corev1.Container { + return corev1.Container{ + Name: "rag-init", + Image: instance.Spec.RAGImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "sh", "-c", + fmt.Sprintf( + "mkdir -p %s && cp -a %s/. %s", + RAGInitCopyDestinationPath, + "/rag", + RAGInitCopyDestinationPath, + ), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: RAGVolumeName, + MountPath: RAGInitVolumeMountPath, + }, + }, + } +} + // addUserCAVolumesAndMounts adds user-provided additional CA certificate volume and mount // if instance.Spec.TLSCACertBundle is set. func addUserCAVolumesAndMounts(volumes *[]corev1.Volume, mounts *[]corev1.VolumeMount, instance *apiv1beta1.OpenStackLightspeed, volumeDefaultMode int32) { diff --git a/internal/controller/llama_stack_config.go b/internal/controller/llama_stack_config.go index 68e7df20..49f22eec 100644 --- a/internal/controller/llama_stack_config.go +++ b/internal/controller/llama_stack_config.go @@ -226,8 +226,8 @@ func buildLlamaStackVectorDB(_ *common_helper.Helper, _ *apiv1beta1.OpenStackLig "table_name": "vector_store", }, "persistence": map[string]interface{}{ - "backend": "kv_default", - "namespace": "vector_persistence", + "backend": "rag_backend", + "namespace": "vector_io:faiss", }, }, }, @@ -270,6 +270,10 @@ func buildLlamaStackStorage(_ *common_helper.Helper, instance *apiv1beta1.OpenSt "ca_cert_path": "/etc/certs/postgres-ca/service-ca.crt", "gss_encmode": "disable", }, + "rag_backend": map[string]interface{}{ + "type": "kv_sqlite", + "db_path": "/rag/rag-0/vector_db/os_product_docs/faiss_store.db", + }, } // Map data stores to backends - all use SQL with table_name @@ -294,37 +298,34 @@ func buildLlamaStackStorage(_ *common_helper.Helper, instance *apiv1beta1.OpenSt } } -func buildLlamaStackVectorDBs(_ *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) []interface{} { - vectorDBs := []interface{}{} +func buildLlamaStackVectorStores(_ *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { + var vectorDBs map[string]interface{} // Use RAG configuration from instance if available rags := buildLCoreRAGConfigs(instance, instance.Status.ActiveOCPRAGVersion) if len(rags) > 0 { - for _, rag := range rags { - vectorDB := map[string]interface{}{ - "embedding_model": "sentence-transformers/all-mpnet-base-v2", - "embedding_dimension": 768, - "provider_id": "faiss", - } - - // Use IndexID if specified, otherwise generate a default - if rag.IndexID != "" { - vectorDB["vector_db_id"] = rag.IndexID - } else { - // Generate a simple ID from the image name - vectorDB["vector_db_id"] = "rag_" + sanitizeID(rag.Image) - } + vectorDBs = map[string]interface{}{ + "default_embedding_model": map[string]interface{}{ + "provider_id": "sentence-transformers", + // "model_id": "all-mpnet-base-v2", + "model_id": "/rag/rag-0/embeddings_model", + }, - vectorDBs = append(vectorDBs, vectorDB) + // "embedding_dimension": 768, + "default_provider_id": "faiss", + // "index_path": rag.IndexPath, } + + vectorDBs["vector_store_id"] = getVectorStoreID() + } else { // Default fallback if no RAG configured - vectorDBs = append(vectorDBs, map[string]interface{}{ + vectorDBs = map[string]interface{}{ "vector_db_id": "my_knowledge_base", "embedding_model": "sentence-transformers/all-mpnet-base-v2", "embedding_dimension": 768, "provider_id": "faiss", - }) + } } return vectorDBs @@ -337,7 +338,7 @@ func buildLlamaStackModels(_ *common_helper.Helper, instance *apiv1beta1.OpenSta "model_id": "sentence-transformers/all-mpnet-base-v2", "model_type": "embedding", "provider_id": "sentence-transformers", - "provider_model_id": "sentence-transformers/all-mpnet-base-v2", + "provider_model_id": "/rag/rag-0/embeddings_model", "metadata": map[string]interface{}{ "embedding_dimension": 768, }, @@ -380,6 +381,47 @@ func buildLlamaStackToolGroups(_ *common_helper.Helper, _ *apiv1beta1.OpenStackL } } +func buildLlamaStackRegisteredResources(_ *common_helper.Helper, instance *apiv1beta1.OpenStackLightspeed) map[string]interface{} { + return map[string]interface{}{ + "models": []interface{}{ + map[string]interface{}{ + "model_id": "sentence-transformers/all-mpnet-base-v2", + "model_type": "embedding", + // "provider_model_id": "all-mpnet-base-v2", + "metadata": map[string]interface{}{ + "embedding_dimension": 768, + }, + + "provider_id": "sentence-transformers", + // "provider_model_id": "all-mpnet-base-v2", + "provider_model_id": "/rag/rag-0/embeddings_model", + }, + + map[string]interface{}{ + "model_id": instance.Spec.ModelName, + "model_type": "llm", + "provider_id": "openstack-lightspeed-provider", + "provider_model_id": instance.Spec.ModelName, + "metadata": map[string]interface{}{ + "max_tokens": 2048, + }, + }, + }, + "vector_stores": []interface{}{ + map[string]interface{}{ + "vector_store_id": getVectorStoreID(), + // "model_id": "sentence-transformers/all-mpnet-base-v2", + "provider_id": "faiss", + // "embedding_model": "all-mpnet-base-v2", + // "embedding_model": "sentence-transformers/all-mpnet-base-v2", + "embedding_model": "sentence-transformers//rag/rag-0/embeddings_model", + "embedding_dimension": 768, + // "provider_model_id": "/rag/rag-0/embeddings_model", + }, + }, + } +} + // buildLlamaStackYAML assembles the complete Llama Stack configuration and converts to YAML func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance *apiv1beta1.OpenStackLightspeed) (string, error) { // Build the complete config as a map @@ -400,12 +442,12 @@ func buildLlamaStackYAML(h *common_helper.Helper, ctx context.Context, instance "tool_runtime": buildLlamaStackToolRuntime(h, instance), "vector_io": buildLlamaStackVectorDB(h, instance), } - // Add top-level fields config["scoring_fns"] = []interface{}{} config["server"] = buildLlamaStackServerConfig(h, instance) config["storage"] = buildLlamaStackStorage(h, instance) - config["vector_dbs"] = buildLlamaStackVectorDBs(h, instance) + config["vector_stores"] = buildLlamaStackVectorStores(h, instance) + config["registered_resources"] = buildLlamaStackRegisteredResources(h, instance) config["models"] = buildLlamaStackModels(h, instance) config["tool_groups"] = buildLlamaStackToolGroups(h, instance) config["telemetry"] = map[string]interface{}{ diff --git a/internal/controller/ocp_version.go b/internal/controller/ocp_version.go index 14f65bc0..cc1b74d9 100644 --- a/internal/controller/ocp_version.go +++ b/internal/controller/ocp_version.go @@ -33,7 +33,7 @@ import ( const ( // OpenStackLightspeedOCPVectorDBPath - base path for OCP vector databases - OpenStackLightspeedOCPVectorDBPath = "/rag/ocp_vector_db/ocp" + OpenStackLightspeedOCPVectorDBPath = RAGRuntimeContentPath + "/ocp_vector_db/ocp" // OpenStackLightspeedOCPIndexPrefix - prefix for OCP index names OpenStackLightspeedOCPIndexPrefix = "ocp-product-docs"