Преглед изворни кода

feat(template): add loco-context template

Thomas Zhang пре 2 месеци
родитељ
комит
be1ea65795

+ 2 - 0
internal/controller/component_controller.go

@@ -171,6 +171,8 @@ func (r *ComponentReconciler) getComponentReconciler(comp *v1alpha1.Component, s
 		return reconciler.NewPhoenixReconciler(r.Client, r.Scheme, stack, comp)
 	case "llama.cpp":
 		return reconciler.NewLlamaCPPReconciler(r.Client, r.Scheme, stack, comp)
+	case "loco-context":
+		return reconciler.NewLocoContextReconciler(r.Client, r.Scheme, stack, comp)
 	default:
 		return reconciler.NewDefaultComponentReconciler(r.Client, r.Scheme, stack, comp)
 	}

+ 187 - 0
internal/reconciler/loco_context.go

@@ -0,0 +1,187 @@
+/*
+Copyright 2026 LocoStack.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package reconciler
+
+import (
+	"context"
+	"fmt"
+	"maps"
+	"sort"
+
+	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/pkg/templates/litellm"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type LocoContextReconciler struct {
+	*DefaultComponentReconciler
+	client    client.Client
+	scheme    *runtime.Scheme
+	stack     *v1alpha1.Stack
+	component *v1alpha1.Component
+}
+
+func NewLocoContextReconciler(client client.Client, scheme *runtime.Scheme, stack *v1alpha1.Stack, component *v1alpha1.Component) *LocoContextReconciler {
+	return &LocoContextReconciler{
+		DefaultComponentReconciler: NewDefaultComponentReconciler(client, scheme, stack, component),
+		client:                     client,
+		scheme:                     scheme,
+		stack:                      stack,
+		component:                  component,
+	}
+}
+
+func (r *LocoContextReconciler) ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error) {
+	endpointResolver := NewEndpointResolver(r.client, r.scheme, r.stack.Namespace)
+
+	deps := make([]client.Object, 0)
+	var embeddingModelEndpoint string
+	var embeddingModelName string
+	var rerankerModelName string
+	var rerankerModelEndpoint string
+	for _, dep := range r.component.Spec.Dependencies {
+		obj, endpoint, err := endpointResolver.ResolveEndpoint(ctx, r.stack, dep)
+		if err != nil {
+			return nil, fmt.Errorf("Failed to resolve endpoint for %s %s: %w", dep.Kind, dep.Name, err)
+		}
+		var category string
+		switch dep.Kind {
+		case "ExternalModel":
+			category = obj.(*v1alpha1.ExternalModel).Spec.Category
+			switch category {
+			case "embedding":
+				embeddingModelEndpoint = endpoint
+				embeddingModelName = obj.(*v1alpha1.ExternalModel).Spec.ModelName
+			case "reranker":
+				rerankerModelEndpoint = endpoint
+				rerankerModelName = obj.(*v1alpha1.ExternalModel).Spec.ModelName
+			}
+		case "ManagedModel":
+			category = obj.(*v1alpha1.ManagedModel).Spec.Category
+			switch category {
+			case "embedding":
+				embeddingModelEndpoint = endpoint
+				embeddingModelName = obj.(*v1alpha1.ManagedModel).Spec.ModelName
+			case "reranker":
+				rerankerModelEndpoint = endpoint
+				rerankerModelName = obj.(*v1alpha1.ManagedModel).Spec.ModelName
+			}
+		}
+
+		deps = append(deps, obj)
+	}
+
+	var vectorStoreEndpoint string
+	var graphStoreEndpoint string
+	if r.stack != nil {
+		vectorStore := r.stack.Status.ComponentStatus.VectorStore
+		if vectorStore != "" && vectorStore != "disabled" {
+			if obj, endpoint, err := r.resolveComponentEndpoint(ctx, r.stack, "VectorStore"); err != nil {
+				return nil, fmt.Errorf("Failed to resolve vector store endpoint: %w", err)
+			} else if obj != nil {
+				deps = append(deps, obj)
+				vectorStoreEndpoint = endpoint
+			}
+		}
+		graphStore := r.stack.Status.ComponentStatus.GraphStore
+		if graphStore != "" && graphStore != "disabled" {
+			if obj, endpoint, err := r.resolveComponentEndpoint(ctx, r.stack, "GraphStore"); err != nil {
+				return nil, fmt.Errorf("Failed to resolve graph store endpoint: %w", err)
+			} else if obj != nil {
+				deps = append(deps, obj)
+				graphStoreEndpoint = endpoint
+			}
+		}
+	}
+
+	gatewayComp, err := endpointResolver.resolveGatewayEndpoint(ctx, r.stack)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to resolve gateway for %s: %w", r.component.Name, err)
+	}
+	masterKey, err := r.gatewayMasterKey(ctx, gatewayComp)
+	if err != nil {
+		return nil, fmt.Errorf("resolving gateway master key: %w", err)
+	}
+
+	// sort deps to make sure the order is deterministic
+	deps = sortObjectsByName(deps)
+
+	config := map[string]string{
+		"embedding_model_endpoint": embeddingModelEndpoint,
+		"embedding_model_api_key":  masterKey,
+		"embedding_model_name":     embeddingModelName,
+		"reranker_model_endpoint":  rerankerModelEndpoint,
+		"reranker_model_api_key":   masterKey,
+		"reranker_model_name":      rerankerModelName,
+		"vector_store_endpoint":    vectorStoreEndpoint,
+		"graph_store_endpoint":     graphStoreEndpoint,
+	}
+	configHash, err := r.ReconcileConfig(ctx, "config.yaml", config, true)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to reconcile config for %s: %w", r.component.Name, err)
+	}
+
+	if tmpl.Metadata.Annotations == nil {
+		tmpl.Metadata.Annotations = make(map[string]string)
+	}
+	maps.Copy(tmpl.Metadata.Annotations, map[string]string{
+		"locostack.com/configHash": configHash,
+	})
+
+	if _, err := r.DefaultComponentReconciler.ReconcileComponent(ctx, tmpl, variables); err != nil {
+		return nil, err
+	}
+	return deps, nil
+}
+
+func (r *LocoContextReconciler) resolveComponentEndpoint(ctx context.Context, stack *v1alpha1.Stack, component string) (client.Object, string, error) {
+	compList := &v1alpha1.ComponentList{}
+	err := r.client.List(ctx, compList, client.InNamespace(stack.Namespace), client.MatchingLabels{"locostack.com/stack": stack.Name, "locostack.com/component": component})
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to list gateway components: %v", err)
+	}
+	if len(compList.Items) == 0 {
+		return nil, "", nil
+	}
+	endpointResolver := NewEndpointResolver(r.client, r.scheme, stack.Namespace)
+	return endpointResolver.ResolveEndpoint(ctx, stack, corev1.ObjectReference{Kind: "Component", Name: compList.Items[0].Name})
+}
+
+func (r *LocoContextReconciler) gatewayMasterKey(ctx context.Context, gatewayComp *v1alpha1.Component) (string, error) {
+	secret := &corev1.Secret{}
+	secretName := litellm.MasterKeySecretName(gatewayComp.Name)
+	if err := r.client.Get(ctx, client.ObjectKey{Name: secretName, Namespace: r.component.GetNamespace()}, secret); err != nil {
+		return "", fmt.Errorf("Failed to get gateway master key secret %s: %w", secretName, err)
+	}
+	key, ok := secret.Data[litellm.LITELLM_AUTH_SECRET_KEY]
+	if !ok {
+		return "", fmt.Errorf("Secret %s does not contain key %q", secretName, litellm.LITELLM_AUTH_SECRET_KEY)
+	}
+	return string(key), nil
+}
+
+func sortObjectsByName(objs []client.Object) []client.Object {
+	sorted := make([]client.Object, len(objs))
+	copy(sorted, objs)
+	// sort by namespace/name
+	sort.Slice(sorted, func(i, j int) bool {
+		return sorted[i].GetNamespace()+"/"+sorted[i].GetName() < sorted[j].GetNamespace()+"/"+sorted[j].GetName()
+	})
+	return sorted
+}

+ 114 - 0
pkg/templates/locostack/context.go

@@ -0,0 +1,114 @@
+/*
+Copyright 2026 LocoStack.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package locostack
+
+import (
+	"fmt"
+
+	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/utils/ptr"
+)
+
+const (
+	DOCUMENT_MOUNT_PATH = "/data"
+)
+
+var LocoContextTemplate = v1alpha1.Template{
+	Name: "loco-context",
+	Spec: v1alpha1.TemplateSpec{
+		Runtime: v1alpha1.RuntimeSpec{
+			Name:  "loco-simple-rag",
+			Image: "locostack.com/loco-simple-rag:latest",
+			Env: []corev1.EnvVar{
+				{Name: "PORT", Value: "$(spec.runtime.port)"},
+				{Name: "QDRANT_COLLECTION", Value: "$(metadata.name)"},
+			},
+			EnvFrom: []corev1.EnvFromSource{
+				{
+					SecretRef: &corev1.SecretEnvSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: "$(metadata.name)-env",
+						},
+						Optional: ptr.To(true),
+					},
+				},
+			},
+			Port: 8000,
+		},
+	},
+}
+
+var LocoSimpleRAGIngestionTemplate = v1alpha1.Template{
+	Name: "loco-context-ingestion",
+	Spec: v1alpha1.TemplateSpec{
+		Runtime: v1alpha1.RuntimeSpec{
+			Name:  "loco-simple-rag",
+			Image: "locostack.com/loco-simple-rag:latest",
+			Args: []string{
+				"ingest",
+				fmt.Sprintf("%s/$(spec.filePath)", DOCUMENT_MOUNT_PATH),
+				"--id",
+				"$(spec.docId)",
+				"--strategy",
+				"$(spec.chunkingStrategy.strategy)",
+				"--chunk-size",
+				"$(spec.chunkingStrategy.chunkSize)",
+				"--chunk-overlap",
+				"$(spec.chunkingStrategy.chunkOverlap)",
+				"--separators",
+				"$(spec.chunkingStrategy.separators)",
+			},
+			Env: []corev1.EnvVar{
+				{Name: "KB_ENDPOINT", Value: "$(spec.kbEndpoint)"},
+			},
+			VolumeMounts: []corev1.VolumeMount{
+				{
+					Name:      "documents",
+					MountPath: DOCUMENT_MOUNT_PATH,
+				},
+			},
+		},
+		Volumes: []corev1.Volume{
+			{
+				Name: "documents",
+				VolumeSource: corev1.VolumeSource{
+					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+						ClaimName: "$(spec.runtime.pvc)",
+					},
+				},
+			},
+		},
+	},
+}
+
+var LocoSimpleRAGEgestionTemplate = v1alpha1.Template{
+	Name: "loco-context-egestion",
+	Spec: v1alpha1.TemplateSpec{
+		Runtime: v1alpha1.RuntimeSpec{
+			Name:  "loco-simple-rag",
+			Image: "locostack.com/loco-simple-rag:latest",
+			Args: []string{
+				"egest",
+				"$(spec.docId)",
+			},
+			Env: []corev1.EnvVar{
+				{Name: "KB_ENDPOINT", Value: "$(spec.kbEndpoint)"},
+			},
+		},
+	},
+}

+ 5 - 0
pkg/templates/templates.go

@@ -24,6 +24,7 @@ import (
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
 	"github.com/LocoStack/loco-operator/pkg/templates/litellm"
 	llamacpp "github.com/LocoStack/loco-operator/pkg/templates/llama_cpp"
+	"github.com/LocoStack/loco-operator/pkg/templates/locostack"
 	"github.com/LocoStack/loco-operator/pkg/templates/neo4j"
 	"github.com/LocoStack/loco-operator/pkg/templates/npx"
 	"github.com/LocoStack/loco-operator/pkg/templates/phoenix"
@@ -267,4 +268,8 @@ func init() {
 	Manager.RegisterRuntime(npx.NpxTemplate.Spec.Runtime)
 	Manager.RegisterTemplate(uvx.UvxTemplate)
 	Manager.RegisterRuntime(uvx.UvxTemplate.Spec.Runtime)
+	Manager.RegisterTemplate(locostack.LocoContextTemplate)
+	Manager.RegisterRuntime(locostack.LocoContextTemplate.Spec.Runtime)
+	Manager.RegisterTemplate(locostack.LocoSimpleRAGIngestionTemplate)
+	Manager.RegisterTemplate(locostack.LocoSimpleRAGEgestionTemplate)
 }