Kaynağa Gözat

feat(agent): implement Agent controller

Thomas Zhang 2 ay önce
ebeveyn
işleme
c3e83b45ac
1 değiştirilmiş dosya ile 154 ekleme ve 11 silme
  1. 154 11
      internal/controller/agent_controller.go

+ 154 - 11
internal/controller/agent_controller.go

@@ -18,13 +18,25 @@ package controller
 
 import (
 	"context"
+	"fmt"
+	"maps"
 
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/internal/reconciler"
+	"github.com/LocoStack/loco-operator/pkg/templates"
 )
 
 // AgentReconciler reconciles a Agent object
@@ -36,28 +48,159 @@ type AgentReconciler struct {
 // +kubebuilder:rbac:groups=locostack.com,resources=agents,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=locostack.com,resources=agents/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=locostack.com,resources=agents/finalizers,verbs=update
+// +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=locostack.com,resources=stacks,verbs=get;list;watch
 
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the Agent object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
 func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = logf.FromContext(ctx)
+	log := logf.FromContext(ctx)
 
-	// TODO(user): your logic here
+	agent := &v1alpha1.Agent{}
+	if err := r.Get(ctx, req.NamespacedName, agent); err != nil {
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		log.Error(err, "Failed to get Agent", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
 
+	patch := client.MergeFrom(agent.DeepCopy())
+
+	componentAvailable, err := r.reconcileComponent(ctx, agent)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	if !componentAvailable {
+		apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionFalse,
+			Reason:             "AgentUnavailable",
+			Message:            "Agent is not available",
+			ObservedGeneration: agent.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionTrue,
+			Reason:             "AgentAvailable",
+			Message:            "Agent is available",
+			ObservedGeneration: agent.Generation,
+		})
+	}
+
+	agent.Status.ObservedGeneration = agent.Generation
+	if err := r.Status().Patch(ctx, agent, patch); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	log.Info("Reconciled Agent", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
 	return ctrl.Result{}, nil
 }
 
+func (r *AgentReconciler) reconcileComponent(ctx context.Context, agent *v1alpha1.Agent) (bool, error) {
+	compName := fmt.Sprintf("agent-%s", agent.Name)
+	comp := &v1alpha1.Component{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      compName,
+			Namespace: agent.Namespace,
+		},
+	}
+	err := r.Get(ctx, client.ObjectKey{Namespace: agent.Namespace, Name: compName}, comp)
+
+	tmpl, err := templates.Manager.ResolveTemplate(agent.Spec.Template, "")
+	if err != nil {
+		apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+			Type:               "AgentRuntimeResolved",
+			Status:             metav1.ConditionFalse,
+			Reason:             "TemplateNotResolved",
+			Message:            fmt.Sprintf("Failed to reconcile template for Agent: %v", err),
+			ObservedGeneration: agent.Generation,
+		})
+		return false, nil
+	}
+	apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+		Type:               "AgentRuntimeResolved",
+		Status:             metav1.ConditionTrue,
+		Reason:             "TemplateResolved",
+		Message:            fmt.Sprintf("Successfully resolved template for Agent: %v", tmpl.Name),
+		ObservedGeneration: agent.Generation,
+	})
+
+	_, err = controllerutil.CreateOrUpdate(ctx, r.Client, comp, func() error {
+		if comp.Labels == nil {
+			comp.Labels = make(map[string]string)
+		}
+		stackName := ""
+		if agent.Spec.StackRef != nil {
+			stackName = agent.Spec.StackRef.Name
+		}
+		maps.Copy(comp.Labels, reconciler.ResourceLabels(stackName, "Agent", agent.Name, ""))
+		dependencies := make([]corev1.ObjectReference, 0)
+		dependencies = append(dependencies, agent.Spec.Model)
+		for _, tool := range agent.Spec.Tools {
+			dependencies = append(dependencies, tool)
+		}
+		for _, kb := range agent.Spec.KnowledgeBases {
+			dependencies = append(dependencies, kb)
+		}
+		comp.Spec.Category = "Agent"
+		comp.Spec.Template = &tmpl
+		comp.Spec.Variables = map[string]string{
+			"systemPrompt": agent.Spec.SystemPrompt,
+		}
+		comp.Spec.Dependencies = dependencies
+		if agent.Spec.StackRef != nil {
+			comp.Spec.StackRef = &corev1.LocalObjectReference{Name: agent.Spec.StackRef.Name}
+		}
+		return controllerutil.SetControllerReference(agent, comp, r.Scheme)
+	})
+
+	available := apimeta.IsStatusConditionTrue(comp.Status.Conditions, "Available")
+	if available {
+		agent.Status.Endpoint = comp.Status.Endpoint
+		apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionTrue,
+			Reason:             "ComponentAvailable",
+			Message:            "Component is available",
+			ObservedGeneration: agent.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&agent.Status.Conditions, metav1.Condition{
+			Type:               "Ready",
+			Status:             metav1.ConditionFalse,
+			Reason:             "ComponentUnavailable",
+			Message:            "Component is not available",
+			ObservedGeneration: agent.Generation,
+		})
+	}
+	return available, err
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *AgentReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	mapComponentToAgents := func(ctx context.Context, obj client.Object) []reconcile.Request {
+		comp, ok := obj.(*v1alpha1.Component)
+		if !ok {
+			return nil
+		}
+		var reqs []reconcile.Request
+		for _, em := range comp.Spec.Dependencies {
+			if em.APIVersion == v1alpha1.GroupVersion.Group && em.Kind == "Agent" {
+				reqs = append(reqs, reconcile.Request{
+					NamespacedName: types.NamespacedName{
+						Name:      em.Name,
+						Namespace: em.Namespace,
+					},
+				})
+			}
+		}
+		return reqs
+	}
+
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.Agent{}).
+		Owns(&v1alpha1.Component{}).
+		Watches(&v1alpha1.Component{}, handler.EnqueueRequestsFromMapFunc(mapComponentToAgents)).
 		Named("agent").
 		Complete(r)
 }