diff --git a/go/autogen/api/models.go b/go/autogen/api/models.go index 16873a7d4..297897691 100644 --- a/go/autogen/api/models.go +++ b/go/autogen/api/models.go @@ -8,15 +8,13 @@ type ModelInfo struct { Family string `json:"family"` } -type CreateArgumentsConfig struct { +type OpenAICreateArgumentsConfig struct { FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` LogitBias map[string]float64 `json:"logit_bias,omitempty"` MaxTokens *int `json:"max_tokens,omitempty"` N *int `json:"n,omitempty"` PresencePenalty *float64 `json:"presence_penalty,omitempty"` - ResponseFormat interface{} `json:"response_format,omitempty"` Seed *int `json:"seed,omitempty"` - Stop interface{} `json:"stop,omitempty"` Temperature *float64 `json:"temperature,omitempty"` TopP *float64 `json:"top_p,omitempty"` User *string `json:"user,omitempty"` @@ -28,14 +26,13 @@ type StreamOptions struct { type BaseOpenAIClientConfig struct { // Base OpenAI fields - Model string `json:"model"` - APIKey *string `json:"api_key,omitempty"` - Timeout *int `json:"timeout,omitempty"` - MaxRetries *int `json:"max_retries,omitempty"` - ModelCapabilities interface{} `json:"model_capabilities,omitempty"` - ModelInfo *ModelInfo `json:"model_info,omitempty"` - StreamOptions *StreamOptions `json:"stream_options,omitempty"` - CreateArgumentsConfig + Model string `json:"model"` + APIKey *string `json:"api_key,omitempty"` + Timeout *int `json:"timeout,omitempty"` + MaxRetries *int `json:"max_retries,omitempty"` + ModelInfo *ModelInfo `json:"model_info,omitempty"` + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + OpenAICreateArgumentsConfig } type OpenAIClientConfig struct { @@ -58,11 +55,11 @@ type AzureOpenAIClientConfig struct { BaseOpenAIClientConfig // AzureOpenAIClientConfig specific fields - AzureEndpoint *string `json:"azure_endpoint,omitempty"` - AzureDeployment *string `json:"azure_deployment,omitempty"` - APIVersion *string `json:"api_version,omitempty"` - AzureADToken *string `json:"azure_ad_token,omitempty"` - AzureADTokenProvider interface{} `json:"azure_ad_token_provider,omitempty"` + AzureEndpoint *string `json:"azure_endpoint,omitempty"` + AzureDeployment *string `json:"azure_deployment,omitempty"` + APIVersion *string `json:"api_version,omitempty"` + AzureADToken *string `json:"azure_ad_token,omitempty"` + Stream *bool `json:"stream,omitempty"` } func (c *AzureOpenAIClientConfig) ToConfig() (map[string]interface{}, error) { @@ -72,3 +69,36 @@ func (c *AzureOpenAIClientConfig) ToConfig() (map[string]interface{}, error) { func (c *AzureOpenAIClientConfig) FromConfig(config map[string]interface{}) error { return fromConfig(c, config) } + +type AnthropicCreateArguments struct { + MaxTokens *int `json:"max_tokens,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"top_p,omitempty"` + TopK *int `json:"top_k,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +type BaseAnthropicClientConfiguration struct { + APIKey *string `json:"api_key,omitempty"` + BaseURL *string `json:"base_url,omitempty"` + Model string `json:"model"` + ModelCapabilities *ModelInfo `json:"model_capabilities,omitempty"` + ModelInfo *ModelInfo `json:"model_info,omitempty"` + Timeout *float64 `json:"timeout,omitempty"` + MaxRetries *int `json:"max_retries,omitempty"` + DefaultHeaders map[string]string `json:"default_headers,omitempty"` + AnthropicCreateArguments +} + +type AnthropicClientConfiguration struct { + BaseAnthropicClientConfiguration +} + +func (c *AnthropicClientConfiguration) ToConfig() (map[string]interface{}, error) { + return toConfig(c) +} + +func (c *AnthropicClientConfiguration) FromConfig(config map[string]interface{}) error { + return fromConfig(c, config) +} diff --git a/go/config/crd/bases/kagent.dev_agents.yaml b/go/config/crd/bases/kagent.dev_agents.yaml index b6f9662c1..ecaf99025 100644 --- a/go/config/crd/bases/kagent.dev_agents.yaml +++ b/go/config/crd/bases/kagent.dev_agents.yaml @@ -19,6 +19,10 @@ spec: jsonPath: .status.conditions[0].status name: Accepted type: string + - description: The ModelConfig resource referenced by this agent. + jsonPath: .spec.modelConfigRef + name: ModelConfig + type: string name: v1alpha1 schema: openAPIV3Schema: @@ -46,6 +50,8 @@ spec: properties: description: type: string + modelConfigRef: + type: string systemMessage: minLength: 1 type: string diff --git a/go/config/crd/bases/kagent.dev_modelconfigs.yaml b/go/config/crd/bases/kagent.dev_modelconfigs.yaml index 65edc8eb6..8092c6e2e 100644 --- a/go/config/crd/bases/kagent.dev_modelconfigs.yaml +++ b/go/config/crd/bases/kagent.dev_modelconfigs.yaml @@ -14,7 +14,14 @@ spec: singular: modelconfig scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.provider + name: Provider + type: string + - jsonPath: .spec.model + name: Model + type: string + name: v1alpha1 schema: openAPIV3Schema: description: ModelConfig is the Schema for the modelconfigs API. @@ -39,16 +46,111 @@ spec: spec: description: ModelConfigSpec defines the desired state of ModelConfig. properties: + anthropicC: + description: Anthropic-specific configuration + properties: + baseUrl: + description: Base URL for the Anthropic API (overrides default) + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + temperature: + description: Temperature for sampling + type: string + topK: + description: Top-k sampling parameter + type: integer + topP: + description: Top-p sampling parameter + type: string + type: object apiKeySecretKey: type: string apiKeySecretName: type: string + azureOpenAI: + description: Azure OpenAI-specific configuration + properties: + apiVersion: + description: API version for the Azure OpenAI API + type: string + azureAdToken: + description: Azure AD token for authentication + type: string + azureDeployment: + description: Deployment name for the Azure OpenAI API + type: string + azureEndpoint: + description: Endpoint for the Azure OpenAI API + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + temperature: + description: Temperature for sampling + type: string + topP: + description: Top-p sampling parameter + type: string + required: + - apiVersion + - azureEndpoint + type: object model: type: string + openAI: + description: OpenAI-specific configuration + properties: + baseUrl: + description: Base URL for the OpenAI API (overrides default) + type: string + frequencyPenalty: + description: Frequency penalty + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + "n": + description: N value + type: integer + organization: + description: Organization ID for the OpenAI API + type: string + presencePenalty: + description: Presence penalty + type: string + seed: + description: Seed value + type: integer + temperature: + description: Temperature for sampling + type: string + timeout: + description: Timeout + type: integer + topP: + description: Top-p sampling parameter + type: string + type: object + provider: + allOf: + - enum: + - Anthropic + - OpenAI + - AzureOpenAI + - enum: + - Anthropic + - OpenAI + - AzureOpenAI + default: OpenAI + description: The provider of the model + type: string required: - apiKeySecretKey - apiKeySecretName - model + - provider type: object status: description: ModelConfigStatus defines the observed state of ModelConfig. diff --git a/go/controller/api/v1alpha1/autogenagent_types.go b/go/controller/api/v1alpha1/autogenagent_types.go index e20ba6711..00bb692ba 100644 --- a/go/controller/api/v1alpha1/autogenagent_types.go +++ b/go/controller/api/v1alpha1/autogenagent_types.go @@ -31,6 +31,8 @@ type AgentSpec struct { Description string `json:"description,omitempty"` // +kubebuilder:validation:MinLength=1 SystemMessage string `json:"systemMessage,omitempty"` + // +optional + ModelConfigRef string `json:"modelConfigRef"` // +kubebuilder:validation:MaxItems=20 Tools []*Tool `json:"tools,omitempty"` } @@ -58,6 +60,7 @@ type AgentStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Accepted",type="string",JSONPath=".status.conditions[0].status",description="Whether or not the agent has been accepted by the system." +// +kubebuilder:printcolumn:name="ModelConfig",type="string",JSONPath=".spec.modelConfigRef",description="The ModelConfig resource referenced by this agent." // Agent is the Schema for the agents API. type Agent struct { diff --git a/go/controller/api/v1alpha1/autogenmodelconfig_types.go b/go/controller/api/v1alpha1/autogenmodelconfig_types.go index f8197269f..94e1e606c 100644 --- a/go/controller/api/v1alpha1/autogenmodelconfig_types.go +++ b/go/controller/api/v1alpha1/autogenmodelconfig_types.go @@ -24,11 +24,154 @@ const ( ModelConfigConditionTypeAccepted = "Accepted" ) +// ModelProvider represents the model provider type +// +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI +type ModelProvider string + +const ( + Anthropic ModelProvider = "Anthropic" + AzureOpenAI ModelProvider = "AzureOpenAI" + OpenAI ModelProvider = "OpenAI" +) + +// ProviderConfig contains provider-specific configurations +type ProviderConfig struct { + // Configuration for Anthropic provider models + // +optional + Anthropic *AnthropicConfig `json:"anthropic,omitempty"` + + // Configuration for OpenAI provider models + // +optional + OpenAI *OpenAIConfig `json:"openAI,omitempty"` + + // Configuration for Azure OpenAI provider models + // +optional + AzureOpenAI *AzureOpenAIConfig `json:"azureOpenAI,omitempty"` +} + +// AnthropicConfig contains Anthropic-specific configuration options +type AnthropicConfig struct { + // Base URL for the Anthropic API (overrides default) + // +optional + BaseURL string `json:"baseUrl,omitempty"` + + // Maximum tokens to generate + // +optional + MaxTokens int `json:"maxTokens,omitempty"` + + // Temperature for sampling + // +optional + Temperature string `json:"temperature,omitempty"` + + // Top-p sampling parameter + // +optional + TopP string `json:"topP,omitempty"` + + // Top-k sampling parameter + // +optional + TopK int `json:"topK,omitempty"` +} + +// OpenAIConfig contains OpenAI-specific configuration options +type OpenAIConfig struct { + // Base URL for the OpenAI API (overrides default) + // +optional + BaseURL string `json:"baseUrl,omitempty"` + + // Organization ID for the OpenAI API + // +optional + Organization string `json:"organization,omitempty"` + + // Temperature for sampling + // +optional + Temperature string `json:"temperature,omitempty"` + + // Maximum tokens to generate + // +optional + MaxTokens *int `json:"maxTokens,omitempty"` + + // Top-p sampling parameter + // +optional + TopP string `json:"topP,omitempty"` + + // Frequency penalty + // +optional + FrequencyPenalty string `json:"frequencyPenalty,omitempty"` + + // Presence penalty + // +optional + PresencePenalty string `json:"presencePenalty,omitempty"` + + // Seed value + // +optional + Seed *int `json:"seed,omitempty"` + + // N value + N *int `json:"n,omitempty"` + + // Timeout + Timeout *int `json:"timeout,omitempty"` +} + +// AzureOpenAIConfig contains Azure OpenAI-specific configuration options +type AzureOpenAIConfig struct { + // Endpoint for the Azure OpenAI API + // +required + Endpoint string `json:"azureEndpoint,omitempty"` + + // API version for the Azure OpenAI API + // +required + APIVersion string `json:"apiVersion,omitempty"` + + // Deployment name for the Azure OpenAI API + // +optional + DeploymentName string `json:"azureDeployment,omitempty"` + + // Azure AD token for authentication + // +optional + AzureADToken string `json:"azureAdToken,omitempty"` + + // Azure AD token provider + // +optional + // TODO (peterj): We need to figure out how to implement this + // AzureADTokenProvider interface{} `json:"azureAdTokenProvider,omitempty"` + + // Temperature for sampling + // +optional + Temperature string `json:"temperature,omitempty"` + + // Maximum tokens to generate + // +optional + MaxTokens *int `json:"maxTokens,omitempty"` + + // Top-p sampling parameter + // +optional + TopP string `json:"topP,omitempty"` +} + // ModelConfigSpec defines the desired state of ModelConfig. type ModelConfigSpec struct { - Model string `json:"model"` + Model string `json:"model"` + + // The provider of the model + // +kubebuilder:default=OpenAI + // +kubebuilder:validation:Enum=Anthropic;OpenAI;AzureOpenAI + Provider ModelProvider `json:"provider"` + APIKeySecretName string `json:"apiKeySecretName"` APIKeySecretKey string `json:"apiKeySecretKey"` + + // OpenAI-specific configuration + // +optional + ProviderOpenAI *OpenAIConfig `json:"openAI,omitempty"` + + // Anthropic-specific configuration + // +optional + ProviderAnthropic *AnthropicConfig `json:"anthropicC,omitempty"` + + // Azure OpenAI-specific configuration + // +optional + ProviderAzureOpenAI *AzureOpenAIConfig `json:"azureOpenAI,omitempty"` } // ModelConfigStatus defines the observed state of ModelConfig. @@ -39,6 +182,8 @@ type ModelConfigStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Provider",type="string",JSONPath=".spec.provider" +// +kubebuilder:printcolumn:name="Model",type="string",JSONPath=".spec.model" // ModelConfig is the Schema for the modelconfigs API. type ModelConfig struct { diff --git a/go/controller/api/v1alpha1/zz_generated.deepcopy.go b/go/controller/api/v1alpha1/zz_generated.deepcopy.go index 90791e90d..971338be3 100644 --- a/go/controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/controller/api/v1alpha1/zz_generated.deepcopy.go @@ -133,6 +133,21 @@ func (in *AgentStatus) DeepCopy() *AgentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnthropicConfig) DeepCopyInto(out *AnthropicConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnthropicConfig. +func (in *AnthropicConfig) DeepCopy() *AnthropicConfig { + if in == nil { + return nil + } + out := new(AnthropicConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AnyType) DeepCopyInto(out *AnyType) { *out = *in @@ -153,6 +168,26 @@ func (in *AnyType) DeepCopy() *AnyType { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AzureOpenAIConfig) DeepCopyInto(out *AzureOpenAIConfig) { + *out = *in + if in.MaxTokens != nil { + in, out := &in.MaxTokens, &out.MaxTokens + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureOpenAIConfig. +func (in *AzureOpenAIConfig) DeepCopy() *AzureOpenAIConfig { + if in == nil { + return nil + } + out := new(AzureOpenAIConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MagenticOneTeamConfig) DeepCopyInto(out *MagenticOneTeamConfig) { *out = *in @@ -188,7 +223,7 @@ func (in *ModelConfig) DeepCopyInto(out *ModelConfig) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -245,6 +280,21 @@ func (in *ModelConfigList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ModelConfigSpec) DeepCopyInto(out *ModelConfigSpec) { *out = *in + if in.ProviderOpenAI != nil { + in, out := &in.ProviderOpenAI, &out.ProviderOpenAI + *out = new(OpenAIConfig) + (*in).DeepCopyInto(*out) + } + if in.ProviderAnthropic != nil { + in, out := &in.ProviderAnthropic, &out.ProviderAnthropic + *out = new(AnthropicConfig) + **out = **in + } + if in.ProviderAzureOpenAI != nil { + in, out := &in.ProviderAzureOpenAI, &out.ProviderAzureOpenAI + *out = new(AzureOpenAIConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModelConfigSpec. @@ -279,6 +329,41 @@ func (in *ModelConfigStatus) DeepCopy() *ModelConfigStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenAIConfig) DeepCopyInto(out *OpenAIConfig) { + *out = *in + if in.MaxTokens != nil { + in, out := &in.MaxTokens, &out.MaxTokens + *out = new(int) + **out = **in + } + if in.Seed != nil { + in, out := &in.Seed, &out.Seed + *out = new(int) + **out = **in + } + if in.N != nil { + in, out := &in.N, &out.N + *out = new(int) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenAIConfig. +func (in *OpenAIConfig) DeepCopy() *OpenAIConfig { + if in == nil { + return nil + } + out := new(OpenAIConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OrTermination) DeepCopyInto(out *OrTermination) { *out = *in @@ -326,6 +411,36 @@ func (in *OrTerminationCondition) DeepCopy() *OrTerminationCondition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderConfig) DeepCopyInto(out *ProviderConfig) { + *out = *in + if in.Anthropic != nil { + in, out := &in.Anthropic, &out.Anthropic + *out = new(AnthropicConfig) + **out = **in + } + if in.OpenAI != nil { + in, out := &in.OpenAI, &out.OpenAI + *out = new(OpenAIConfig) + (*in).DeepCopyInto(*out) + } + if in.AzureOpenAI != nil { + in, out := &in.AzureOpenAI, &out.AzureOpenAI + *out = new(AzureOpenAIConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfig. +func (in *ProviderConfig) DeepCopy() *ProviderConfig { + if in == nil { + return nil + } + out := new(ProviderConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoundRobinTeamConfig) DeepCopyInto(out *RoundRobinTeamConfig) { *out = *in diff --git a/go/controller/internal/autogen/autogen_api_translator.go b/go/controller/internal/autogen/autogen_api_translator.go index f2f3234cb..2a2580e9f 100644 --- a/go/controller/internal/autogen/autogen_api_translator.go +++ b/go/controller/internal/autogen/autogen_api_translator.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "strings" "github.com/kagent-dev/kagent/go/autogen/api" @@ -70,6 +71,18 @@ func NewAutogenApiTranslator( } func (a *apiTranslator) TranslateGroupChatForAgent(ctx context.Context, agent *v1alpha1.Agent) (*autogen_client.Team, error) { + modelConfig := a.defaultModelConfig + // Use the provided model config if set, otherwise use the default one + if agent.Spec.ModelConfigRef != "" { + modelConfig = types.NamespacedName{ + Name: agent.Spec.ModelConfigRef, + Namespace: agent.Namespace, + } + } + if err := a.kube.Get(ctx, modelConfig, &v1alpha1.ModelConfig{}); err != nil { + return nil, err + } + // generate an internal round robin "team" for the individual agent team := &v1alpha1.Team{ ObjectMeta: agent.ObjectMeta, @@ -80,6 +93,7 @@ func (a *apiTranslator) TranslateGroupChatForAgent(ctx context.Context, agent *v Spec: v1alpha1.TeamSpec{ Participants: []string{agent.Name}, Description: agent.Spec.Description, + ModelConfig: modelConfig.Name, RoundRobinTeamConfig: &v1alpha1.RoundRobinTeamConfig{}, TerminationCondition: v1alpha1.TerminationCondition{ StopMessageTermination: &v1alpha1.StopMessageTermination{}, @@ -149,34 +163,14 @@ func (a *apiTranslator) translateGroupChatForTeam( return nil, fmt.Errorf("model api key not found") } - modelClientWithStreaming := &api.Component{ - Provider: "autogen_ext.models.openai.OpenAIChatCompletionClient", - ComponentType: "model", - Version: makePtr(1), - //ComponentVersion: 1, - Config: api.MustToConfig(&api.OpenAIClientConfig{ - BaseOpenAIClientConfig: api.BaseOpenAIClientConfig{ - Model: modelConfig.Spec.Model, - APIKey: makePtr(string(modelApiKey)), - // By default, we include usage in the stream - // If we aren't streaming this may break, but I think we're good for now - StreamOptions: &api.StreamOptions{ - IncludeUsage: true, - }, - }, - }), + modelClientWithStreaming, err := createModelClientForProvider(modelConfig, modelApiKey, true) + if err != nil { + return nil, err } - modelClientWithoutStreaming := &api.Component{ - Provider: "autogen_ext.models.openai.OpenAIChatCompletionClient", - ComponentType: "model", - Version: makePtr(1), - //ComponentVersion: 1, - Config: api.MustToConfig(&api.OpenAIClientConfig{ - BaseOpenAIClientConfig: api.BaseOpenAIClientConfig{ - Model: modelConfig.Spec.Model, - APIKey: makePtr(string(modelApiKey)), - }, - }), + + modelClientWithoutStreaming, err := createModelClientForProvider(modelConfig, modelApiKey, false) + if err != nil { + return nil, err } modelContext := &api.Component{ @@ -328,6 +322,7 @@ func (a *apiTranslator) translateTaskAgent( Spec: v1alpha1.TeamSpec{ Participants: []string{agent.Name}, Description: agent.Spec.Description, + ModelConfig: agent.Spec.ModelConfigRef, RoundRobinTeamConfig: &v1alpha1.RoundRobinTeamConfig{}, TerminationCondition: v1alpha1.TerminationCondition{ TextMessageTermination: &v1alpha1.TextMessageTermination{ @@ -577,3 +572,188 @@ func addModelClientToConfig( (*toolConfig)["model_client"] = cfg return nil } + +// createModelClientForProvider creates a model client component based on the model provider +func createModelClientForProvider(modelConfig *v1alpha1.ModelConfig, apiKey []byte, includeUsage bool) (*api.Component, error) { + switch modelConfig.Spec.Provider { + case v1alpha1.Anthropic: + config := &api.AnthropicClientConfiguration{ + BaseAnthropicClientConfiguration: api.BaseAnthropicClientConfiguration{ + APIKey: makePtr(string(apiKey)), + Model: modelConfig.Spec.Model, + }, + } + + // Add provider-specific configurations + if modelConfig.Spec.ProviderAnthropic != nil { + anthropicConfig := modelConfig.Spec.ProviderAnthropic + + if anthropicConfig.BaseURL != "" { + config.BaseURL = &anthropicConfig.BaseURL + } + + if anthropicConfig.MaxTokens > 0 { + maxTokens := int(anthropicConfig.MaxTokens) + config.MaxTokens = &maxTokens + } + + if anthropicConfig.Temperature != "" { + temp, err := strconv.ParseFloat(anthropicConfig.Temperature, 64) + if err == nil { + config.Temperature = &temp + } + } + + if anthropicConfig.TopP != "" { + topP, err := strconv.ParseFloat(anthropicConfig.TopP, 64) + if err == nil { + config.TopP = &topP + } + } + + if anthropicConfig.TopK > 0 { + topK := int(anthropicConfig.TopK) + config.TopK = &topK + } + } + + // Convert to map + configMap, err := config.ToConfig() + if err != nil { + return nil, fmt.Errorf("failed to convert Anthropic config: %w", err) + } + + return &api.Component{ + Provider: "autogen_ext.models.anthropic.AnthropicChatCompletionClient", + ComponentType: "model", + Version: makePtr(1), + Config: configMap, + }, nil + + case v1alpha1.AzureOpenAI: + config := &api.AzureOpenAIClientConfig{ + BaseOpenAIClientConfig: api.BaseOpenAIClientConfig{ + Model: modelConfig.Spec.Model, + APIKey: makePtr(string(apiKey)), + }, + Stream: makePtr(true), + } + + if includeUsage { + config.StreamOptions = &api.StreamOptions{ + IncludeUsage: true, + } + } + + // Add provider-specific configurations + if modelConfig.Spec.ProviderAzureOpenAI != nil { + azureConfig := modelConfig.Spec.ProviderAzureOpenAI + + if azureConfig.Endpoint != "" { + config.AzureEndpoint = &azureConfig.Endpoint + } + + if azureConfig.APIVersion != "" { + config.APIVersion = &azureConfig.APIVersion + } + + if azureConfig.DeploymentName != "" { + config.AzureDeployment = &azureConfig.DeploymentName + } + + if azureConfig.AzureADToken != "" { + config.AzureADToken = &azureConfig.AzureADToken + } + + if azureConfig.Temperature != "" { + temp, err := strconv.ParseFloat(azureConfig.Temperature, 64) + if err == nil { + config.Temperature = &temp + } + } + + if azureConfig.TopP != "" { + topP, err := strconv.ParseFloat(azureConfig.TopP, 64) + if err == nil { + config.TopP = &topP + } + } + } + + return &api.Component{ + Provider: "autogen_ext.models.openai.AzureOpenAIChatCompletionClient", + ComponentType: "model", + Version: makePtr(1), + Config: api.MustToConfig(config), + }, nil + + case v1alpha1.OpenAI: + config := &api.OpenAIClientConfig{ + BaseOpenAIClientConfig: api.BaseOpenAIClientConfig{ + Model: modelConfig.Spec.Model, + APIKey: makePtr(string(apiKey)), + }, + } + + if includeUsage { + config.StreamOptions = &api.StreamOptions{ + IncludeUsage: true, + } + } + + // Add provider-specific configurations + if modelConfig.Spec.ProviderOpenAI != nil { + openAIConfig := modelConfig.Spec.ProviderOpenAI + + if openAIConfig.BaseURL != "" { + config.BaseURL = &openAIConfig.BaseURL + } + + if openAIConfig.Organization != "" { + config.Organization = &openAIConfig.Organization + } + + if *openAIConfig.MaxTokens > 0 { + config.MaxTokens = openAIConfig.MaxTokens + } + + if openAIConfig.Temperature != "" { + temp, err := strconv.ParseFloat(openAIConfig.Temperature, 64) + if err == nil { + config.Temperature = &temp + } + } + + if openAIConfig.TopP != "" { + topP, err := strconv.ParseFloat(openAIConfig.TopP, 64) + if err == nil { + config.TopP = &topP + } + } + + if openAIConfig.FrequencyPenalty != "" { + freqP, err := strconv.ParseFloat(openAIConfig.FrequencyPenalty, 64) + if err == nil { + config.FrequencyPenalty = &freqP + } + } + + if openAIConfig.PresencePenalty != "" { + presP, err := strconv.ParseFloat(openAIConfig.PresencePenalty, 64) + if err == nil { + config.PresencePenalty = &presP + } + } + } + + return &api.Component{ + Provider: "autogen_ext.models.openai.OpenAIChatCompletionClient", + ComponentType: "model", + Version: makePtr(1), + Config: api.MustToConfig(config), + }, nil + + default: + return nil, fmt.Errorf("unsupported model provider: %s", modelConfig.Spec.Provider) + } +} diff --git a/go/controller/internal/autogen/autogen_reconciler.go b/go/controller/internal/autogen/autogen_reconciler.go index 478500d1f..c333b272e 100644 --- a/go/controller/internal/autogen/autogen_reconciler.go +++ b/go/controller/internal/autogen/autogen_reconciler.go @@ -314,18 +314,12 @@ func (a *autogenReconciler) findAgentsUsingModel(ctx context.Context, req ctrl.R } var agents []*v1alpha1.Agent - appendAgentIfUsesModel := func(agent *v1alpha1.Agent) { - // TODO currently all agents use the default model config - // eventually we will want to support per-agent overrides - // then we will want to update this to check the agent's spec - if a.defaultModelConfig.Name == req.Name && a.defaultModelConfig.Namespace == req.Namespace { + for i := range agentsList.Items { + agent := &agentsList.Items[i] + if agent.Spec.ModelConfigRef == req.Name { agents = append(agents, agent) } } - for _, agent := range agentsList.Items { - agent := agent - appendAgentIfUsesModel(&agent) - } return agents, nil } @@ -337,46 +331,40 @@ func (a *autogenReconciler) findAgentsUsingApiKeySecret(ctx context.Context, req &modelsList, client.InNamespace(req.Namespace), ); err != nil { - return nil, fmt.Errorf("failed to list secrets: %v", err) + return nil, fmt.Errorf("failed to list model configs: %v", err) } var models []string - appendModelIfUsesApiKeySecret := func(model v1alpha1.ModelConfig) { + for _, model := range modelsList.Items { if model.Spec.APIKeySecretName == req.Name { models = append(models, model.Name) } } - for _, model := range modelsList.Items { - appendModelIfUsesApiKeySecret(model) - } var agents []*v1alpha1.Agent - appendUniqueAgent := func(agent *v1alpha1.Agent) { - for _, t := range agents { - if t.Name == agent.Name { - return - } - } - agents = append(agents, agent) - } + uniqueAgents := make(map[string]bool) - for _, model := range models { + for _, modelName := range models { agentsUsingModel, err := a.findAgentsUsingModel(ctx, ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: req.Namespace, - Name: model, + Name: modelName, }, }) if err != nil { - return nil, fmt.Errorf("failed to find agents for model %s: %v", model, err) + return nil, fmt.Errorf("failed to find agents for model %s: %v", modelName, err) } + for _, agent := range agentsUsingModel { - appendUniqueAgent(agent) + key := fmt.Sprintf("%s/%s", agent.Namespace, agent.Name) + if !uniqueAgents[key] { + uniqueAgents[key] = true + agents = append(agents, agent) + } } } return agents, nil - } func (a *autogenReconciler) findTeamsUsingAgent(ctx context.Context, req ctrl.Request) ([]*v1alpha1.Team, error) { @@ -390,7 +378,8 @@ func (a *autogenReconciler) findTeamsUsingAgent(ctx context.Context, req ctrl.Re } var teams []*v1alpha1.Team - appendTeamIfUsesAgent := func(team *v1alpha1.Team) { + for i := range teamsList.Items { + team := &teamsList.Items[i] for _, participant := range team.Spec.Participants { if participant == req.Name { teams = append(teams, team) @@ -398,10 +387,6 @@ func (a *autogenReconciler) findTeamsUsingAgent(ctx context.Context, req ctrl.Re } } } - for _, team := range teamsList.Items { - team := team - appendTeamIfUsesAgent(&team) - } return teams, nil } @@ -417,15 +402,12 @@ func (a *autogenReconciler) findTeamsUsingModel(ctx context.Context, req ctrl.Re } var teams []*v1alpha1.Team - appendTeamIfUsesModel := func(team *v1alpha1.Team) { + for i := range teamsList.Items { + team := &teamsList.Items[i] if team.Spec.ModelConfig == req.Name { teams = append(teams, team) } } - for _, team := range teamsList.Items { - team := team - appendTeamIfUsesModel(&team) - } return teams, nil } @@ -437,44 +419,38 @@ func (a *autogenReconciler) findTeamsUsingApiKeySecret(ctx context.Context, req &modelsList, client.InNamespace(req.Namespace), ); err != nil { - return nil, fmt.Errorf("failed to list secrets: %v", err) + return nil, fmt.Errorf("failed to list model configs: %v", err) } var models []string - appendModelIfUsesApiKeySecret := func(model v1alpha1.ModelConfig) { + for _, model := range modelsList.Items { if model.Spec.APIKeySecretName == req.Name { models = append(models, model.Name) } } - for _, model := range modelsList.Items { - appendModelIfUsesApiKeySecret(model) - } var teams []*v1alpha1.Team - appendUniqueTeam := func(team *v1alpha1.Team) { - for _, t := range teams { - if t.Name == team.Name { - return - } - } - teams = append(teams, team) - } + uniqueTeams := make(map[string]bool) - for _, model := range models { + for _, modelName := range models { teamsUsingModel, err := a.findTeamsUsingModel(ctx, ctrl.Request{ NamespacedName: types.NamespacedName{ Namespace: req.Namespace, - Name: model, + Name: modelName, }, }) if err != nil { - return nil, fmt.Errorf("failed to find teams for model %s: %v", model, err) + return nil, fmt.Errorf("failed to find teams for model %s: %v", modelName, err) } + for _, team := range teamsUsingModel { - appendUniqueTeam(team) + key := fmt.Sprintf("%s/%s", team.Namespace, team.Name) + if !uniqueTeams[key] { + uniqueTeams[key] = true + teams = append(teams, team) + } } } return teams, nil - } diff --git a/go/controller/internal/autogen/autogen_translator_test.go b/go/controller/internal/autogen/autogen_translator_test.go index 67c623cda..748a3d688 100644 --- a/go/controller/internal/autogen/autogen_translator_test.go +++ b/go/controller/internal/autogen/autogen_translator_test.go @@ -2,6 +2,7 @@ package autogen_test import ( "context" + "net/http" "os" "os/exec" "time" @@ -30,18 +31,31 @@ var _ = Describe("AutogenClient", func() { It("should interact with autogen server", func() { ctx := context.Background() - // go func() { - // // start autogen server - // startAutogenServer(ctx) - // }() + go func() { + // start autogen server + startAutogenServer(ctx) + }() - // sleep for a while to allow autogen server to start - <-time.After(3 * time.Second) + // Make requests to /api/health until it returns 200 + // Do it for max 20 seconds + c := &http.Client{} + req, err := http.NewRequest("GET", "http://localhost:8081/api/health", nil) + Expect(err).NotTo(HaveOccurred()) + var resp *http.Response + for i := 0; i < 20; i++ { + resp, err = c.Do(req) + if err == nil && resp.StatusCode == 200 { + break + } + <-time.After(1 * time.Second) + } + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(200)) client := autogen_client.New("http://localhost:8081/api", "ws://localhost:8081/api/ws") scheme := scheme.Scheme - err := v1alpha1.AddToScheme(scheme) + err = v1alpha1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) kubeClient := fake.NewClientBuilder().WithScheme(scheme).Build() @@ -66,8 +80,14 @@ var _ = Describe("AutogenClient", func() { }, Spec: v1alpha1.ModelConfigSpec{ Model: "gpt-4o", + Provider: v1alpha1.OpenAI, APIKeySecretName: apikeySecret.Name, APIKeySecretKey: apikeySecretKey, + ProviderOpenAI: &v1alpha1.OpenAIConfig{ + Temperature: "0.7", + MaxTokens: func(i int) *int { return &i }(1024), + TopP: "0.95", + }, }, } @@ -77,9 +97,10 @@ var _ = Describe("AutogenClient", func() { Namespace: namespace, }, Spec: v1alpha1.AgentSpec{ - Description: "a test participant", - SystemMessage: "You are a test participant", - Tools: nil, + Description: "a test participant", + SystemMessage: "You are a test participant", + ModelConfigRef: modelConfig.Name, + Tools: nil, }, } @@ -89,9 +110,10 @@ var _ = Describe("AutogenClient", func() { Namespace: namespace, }, Spec: v1alpha1.AgentSpec{ - Description: "a test participant", - SystemMessage: "You are a test participant", - Tools: nil, + Description: "a test participant", + SystemMessage: "You are a test participant", + ModelConfigRef: modelConfig.Name, + Tools: nil, }, } @@ -137,20 +159,37 @@ var _ = Describe("AutogenClient", func() { Expect(err).NotTo(HaveOccurred()) Expect(autogenTeam).NotTo(BeNil()) + listBefore, err := client.ListTeams(autogenTeam.UserID) + Expect(err).NotTo(HaveOccurred()) + err = client.CreateTeam(autogenTeam) Expect(err).NotTo(HaveOccurred()) list, err := client.ListTeams(autogenTeam.UserID) Expect(err).NotTo(HaveOccurred()) Expect(list).NotTo(BeNil()) - Expect(len(list)).To(Equal(1)) - Expect(list[0].Id).To(Equal(autogenTeam.Id)) + Expect(len(list)).To(Equal(len(listBefore) + 1)) + + // check the autogen team that was created is returned + found := false + for _, t := range list { + if t.Id == autogenTeam.Id { + Expect(t.Component.Label).To(Equal(autogenTeam.Component.Label)) + Expect(t.Component.Provider).To(Equal(autogenTeam.Component.Provider)) + Expect(t.Component.Version).To(Equal(autogenTeam.Component.Version)) + Expect(t.Component.Description).To(Equal(autogenTeam.Component.Description)) + Expect(t.Component.Config).To(Equal(autogenTeam.Component.Config)) + found = true + break + } + } + Expect(found).To(BeTrue()) }) }) func startAutogenServer(ctx context.Context) { defer GinkgoRecover() - cmd := exec.CommandContext(ctx, "bash", "-c", "source .venv/bin/activate && uv run autogenstudio ui") + cmd := exec.CommandContext(ctx, "bash", "-c", "source .venv/bin/activate && uv run kagent-engine serve") cmd.Dir = "../../../../python" cmd.Stdout = GinkgoWriter cmd.Stderr = GinkgoWriter diff --git a/go/controller/internal/httpserver/handlers/modelconfig.go b/go/controller/internal/httpserver/handlers/modelconfig.go index 50d078f1d..679a415df 100644 --- a/go/controller/internal/httpserver/handlers/modelconfig.go +++ b/go/controller/internal/httpserver/handlers/modelconfig.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" + "k8s.io/apimachinery/pkg/types" ) // ModelConfigHandler handles model configuration requests @@ -34,3 +35,22 @@ func (h *ModelConfigHandler) HandleListModelConfigs(w http.ResponseWriter, r *ht RespondWithJSON(w, http.StatusOK, configs) } + +func (h *ModelConfigHandler) HandleGetModelConfig(w http.ResponseWriter, r *http.Request) { + configName, err := GetPathParam(r, "configName") + if err != nil { + RespondWithError(w, http.StatusBadRequest, err.Error()) + return + } + + modelConfig := &v1alpha1.ModelConfig{} + if err := h.KubeClient.Get(r.Context(), types.NamespacedName{ + Name: configName, + Namespace: DefaultResourceNamespace, + }, modelConfig); err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + + RespondWithJSON(w, http.StatusOK, modelConfig) +} diff --git a/go/controller/internal/httpserver/handlers/teams.go b/go/controller/internal/httpserver/handlers/teams.go index f8b0d1d56..31e902ea9 100644 --- a/go/controller/internal/httpserver/handlers/teams.go +++ b/go/controller/internal/httpserver/handlers/teams.go @@ -2,7 +2,7 @@ package handlers import ( "net/http" - "strconv" + "strings" "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" "github.com/kagent-dev/kagent/go/controller/internal/autogen" @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/types" autogen_client "github.com/kagent-dev/kagent/go/autogen/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) // DefaultResourceNamespace is the default namespace for resources @@ -25,6 +26,10 @@ func NewTeamsHandler(base *Base) *TeamsHandler { return &TeamsHandler{Base: base} } +func convertToKubernetesIdentifier(name string) string { + return strings.ReplaceAll(name, "_", "-") +} + // HandleListTeams handles GET /api/teams requests func (h *TeamsHandler) HandleListTeams(w http.ResponseWriter, r *http.Request) { userID, err := GetUserID(r) @@ -33,13 +38,47 @@ func (h *TeamsHandler) HandleListTeams(w http.ResponseWriter, r *http.Request) { return } - teams, err := h.AutogenClient.ListTeams(userID) - if err != nil { + agentList := &v1alpha1.AgentList{} + if err := h.KubeClient.List(r.Context(), agentList); err != nil { RespondWithError(w, http.StatusInternalServerError, err.Error()) return } - RespondWithJSON(w, http.StatusOK, teams) + teamsWithID := make([]map[string]interface{}, 0) + for _, team := range agentList.Items { + autogenTeam, err := h.AutogenClient.GetTeam(convertToKubernetesIdentifier(team.Name), userID) + if err != nil { + RespondWithError(w, http.StatusInternalServerError, err.Error()) + return + } + + if autogenTeam == nil { + continue + } + + // Get the model config for the team + modelConfig := &v1alpha1.ModelConfig{} + if err := h.KubeClient.Get(r.Context(), types.NamespacedName{ + Name: team.Spec.ModelConfigRef, + Namespace: DefaultResourceNamespace, + }, modelConfig); err != nil { + continue + } + + if modelConfig == nil { + continue + } + + teamsWithID = append(teamsWithID, map[string]interface{}{ + "id": autogenTeam.Id, + "agent": team, + "component": autogenTeam.Component, + "provider": modelConfig.Spec.Provider, + "model": modelConfig.Spec.Model, + }) + } + + RespondWithJSON(w, http.StatusOK, teamsWithID) } // HandleUpdateTeam handles PUT /api/teams requests @@ -56,19 +95,17 @@ func (h *TeamsHandler) HandleUpdateTeam(w http.ResponseWriter, r *http.Request) Name: teamRequest.Name, Namespace: DefaultResourceNamespace, }, existingTeam); err != nil { + ctrllog.Log.Error(err, "Failed to get team") RespondWithError(w, http.StatusInternalServerError, err.Error()) return } - // The incoming request doesn't have a namespace set, so we - // use the namespace from the existing resource - teamRequest.SetNamespace(existingTeam.GetNamespace()) - // We set the .spec from the incoming request, so // we don't have to copy/set any other fields existingTeam.Spec = teamRequest.Spec if err := h.KubeClient.Update(r.Context(), existingTeam); err != nil { + ctrllog.Log.Error(err, "Failed to update team") RespondWithError(w, http.StatusInternalServerError, err.Error()) return } @@ -114,6 +151,8 @@ func (h *TeamsHandler) HandleCreateTeam(w http.ResponseWriter, r *http.Request) } if !validationResp.IsValid { + ctrllog.Log.Info("Invalid team", "errors", validationResp.Errors, "warnings", validationResp.Warnings) + RespondWithError(w, http.StatusNotAcceptable, "Invalid team") return } @@ -127,33 +166,57 @@ func (h *TeamsHandler) HandleCreateTeam(w http.ResponseWriter, r *http.Request) RespondWithJSON(w, http.StatusCreated, teamRequest) } -// HandleGetTeam handles GET /api/teams/{teamLabel} requests +// HandleGetTeam handles GET /api/teams/{teamID} requests func (h *TeamsHandler) HandleGetTeam(w http.ResponseWriter, r *http.Request) { - teamLabel, err := GetPathParam(r, "teamLabel") + userID, err := GetUserID(r) if err != nil { RespondWithError(w, http.StatusBadRequest, err.Error()) return } - userID, err := GetUserID(r) + teamID, err := GetIntPathParam(r, "teamID") if err != nil { RespondWithError(w, http.StatusBadRequest, err.Error()) return } - teamID, err := strconv.Atoi(teamLabel) + autogenTeam, err := h.AutogenClient.GetTeamByID(teamID, userID) if err != nil { - RespondWithError(w, http.StatusBadRequest, "Invalid team_label, must be an integer") + RespondWithError(w, http.StatusInternalServerError, err.Error()) return } - team, err := h.AutogenClient.GetTeamByID(teamID, userID) - if err != nil { + teamLabel := convertToKubernetesIdentifier(*autogenTeam.Component.Label) + + team := &v1alpha1.Agent{} + if err := h.KubeClient.Get(r.Context(), types.NamespacedName{ + Name: teamLabel, + Namespace: DefaultResourceNamespace, + }, team); err != nil { + RespondWithError(w, http.StatusNotFound, "Team not found") + return + } + + // Get the model config for the team + modelConfig := &v1alpha1.ModelConfig{} + if err := h.KubeClient.Get(r.Context(), types.NamespacedName{ + Name: team.Spec.ModelConfigRef, + Namespace: DefaultResourceNamespace, + }, modelConfig); err != nil { RespondWithError(w, http.StatusInternalServerError, err.Error()) return } - RespondWithJSON(w, http.StatusOK, team) + // Create a new object that contains the team information from team and the ID from the autogenTeam + teamWithID := &map[string]interface{}{ + "id": autogenTeam.Id, + "agent": team, + "component": autogenTeam.Component, + "provider": modelConfig.Spec.Provider, + "model": modelConfig.Spec.Model, + } + + RespondWithJSON(w, http.StatusOK, teamWithID) } // HandleDeleteTeam handles DELETE /api/teams/{teamLabel} requests diff --git a/go/controller/internal/httpserver/server.go b/go/controller/internal/httpserver/server.go index 55aeea054..53495278a 100644 --- a/go/controller/internal/httpserver/server.go +++ b/go/controller/internal/httpserver/server.go @@ -112,6 +112,7 @@ func (s *HTTPServer) setupRoutes() { // Model configs s.router.HandleFunc(APIPathModelConfig, s.handlers.ModelConfig.HandleListModelConfigs).Methods(http.MethodGet) + s.router.HandleFunc(APIPathModelConfig+"/{configName}", s.handlers.ModelConfig.HandleGetModelConfig).Methods(http.MethodGet) // Runs s.router.HandleFunc(APIPathRuns, s.handlers.Runs.HandleCreateRun).Methods(http.MethodPost) @@ -137,7 +138,7 @@ func (s *HTTPServer) setupRoutes() { s.router.HandleFunc(APIPathTeams, s.handlers.Teams.HandleListTeams).Methods(http.MethodGet) s.router.HandleFunc(APIPathTeams, s.handlers.Teams.HandleCreateTeam).Methods(http.MethodPost) s.router.HandleFunc(APIPathTeams, s.handlers.Teams.HandleUpdateTeam).Methods(http.MethodPut) - s.router.HandleFunc(APIPathTeams+"/{teamLabel}", s.handlers.Teams.HandleGetTeam).Methods(http.MethodGet) + s.router.HandleFunc(APIPathTeams+"/{teamID}", s.handlers.Teams.HandleGetTeam).Methods(http.MethodGet) s.router.HandleFunc(APIPathTeams+"/{teamLabel}", s.handlers.Teams.HandleDeleteTeam).Methods(http.MethodDelete) // Use middleware for common functionality diff --git a/go/test/e2e/e2e_test.go b/go/test/e2e/e2e_test.go index 24cc93728..53ee7a9f2 100644 --- a/go/test/e2e/e2e_test.go +++ b/go/test/e2e/e2e_test.go @@ -9,6 +9,7 @@ import ( "github.com/kagent-dev/kagent/go/controller/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -21,11 +22,49 @@ var ( ) var _ = Describe("E2e", func() { - It("configures the agent", func() { - + It("configures the agent and model", func() { // add a team namespace := "team-ns" + // Create API Key Secret + apiKeySecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openai-api-key-secret", + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + Data: map[string][]byte{ + apikeySecretKey: []byte(openaiApiKey), + }, + } + + // Create ModelConfig + modelConfig := &v1alpha1.ModelConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gpt-model-config", + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "ModelConfig", + APIVersion: "kagent.dev/v1alpha1", + }, + Spec: v1alpha1.ModelConfigSpec{ + Model: "gpt-4o", + Provider: v1alpha1.OpenAI, + APIKeySecretName: apiKeySecret.Name, + APIKeySecretKey: apikeySecretKey, + ProviderOpenAI: &v1alpha1.OpenAIConfig{ + Temperature: "0.7", + MaxTokens: ptrToInt(2048), + TopP: "0.95", + }, + }, + } + + // Agent with required ModelConfigRef kubeExpert := &v1alpha1.Agent{ ObjectMeta: metav1.ObjectMeta{ Name: "kube-expert", @@ -36,8 +75,9 @@ var _ = Describe("E2e", func() { APIVersion: "kagent.dev/v1alpha1", }, Spec: v1alpha1.AgentSpec{ - Description: "The Kubernetes Expert AI Agent specializing in cluster operations, troubleshooting, and maintenance.", - SystemMessage: readFileAsString("systemprompts/kube-expert-system-prompt.txt"), + Description: "The Kubernetes Expert AI Agent specializing in cluster operations, troubleshooting, and maintenance.", + SystemMessage: readFileAsString("systemprompts/kube-expert-system-prompt.txt"), + ModelConfigRef: modelConfig.Name, // Added required ModelConfigRef Tools: []*v1alpha1.Tool{ {Provider: "kagent.tools.k8s.AnnotateResource"}, {Provider: "kagent.tools.k8s.ApplyManifest"}, @@ -85,6 +125,19 @@ var _ = Describe("E2e", func() { }, } + // Write Secret + writeKubeObjects( + "manifests/api-key-secret.yaml", + apiKeySecret, + ) + + // Write ModelConfig + writeKubeObjects( + "manifests/gpt-model-config.yaml", + modelConfig, + ) + + // Write Agent writeKubeObjects( "manifests/kube-expert-agent.yaml", kubeExpert, @@ -111,6 +164,10 @@ func writeKubeObjects(file string, objects ...metav1.Object) { Expect(err).NotTo(HaveOccurred()) } +func ptrToInt(v int) *int { + return &v +} + func readFileAsString(path string) string { bytes, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) diff --git a/go/test/e2e/manifests/gpt-model-config.yaml b/go/test/e2e/manifests/gpt-model-config.yaml new file mode 100644 index 000000000..a367f6542 --- /dev/null +++ b/go/test/e2e/manifests/gpt-model-config.yaml @@ -0,0 +1,20 @@ +apiVersion: kagent.dev/v1alpha1 +kind: ModelConfig +metadata: + creationTimestamp: null + name: gpt-model-config + namespace: team-ns +spec: + apiKeySecretKey: api-key + apiKeySecretName: openai-api-key-secret + model: gpt-4o + provider: OpenAI + providerConfig: + openAI: + maxTokens: 2048 + temperature: "0.7" + topP: "0.95" +status: + conditions: null + observedGeneration: 0 +--- diff --git a/go/test/e2e/manifests/kube-expert-agent.yaml b/go/test/e2e/manifests/kube-expert-agent.yaml index cfd78e9bc..e606613b0 100644 --- a/go/test/e2e/manifests/kube-expert-agent.yaml +++ b/go/test/e2e/manifests/kube-expert-agent.yaml @@ -1,225 +1,195 @@ apiVersion: kagent.dev/v1alpha1 kind: Agent metadata: + creationTimestamp: null name: kube-expert namespace: team-ns spec: description: The Kubernetes Expert AI Agent specializing in cluster operations, troubleshooting, and maintenance. + modelConfigRef: gpt-model-config systemMessage: |- - You are a Kubernetes and Istio Expert AI Agent with comprehensive knowledge of container orchestration, service mesh architecture, and cloud-native systems. You have access to a wide range of specialized tools that enable you to interact with Kubernetes clusters and Istio service mesh implementations to perform diagnostics, configuration, management, and troubleshooting. - - Core Expertise: - - 1. Kubernetes Capabilities - - Cluster architecture and components - - Resource management and scheduling - - Networking, services, and ingress - - Storage systems and volumes - - Security and RBAC - - Configuration and secrets - - Deployment strategies - - Monitoring and logging - - High availability and scaling - - Troubleshooting methodologies - - 2. Istio Capabilities - - Service mesh architecture - - Traffic management - - Security (mTLS, authorization) - - Observability and telemetry - - Waypoint proxies - - Multi-cluster deployments - - Gateway configurations - - Virtual services and destination rules - - Sidecar injection - - Canary deployments - - Available Tools: - - 1. Kubernetes Resource Management: - - `GetResources`: Retrieve Kubernetes resources by type, namespace, and filters - - `GetResourceYAML`: Retrieve the YAML definition of a specific resource - - `DescribeResource`: Get detailed information about a specific resource - - `CreateResource`: Create a new Kubernetes resource from YAML - - `DeleteResource`: Delete a Kubernetes resource - - `ApplyManifest`: Apply a YAML manifest to create or update resources - - `PatchResource`: Apply a partial update to a resource - - `CreateResourceFromUrl`: Create a resource from a URL-hosted manifest - - 2. Kubernetes Cluster Information: - - `GetClusterConfiguration`: Retrieve cluster configuration settings - - `GetAvailableAPIResources`: List available API resources in the cluster - - `GetEvents`: Retrieve events from the cluster - - `GetPodLogs`: Retrieve logs from a specific pod - - `ExecuteCommand`: Execute a command in a container - - `CheckServiceConnectivity`: Verify connectivity between services - - 3. Kubernetes Resource Manipulation: - - `AnnotateResource`: Add annotations to a resource - - `LabelResource`: Add labels to a resource - - `RemoveAnnotation`: Remove annotations from a resource - - `RemoveLabel`: Remove labels from a resource - - `Scale`: Scale the number of replicas for a resource - - `Rollout`: Manage rollouts for Deployments, StatefulSets, etc. - - `GenerateResourceTool`: Generate Kubernetes resource definitions - - 4. Istio Service Mesh Management: - - `ZTunnelConfig`: Retrieve or configure Istio ZTunnel settings - - `WaypointStatus`: Check the status of Istio waypoints - - `ListWaypoints`: List all Istio waypoints in the mesh - - `GenerateWaypoint`: Generate Istio waypoint configurations - - `DeleteWaypoint`: Remove Istio waypoints - - `ApplyWaypoint`: Apply Istio waypoint configurations - - `RemoteClusters`: Manage remote clusters in an Istio multi-cluster setup - - `ProxyStatus`: Check the status of Istio proxies - - `ProxyConfig`: Retrieve or modify Istio proxy configurations - - `GenerateManifest`: Generate Istio manifests - - `InstallIstio`: Install or upgrade Istio - - `AnalyzeClusterConfig`: Analyze cluster configuration for Istio compatibility - - 5. Documentation and Information: - - `QueryTool`: Query documentation and best practices - - Operational Protocol: - - 1. Initial Assessment - - Gather information about the cluster and relevant resources - - Identify the scope and nature of the task or issue - - Determine required permissions and access levels - - Plan the approach with safety and minimal disruption - - 2. Execution Strategy - - Use read-only operations first for information gathering - - Validate planned changes before execution - - Implement changes incrementally when possible - - Verify results after each significant change - - Document all actions and outcomes - - 3. Troubleshooting Methodology - - Systematically narrow down problem sources - - Analyze logs, events, and metrics - - Check resource configurations and relationships - - Verify network connectivity and policies - - Review recent changes and deployments - - Isolate service mesh configuration issues - - Safety Guidelines: - - 1. Cluster Operations - - Prioritize non-disruptive operations - - Verify contexts before executing changes - - Understand blast radius of all operations - - Backup critical configurations before modifications - - Consider scaling implications of all changes - - 2. Service Mesh Management - - Test Istio changes in isolated namespaces first - - Verify mTLS and security policies before implementation - - Gradually roll out traffic routing changes - - Monitor for unexpected side effects - - Maintain fallback configurations - + You are a Kubernetes Expert AI Agent specializing in cluster operations, troubleshooting, and maintenance. You possess deep knowledge of Kubernetes architecture, components, and best practices, with a focus on helping users resolve complex operational challenges while maintaining security and stability. + + Core Expertise and Capabilities: + + You have comprehensive understanding of: + - Kubernetes architecture, components, and their interactions + - Container orchestration principles and patterns + - Networking concepts including Services, Ingress, and CNI implementations + - Storage systems including PersistentVolumes, PersistentVolumeClaims, and StorageClasses + - Resource management, scheduling, and cluster optimization + - Security principles including RBAC, Pod Security Policies, and network policies + - Monitoring, logging, and observability practices + + Operational Approach: + + When addressing issues, you: + 1. Begin with a systematic assessment of the situation by: + - Gathering essential cluster information + - Reviewing relevant logs and metrics + - Understanding the scope and impact of the issue + - Identifying potential risks and dependencies + + 2. Follow a structured troubleshooting methodology: + - Start with non-intrusive diagnostic commands + - Escalate investigation depth based on findings + - Document each step and its outcome + - Maintain clear communication about progress + - Verify impact before suggesting changes + + 3. Prioritize cluster stability and security by: + - Recommending least-privileged solutions + - Suggesting dry-runs for significant changes + - Including rollback procedures in recommendations + - Considering cluster-wide impact of actions + - Verifying backup status before major operations + + Communication Protocol: + + You communicate with: + - Technical precision while remaining accessible + - Clear explanations of technical concepts + - Step-by-step documentation of procedures + - Regular status updates for complex operations + - Explicit warning about potential risks + - Citations of relevant Kubernetes documentation + + Problem-Solving Framework: + + When addressing issues, you follow this sequence: + + 1. Initial Assessment: + - Verify the Kubernetes version and configuration + - Check node status and resource capacity + - Review recent changes or deployments + - Assess current resource utilization + - Identify affected components and services + + 2. Investigation: + - Use appropriate diagnostic commands + - Analyze relevant logs and metrics + - Review configuration and manifests + - Check for common failure patterns + - Consider infrastructure dependencies + + 3. Solution Development: + - Propose solutions in order of least to most intrusive + - Include prerequisite checks and validation steps + - Provide specific commands with explanations + - Document potential side effects + - Include rollback procedures + + Command Knowledge: + + You are proficient with essential Kubernetes commands and tools: + + Basic Operations: + ```bash + kubectl get + kubectl describe + kubectl logs + kubectl exec -it -- + kubectl apply -f + kubectl delete + ``` + + Advanced Operations: + ```bash + kubectl drain + kubectl cordon/uncordon + kubectl port-forward + kubectl auth can-i + kubectl debug + ``` + + Diagnostic Tools: + ```bash + crictl + kubelet logs + journalctl + tcpdump + netstat + ``` + + Safety Protocols: + + You always: + 1. Recommend backing up critical resources before changes + 2. Suggest testing changes in non-production first + 3. Include validation steps in procedures + 4. Provide rollback instructions + 5. Warn about potential service impacts + Response Format: - - 1. Analysis and Diagnostics - ```yaml - analysis: - observations: - - key_finding_1 - - key_finding_2 - status: "overall status assessment" - potential_issues: - - issue_1: "description" - - issue_2: "description" - recommended_actions: - - action_1: "description" - - action_2: "description" - ``` - - 2. Implementation Plan - ```yaml - implementation: - objective: "goal of the changes" - steps: - - step_1: - tool: "tool_name" - parameters: "parameter details" - purpose: "what this accomplishes" - - step_2: - tool: "tool_name" - parameters: "parameter details" - purpose: "what this accomplishes" - verification: - - verification_step_1 - - verification_step_2 - rollback: - - rollback_step_1 - - rollback_step_2 - ``` - - Best Practices: - - 1. Resource Management - - Use namespaces for logical separation - - Implement resource quotas and limits - - Use labels and annotations for organization - - Follow the principle of least privilege for RBAC - - Implement network policies for segmentation - - 2. Istio Configuration - - Use PeerAuthentication for mTLS settings - - Configure RequestAuthentication for JWT validation - - Implement AuthorizationPolicy for fine-grained access control - - Use DestinationRule for traffic policies - - Configure VirtualService for intelligent routing - - 3. Monitoring and Observability - - Utilize Istio telemetry for service metrics - - Implement distributed tracing - - Configure proper log levels - - Set up alerts for critical services - - Monitor proxy performance and resource usage - - Common Scenarios: - - 1. Kubernetes Troubleshooting - - Pod scheduling failures - - Service discovery issues - - Resource constraints - - ConfigMap and Secret misconfigurations - - Persistent volume issues - - Network policy conflicts - - 2. Istio Troubleshooting - - Proxy injection failures - - Traffic routing problems - - mTLS configuration issues - - Authentication and authorization errors - - Gateway configuration problems - - Performance degradation - - Multi-cluster connectivity issues - - Your primary goal is to provide expert assistance with Kubernetes and Istio environments by leveraging your specialized tools while following best practices for safety, reliability, and performance. Always aim to not just solve immediate issues but to improve the overall system architecture and operational practices. + + For each issue, you structure your response as follows: + + 1. Understanding Confirmation + - Restate the issue to confirm understanding + - Ask for clarification if needed + - Identify missing critical information + + 2. Initial Assessment + - Describe the potential impact + - List required information/access + - Outline investigation approach + + 3. Solution Proposal + - Step-by-step procedure + - Required commands with explanations + - Safety checks and validations + - Expected outcomes + - Potential risks and mitigations + + 4. Follow-up + - Verification steps + - Success criteria + - Additional recommendations + - Preventive measures + + Limitations and Boundaries: + + You will: + - Never execute commands directly on clusters + - Always explain risks before suggesting changes + - Recommend human review for critical operations + - Acknowledge when issues are beyond your expertise + - Suggest escalation when appropriate + + You aim to not just solve immediate issues but to help users understand the underlying concepts and best practices, promoting long-term cluster health and stability. tools: - provider: kagent.tools.k8s.AnnotateResource - provider: kagent.tools.k8s.ApplyManifest + - provider: kagent.tools.k8s.CheckServiceConnectivity - provider: kagent.tools.k8s.CreateResource - - provider: kagent.tools.k8s.CreateResourceFromUrl - provider: kagent.tools.k8s.DeleteResource - provider: kagent.tools.k8s.DescribeResource - provider: kagent.tools.k8s.ExecuteCommand + - provider: kagent.tools.k8s.GetAvailableAPIResources + - provider: kagent.tools.k8s.GetClusterConfiguration - provider: kagent.tools.k8s.GetEvents - provider: kagent.tools.k8s.GetPodLogs - provider: kagent.tools.k8s.GetResources + - provider: kagent.tools.k8s.GetResourceYAML - provider: kagent.tools.k8s.LabelResource - provider: kagent.tools.k8s.PatchResource - provider: kagent.tools.k8s.RemoveAnnotation - provider: kagent.tools.k8s.RemoveLabel + - provider: kagent.tools.k8s.Rollout + - provider: kagent.tools.k8s.Scale - provider: kagent.tools.k8s.GenerateResourceTool + - provider: kagent.tools.k8s.GenerateResourceToolConfig + - provider: kagent.tools.istio.ZTunnelConfig + - provider: kagent.tools.istio.WaypointStatus + - provider: kagent.tools.istio.ListWaypoints + - provider: kagent.tools.istio.GenerateWaypoint + - provider: kagent.tools.istio.DeleteWaypoint + - provider: kagent.tools.istio.ApplyWaypoint + - provider: kagent.tools.istio.RemoteClusters - provider: kagent.tools.istio.ProxyStatus - provider: kagent.tools.istio.GenerateManifest - - provider: kagent.tools.istio.InstallIstio + - provider: kagent.tools.istio.Install - provider: kagent.tools.istio.AnalyzeClusterConfig - provider: kagent.tools.istio.ProxyConfig - config: diff --git a/helm/crds/kagent.dev_agents.yaml b/helm/crds/kagent.dev_agents.yaml index b6f9662c1..ecaf99025 100644 --- a/helm/crds/kagent.dev_agents.yaml +++ b/helm/crds/kagent.dev_agents.yaml @@ -19,6 +19,10 @@ spec: jsonPath: .status.conditions[0].status name: Accepted type: string + - description: The ModelConfig resource referenced by this agent. + jsonPath: .spec.modelConfigRef + name: ModelConfig + type: string name: v1alpha1 schema: openAPIV3Schema: @@ -46,6 +50,8 @@ spec: properties: description: type: string + modelConfigRef: + type: string systemMessage: minLength: 1 type: string diff --git a/helm/crds/kagent.dev_modelconfigs.yaml b/helm/crds/kagent.dev_modelconfigs.yaml index 65edc8eb6..8092c6e2e 100644 --- a/helm/crds/kagent.dev_modelconfigs.yaml +++ b/helm/crds/kagent.dev_modelconfigs.yaml @@ -14,7 +14,14 @@ spec: singular: modelconfig scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.provider + name: Provider + type: string + - jsonPath: .spec.model + name: Model + type: string + name: v1alpha1 schema: openAPIV3Schema: description: ModelConfig is the Schema for the modelconfigs API. @@ -39,16 +46,111 @@ spec: spec: description: ModelConfigSpec defines the desired state of ModelConfig. properties: + anthropicC: + description: Anthropic-specific configuration + properties: + baseUrl: + description: Base URL for the Anthropic API (overrides default) + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + temperature: + description: Temperature for sampling + type: string + topK: + description: Top-k sampling parameter + type: integer + topP: + description: Top-p sampling parameter + type: string + type: object apiKeySecretKey: type: string apiKeySecretName: type: string + azureOpenAI: + description: Azure OpenAI-specific configuration + properties: + apiVersion: + description: API version for the Azure OpenAI API + type: string + azureAdToken: + description: Azure AD token for authentication + type: string + azureDeployment: + description: Deployment name for the Azure OpenAI API + type: string + azureEndpoint: + description: Endpoint for the Azure OpenAI API + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + temperature: + description: Temperature for sampling + type: string + topP: + description: Top-p sampling parameter + type: string + required: + - apiVersion + - azureEndpoint + type: object model: type: string + openAI: + description: OpenAI-specific configuration + properties: + baseUrl: + description: Base URL for the OpenAI API (overrides default) + type: string + frequencyPenalty: + description: Frequency penalty + type: string + maxTokens: + description: Maximum tokens to generate + type: integer + "n": + description: N value + type: integer + organization: + description: Organization ID for the OpenAI API + type: string + presencePenalty: + description: Presence penalty + type: string + seed: + description: Seed value + type: integer + temperature: + description: Temperature for sampling + type: string + timeout: + description: Timeout + type: integer + topP: + description: Top-p sampling parameter + type: string + type: object + provider: + allOf: + - enum: + - Anthropic + - OpenAI + - AzureOpenAI + - enum: + - Anthropic + - OpenAI + - AzureOpenAI + default: OpenAI + description: The provider of the model + type: string required: - apiKeySecretKey - apiKeySecretName - model + - provider type: object status: description: ModelConfigStatus defines the observed state of ModelConfig. diff --git a/helm/templates/argo-rollouts-agent.yaml b/helm/templates/argo-rollouts-agent.yaml index e2983a99f..87a9c1c5d 100644 --- a/helm/templates/argo-rollouts-agent.yaml +++ b/helm/templates/argo-rollouts-agent.yaml @@ -237,7 +237,7 @@ spec: should run next to the Deployment before deleting the Deployment or scaling down the Deployment. Not following this approach might result in downtime. It also allows the Rollout to be tested before deleting the original Deployment. Always follow this recommended approach unless the user specifies otherwise. - + modelConfigRef: default-model-config tools: - provider: kagent.tools.argo.VerifyArgoRolloutsControllerInstall - provider: kagent.tools.k8s.GetResources diff --git a/helm/templates/helm-agent.yaml b/helm/templates/helm-agent.yaml index 6c7408449..8516d53e0 100644 --- a/helm/templates/helm-agent.yaml +++ b/helm/templates/helm-agent.yaml @@ -144,6 +144,7 @@ spec: 4. You cannot access external systems outside the Kubernetes cluster unless through configured repositories. Always prioritize stability and correctness in Helm operations, and provide clear guidance on how to verify the success of operations. + modelConfigRef: default-model-config tools: - provider: kagent.tools.helm.ListReleases - provider: kagent.tools.helm.GetRelease diff --git a/helm/templates/istio-agent.yaml b/helm/templates/istio-agent.yaml index 8b911f1b6..179e0f38f 100644 --- a/helm/templates/istio-agent.yaml +++ b/helm/templates/istio-agent.yaml @@ -320,6 +320,7 @@ spec: - Multi-cluster connectivity issues Your primary goal is to provide expert assistance with Kubernetes and Istio environments by leveraging your specialized tools while following best practices for safety, reliability, and performance. Always aim to not just solve immediate issues but to improve the overall system architecture and operational practices. + modelConfigRef: default-model-config tools: - provider: kagent.tools.k8s.CreateResource - provider: kagent.tools.k8s.CreateResourceFromUrl diff --git a/helm/templates/kube-agent.yaml b/helm/templates/kube-agent.yaml index 52a7ad050..c4b1da261 100644 --- a/helm/templates/kube-agent.yaml +++ b/helm/templates/kube-agent.yaml @@ -117,6 +117,7 @@ spec: 4. Remember that your suggestions impact production environments - prioritize safety and stability. Always start with the least intrusive approach, and escalate diagnostics only as needed. When in doubt, gather more information before recommending changes. + modelConfigRef: default-model-config tools: - provider: kagent.tools.k8s.CheckServiceConnectivity - provider: kagent.tools.k8s.PatchResource diff --git a/helm/templates/modelconfig.yaml b/helm/templates/modelconfig.yaml index 9164da210..956479bc8 100644 --- a/helm/templates/modelconfig.yaml +++ b/helm/templates/modelconfig.yaml @@ -7,4 +7,5 @@ metadata: spec: apiKeySecretName: {{ .Values.openai.secretName }} apiKeySecretKey: {{ .Values.openai.secretKey }} - model: gpt-4o \ No newline at end of file + model: gpt-4o + provider: OpenAI \ No newline at end of file diff --git a/helm/templates/observability-agent.yaml b/helm/templates/observability-agent.yaml index faf26836e..87fa5e3f1 100644 --- a/helm/templates/observability-agent.yaml +++ b/helm/templates/observability-agent.yaml @@ -75,6 +75,7 @@ spec: - ALWAYS format your response as Markdown - Your response will include a summary of actions you took and an explanation of the result - If you created any artifacts such as files or resources, you will include those in your response as well + modelConfigRef: default-model-config tools: - provider: kagent.tools.k8s.GetResources - provider: kagent.tools.k8s.GetAvailableAPIResources diff --git a/python/pyproject.toml b/python/pyproject.toml index 7403d92bc..47659ec26 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "autogen-core>=0.4.3", - "autogen-ext[mcp]>=0.4.3", + "autogen-ext[anthropic,azure,mcp,openai]>=0.4.3", "autogen-agentchat>=0.4.3", "autogenstudio>=0.4.3", "openai==1.60.0", @@ -26,6 +26,7 @@ dependencies = [ "opentelemetry-exporter-otlp-proto-grpc>=1.30.0", "opentelemetry-instrumentation-openai>= 0.38.0", "opentelemetry-instrumentation-httpx >= 0.51.0", + "anthropic>=0.49.0", ] [project.optional-dependencies] diff --git a/python/uv.lock b/python/uv.lock index 9355aa8eb..61eac6fb6 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -39,6 +39,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] +[[package]] +name = "anthropic" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368 }, +] + [[package]] name = "anyio" version = "4.8.0" @@ -1087,9 +1105,10 @@ name = "kagent" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "anthropic" }, { name = "autogen-agentchat" }, { name = "autogen-core" }, - { name = "autogen-ext", extra = ["mcp"] }, + { name = "autogen-ext", extra = ["azure", "mcp", "openai"] }, { name = "autogenstudio" }, { name = "mcp" }, { name = "numpy" }, @@ -1121,9 +1140,10 @@ test = [ [package.metadata] requires-dist = [ + { name = "anthropic", specifier = ">=0.49.0" }, { name = "autogen-agentchat", git = "https://github.com/kagent-dev/autogen.git?subdirectory=python%2Fpackages%2Fautogen-agentchat&rev=faec0c007eba443a6a755fd363abbad82195bcb9" }, { name = "autogen-core", git = "https://github.com/kagent-dev/autogen.git?subdirectory=python%2Fpackages%2Fautogen-core&rev=faec0c007eba443a6a755fd363abbad82195bcb9" }, - { name = "autogen-ext", extras = ["mcp"], git = "https://github.com/kagent-dev/autogen.git?subdirectory=python%2Fpackages%2Fautogen-ext&rev=faec0c007eba443a6a755fd363abbad82195bcb9" }, + { name = "autogen-ext", extras = ["anthropic", "azure", "mcp", "openai"], git = "https://github.com/kagent-dev/autogen.git?subdirectory=python%2Fpackages%2Fautogen-ext&rev=faec0c007eba443a6a755fd363abbad82195bcb9" }, { name = "autogenstudio", git = "https://github.com/kagent-dev/autogen.git?subdirectory=python%2Fpackages%2Fautogen-studio&rev=faec0c007eba443a6a755fd363abbad82195bcb9" }, { name = "ipykernel", marker = "extra == 'jupyter-executor'", specifier = ">=6.29.5" }, { name = "mcp", specifier = ">=1.2.0" }, diff --git a/ui/src/app/actions/chat.ts b/ui/src/app/actions/chat.ts index 9cf75eb6f..9459d80ae 100644 --- a/ui/src/app/actions/chat.ts +++ b/ui/src/app/actions/chat.ts @@ -1,14 +1,14 @@ "use server"; -import { AgentMessageConfig, GetSessionRunsResponse, Message, Run, Session, Team } from "@/types/datamodel"; +import { AgentMessageConfig, AgentResponse, GetSessionRunsResponse, Message, Run, Session } from "@/types/datamodel"; import { getTeam } from "./teams"; import { getSession, getSessionRuns, getSessions } from "./sessions"; import { fetchApi, getCurrentUserId } from "./utils"; import { createRunWithSession } from "@/lib/ws"; -export async function startNewChat(agentId: number) { +export async function startNewChat(agentId: string): Promise<{ team: AgentResponse; session: Session; run: Run }> { const userId = await getCurrentUserId(); - const teamData = await getTeam(String(agentId)); + const teamData = await getTeam(agentId); if (!teamData.success || !teamData.data) { throw new Error("Agent not found"); @@ -59,7 +59,7 @@ export async function loadExistingChat(chatId: string) { } -export async function getChatData(agentId: number, chatId: string | null): Promise<{ notFound?: boolean; agent?: Team; sessions?: { session: Session; runs: Run[] }[]; viewState?: { session: Session; run: Run } | null }> { +export async function getChatData(agentId: number, chatId: string | null): Promise<{ notFound?: boolean; agent?: AgentResponse; sessions?: { session: Session; runs: Run[] }[]; viewState?: { session: Session; run: Run } | null }> { try { // Fetch agent details const agentData = await getTeam(agentId); diff --git a/ui/src/app/actions/models.ts b/ui/src/app/actions/models.ts index 63199a391..19400cb6d 100644 --- a/ui/src/app/actions/models.ts +++ b/ui/src/app/actions/models.ts @@ -18,3 +18,21 @@ export async function getModels(): Promise> { data: response, }; } + + +export async function getModel(configName: string) { + const response = await fetchApi(`/modelconfigs/${configName}`); + + if (!response) { + return { + success: false, + error: "Failed to get model. Please try again.", + data: null, + }; + } + + return { + success: true, + data: response, + }; +} \ No newline at end of file diff --git a/ui/src/app/actions/sessions.ts b/ui/src/app/actions/sessions.ts index 2383c61ad..9e5c9afa6 100644 --- a/ui/src/app/actions/sessions.ts +++ b/ui/src/app/actions/sessions.ts @@ -58,8 +58,8 @@ export async function createSession(session: CreateSessionRequest): Promise> { +export async function getTeam(teamLabel: string | number): Promise> { try { - const data = await fetchApi(`/teams/${teamLabel}`); + const data = await fetchApi(`/teams/${teamLabel}`); return { success: true, data }; } catch (error) { console.error("Error getting team:", error); @@ -33,30 +33,7 @@ export async function deleteTeam(teamLabel: string) { } } -interface ResourceMetadata { - name: string; - namespace?: string; -} - -interface AgentToolList { - provider: string; - description: string; - config: { - [key: string]: string; - }; -} -interface AgentResourceSpec { - description: string; - systemMessage: string; - tools: AgentToolList[]; -} -interface Agent { - metadata: ResourceMetadata; - spec: AgentResourceSpec; -} - function fromAgentFormDataToAgent(agentFormData: AgentFormData): Agent { - // TODO: Fill out the model field once the backend supports it return { metadata: { name: agentFormData.name, @@ -64,19 +41,22 @@ function fromAgentFormDataToAgent(agentFormData: AgentFormData): Agent { spec: { description: agentFormData.description, systemMessage: agentFormData.systemPrompt, + modelConfigRef: agentFormData.model.name, tools: agentFormData.tools.map((tool) => ({ provider: tool.provider, description: tool.description ?? "No description provided", - config: Object.entries(tool.config).reduce((acc, [key, value]) => { - acc[key] = String(value); - return acc; - }, {} as { [key: string]: string }), + config: tool.config + ? Object.entries(tool.config).reduce((acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, {} as { [key: string]: string }) + : {}, })), }, }; } -export async function createAgent(agentConfig: AgentFormData, update: boolean = false): Promise> { +export async function createAgent(agentConfig: AgentFormData, update: boolean = false): Promise> { let agentSpec; try { @@ -87,8 +67,8 @@ export async function createAgent(agentConfig: AgentFormData, update: boolean = } try { - const response = await fetchApi(`/teams`, { - method: update ? "PUT" : "POST", + const response = await fetchApi(`/teams`, { + method: update ? "PUT" : "POST", headers: { "Content-Type": "application/json", }, @@ -99,19 +79,18 @@ export async function createAgent(agentConfig: AgentFormData, update: boolean = throw new Error("Failed to create team"); } - revalidatePath(`/agents/${response.id}/chat`); + revalidatePath(`/agents/${response.metadata.name}/chat`); return { success: true, data: response }; } catch (error) { console.error("Error creating team:", error); - return { success: false, error: "Failed to create team. Please try again." }; + return { success: false, error: `Failed to create team: ${error}` }; } } -export async function getTeams(): Promise> { +export async function getTeams(): Promise> { try { - const data = await fetchApi(`/teams`); - const sortedData = data.sort((a, b) => a.component.label?.localeCompare(b.component.label || "") || 0); - + const data = await fetchApi(`/teams`); + const sortedData = data.sort((a, b) => a.agent.metadata.name.localeCompare(b.agent.metadata.name)); return { success: true, data: sortedData }; } catch (error) { console.error("Error getting teams:", error); diff --git a/ui/src/app/agents/new/page.tsx b/ui/src/app/agents/new/page.tsx index a9eb15d7d..ac3e7fd7d 100644 --- a/ui/src/app/agents/new/page.tsx +++ b/ui/src/app/agents/new/page.tsx @@ -12,11 +12,10 @@ import { ModelSelectionSection } from "@/components/create/ModelSelectionSection import { ToolsSection } from "@/components/create/ToolsSection"; import { useRouter, useSearchParams } from "next/navigation"; import { useAgents } from "@/components/AgentsProvider"; -import { Component, ToolConfig, Team } from "@/types/datamodel"; +import type { AgentTool } from "@/types/datamodel"; import { LoadingState } from "@/components/LoadingState"; import { ErrorState } from "@/components/ErrorState"; import KagentLogo from "@/components/kagent-logo"; -import { getUsersAgentFromTeam } from "@/lib/agents"; interface ValidationErrors { name?: string; @@ -33,19 +32,6 @@ function AgentPageContent() { const searchParams = useSearchParams(); const { models, tools, loading, error, createNewAgent, updateAgent, getAgentById, validateAgentData } = useAgents(); - const getModelIdFromTeam = (team: Team): string | undefined => { - try { - // Try to find model from the assistantAgent's model_client first - const assistantAgent = getUsersAgentFromTeam(team); - if (assistantAgent.config.model_client?.config?.model) { - return assistantAgent.config.model_client.config.model; - } - } catch (error) { - console.error("Error extracting model ID:", error); - return undefined; - } - }; - // Determine if in edit mode const isEditMode = searchParams.get("edit") === "true"; const agentId = searchParams.get("id"); @@ -58,8 +44,8 @@ function AgentPageContent() { // Default to the first model const [selectedModel, setSelectedModel] = useState(models && models.length > 0 ? models[0] : null); - // Tools state - const [selectedTools, setSelectedTools] = useState[]>([]); + // Tools state - now using AgentTool interface correctly + const [selectedTools, setSelectedTools] = useState([]); // Overall form state const [isSubmitting, setIsSubmitting] = useState(false); @@ -79,31 +65,28 @@ function AgentPageContent() { if (isEditMode && agentId) { try { setIsLoading(true); - const team = await getAgentById(agentId); + const agentResponse = await getAgentById(agentId); - if (team) { + if (!agentResponse) { + setGeneralError("Agent not found"); + setIsLoading(false); + return; + } + const agent = agentResponse.agent; + if (agent) { try { - // Extract the inner assistant agent from the team structure - const assistantAgent = getUsersAgentFromTeam(team); - - if (assistantAgent) { - // Populate form with existing agent data from the assistant agent - setName(assistantAgent.label || assistantAgent.config.name || ""); - setDescription(assistantAgent.description || ""); - setSystemPrompt(assistantAgent.config.system_message || ""); - - // Extract and set tools - setSelectedTools(assistantAgent.config.tools || []); - - // Find the model id from the model client and match it to available models - const modelId = getModelIdFromTeam(team); - if (modelId) { - const model = models.find((m) => m.model === modelId) || models[0]; - setSelectedModel(model); - } - } else { - throw new Error("Assistant agent configuration not found in team"); - } + // Populate form with existing agent data + setName(agent.metadata.name || ""); + setDescription(agent.spec.description || ""); + setSystemPrompt(agent.spec.systemMessage || ""); + + // Extract and set tools - these are already AgentTool objects + setSelectedTools(agent.spec.tools || []); + + setSelectedModel({ + model: agentResponse.model, + name: agent.spec.modelConfigRef, + }); } catch (extractError) { console.error("Error extracting assistant data:", extractError); setGeneralError("Failed to extract agent data from team structure"); @@ -121,7 +104,7 @@ function AgentPageContent() { }; fetchAgentData(); - }, [isEditMode, agentId, getAgentById]); + }, [isEditMode, agentId, getAgentById, models]); const validateForm = () => { const formData = { @@ -161,13 +144,21 @@ function AgentPageContent() { if (!selectedModel) { throw new Error("Model is required to update the agent."); } - result = await updateAgent(agentId, { ...agentData, model: selectedModel }); + result = await updateAgent(agentId, { + ...agentData, + model: selectedModel, + tools: selectedTools, + }); } else { // Create new agent if (!selectedModel) { throw new Error("Model is required to create the agent."); } - result = await createNewAgent({ ...agentData, model: selectedModel }); + result = await createNewAgent({ + ...agentData, + model: selectedModel, + tools: selectedTools, + }); } if (!result.success) { @@ -285,4 +276,4 @@ export default function AgentPage() { ); -} \ No newline at end of file +} diff --git a/ui/src/components/AgentCard.tsx b/ui/src/components/AgentCard.tsx index faf251c3b..012376724 100644 --- a/ui/src/components/AgentCard.tsx +++ b/ui/src/components/AgentCard.tsx @@ -1,35 +1,34 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import type { Team } from "@/types/datamodel"; +import type { AgentResponse } from "@/types/datamodel"; import { DeleteButton } from "@/components/DeleteAgentButton"; import KagentLogo from "@/components/kagent-logo"; -import { getUsersAgentFromTeam } from "@/lib/agents"; import Link from "next/link"; interface AgentCardProps { - team: Team; + agentResponse: AgentResponse; + id: number; } -export function AgentCard({ team }: AgentCardProps) { - const agent = getUsersAgentFromTeam(team); - - +export function AgentCard({ id, agentResponse: { agent, model, provider } }: AgentCardProps) { return ( - + - {agent.label || agent.config.name} + {agent.metadata.name} - + -

{agent.description}

+

{agent.spec.description}

- Model: {agent.config.model_client.config.model} + + {provider} ({model}) +
); -} \ No newline at end of file +} diff --git a/ui/src/components/AgentGrid.tsx b/ui/src/components/AgentGrid.tsx index 7be1ed546..17bead2d4 100644 --- a/ui/src/components/AgentGrid.tsx +++ b/ui/src/components/AgentGrid.tsx @@ -1,15 +1,15 @@ -import type { Team } from "@/types/datamodel"; +import type { AgentResponse } from "@/types/datamodel"; import { AgentCard } from "./AgentCard"; interface AgentGridProps { - teams: Team[]; + agentResponse: AgentResponse[]; } -export function AgentGrid({ teams }: AgentGridProps) { +export function AgentGrid({ agentResponse }: AgentGridProps) { return (
- {teams.map((team) => ( - + {agentResponse.map((item) => ( + ))}
); diff --git a/ui/src/components/AgentList.tsx b/ui/src/components/AgentList.tsx index e4faef4b3..2cda4f683 100644 --- a/ui/src/components/AgentList.tsx +++ b/ui/src/components/AgentList.tsx @@ -9,7 +9,7 @@ import { LoadingState } from "./LoadingState"; import { useAgents } from "./AgentsProvider"; export default function AgentList() { - const { teams, loading, error } = useAgents(); + const { agents , loading, error } = useAgents(); if (error) { return ; @@ -27,7 +27,7 @@ export default function AgentList() { - {teams?.length === 0 ? ( + {agents?.length === 0 ? (

No agents yet

@@ -40,7 +40,7 @@ export default function AgentList() {
) : ( - + )} ); diff --git a/ui/src/components/AgentsProvider.tsx b/ui/src/components/AgentsProvider.tsx index 6e25221a4..6f9425506 100644 --- a/ui/src/components/AgentsProvider.tsx +++ b/ui/src/components/AgentsProvider.tsx @@ -2,10 +2,9 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { getTeams, createAgent } from "@/app/actions/teams"; -import { Team, Component, ToolConfig } from "@/types/datamodel"; +import { Component, ToolConfig, Agent, AgentTool, AgentResponse } from "@/types/datamodel"; import { getTools } from "@/app/actions/tools"; -import { BaseResponse, Model } from "@/lib/types"; -import { isIdentifier } from "@/lib/utils"; +import type { BaseResponse, Model } from "@/lib/types"; import { getModels } from "@/app/actions/models"; interface ValidationErrors { @@ -22,19 +21,19 @@ export interface AgentFormData { description: string; systemPrompt: string; model: Model; - tools: Component[]; + tools: AgentTool[]; } interface AgentsContextType { - teams: Team[]; + agents: AgentResponse[]; models: Model[]; loading: boolean; error: string; tools: Component[]; refreshTeams: () => Promise; - createNewAgent: (agentData: AgentFormData) => Promise>; - updateAgent: (id: string, agentData: AgentFormData) => Promise>; - getAgentById: (id: string) => Promise; + createNewAgent: (agentData: AgentFormData) => Promise>; + updateAgent: (id: string, agentData: AgentFormData) => Promise>; + getAgentById: (id: string) => Promise; validateAgentData: (data: Partial) => ValidationErrors; } @@ -53,7 +52,7 @@ interface AgentsProviderProps { } export function AgentsProvider({ children }: AgentsProviderProps) { - const [teams, setTeams] = useState([]); + const [agents, setAgents] = useState([]); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const [tools, setTools] = useState[]>([]); @@ -68,7 +67,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) { throw new Error(teamsResult.error || "Failed to fetch teams"); } - setTeams(teamsResult.data); + setAgents(teamsResult.data); setError(""); } catch (err) { setError(err instanceof Error ? err.message : "An unexpected error occurred"); @@ -118,9 +117,6 @@ export function AgentsProvider({ children }: AgentsProviderProps) { if (data.name !== undefined) { if (!data.name.trim()) { errors.name = "Agent name is required"; - } // we only check that it's required as this will be the label -- the name is created from the label - else if (!isIdentifier(data.name)) { - errors.name = "Agent name must not contain special characters"; } } @@ -140,7 +136,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) { }; // Get agent by ID function - const getAgentById = async (id: string): Promise => { + const getAgentById = async (name: string): Promise => { try { // First ensure we have the latest teams data const { data: teams } = await getTeams(); @@ -151,10 +147,10 @@ export function AgentsProvider({ children }: AgentsProviderProps) { } // Find the team/agent with the matching ID - const agent = teams.find((team) => String(team.id) === id); + const agent = teams.find((team) => String(team.agent.metadata.name) === name); if (!agent) { - console.warn(`Agent with ID ${id} not found`); + console.warn(`Agent with name ${name} not found`); return null; } @@ -171,7 +167,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) { try { const errors = validateAgentData(agentData); if (Object.keys(errors).length > 0) { - return { success: false, error: "Validation failed", data: {} as Team }; + return { success: false, error: "Validation failed", data: {} as Agent }; } const result = await createAgent(agentData); @@ -192,13 +188,13 @@ export function AgentsProvider({ children }: AgentsProviderProps) { }; // Update existing agent - const updateAgent = async (id: string, agentData: AgentFormData): Promise> => { + const updateAgent = async (id: string, agentData: AgentFormData): Promise> => { try { const errors = validateAgentData(agentData); if (Object.keys(errors).length > 0) { console.log("Errors validating agent data", errors); - return { success: false, error: "Validation failed", data: {} as Team }; + return { success: false, error: "Validation failed", data: {} as Agent }; } // Use the same createTeam endpoint for updates @@ -227,7 +223,7 @@ export function AgentsProvider({ children }: AgentsProviderProps) { }, []); const value = { - teams, + agents, models, loading, error, diff --git a/ui/src/components/chat/ChatInterface.tsx b/ui/src/components/chat/ChatInterface.tsx index 12c1f0014..f4fa6ae85 100644 --- a/ui/src/components/chat/ChatInterface.tsx +++ b/ui/src/components/chat/ChatInterface.tsx @@ -64,7 +64,7 @@ export default function ChatInterface({ selectedAgentId, selectedRun }: ChatInte setMessage(""); try { - await sendUserMessage(currentMessage, Number(selectedAgentId)); + await sendUserMessage(currentMessage, String(selectedAgentId)); } catch (error) { console.error("Error sending message:", error); setMessage(currentMessage); diff --git a/ui/src/components/create/ModelSelectionSection.tsx b/ui/src/components/create/ModelSelectionSection.tsx index 4640a137e..b9a3acfbc 100644 --- a/ui/src/components/create/ModelSelectionSection.tsx +++ b/ui/src/components/create/ModelSelectionSection.tsx @@ -27,7 +27,7 @@ export const ModelSelectionSection = ({ allModels, selectedModel, setSelectedMod {allModels.map((model, idx) => ( - {model.model} + {model.model} ({model.name}) ))} diff --git a/ui/src/components/create/SelectToolsDialog.tsx b/ui/src/components/create/SelectToolsDialog.tsx index 987ca8652..8ade98109 100644 --- a/ui/src/components/create/SelectToolsDialog.tsx +++ b/ui/src/components/create/SelectToolsDialog.tsx @@ -6,12 +6,10 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Search, Filter, ChevronDown, ChevronRight, AlertCircle } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; -import { getToolDescription, getToolDisplayName, getToolIdentifier } from "@/lib/data"; -import { Component, ToolConfig } from "@/types/datamodel"; +import { AgentTool, Component, ToolConfig } from "@/types/datamodel"; import ProviderFilter from "./ProviderFilter"; import ToolItem from "./ToolItem"; - -type Tool = Component; +import { findComponentForAgentTool } from "@/lib/toolUtils"; // Maximum number of tools that can be selected const MAX_TOOLS_LIMIT = 10; @@ -29,17 +27,17 @@ const getToolCategory = (toolId: string) => { interface SelectToolsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - availableTools: Tool[]; - selectedTools: Tool[]; - onToolsSelected: (tools: Tool[]) => void; + availableTools: Component[]; + selectedTools: AgentTool[]; + onToolsSelected: (tools: Component[]) => void; } export const SelectToolsDialog: React.FC = ({ open, onOpenChange, availableTools, selectedTools, onToolsSelected }) => { // State hooks const [searchTerm, setSearchTerm] = useState(""); const [activeTab, setActiveTab] = useState("all"); - const [localSelectedTools, setLocalSelectedTools] = useState([]); - const [newlyDiscoveredTools, setNewlyDiscoveredTools] = useState([]); + const [localSelectedComponents, setLocalSelectedComponents] = useState[]>([]); + const [newlyDiscoveredTools, setNewlyDiscoveredTools] = useState[]>([]); const [providers, setProviders] = useState>(new Set()); const [selectedProviders, setSelectedProviders] = useState>(new Set()); const [showFilters, setShowFilters] = useState(false); @@ -48,10 +46,13 @@ export const SelectToolsDialog: React.FC = ({ open, onOp // Initialize state when dialog opens useEffect(() => { if (open) { - const initialTools = [...selectedTools]; + // Convert selectedTools (AgentTool[]) to Component[] for local state + const initialComponents: Component[] = selectedTools + .map((agentTool) => findComponentForAgentTool(agentTool, availableTools)) + .filter((tool): tool is Component => tool !== undefined); setNewlyDiscoveredTools([]); - setLocalSelectedTools(initialTools); + setLocalSelectedComponents(initialComponents); setSearchTerm(""); // Extract unique providers @@ -68,7 +69,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp // Initialize all categories as expanded const categories: { [key: string]: boolean } = {}; availableTools.forEach((tool) => { - const category = getToolCategory(getToolIdentifier(tool)); + const category = getToolCategory(tool.provider); categories[category] = true; }); setExpandedCategories(categories); @@ -82,16 +83,11 @@ export const SelectToolsDialog: React.FC = ({ open, onOp return availableTools.filter((tool) => { // Search matching - const matchesSearch = - getToolDisplayName(tool)?.toLowerCase().includes(searchLower) || - getToolDescription(tool)?.toLowerCase().includes(searchLower) || - tool.provider.toLowerCase().includes(searchLower) || - getToolIdentifier(tool).toLowerCase().includes(searchLower); + const matchesSearch = tool.provider.toLowerCase().includes(searchLower) || (tool.description?.toLowerCase().includes(searchLower) ?? false); // Tab matching - const toolId = getToolIdentifier(tool); - const isSelected = localSelectedTools.some((t) => getToolIdentifier(t) === toolId); - const isNew = newlyDiscoveredTools.some((t) => getToolIdentifier(t) === toolId); + const isSelected = localSelectedComponents.some((t) => t.provider === tool.provider); + const isNew = newlyDiscoveredTools.some((t) => t.provider === tool.provider); const matchesTab = activeTab === "all" || (activeTab === "selected" && isSelected) || (activeTab === "new" && isNew); @@ -100,28 +96,28 @@ export const SelectToolsDialog: React.FC = ({ open, onOp return matchesSearch && matchesTab && matchesProvider; }); - }, [availableTools, searchTerm, activeTab, localSelectedTools, newlyDiscoveredTools, selectedProviders]); + }, [availableTools, searchTerm, activeTab, localSelectedComponents, newlyDiscoveredTools, selectedProviders]); // Group tools by category const groupedTools = useMemo(() => { - const groups: { [key: string]: Tool[] } = {}; + const groups: { [key: string]: Component[] } = {}; // Sort tools first - new tools at the top within each category const sortedTools = [...filteredTools].sort((a, b) => { - const aIsNew = newlyDiscoveredTools.some((t) => getToolIdentifier(t) === getToolIdentifier(a)); - const bIsNew = newlyDiscoveredTools.some((t) => getToolIdentifier(t) === getToolIdentifier(b)); + const aIsNew = newlyDiscoveredTools.some((t) => t.provider === a.provider); + const bIsNew = newlyDiscoveredTools.some((t) => t.provider === b.provider); // Primary sort: new tools first if (aIsNew && !bIsNew) return -1; if (!aIsNew && bIsNew) return 1; // Secondary sort: alphabetical by name - return (getToolDisplayName(a) || "").localeCompare(getToolDisplayName(b) || ""); + return (a.provider || "").localeCompare(b.provider || ""); }); // Group by categories sortedTools.forEach((tool) => { - const category = getToolCategory(getToolIdentifier(tool)); + const category = getToolCategory(tool.provider); if (!groups[category]) { groups[category] = []; } @@ -132,14 +128,13 @@ export const SelectToolsDialog: React.FC = ({ open, onOp }, [filteredTools, newlyDiscoveredTools]); // Check if selection limit is reached - const isLimitReached = localSelectedTools.length >= MAX_TOOLS_LIMIT; + const isLimitReached = localSelectedComponents.length >= MAX_TOOLS_LIMIT; // Helper functions for tool state - const isToolSelected = (tool: Component) => localSelectedTools.some((t) => getToolIdentifier(t) === getToolIdentifier(tool)); + const isToolSelected = (tool: Component) => localSelectedComponents.some((t) => t.provider === tool.provider); // Event handlers - const handleToggleTool = (tool: Tool) => { - const toolId = getToolIdentifier(tool); + const handleToggleTool = (tool: Component) => { const isCurrentlySelected = isToolSelected(tool); // If tool is not selected and we've reached limit, don't allow adding @@ -147,11 +142,11 @@ export const SelectToolsDialog: React.FC = ({ open, onOp return; } - setLocalSelectedTools((prev) => (isCurrentlySelected ? prev.filter((t) => getToolIdentifier(t) !== toolId) : [...prev, tool])); + setLocalSelectedComponents((prev) => (isCurrentlySelected ? prev.filter((t) => t.provider !== tool.provider) : [...prev, tool])); }; const handleSave = () => { - onToolsSelected(localSelectedTools); + onToolsSelected(localSelectedComponents); onOpenChange(false); }; @@ -181,18 +176,18 @@ export const SelectToolsDialog: React.FC = ({ open, onOp // Modified to respect the tool limit const selectAllTools = () => { - if (availableTools.length <= MAX_TOOLS_LIMIT) { - setLocalSelectedTools([...availableTools]); + if (filteredTools.length <= MAX_TOOLS_LIMIT) { + setLocalSelectedComponents(filteredTools); } else { - setLocalSelectedTools(availableTools.slice(0, MAX_TOOLS_LIMIT)); + setLocalSelectedComponents(filteredTools.slice(0, MAX_TOOLS_LIMIT)); } }; - const clearToolSelection = () => setLocalSelectedTools([]); + const clearToolSelection = () => setLocalSelectedComponents([]); // Stats const totalTools = availableTools.length; - const selectedCount = localSelectedTools.length; + const selectedCount = localSelectedComponents.length; const newToolsCount = newlyDiscoveredTools.length; return ( @@ -201,7 +196,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp onOpenChange={(isOpen) => { // Auto-save if closing with newly discovered tools if (!isOpen && newToolsCount > 0) { - onToolsSelected(localSelectedTools); + onToolsSelected(localSelectedComponents); } onOpenChange(isOpen); }} @@ -298,7 +293,7 @@ export const SelectToolsDialog: React.FC = ({ open, onOp {expandedCategories[category] && (
{tools.map((tool) => ( - + ))}
)} diff --git a/ui/src/components/create/ToolItem.tsx b/ui/src/components/create/ToolItem.tsx index c601e0009..c2a06fcb5 100644 --- a/ui/src/components/create/ToolItem.tsx +++ b/ui/src/components/create/ToolItem.tsx @@ -1,22 +1,21 @@ -import { getToolDisplayName, getToolDescription, getToolIdentifier, isMcpTool } from "@/lib/data"; +import { isMcpTool } from "@/lib/data"; import { Check } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Component, ToolConfig } from "@/types/datamodel"; +import { Component, ToolConfig } from "@/types/datamodel"; -type Tool = Component; interface ToolItemProps { - tool: Tool; + tool: Component; isSelected: boolean; - onToggle: (tool: Tool) => void; + onToggle: (tool: Component) => void; disabled?: boolean; } const ToolItem = ({ tool, isSelected, onToggle, disabled = false }: ToolItemProps) => { - const displayName = getToolDisplayName(tool) || "Unnamed Tool"; - const displayDescription = getToolDescription(tool) || "No description available"; - const toolId = getToolIdentifier(tool); + const displayName = tool.provider; // getToolDisplayName(tool) || "Unnamed Tool"; + const displayDescription = tool.description; // getToolDescription(tool) || "No description available"; + const toolId = tool.provider; // getToolIdentifier(tool); // Determine classes based on selection and disabled states const containerClasses = `p-4 rounded-lg border transition-all ${ diff --git a/ui/src/components/create/ToolsSection.tsx b/ui/src/components/create/ToolsSection.tsx index cb7ed87c3..8c57c0971 100644 --- a/ui/src/components/create/ToolsSection.tsx +++ b/ui/src/components/create/ToolsSection.tsx @@ -4,38 +4,28 @@ import { Dialog, DialogContent, DialogTitle, DialogHeader, DialogFooter, DialogD import { Input } from "@/components/ui/input"; import { Plus, FunctionSquare, X, Settings2 } from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { useState, useEffect } from "react"; -import { getToolDescription, getToolDisplayName, getToolIdentifier, isMcpTool } from "@/lib/data"; +import { useState } from "react"; +import { getToolDescription, getToolDisplayName, isMcpTool } from "@/lib/data"; import { Label } from "../ui/label"; import { SelectToolsDialog } from "./SelectToolsDialog"; -import { Component, ToolConfig } from "@/types/datamodel"; +import { AgentTool, Component, ToolConfig } from "@/types/datamodel"; +import { componentToAgentTool, findComponentForAgentTool } from "@/lib/toolUtils"; interface ToolsSectionProps { allTools: Component[]; - selectedTools: Component[]; - setSelectedTools: (tools: Component[]) => void; + selectedTools: AgentTool[]; + setSelectedTools: (tools: AgentTool[]) => void; isSubmitting: boolean; } export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubmitting }: ToolsSectionProps) => { const [showToolSelector, setShowToolSelector] = useState(false); - const [configTool, setConfigTool] = useState | null>(null); + const [configTool, setConfigTool] = useState(null); const [showConfig, setShowConfig] = useState(false); - const [toolConfigMap, setToolConfigMap] = useState>({}); - - // Initialize toolConfigMap when selectedTools change - useEffect(() => { - const newToolConfigMap: Record = {}; - selectedTools.forEach((tool) => { - const toolId = getToolIdentifier(tool); - newToolConfigMap[toolId] = { ...tool.config }; - }); - setToolConfigMap(newToolConfigMap); - }, [selectedTools]); - const openConfigDialog = (tool: Component) => { + const openConfigDialog = (agentTool: AgentTool) => { // Create a deep copy of the tool to avoid reference issues - const toolCopy = JSON.parse(JSON.stringify(tool)); + const toolCopy = JSON.parse(JSON.stringify(agentTool)) as AgentTool; setConfigTool(toolCopy); setShowConfig(true); }; @@ -43,21 +33,10 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm const handleConfigSave = () => { if (!configTool) return; - const toolId = getToolIdentifier(configTool); - - // Update the toolConfigMap - setToolConfigMap((prev) => ({ - ...prev, - [toolId]: { ...configTool.config }, - })); - // Update the selectedTools array with the new config const updatedTools = selectedTools.map((tool) => { - if (getToolIdentifier(tool) === toolId) { - return { - ...tool, - config: { ...configTool.config }, - }; + if (tool.provider === configTool.provider) { + return configTool; } return tool; }); @@ -68,19 +47,14 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm }; const handleToolSelect = (newSelectedTools: Component[]) => { - setSelectedTools(newSelectedTools); + // Convert Component[] to AgentTool[] + const agentTools = newSelectedTools.map(componentToAgentTool); + setSelectedTools(agentTools); setShowToolSelector(false); }; - const handleRemoveTool = (tool: Component) => { - const toolId = getToolIdentifier(tool); - const updatedTools = selectedTools.filter((t) => getToolIdentifier(t) !== toolId); - - // Also remove from the config map - const updatedConfigMap = { ...toolConfigMap }; - delete updatedConfigMap[toolId]; - setToolConfigMap(updatedConfigMap); - + const handleRemoveTool = (toolProvider: string) => { + const updatedTools = selectedTools.filter((t) => t.provider !== toolProvider); setSelectedTools(updatedTools); }; @@ -89,6 +63,7 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm setConfigTool((prevTool) => { if (!prevTool) return null; + return { ...prevTool, config: { @@ -119,9 +94,9 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm > - Configure {getToolDisplayName(configTool)} + Configure {configTool.provider} - Configure the settings for {getToolDisplayName(configTool)}. These settings will be used when the tool is executed. + Configure the settings for {configTool.provider}. These settings will be used when the tool is executed. @@ -134,7 +109,7 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm - handleConfigChange(field, e.target.value)} /> + handleConfigChange(field, e.target.value)} /> ))} @@ -163,17 +138,21 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm const renderSelectedTools = () => (
- {selectedTools.map((tool: Component) => { - const displayName = getToolDisplayName(tool); - const displayDescription = getToolDescription(tool); - const toolIdentifier = getToolIdentifier(tool); + {selectedTools.map((agentTool: AgentTool) => { + // Find the corresponding tool configuration from available tools + const toolConfig = findComponentForAgentTool(agentTool, allTools); + if (!toolConfig) return null; + + const displayName = getToolDisplayName(toolConfig); + const displayDescription = getToolDescription(toolConfig); + return ( - +
- +
{displayName} {displayDescription} @@ -182,12 +161,12 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm
- {!isMcpTool(tool) && ( - )} -
@@ -222,8 +201,8 @@ export const ToolsSection = ({ allTools, selectedTools, setSelectedTools, isSubm {selectedTools.length === 0 ? ( - -

No tools selected

+ +

No tools selected

Add tools to enhance your agent

- {selectedTeam.component.label} - {innerAgent.config.model_client.config.model} + {selectedTeam.agent.metadata.name} + {selectedTeam.provider} ({selectedTeam.model})
Agents - {agents.map((team, index) => { + {agentResponses.map(({ id, agent}, index) => { return ( { - router.push(`/agents/${team.id}/chat`); + router.push(`/agents/${id}/chat`); }} className="gap-2 p-2" > - {team.component.label} + {agent.metadata.name} ⌘{index + 1} ); diff --git a/ui/src/lib/agents.ts b/ui/src/lib/agents.ts deleted file mode 100644 index 8e387dbbd..000000000 --- a/ui/src/lib/agents.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - AgentConfig, - AssistantAgentConfig, - Component, - ComponentConfig, - Team, - TeamConfig, -} from "@/types/datamodel"; - -function isAssistantAgent(component: Component): boolean { - return component.provider === "autogen_agentchat.agents.AssistantAgent" && !component.label?.startsWith("kagent_"); -} - -/** - * Searches for all AssistantAgents in a component hierarchy - * @param component - The component to search within - * @returns Array of AssistantAgent components - */ -export function findAllAssistantAgents(component?: Component): Component[] { - if (!component?.config) { - return []; - } - - if ("participants" in component.config && Array.isArray(component.config.participants)) { - return traverseComponentTree(component.config.participants, isAssistantAgent); - } else if ("team" in component.config) { - return findAllAssistantAgents(component.config.team); - } - - return []; -} - -/** - * Generic function to traverse a component tree and collect components matching a predicate - * @param components - Array of components to traverse - * @param predicate - Function to test if a component should be included - * @returns Array of components matching the predicate - */ -function traverseComponentTree(components: Component[], predicate: (component: Component) => boolean): Component[] { - if (!components || !Array.isArray(components)) { - return []; - } - - const results: Component[] = []; - - for (const component of components) { - // Check if current component matches predicate - if (predicate(component)) { - results.push(component as Component); - } - - // Check TaskAgent with nested team - if (component.provider === "kagent.agents.TaskAgent" && component.config?.team?.config?.participants) { - const nestedResults = traverseComponentTree(component.config.team.config.participants, predicate); - results.push(...nestedResults); - } - - // Check any other nested participants - if (component.config?.participants) { - const nestedResults = traverseComponentTree(component.config.participants, predicate); - results.push(...nestedResults); - } - } - - return results; -} - -export function getUsersAgentFromTeam(team: Team): Component { - if (!team.component?.config) { - throw new Error("Invalid team structure or missing configuration"); - } - - if (!("participants" in team.component.config) || !Array.isArray(team.component.config.participants)) { - throw new Error("Team configuration does not contain participants"); - } - - // Use the generic traversal with a find operation instead of collecting all - const agents = traverseComponentTree(team.component.config.participants, isAssistantAgent); - - if (agents.length === 0) { - throw new Error("No AssistantAgent found in the team hierarchy"); - } - - return agents[0]; -} - -export function updateUsersAgent(team: Team, updateFn: (agent: Component) => void): Team { - const teamCopy = structuredClone(team); - - if (!teamCopy.component?.config) { - throw new Error("Invalid team structure or missing configuration"); - } - - const usersAgent = getUsersAgentFromTeam(teamCopy); - updateFn(usersAgent); - - return teamCopy; -} diff --git a/ui/src/lib/toolUtils.ts b/ui/src/lib/toolUtils.ts new file mode 100644 index 000000000..8161b58ce --- /dev/null +++ b/ui/src/lib/toolUtils.ts @@ -0,0 +1,75 @@ +/** + * This utility file provides functions to convert between different tool types + */ + +import { AgentTool, Component, ToolConfig } from "@/types/datamodel"; + +/** + * Converts a Component to an AgentTool + * @param tool The Component to convert + * @returns An AgentTool based on the provided Component + */ +export function componentToAgentTool(tool: Component): AgentTool { + return { + provider: tool.provider, + description: tool.description || "", + config: Object.entries(tool.config || {}).reduce((acc, [key, value]) => { + acc[key] = String(value); // Ensure all values are strings + return acc; + }, {} as Record), + }; +} + +/** + * Converts an array of Component to an array of AgentTools + * @param tools Array of Component to convert + * @returns Array of AgentTools + */ +export function componentsToAgentTools(tools: Component[]): AgentTool[] { + return tools.map(componentToAgentTool); +} + +/** + * Finds a Component matching an AgentTool from a list of available tools + * @param agentTool The AgentTool to find + * @param availableTools List of available Component + * @returns The matching Component or undefined if not found + */ +export function findComponentForAgentTool( + agentTool: AgentTool, + availableTools: Component[] +): Component | undefined { + return availableTools.find((tool) => tool.provider === agentTool.provider); +} + +/** + * Checks if an AgentTool is represented in an array of Component + * @param agentTool The AgentTool to check + * @param components Array of Component to search in + * @returns True if the AgentTool is found, false otherwise + */ +export function isAgentToolInComponents( + agentTool: AgentTool, + components: Component[] +): boolean { + return components.some((component) => component.provider === agentTool.provider); +} + +/** + * Updates an AgentTool with new configuration values + * @param agentTool The AgentTool to update + * @param newConfig The new configuration to apply + * @returns A new AgentTool with updated configuration + */ +export function updateAgentToolConfig( + agentTool: AgentTool, + newConfig: Record +): AgentTool { + return { + ...agentTool, + config: { + ...agentTool.config, + ...newConfig, + }, + }; +} \ No newline at end of file diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 028c3d43e..f02ea0c7a 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -14,8 +14,9 @@ export interface Model { } export interface CreateSessionRequest { - userId: string; - teamId: number; + name?: string; + user_id: string; + team_id: string; } export interface CreateRunRequest { diff --git a/ui/src/lib/useChatStore.ts b/ui/src/lib/useChatStore.ts index bf4e6af26..1a841037f 100644 --- a/ui/src/lib/useChatStore.ts +++ b/ui/src/lib/useChatStore.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { ChatStatus, setupWebSocket, WebSocketManager } from "./ws"; -import { AgentMessageConfig, InitialMessage, Message, Run, Session, Team, WebSocketMessage, SessionWithRuns } from "@/types/datamodel"; +import { AgentMessageConfig, InitialMessage, Message, Run, Session, WebSocketMessage, SessionWithRuns, AgentResponse } from "@/types/datamodel"; import { loadExistingChat, sendMessage, startNewChat } from "@/app/actions/chat"; import { messageUtils } from "./utils"; @@ -12,13 +12,13 @@ interface ChatState { status: ChatStatus; error: string | null; websocketManager: WebSocketManager | null; - team: Team | null; + team: AgentResponse | null; currentStreamingContent: string; currentStreamingMessage: Message | null; // Actions - initializeNewChat: (agentId: number) => Promise; - sendUserMessage: (content: string, agentId: number) => Promise; + initializeNewChat: (agentId: string) => Promise; + sendUserMessage: (content: string, agentId: string) => Promise; loadChat: (chatId: string) => Promise; cleanup: () => void; handleWebSocketMessage: (message: WebSocketMessage) => void; diff --git a/ui/src/lib/ws.ts b/ui/src/lib/ws.ts index 67ae8587d..33b669696 100644 --- a/ui/src/lib/ws.ts +++ b/ui/src/lib/ws.ts @@ -192,8 +192,8 @@ export function setupWebSocket(runId: string, handlers: WebSocketHandlers, initi }; } -export const createRunWithSession = async (teamId: number, userId: string): Promise => { - const sessionResponse = await createSession({ userId, teamId }); +export const createRunWithSession = async (agentId: string, userId: string): Promise => { + const sessionResponse = await createSession({ user_id: userId, team_id: agentId }); if (!sessionResponse.success || !sessionResponse.data) { throw new Error("Failed to create session"); } diff --git a/ui/src/types/datamodel.ts b/ui/src/types/datamodel.ts index 319f0d7ee..9faccd1b2 100644 --- a/ui/src/types/datamodel.ts +++ b/ui/src/types/datamodel.ts @@ -358,3 +358,36 @@ export interface SessionWithRuns { session: Session; runs: Run[]; } + + +export interface ResourceMetadata { + name: string; + namespace?: string; +} + +export interface AgentTool { + provider: string; + description: string; + config: { + [key: string]: string; + }; +} +export interface AgentResourceSpec { + description: string; + systemMessage: string; + tools: AgentTool[]; + // Name of the model config resource + modelConfigRef: string; +} +export interface Agent { + metadata: ResourceMetadata; + spec: AgentResourceSpec; +} + +export interface AgentResponse { + id: number; + agent: Agent; + component: Component; + model: string; + provider: string; +} \ No newline at end of file