diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/cmd/init.go index c75eaab8049..43a79cecb74 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/cmd/init.go @@ -701,7 +701,7 @@ func (a *InitAction) downloadAgentYaml( return nil, "", fmt.Errorf("marshaling agent manifest to YAML after parameter processing: %w", err) } - agentId := agentManifest.Agent.Name + agentId := agentManifest.Name // Use targetDir if provided or set to local file pointer, otherwise default to "src/{agentId}" if targetDir == "" { @@ -719,12 +719,18 @@ func (a *InitAction) downloadAgentYaml( return nil, "", fmt.Errorf("saving file to %s: %w", filePath, err) } - if isGitHubUrl && agentManifest.Agent.Kind == agent_yaml.AgentKindHosted { - // For hosted agents, download the entire parent directory - fmt.Println("Downloading full directory for hosted agent") - err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console) - if err != nil { - return nil, "", fmt.Errorf("downloading parent directory: %w", err) + if isGitHubUrl { + // Check if the template is a HostedContainerAgent or ContainerAgent + _, isHostedContainer := agentManifest.Template.(agent_yaml.HostedContainerAgent) + _, isContainerAgent := agentManifest.Template.(agent_yaml.ContainerAgent) + + if isHostedContainer || isContainerAgent { + // For container agents, download the entire parent directory + fmt.Println("Downloading full directory for container agent") + err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console) + if err != nil { + return nil, "", fmt.Errorf("downloading parent directory: %w", err) + } } } @@ -735,15 +741,40 @@ func (a *InitAction) downloadAgentYaml( func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentManifest *agent_yaml.AgentManifest) error { var host string - switch agentManifest.Agent.Kind { + + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return fmt.Errorf("failed to marshal agent template to JSON: %w", err) + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return fmt.Errorf("failed to unmarshal agent template from JSON: %w", err) + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return fmt.Errorf("failed to marshal templateDict to JSON: %w", err) + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) + } + + switch agentDef.Kind { case "container": host = "containerapp" default: - host = "foundry.agent" + host = "foundry.containeragent" } serviceConfig := &azdext.ServiceConfig{ - Name: agentManifest.Agent.Name, + Name: strings.ReplaceAll(agentDef.Name, " ", ""), RelativePath: targetDir, Host: host, Language: "python", @@ -755,7 +786,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa return fmt.Errorf("adding agent service to project: %w", err) } - fmt.Printf("Added service '%s' to azure.yaml\n", agentManifest.Agent.Name) + fmt.Printf("Added service '%s' to azure.yaml\n", agentDef.Name) return nil } @@ -1222,7 +1253,31 @@ func downloadDirectoryContents( // } func (a *InitAction) updateEnvironment(ctx context.Context, agentManifest *agent_yaml.AgentManifest) error { - fmt.Printf("Updating environment variables for agent kind: %s\n", agentManifest.Agent.Kind) + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return fmt.Errorf("failed to marshal agent template to JSON: %w", err) + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return fmt.Errorf("failed to unmarshal agent template from JSON: %w", err) + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return fmt.Errorf("failed to marshal templateDict to JSON: %w", err) + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) + } + + fmt.Printf("Updating environment variables for agent kind: %s\n", agentDef.Kind) // Get current environment envResponse, err := a.azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) @@ -1237,25 +1292,25 @@ func (a *InitAction) updateEnvironment(ctx context.Context, agentManifest *agent envName := envResponse.Environment.Name // Set environment variables based on agent kind - switch agentManifest.Agent.Kind { - case "hosted": + switch agentDef.Kind { + case agent_yaml.AgentKindPrompt: + agentDef := agentManifest.Template.(agent_yaml.PromptAgent) + if err := a.setEnvVar(ctx, envName, "AZURE_AI_FOUNDRY_MODEL_NAME", agentDef.Model.Id); err != nil { + return err + } + case agent_yaml.AgentKindHosted: // Set environment variables for hosted agents if err := a.setEnvVar(ctx, envName, "ENABLE_HOSTED_AGENTS", "true"); err != nil { return err } - case "container": + case agent_yaml.AgentKindYamlContainerApp: // Set environment variables for foundry agents if err := a.setEnvVar(ctx, envName, "ENABLE_CONTAINER_AGENTS", "true"); err != nil { return err } } - // Model information should be set regardless of agent kind - if err := a.setEnvVar(ctx, envName, "AZURE_AI_FOUNDRY_MODEL_NAME", agentManifest.Agent.Model.Id); err != nil { - return err - } - - fmt.Printf("Successfully updated environment variables for agent kind: %s\n", agentManifest.Agent.Kind) + fmt.Printf("Successfully updated environment variables for agent kind: %s\n", agentDef.Kind) return nil } diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/map.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/map.go index 1936962f41c..2d44222b6b6 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/map.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/map.go @@ -8,6 +8,8 @@ import ( "strings" "azureaiagent/internal/pkg/agents/agent_api" + + "go.yaml.in/yaml/v3" ) /* @@ -92,74 +94,86 @@ func WithEnvironmentVariables(envVars map[string]string) AgentBuildOption { } } -// BuildAgentDefinitionFromManifest constructs an AgentDefinition from the given AgentManifest -// with optional build-time configuration. It returns both the agent definition and build config. -func BuildAgentDefinitionFromManifest(agentManifest AgentManifest, options ...AgentBuildOption) (AgentDefinition, *AgentBuildConfig, error) { - // Apply options +func constructBuildConfig(options ...AgentBuildOption) *AgentBuildConfig { config := &AgentBuildConfig{} for _, option := range options { option(config) } - - // Return the agent definition and build config separately - // The build config will be used later when creating the API request - return agentManifest.Agent, config, nil + return config } // CreateAgentAPIRequestFromManifest creates a CreateAgentRequest from AgentManifest with strong typing func CreateAgentAPIRequestFromManifest(agentManifest AgentManifest, options ...AgentBuildOption) (*agent_api.CreateAgentRequest, error) { - agentDef, buildConfig, err := BuildAgentDefinitionFromManifest(agentManifest, options...) - if err != nil { - return nil, err + buildConfig := constructBuildConfig(options...) + + templateBytes, _ := yaml.Marshal(agentManifest.Template) + + var agentDef AgentDefinition + if err := yaml.Unmarshal(templateBytes, &agentDef); err != nil { + return nil, fmt.Errorf("failed to parse template to determine agent kind while creating api request") } // Route to appropriate handler based on agent kind switch agentDef.Kind { case AgentKindPrompt: - return CreatePromptAgentAPIRequest(agentDef, buildConfig) + promptDef := agentManifest.Template.(PromptAgent) + return CreatePromptAgentAPIRequest(promptDef, buildConfig) case AgentKindHosted: - return CreateHostedAgentAPIRequest(agentDef, buildConfig) + hostedDef := agentManifest.Template.(HostedContainerAgent) + return CreateHostedAgentAPIRequest(hostedDef, buildConfig) default: return nil, fmt.Errorf("unsupported agent kind: %s. Supported kinds are: prompt, hosted", agentDef.Kind) } } // CreatePromptAgentAPIRequest creates a CreateAgentRequest for prompt-based agents -func CreatePromptAgentAPIRequest(agentDefinition AgentDefinition, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { - // TODO QUESTION: Should I expect a PromptAgent type instead of AgentDefinition? - // The AgentDefinition has all the fields but PromptAgent might have additional prompt-specific fields - +func CreatePromptAgentAPIRequest(promptAgent PromptAgent, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { + // Extract model information from the prompt agent + var modelId string + var instructions *string + var temperature *float32 + var topP *float32 + + // Get model ID + if promptAgent.Model.Id != "" { + modelId = promptAgent.Model.Id + } else { + return nil, fmt.Errorf("model.id is required for prompt agents") + } + + // Get instructions + if promptAgent.Instructions != nil { + instructions = promptAgent.Instructions + } + + // Extract temperature and topP from model options if available + if promptAgent.Model.Options != nil { + if promptAgent.Model.Options.Temperature != nil { + tempFloat32 := float32(*promptAgent.Model.Options.Temperature) + temperature = &tempFloat32 + } + if promptAgent.Model.Options.TopP != nil { + tpFloat32 := float32(*promptAgent.Model.Options.TopP) + topP = &tpFloat32 + } + } + promptDef := agent_api.PromptAgentDefinition{ AgentDefinition: agent_api.AgentDefinition{ - Kind: agent_api.AgentKindPrompt, // This sets Kind to "prompt" - }, - Model: agentDefinition.Model.Id, // TODO QUESTION: Is Model.Id the right field to use? - Instructions: &agentDefinition.Instructions, - - // TODO QUESTION: How should I map Model.Options to these fields? - // The agent_yaml.Model has ModelOptions with a Kind field, but how do I get: - // - Temperature (float32) - from Model.Options or somewhere else? - // - TopP (float32) - from Model.Options or somewhere else? - // - // Example: if agentDefinition.Model.Options has structured data: - // Temperature: extractFloat32FromOptions(agentDefinition.Model.Options, "temperature"), - // TopP: extractFloat32FromOptions(agentDefinition.Model.Options, "top_p"), - - // TODO QUESTION: How should I map Tools from agent_yaml to agent_api? - // agent_yaml.Tool vs agent_api.Tool - are they compatible or do I need conversion? - // Tools: convertYamlToolsToApiTools(agentDefinition.Tools), - - // TODO QUESTION: What about these advanced fields? - // - Reasoning (*agent_api.Reasoning) - where does this come from in YAML? - // - Text (*agent_api.ResponseTextFormatConfiguration) - related to output format? - // - StructuredInputs (map[string]agent_api.StructuredInputDefinition) - from InputSchema? - // - // Possible mappings: - // Text: mapOutputSchemaToTextFormat(agentDefinition.OutputSchema), - // StructuredInputs: mapInputSchemaToStructuredInputs(agentDefinition.InputSchema), + Kind: agent_api.AgentKindPrompt, + }, + Model: modelId, + Instructions: instructions, + Temperature: temperature, + TopP: topP, + + // TODO: Handle additional fields like Tools, Reasoning, etc. + // Tools: convertYamlToolsToApiTools(promptAgent.Tools), + // Text: mapOutputSchemaToTextFormat(promptAgent.OutputSchema), + // StructuredInputs: mapInputSchemaToStructuredInputs(promptAgent.InputSchema), } - return createAgentAPIRequest(agentDefinition, promptDef) + return createAgentAPIRequest(promptAgent.AgentDefinition, promptDef) } // Helper functions for type conversion (TODO: Implement based on answers to questions above) @@ -179,25 +193,22 @@ func convertYamlToolsToApiTools(yamlTools []Tool) []agent_api.Tool { return nil // Placeholder } -// mapInputSchemaToStructuredInputs converts InputSchema to StructuredInputs -func mapInputSchemaToStructuredInputs(inputSchema InputSchema) map[string]agent_api.StructuredInputDefinition { - // TODO QUESTION: How does InputSchema map to StructuredInputDefinition? - // InputSchema might have parameters that become structured inputs +// mapInputSchemaToStructuredInputs converts PropertySchema to StructuredInputs +func mapInputSchemaToStructuredInputs(inputSchema *PropertySchema) map[string]agent_api.StructuredInputDefinition { + // TODO QUESTION: How does PropertySchema map to StructuredInputDefinition? + // PropertySchema might have parameters that become structured inputs return nil // Placeholder } -// mapOutputSchemaToTextFormat converts OutputSchema to text response format -func mapOutputSchemaToTextFormat(outputSchema OutputSchema) *agent_api.ResponseTextFormatConfiguration { - // TODO QUESTION: How does OutputSchema influence text formatting? - // OutputSchema might specify response structure that affects text config +// mapOutputSchemaToTextFormat converts PropertySchema to text response format +func mapOutputSchemaToTextFormat(outputSchema *PropertySchema) *agent_api.ResponseTextFormatConfiguration { + // TODO QUESTION: How does PropertySchema influence text formatting? + // PropertySchema might specify response structure that affects text config return nil // Placeholder } // CreateHostedAgentAPIRequest creates a CreateAgentRequest for hosted agents -func CreateHostedAgentAPIRequest(agentDefinition AgentDefinition, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { - // TODO QUESTION: Should I expect a ContainerAgent type instead of AgentDefinition? - // ContainerAgent has additional fields like Protocol and Options that might be relevant - +func CreateHostedAgentAPIRequest(hostedAgent HostedContainerAgent, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { // Check if we have an image URL set via the build config imageURL := "" cpu := "1" // Default CPU @@ -218,36 +229,49 @@ func CreateHostedAgentAPIRequest(agentDefinition AgentDefinition, buildConfig *A envVars = buildConfig.EnvironmentVariables } } - + + // Try to get image URL from the hosted agent container definition if not provided in build config + if imageURL == "" && hostedAgent.Container.Image != nil && *hostedAgent.Container.Image != "" { + imageURL = *hostedAgent.Container.Image + } + if imageURL == "" { - return nil, fmt.Errorf("image URL is required for hosted agents - use WithImageURL build option") + return nil, fmt.Errorf("image URL is required for hosted agents - use WithImageURL build option or specify in container.image") } - // TODO QUESTION: Should protocol versions come from YAML definition or be configurable via build options? - // ContainerAgent.Protocol might specify this, or should it be in build config? - - // Set default protocol versions - protocolVersions := []agent_api.ProtocolVersionRecord{ - {Protocol: agent_api.AgentProtocolResponses, Version: "v1"}, + // Map protocol versions from the hosted agent definition + protocolVersions := make([]agent_api.ProtocolVersionRecord, 0) + if len(hostedAgent.Protocols) > 0 { + for _, protocol := range hostedAgent.Protocols { + protocolVersions = append(protocolVersions, agent_api.ProtocolVersionRecord{ + Protocol: agent_api.AgentProtocol(protocol.Protocol), + Version: protocol.Version, + }) + } + } else { + // Set default protocol versions if none specified + protocolVersions = []agent_api.ProtocolVersionRecord{ + {Protocol: agent_api.AgentProtocolResponses, Version: "v1"}, + } } hostedDef := agent_api.HostedAgentDefinition{ AgentDefinition: agent_api.AgentDefinition{ - Kind: agent_api.AgentKindHosted, // This sets Kind to "hosted" - }, + Kind: agent_api.AgentKindHosted, + }, ContainerProtocolVersions: protocolVersions, CPU: cpu, Memory: memory, EnvironmentVariables: envVars, } - - // Set the image from build configuration + + // Set the image from build configuration or container definition imageHostedDef := agent_api.ImageBasedHostedAgentDefinition{ HostedAgentDefinition: hostedDef, Image: imageURL, } - return createAgentAPIRequest(agentDefinition, imageHostedDef) + return createAgentAPIRequest(hostedAgent.AgentDefinition, imageHostedDef) } // createAgentAPIRequest is a helper function to create the final request with common fields @@ -256,7 +280,7 @@ func createAgentAPIRequest(agentDefinition AgentDefinition, agentDef interface{} metadata := make(map[string]string) if agentDefinition.Metadata != nil { // Handle authors specially - convert slice to comma-separated string - if authors, exists := agentDefinition.Metadata["authors"]; exists { + if authors, exists := (*agentDefinition.Metadata)["authors"]; exists { if authorsSlice, ok := authors.([]interface{}); ok { var authorsStr []string for _, author := range authorsSlice { @@ -268,7 +292,7 @@ func createAgentAPIRequest(agentDefinition AgentDefinition, agentDef interface{} } } // Copy other metadata as strings - for key, value := range agentDefinition.Metadata { + for key, value := range *agentDefinition.Metadata { if key != "authors" { if strValue, ok := value.(string); ok { metadata[key] = strValue @@ -291,8 +315,8 @@ func createAgentAPIRequest(agentDefinition AgentDefinition, agentDef interface{} }, } - if agentDefinition.Description != "" { - request.Description = &agentDefinition.Description + if agentDefinition.Description != nil && *agentDefinition.Description != "" { + request.Description = agentDefinition.Description } if len(metadata) > 0 { @@ -301,16 +325,3 @@ func createAgentAPIRequest(agentDefinition AgentDefinition, agentDef interface{} return request, nil } - -// Legacy function for backward compatibility - delegates to the new structured approach -func CreateAgentAPIRequestFromAgentDefinition(agentDefinition AgentDefinition, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { - // Route to appropriate handler based on agent kind - switch agentDefinition.Kind { - case AgentKindPrompt: - return CreatePromptAgentAPIRequest(agentDefinition, buildConfig) - case AgentKindHosted: - return CreateHostedAgentAPIRequest(agentDefinition, buildConfig) - default: - return nil, fmt.Errorf("unsupported agent kind: %s. Supported kinds are: prompt, hosted", agentDefinition.Kind) - } -} \ No newline at end of file diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse.go index 7026fedc673..ae5de1915e5 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse.go @@ -11,11 +11,17 @@ import ( // LoadAndValidateAgentManifest parses YAML content and validates it as an AgentManifest // Returns the parsed manifest and any validation errors -func LoadAndValidateAgentManifest(yamlContent []byte) (*AgentManifest, error) { +func LoadAndValidateAgentManifest(manifestYamlContent []byte) (*AgentManifest, error) { + agentDef, err := ExtractAgentDefinition(manifestYamlContent) + if err != nil { + return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: %w", err) + } + var manifest AgentManifest - if err := yaml.Unmarshal(yamlContent, &manifest); err != nil { + if err := yaml.Unmarshal(manifestYamlContent, &manifest); err != nil { return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: %w", err) } + manifest.Template = agentDef if err := ValidateAgentManifest(&manifest); err != nil { return nil, err @@ -24,27 +30,122 @@ func LoadAndValidateAgentManifest(yamlContent []byte) (*AgentManifest, error) { return &manifest, nil } +// Returns a specific agent definition based on the "kind" field in the template +func ExtractAgentDefinition(manifestYamlContent []byte) (any, error) { + var genericManifest map[string]interface{} + if err := yaml.Unmarshal(manifestYamlContent, &genericManifest); err != nil { + return nil, fmt.Errorf("YAML content is not valid: %w", err) + } + + template := genericManifest["template"].(map[string]interface{}) + templateBytes, _ := yaml.Marshal(template) + + var agentDef AgentDefinition + if err := yaml.Unmarshal(templateBytes, &agentDef); err != nil { + return nil, fmt.Errorf("failed to unmarshal to AgentDefinition: %v\n", err) + } + + switch agentDef.Kind { + case AgentKindPrompt: + var agent PromptAgent + if err := yaml.Unmarshal(templateBytes, &agent); err != nil { + return nil, fmt.Errorf("failed to unmarshal to PromptAgent: %v\n", err) + } + + agent.AgentDefinition = agentDef + return agent, nil + case AgentKindHosted: + var agent HostedContainerAgent + if err := yaml.Unmarshal(templateBytes, &agent); err != nil { + return nil, fmt.Errorf("failed to unmarshal to HostedContainerAgent: %v\n", err) + } + + agent.AgentDefinition = agentDef + return agent, nil + case AgentKindContainerApp, AgentKindYamlContainerApp: + var agent ContainerAgent + if err := yaml.Unmarshal(templateBytes, &agent); err != nil { + return nil, fmt.Errorf("failed to unmarshal to ContainerAgent: %v\n", err) + } + + agent.AgentDefinition = agentDef + return agent, nil + } + + return nil, fmt.Errorf("unrecognized agent kind: %s", agentDef.Kind) +} + // ValidateAgentManifest performs basic validation of an AgentManifest // Returns an error if the manifest is invalid, nil if valid func ValidateAgentManifest(manifest *AgentManifest) error { var errors []string - // Validate Agent Definition - only the essential fields - if manifest.Agent.Name == "" { - errors = append(errors, "agent.name is required") - } - if manifest.Agent.Kind == "" { - errors = append(errors, "agent.kind is required") - } else if !IsValidAgentKind(manifest.Agent.Kind) { - validKinds := ValidAgentKinds() - validKindStrings := make([]string, len(validKinds)) - for i, kind := range validKinds { - validKindStrings[i] = string(kind) + // First, extract the kind from the template to determine the agent type + templateBytes, _ := yaml.Marshal(manifest.Template) + + var agentDef AgentDefinition + if err := yaml.Unmarshal(templateBytes, &agentDef); err != nil { + errors = append(errors, "failed to parse template to determine agent kind") + } else { + // Validate the kind is supported + if !IsValidAgentKind(agentDef.Kind) { + validKinds := ValidAgentKinds() + validKindStrings := make([]string, len(validKinds)) + for i, validKind := range validKinds { + validKindStrings[i] = string(validKind) + } + errors = append(errors, fmt.Sprintf("template.kind must be one of: %v, got '%s'", validKindStrings, agentDef.Kind)) + } else { + switch AgentKind(agentDef.Kind) { + case AgentKindPrompt: + var agent PromptAgent + if err := yaml.Unmarshal(templateBytes, &agent); err == nil { + if agent.Name == "" { + errors = append(errors, "template.name is required") + } + if agent.Model.Id == "" { + errors = append(errors, "template.model.id is required") + } + } else { + errors = append(errors, fmt.Sprintf("Failed to unmarshal to PromptAgent: %v\n", err)) + } + case AgentKindHosted: + var agent HostedContainerAgent + if err := yaml.Unmarshal(templateBytes, &agent); err == nil { + if agent.Name == "" { + errors = append(errors, "template.name is required") + } + // TODO: Do we need this? + // if len(agent.Models) == 0 { + // errors = append(errors, "template.models is required and must not be empty") + // } + } else { + errors = append(errors, fmt.Sprintf("Failed to unmarshal to HostedContainerAgent: %v\n", err)) + } + case AgentKindContainerApp, AgentKindYamlContainerApp: + var agent ContainerAgent + if err := yaml.Unmarshal(templateBytes, &agent); err == nil { + if agent.Name == "" { + errors = append(errors, "template.name is required") + } + if len(agent.Models) == 0 { + errors = append(errors, "template.models is required and must not be empty") + } + } else { + errors = append(errors, fmt.Sprintf("Failed to unmarshal to ContainerAgent: %v\n", err)) + } + case AgentKindWorkflow: + var agent WorkflowAgent + if err := yaml.Unmarshal(templateBytes, &agent); err == nil { + if agent.Name == "" { + errors = append(errors, "template.name is required") + } + // WorkflowAgent doesn't have models, so no model validation needed + } else { + errors = append(errors, fmt.Sprintf("Failed to unmarshal to WorkflowAgent: %v\n", err)) + } + } } - errors = append(errors, fmt.Sprintf("agent.kind must be one of: %v, got '%s'", validKindStrings, manifest.Agent.Kind)) - } - if manifest.Agent.Model.Id == "" { - errors = append(errors, "agent.model.id is required") } if len(errors) > 0 { diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go.old similarity index 100% rename from cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go rename to cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go.old diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/sample_integration_test.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/sample_integration_test.go.old similarity index 100% rename from cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/sample_integration_test.go rename to cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/sample_integration_test.go.old diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml.go index 4c05a608809..d7f66003f82 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml.go @@ -35,356 +35,358 @@ func ValidAgentKinds() []AgentKind { } } -// AgentDefinition represents The following is a specification for defining AI agents with structured metadata, inputs, outputs, tools, and templates. +// AgentDefinition is a specification for defining AI agents with structured metadata, inputs, outputs, tools, and templates. // It provides a way to create reusable and composable AI agents that can be executed with specific configurations. // The specification includes metadata about the agent, model configuration, input parameters, expected outputs, // available tools, and template configurations for prompt rendering. type AgentDefinition struct { - Kind AgentKind `json:"kind"` // Kind represented by the document - Name string `json:"name"` // Human-readable name of the agent - Description string `json:"description,omitempty"` // Description of the agent's capabilities and purpose - Instructions string `json:"instructions,omitempty"` // Give your agent clear directions on what to do and how to do it - Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata including authors, tags, and other arbitrary properties - Model Model `json:"model"` // Primary AI model configuration for the agent - InputSchema InputSchema `json:"inputSchema,omitempty"` // Input parameters that participate in template rendering - OutputSchema OutputSchema `json:"outputSchema,omitempty"` // Expected output format and structure from the agent - Tools []Tool `json:"tools,omitempty"` // Tools available to the agent for extended functionality -} - -// PromptAgent represents Prompt based agent definition. Used to create agents that can be executed directly. + Kind AgentKind `json:"kind" yaml:"kind"` + Name string `json:"name" yaml:"name"` + DisplayName *string `json:"displayName,omitempty" yaml:"displayName,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Metadata *map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` + InputSchema *PropertySchema `json:"inputSchema,omitempty" yaml:"inputSchema,omitempty"` + OutputSchema *PropertySchema `json:"outputSchema,omitempty" yaml:"outputSchema,omitempty"` + Tools *[]Tool `json:"tools,omitempty" yaml:"tools,omitempty"` +} + +// PromptAgent is a prompt based agent definition used to create agents that can be executed directly. // These agents can leverage tools, input parameters, and templates to generate responses. // They are designed to be straightforward and easy to use for various applications. type PromptAgent struct { - AgentDefinition - Kind AgentKind `json:"kind"` // Type of agent, e.g., 'prompt' - Template Template `json:"template,omitempty"` // Template configuration for prompt rendering - Instructions string `json:"instructions,omitempty"` // Give your agent clear directions on what to do and how to do it. Include specific tasks, their order, and any special instructions like tone or engagement style. (can use this for a pure yaml declaration or as content in the markdown format) - AdditionalInstructions string `json:"additionalInstructions,omitempty"` // Additional instructions or context for the agent, can be used to provide extra guidance (can use this for a pure yaml declaration) + AgentDefinition `json:",inline" yaml:",inline"` + Model Model `json:"model" yaml:"model"` + Template *Template `json:"template,omitempty" yaml:"template,omitempty"` + Instructions *string `json:"instructions,omitempty" yaml:"instructions,omitempty"` + AdditionalInstructions *string `json:"additionalInstructions,omitempty" yaml:"additionalInstructions,omitempty"` +} + +// HostedContainerAgent represents a container based agent hosted by the provider/publisher. +// The intent is to represent a container application that the user wants to run +// in a hosted environment that the provider manages. +type HostedContainerAgent struct { + AgentDefinition `json:",inline" yaml:",inline"` + Protocols []ProtocolVersionRecord `json:"protocols" yaml:"protocols"` + Models []Model `json:"models" yaml:"models"` + Container HostedContainerDefinition `json:"container" yaml:"container"` } -// ContainerAgent represents The following represents a containerized agent that can be deployed and hosted. +// ContainerAgent represents a containerized agent that can be deployed and hosted. // It includes details about the container image, registry information, and environment variables. // This model allows for the definition of agents that can run in isolated environments, // making them suitable for deployment in various cloud or on-premises scenarios. -// // The containerized agent can communicate using specified protocols and can be scaled -// based on the provided configuration. -// -// This kind of agent represents the users intent to bring their own container specific -// app hosting platform that they manage. +// based on the provided configuration. This kind of agent represents the users intent +// to bring their own container specific app hosting platform that they manage. type ContainerAgent struct { - AgentDefinition - Kind AgentKind `json:"kind"` // Type of agent, e.g., 'container' - Protocol string `json:"protocol"` // Protocol used by the containerized agent - Options map[string]interface{} `json:"options,omitempty"` // Container definition including image, registry, and scaling information -} - -// AgentManifest represents The following represents a manifest that can be used to create agents dynamically. -// It includes a list of models that the publisher of the manifest has tested and -// has confidence will work with an instantiated prompt agent. -// The manifest also includes parameters that can be used to configure the agent's behavior. + AgentDefinition `json:",inline" yaml:",inline"` + Protocols []ProtocolVersionRecord `json:"protocols" yaml:"protocols"` + Models []Model `json:"models" yaml:"models"` + Resource string `json:"resource" yaml:"resource"` + IngressSuffix string `json:"ingressSuffix" yaml:"ingressSuffix"` + Options *map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` +} + +// WorkflowAgent is a workflow agent that can orchestrate multiple steps and actions. +// This agent type is designed to handle complex workflows that may involve +// multiple tools, models, and decision points. The workflow agent can be configured +// with a series of steps that define the flow of execution, including conditional +// logic and parallel processing. This allows for the creation of sophisticated +// AI-driven processes that can adapt to various scenarios and requirements. +// Note: The detailed structure of the workflow steps and actions is not defined here +// and would need to be implemented based on specific use cases and requirements. +type WorkflowAgent struct { + AgentDefinition `json:",inline" yaml:",inline"` + Trigger *map[string]interface{} `json:"trigger,omitempty" yaml:"trigger,omitempty"` +} + +// AgentManifest represents a manifest that can be used to create agents dynamically. +// It includes parameters that can be used to configure the agent's behavior. // These parameters include values that can be used as publisher parameters that can // be used to describe additional variables that have been tested and are known to work. -// // Variables described here are then used to project into a prompt agent that can be executed. // Once parameters are provided, these can be referenced in the manifest using the following notation: -// -// `${param:MyParameter}` -// -// This allows for dynamic configuration of the agent based on the provided parameters. -// (This notation is used elsewhere, but only the `param` scope is supported here) +// `{{myParameter}}` This allows for dynamic configuration of the agent based on the provided parameters. type AgentManifest struct { - Agent AgentDefinition `json:"agent"` // The agent that this manifest is based on - // Models []Model `json:"models"` // Additional models that are known to work with this prompt - Parameters []Parameter `json:"parameters"` // Parameters for configuring the agent's behavior and execution + Name string `json:"name" yaml:"name"` + DisplayName string `json:"displayName" yaml:"displayName"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Metadata *map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Template any `json:"template" yaml:"template"` // can be PromptAgent, HostedContainerAgent, ContainerAgent, or WorkflowAgent + Parameters *map[string]Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` } -// Binding represents Represents a binding between an input property and a tool parameter. +// Binding represents a binding between an input property and a tool parameter. type Binding struct { - Name string `json:"name"` // Name of the binding - Input string `json:"input"` // The input property that will be bound to the tool parameter argument + Name string `json:"name" yaml:"name"` + Input string `json:"input" yaml:"input"` } -// BingSearchConfiguration represents Configuration options for the Bing search tool. -type BingSearchConfiguration struct { - Name string `json:"name"` // The name of the Bing search tool instance, used to identify the specific instance in the system - Market string `json:"market,omitempty"` // The market where the results come from. - SetLang string `json:"setLang,omitempty"` // The language to use for user interface strings when calling Bing API. - Count int64 `json:"count,omitempty"` // The number of search results to return in the bing api response - Freshness string `json:"freshness,omitempty"` // Filter search results by a specific time range. Accepted values: https://learn.microsoft.com/bing/search-apis/bing-web-search/reference/query-parameters +// BingSearchOption provides configuration options for the Bing search tool. +type BingSearchOption struct { + Name string `json:"name" yaml:"name"` + Market *string `json:"market,omitempty" yaml:"market,omitempty"` + SetLang *string `json:"setLang,omitempty" yaml:"setLang,omitempty"` + Count *int `json:"count,omitempty" yaml:"count,omitempty"` + Freshness *string `json:"freshness,omitempty" yaml:"freshness,omitempty"` } -// Connection represents Connection configuration for AI agents. +// Connection configuration for AI agents. // `provider`, `kind`, and `endpoint` are required properties here, // but this section can accept additional via options. type Connection struct { - Kind string `json:"kind"` // The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - Authority string `json:"authority"` // The authority level for the connection, indicating under whose authority the connection is made (e.g., 'user', 'agent', 'system') - UsageDescription string `json:"usageDescription,omitempty"` // The usage description for the connection, providing context on how this connection will be used + Kind string `json:"kind" yaml:"kind"` + Authority string `json:"authority" yaml:"authority"` + UsageDescription *string `json:"usageDescription,omitempty" yaml:"usageDescription,omitempty"` } -// GenericConnection represents Generic connection configuration for AI services. -type GenericConnection struct { - Connection - Kind string `json:"kind"` // The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - Options map[string]interface{} `json:"options,omitempty"` // Additional options for the connection +// ReferenceConnection provides connection configuration for AI services using named connections. +type ReferenceConnection struct { + Connection `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Name string `json:"name" yaml:"name"` } -// ReferenceConnection represents Connection configuration for AI services using named connections. -type ReferenceConnection struct { - Connection - Kind string `json:"kind"` // The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - Name string `json:"name"` // The name of the connection +// TokenCredentialConnection provides connection configuration for AI services using token credentials. +type TokenCredentialConnection struct { + Connection `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Endpoint string `json:"endpoint" yaml:"endpoint"` } -// KeyConnection represents Connection configuration for AI services using API keys. -type KeyConnection struct { - Connection - Kind string `json:"kind"` // The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - Endpoint string `json:"endpoint"` // The endpoint URL for the AI service - Key string `json:"key"` // The API key for authenticating with the AI service +// ApiKeyConnection provides connection configuration for AI services using API keys. +type ApiKeyConnection struct { + Connection `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Endpoint string `json:"endpoint" yaml:"endpoint"` + ApiKey string `json:"apiKey" yaml:"apiKey"` } -// OAuthConnection represents Connection configuration for AI services using OAuth authentication. -type OAuthConnection struct { - Connection - Kind string `json:"kind"` // The Authentication kind for the AI service (e.g., 'key' for API key, 'oauth' for OAuth tokens) - Endpoint string `json:"endpoint"` // The endpoint URL for the AI service - ClientId string `json:"clientId"` // The OAuth client ID for authenticating with the AI service - ClientSecret string `json:"clientSecret"` // The OAuth client secret for authenticating with the AI service - TokenUrl string `json:"tokenUrl"` // The OAuth token URL for obtaining access tokens - Scopes []interface{} `json:"scopes"` // The scopes required for the OAuth token +// EnvironmentVariable represents an environment variable configuration. +type EnvironmentVariable struct { + Name string `json:"name" yaml:"name"` + Value string `json:"value" yaml:"value"` } -// Format represents Template format definition +// Format represents the Format from _Format.py type Format struct { - Kind string `json:"kind"` // Template rendering engine used for slot filling prompts (e.g., mustache, jinja2) - Strict bool `json:"strict,omitempty"` // Whether the template can emit structural text for parsing output - Options map[string]interface{} `json:"options,omitempty"` // Options for the template engine + Kind string `json:"kind" yaml:"kind"` + Strict *bool `json:"strict,omitempty" yaml:"strict,omitempty"` + Options *map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` } -// HostedContainerDefinition represents Definition for a containerized AI agent hosted by the provider. -// This includes the container registry information and scaling configuration. +// HostedContainerDefinition represents the HostedContainerDefinition from _HostedContainerDefinition.py type HostedContainerDefinition struct { - Scale Scale `json:"scale"` // Instance scaling configuration - Context interface{} `json:"context"` // Container context for building the container image -} - -// Input represents Represents a single input property for a prompt. -// * This model defines the structure of input properties that can be used in prompts, -// including their type, description, whether they are required, and other attributes. -// * It allows for the definition of dynamic inputs that can be filled with data -// and processed to generate prompts for AI models. -type Input struct { - Name string `json:"name"` // Name of the input property - Kind string `json:"kind"` // The data type of the input property - Description string `json:"description,omitempty"` // A short description of the input property - Required bool `json:"required,omitempty"` // Whether the input property is required - Strict bool `json:"strict,omitempty"` // Whether the input property can emit structural text when parsing output - Default interface{} `json:"default,omitempty"` // The default value of the input - this represents the default value if none is provided - Sample interface{} `json:"sample,omitempty"` // A sample value of the input for examples and tooling -} - -// ArrayInput represents Represents an array output property. -// This extends the base Output model to represent an array of items. -type ArrayInput struct { - Input - Kind string `json:"kind"` - Items Input `json:"items"` // The type of items contained in the array -} - -// ObjectInput represents Represents an object output property. -// This extends the base Output model to represent a structured object. -type ObjectInput struct { - Input - Kind string `json:"kind"` - Properties []interface{} `json:"properties"` // The properties contained in the object -} - -// InputSchema represents Definition for the input schema of a prompt. -// This includes the properties and example records. -type InputSchema struct { - Examples []interface{} `json:"examples,omitempty"` // Example records for the input schema - Strict bool `json:"strict,omitempty"` // Whether the input schema is strict - if true, only the defined properties are allowed - Properties []Input `json:"properties"` // The input properties for the schema + Scale Scale `json:"scale" yaml:"scale"` + Image *string `json:"image,omitempty" yaml:"image,omitempty"` + Context map[string]interface{} `json:"context" yaml:"context"` + EnvironmentVariables *[]EnvironmentVariable `json:"environmentVariables,omitempty" yaml:"environmentVariables,omitempty"` } -// Model represents Model for defining the structure and behavior of AI agents. +// McpServerApprovalMode represents the McpServerApprovalMode from _McpServerApprovalMode.py +type McpServerApprovalMode struct { + Mode string `json:"mode" yaml:"mode"` + AlwaysRequireApprovalTools []string `json:"alwaysRequireApprovalTools" yaml:"alwaysRequireApprovalTools"` + NeverRequireApprovalTools []string `json:"neverRequireApprovalTools" yaml:"neverRequireApprovalTools"` +} + +// Model defines the structure and behavior of AI agents. // This model includes properties for specifying the model's provider, connection details, and various options. // It allows for flexible configuration of AI models to suit different use cases and requirements. type Model struct { - Id string `json:"id"` // The unique identifier of the model - can be used as the single property shorthand - Publisher string `json:"publisher,omitempty"` // The publisher of the model (e.g., 'openai', 'azure', 'anthropic') - Connection Connection `json:"connection,omitempty"` // The connection configuration for the model - Options ModelOptions `json:"options,omitempty"` // Additional options for the model + Id string `json:"id" yaml:"id"` + Provider *string `json:"provider,omitempty" yaml:"provider,omitempty"` + ApiType string `json:"apiType" yaml:"apiType"` + Deployment *string `json:"deployment,omitempty" yaml:"deployment,omitempty"` + Version *string `json:"version,omitempty" yaml:"version,omitempty"` + Connection *Connection `json:"connection,omitempty" yaml:"connection,omitempty"` + Options *ModelOptions `json:"options,omitempty" yaml:"options,omitempty"` } -// ModelOptions represents Options for configuring the behavior of the AI model. -// `kind` is a required property here, but this section can accept additional via options. +// ModelOptions represents the ModelOptions from _ModelOptions.py type ModelOptions struct { - Kind string `json:"kind"` + FrequencyPenalty *float64 `json:"frequencyPenalty,omitempty" yaml:"frequencyPenalty,omitempty"` + MaxOutputTokens *int `json:"maxOutputTokens,omitempty" yaml:"maxOutputTokens,omitempty"` + PresencePenalty *float64 `json:"presencePenalty,omitempty" yaml:"presencePenalty,omitempty"` + Seed *int `json:"seed,omitempty" yaml:"seed,omitempty"` + Temperature *float64 `json:"temperature,omitempty" yaml:"temperature,omitempty"` + TopK *int `json:"topK,omitempty" yaml:"topK,omitempty"` + TopP *float64 `json:"topP,omitempty" yaml:"topP,omitempty"` + StopSequences *[]string `json:"stopSequences,omitempty" yaml:"stopSequences,omitempty"` + AllowMultipleToolCalls *bool `json:"allowMultipleToolCalls,omitempty" yaml:"allowMultipleToolCalls,omitempty"` + AdditionalProperties *map[string]interface{} `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` +} + +// Parameter represents the Parameter from _Parameter.py +type Parameter struct { + Name string `json:"name" yaml:"name"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Required *bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema ParameterSchema `json:"schema" yaml:"schema"` } -// Output represents Represents the output properties of an AI agent. -// Each output property can be a simple kind, an array, or an object. -type Output struct { - Name string `json:"name"` // Name of the output property - Kind string `json:"kind"` // The data kind of the output property - Description string `json:"description,omitempty"` // A short description of the output property - Required bool `json:"required,omitempty"` // Whether the output property is required +// ParameterSchema represents the ParameterSchema from _ParameterSchema.py +type ParameterSchema struct { + Type string `json:"type" yaml:"type"` + Default *interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Enum *[]interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + Extensions *map[string]interface{} `json:"extensions,omitempty" yaml:"extensions,omitempty"` } -// ArrayOutput represents Represents an array output property. -// This extends the base Output model to represent an array of items. -type ArrayOutput struct { - Output - Kind string `json:"kind"` - Items Output `json:"items"` // The type of items contained in the array +// StringParameterSchema represents a string parameter schema. +type StringParameterSchema struct { + ParameterSchema `yaml:",inline"` // Embedded parent struct + Type string `json:"type" yaml:"type"` + MinLength *int `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *int `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + Pattern *string `json:"pattern,omitempty" yaml:"pattern,omitempty"` } -// ObjectOutput represents Represents an object output property. -// This extends the base Output model to represent a structured object. -type ObjectOutput struct { - Output - Kind string `json:"kind"` - Properties []interface{} `json:"properties"` // The properties contained in the object +// DigitParameterSchema represents a digit parameter schema. +type DigitParameterSchema struct { + ParameterSchema `yaml:",inline"` // Embedded parent struct + Type string `json:"type" yaml:"type"` + Minimum *int `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *int `json:"maximum,omitempty" yaml:"maximum,omitempty"` + ExclusiveMinimum *bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMaximum *bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` } -// OutputSchema represents Definition for the output schema of an AI agent. -// This includes the properties and example records. -type OutputSchema struct { - Examples []interface{} `json:"examples,omitempty"` // Example records for the output schema - Properties []Output `json:"properties"` // The output properties for the schema +// Parser represents the Parser from _Parser.py +type Parser struct { + Kind string `json:"kind" yaml:"kind"` + Options *map[string]interface{} `json:"options,omitempty" yaml:"options,omitempty"` } -// Parameter represents Represents a parameter for a tool. -type Parameter struct { - Name string `json:"name"` // Name of the parameter - Kind string `json:"kind"` // The data type of the parameter - Description string `json:"description,omitempty"` // A short description of the property - Required bool `json:"required,omitempty"` // Whether the tool parameter is required - Default interface{} `json:"default,omitempty"` // The default value of the parameter - this represents the default value if none is provided - Value interface{} `json:"value,omitempty"` // Parameter value used for initializing manifest examples and tooling - Enum []interface{} `json:"enum,omitempty"` // Allowed enumeration values for the parameter +// Property represents the Property from _Property.py +type Property struct { + Name string `json:"name" yaml:"name"` + Kind string `json:"kind" yaml:"kind"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Required *bool `json:"required,omitempty" yaml:"required,omitempty"` + Strict *bool `json:"strict,omitempty" yaml:"strict,omitempty"` + Default *interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Example *interface{} `json:"example,omitempty" yaml:"example,omitempty"` + EnumValues *[]interface{} `json:"enumValues,omitempty" yaml:"enumValues,omitempty"` } -// ObjectParameter represents Represents an object parameter for a tool. -type ObjectParameter struct { - Parameter - Kind string `json:"kind"` - Properties []Parameter `json:"properties"` // The properties of the object parameter +// ArrayProperty represents an array property. +// This extends the base Property model to represent an array of items. +type ArrayProperty struct { + Property `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Items Property `json:"items" yaml:"items"` } -// ArrayParameter represents Represents an array parameter for a tool. -type ArrayParameter struct { - Parameter - Kind string `json:"kind"` - Items interface{} `json:"items"` // The kind of items contained in the array +// ObjectProperty represents an object property. +// This extends the base Property model to represent a structured object. +type ObjectProperty struct { + Property `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Properties []Property `json:"properties" yaml:"properties"` } -// Parser represents Template parser definition -type Parser struct { - Kind string `json:"kind"` // Parser used to process the rendered template into API-compatible format - Options map[string]interface{} `json:"options,omitempty"` // Options for the parser +// PropertySchema defines the property schema of a model. +// This includes the properties and example records. +type PropertySchema struct { + Examples *[]map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` + Strict *bool `json:"strict,omitempty" yaml:"strict,omitempty"` + Properties []Property `json:"properties" yaml:"properties"` } -// Scale represents Configuration for scaling container instances. +// ProtocolVersionRecord represents the ProtocolVersionRecord from _ProtocolVersionRecord.py +type ProtocolVersionRecord struct { + Protocol string `json:"protocol" yaml:"protocol"` + Version string `json:"version" yaml:"version"` +} + +// Scale represents the Scale from _Scale.py type Scale struct { - MinReplicas int32 `json:"minReplicas,omitempty"` // Minimum number of container instances to run - MaxReplicas int32 `json:"maxReplicas,omitempty"` // Maximum number of container instances to run - Cpu float32 `json:"cpu"` // CPU allocation per instance (in cores) - Memory float32 `json:"memory"` // Memory allocation per instance (in GB) -} - -// Template represents Template model for defining prompt templates. -// -// This model specifies the rendering engine used for slot filling prompts, -// the parser used to process the rendered template into API-compatible format, -// and additional options for the template engine. -// -// It allows for the creation of reusable templates that can be filled with dynamic data -// and processed to generate prompts for AI models. + MinReplicas *int `json:"minReplicas,omitempty" yaml:"minReplicas,omitempty"` + MaxReplicas *int `json:"maxReplicas,omitempty" yaml:"maxReplicas,omitempty"` + Cpu float64 `json:"cpu" yaml:"cpu"` + Memory float64 `json:"memory" yaml:"memory"` +} + +// Template represents the Template from _Template.py type Template struct { - Format Format `json:"format"` // Template rendering engine used for slot filling prompts (e.g., mustache, jinja2) - Parser Parser `json:"parser"` // Parser used to process the rendered template into API-compatible format + Format Format `json:"format" yaml:"format"` + Parser Parser `json:"parser" yaml:"parser"` } -// Tool represents Represents a tool that can be used in prompts. +// Tool represents a tool that can be used in prompts. type Tool struct { - Name string `json:"name"` // Name of the tool. If a function tool, this is the function name, otherwise it is the type - Kind string `json:"kind"` // The kind identifier for the tool - Description string `json:"description,omitempty"` // A short description of the tool for metadata purposes - Bindings []Binding `json:"bindings,omitempty"` // Tool argument bindings to input properties + Name string `json:"name" yaml:"name"` + Kind string `json:"kind" yaml:"kind"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + Bindings *[]Binding `json:"bindings,omitempty" yaml:"bindings,omitempty"` } -// FunctionTool represents Represents a local function tool. +// FunctionTool represents a local function tool. type FunctionTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for function tools - Parameters []Parameter `json:"parameters"` // Parameters accepted by the function tool + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Parameters PropertySchema `json:"parameters" yaml:"parameters"` + Strict *bool `json:"strict,omitempty" yaml:"strict,omitempty"` } -// ServerTool represents Represents a generic server tool that runs on a server -// This tool kind is designed for operations that require server-side execution -// It may include features such as authentication, data storage, and long-running processes -// This tool kind is ideal for tasks that involve complex computations or access to secure resources -// Server tools can be used to offload heavy processing from client applications +// ServerTool represents a generic server tool that runs on a server. +// This tool kind is designed for operations that require server-side execution. +// It may include features such as authentication, data storage, and long-running processes. +// This tool kind is ideal for tasks that involve complex computations or access to secure resources. +// Server tools can be used to offload heavy processing from client applications. type ServerTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for server tools. This is a wildcard and can represent any server tool type not explicitly defined. - Connection interface{} `json:"connection"` // Connection configuration for the server tool - Options map[string]interface{} `json:"options"` // Configuration options for the server tool + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Connection Connection `json:"connection" yaml:"connection"` + Options map[string]interface{} `json:"options" yaml:"options"` } -// BingSearchTool represents The Bing search tool. +// BingSearchTool represents the Bing search tool. type BingSearchTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for Bing search tools - Connection interface{} `json:"connection"` // The connection configuration for the Bing search tool - Configurations []BingSearchConfiguration `json:"configurations"` // The configuration options for the Bing search tool + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Connection Connection `json:"connection" yaml:"connection"` + Options []BingSearchOption `json:"options" yaml:"options"` } -// FileSearchTool represents A tool for searching files. +// FileSearchTool is a tool for searching files. // This tool allows an AI agent to search for files based on a query. type FileSearchTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for file search tools - Connection interface{} `json:"connection"` // The connection configuration for the file search tool - MaxNumResults int32 `json:"maxNumResults,omitempty"` // The maximum number of search results to return. - Ranker string `json:"ranker"` // File search ranker. - ScoreThreshold float32 `json:"scoreThreshold"` // Ranker search threshold. - VectorStoreIds []interface{} `json:"vectorStoreIds"` // The IDs of the vector stores to search within. + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Connection Connection `json:"connection" yaml:"connection"` + VectorStoreIds []string `json:"vectorStoreIds" yaml:"vectorStoreIds"` + MaxNumResults *int `json:"maxNumResults,omitempty" yaml:"maxNumResults,omitempty"` + Ranker string `json:"ranker" yaml:"ranker"` + ScoreThreshold float64 `json:"scoreThreshold" yaml:"scoreThreshold"` + Filters *map[string]interface{} `json:"filters,omitempty" yaml:"filters,omitempty"` } -// McpTool represents The MCP Server tool. +// McpTool represents the MCP Server tool. type McpTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for MCP tools - Connection interface{} `json:"connection"` // The connection configuration for the MCP tool - Name string `json:"name"` // The name of the MCP tool - Url string `json:"url"` // The URL of the MCP server - Allowed []interface{} `json:"allowed"` // List of allowed operations or resources for the MCP tool -} - -// ModelTool represents The MCP Server tool. -type ModelTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for a model connection as a tool - Model interface{} `json:"model"` // The connection configuration for the model tool + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Connection Connection `json:"connection" yaml:"connection"` + Name string `json:"name" yaml:"name"` + Url string `json:"url" yaml:"url"` + ApprovalMode McpServerApprovalMode `json:"approvalMode" yaml:"approvalMode"` + AllowedTools []string `json:"allowedTools" yaml:"allowedTools"` } -// OpenApiTool represents +// OpenApiTool represents an OpenAPI tool. type OpenApiTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for OpenAPI tools - Connection interface{} `json:"connection"` // The connection configuration for the OpenAPI tool - Specification string `json:"specification"` // The URL or relative path to the OpenAPI specification document (JSON or YAML format) + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + Connection Connection `json:"connection" yaml:"connection"` + Specification string `json:"specification" yaml:"specification"` } -// CodeInterpreterTool represents A tool for interpreting and executing code. +// CodeInterpreterTool is a tool for interpreting and executing code. // This tool allows an AI agent to run code snippets and analyze data files. type CodeInterpreterTool struct { - Tool - Kind string `json:"kind"` // The kind identifier for code interpreter tools - FileIds []interface{} `json:"fileIds"` // The IDs of the files to be used by the code interpreter tool. + Tool `yaml:",inline"` // Embedded parent struct + Kind string `json:"kind" yaml:"kind"` + FileIds []string `json:"fileIds" yaml:"fileIds"` } diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml_test.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml_test.go index 31b19dc8fee..fba8c971244 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml_test.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/agent_yaml/yaml_test.go @@ -8,18 +8,18 @@ import ( "testing" ) -// TestArrayInput_BasicSerialization tests basic JSON serialization -func TestArrayInput_BasicSerialization(t *testing.T) { - // Test that we can create and marshal a ArrayInput - obj := &ArrayInput{} - +// TestArrayProperty_BasicSerialization tests basic JSON serialization +func TestArrayProperty_BasicSerialization(t *testing.T) { + // Test that we can create and marshal a ArrayProperty + obj := &ArrayProperty{} + data, err := json.Marshal(obj) if err != nil { - t.Fatalf("Failed to marshal ArrayInput: %v", err) + t.Fatalf("Failed to marshal ArrayProperty: %v", err) } - - var obj2 ArrayInput + + var obj2 ArrayProperty if err := json.Unmarshal(data, &obj2); err != nil { - t.Fatalf("Failed to unmarshal ArrayInput: %v", err) + t.Fatalf("Failed to unmarshal ArrayProperty: %v", err) } -} \ No newline at end of file +} diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/registry_api/helpers.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/registry_api/helpers.go index 2cb638b04c5..3070d591f76 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/registry_api/helpers.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/pkg/agents/registry_api/helpers.go @@ -38,19 +38,17 @@ func ProcessRegistryManifest(ctx context.Context, manifest *Manifest, azdClient // Create the AgentManifest with the converted AgentDefinition result := &agent_yaml.AgentManifest{ - Agent: *agentDef, - Parameters: parameters, + Name: manifest.Name, + DisplayName: manifest.DisplayName, + Description: &manifest.Description, + Template: *agentDef, + Parameters: parameters, } return result, nil } func ConvertAgentDefinition(template agent_api.PromptAgentDefinition) (*agent_yaml.AgentDefinition, error) { - // Convert the model string to Model struct - model := agent_yaml.Model{ - Id: template.Model, - } - // Convert tools from agent_api.Tool to agent_yaml.Tool var tools []agent_yaml.Tool for _, apiTool := range template.Tools { @@ -61,72 +59,66 @@ func ConvertAgentDefinition(template agent_api.PromptAgentDefinition) (*agent_ya tools = append(tools, yamlTool) } - // Get instructions, defaulting to empty string if nil - instructions := "" - if template.Instructions != nil { - instructions = *template.Instructions - } - // Create the AgentDefinition agentDef := &agent_yaml.AgentDefinition{ - Kind: agent_yaml.AgentKindPrompt, // Set to prompt kind - Name: "", // Will be set later from manifest or user input - Description: "", // Will be set later from manifest or user input - Instructions: instructions, - Model: model, - Tools: tools, + Kind: agent_yaml.AgentKindPrompt, // Set to prompt kind + Name: "", // Will be set later from manifest or user input + Description: nil, // Will be set later from manifest or user input + Tools: &tools, // Metadata: make(map[string]interface{}), // TODO, Where does this come from? } return agentDef, nil } -func ConvertParameters(parameters map[string]OpenApiParameter) ([]agent_yaml.Parameter, error) { +func ConvertParameters(parameters map[string]OpenApiParameter) (*map[string]agent_yaml.Parameter, error) { if len(parameters) == 0 { - return []agent_yaml.Parameter{}, nil + return nil, nil } - result := make([]agent_yaml.Parameter, 0, len(parameters)) + result := make(map[string]agent_yaml.Parameter, len(parameters)) for paramName, openApiParam := range parameters { // Create a basic Parameter from the OpenApiParameter param := agent_yaml.Parameter{ Name: paramName, - Description: openApiParam.Description, - Required: openApiParam.Required, + Description: &openApiParam.Description, + Required: &openApiParam.Required, } // Extract type/kind from schema if available if openApiParam.Schema != nil { - param.Kind = openApiParam.Schema.Type - param.Default = openApiParam.Schema.Default + param.Schema = agent_yaml.ParameterSchema{ + Type: openApiParam.Schema.Type, + Default: &openApiParam.Schema.Default, + } // Convert enum values if present if len(openApiParam.Schema.Enum) > 0 { - param.Enum = openApiParam.Schema.Enum + param.Schema.Enum = &openApiParam.Schema.Enum } } // Use example as default if no schema default is provided - if param.Default == nil && openApiParam.Example != nil { - param.Default = openApiParam.Example + if param.Schema.Default == nil && openApiParam.Example != nil { + param.Schema.Default = &openApiParam.Example } // Fallback to string type if no type specified - if param.Kind == "" { - param.Kind = "string" + if param.Schema.Type == "" { + param.Schema.Type = "string" } - result = append(result, param) + result[paramName] = param } - return result, nil + return &result, nil } // ProcessManifestParameters prompts the user for parameter values and injects them into the template func ProcessManifestParameters(ctx context.Context, manifest *agent_yaml.AgentManifest, azdClient *azdext.AzdClient) (*agent_yaml.AgentManifest, error) { // If no parameters are defined, return the manifest as-is - if len(manifest.Parameters) == 0 { + if manifest.Parameters == nil || len(*manifest.Parameters) == 0 { fmt.Println("The manifest does not contain parameters that need to be configured.") return manifest, nil } @@ -135,7 +127,7 @@ func ProcessManifestParameters(ctx context.Context, manifest *agent_yaml.AgentMa fmt.Println() // Collect parameter values from user - paramValues, err := promptForYamlParameterValues(ctx, manifest.Parameters, azdClient) + paramValues, err := promptForYamlParameterValues(ctx, *manifest.Parameters, azdClient) if err != nil { return nil, fmt.Errorf("failed to collect parameter values: %w", err) } @@ -150,23 +142,26 @@ func ProcessManifestParameters(ctx context.Context, manifest *agent_yaml.AgentMa } // promptForYamlParameterValues prompts the user for values for each YAML parameter -func promptForYamlParameterValues(ctx context.Context, parameters []agent_yaml.Parameter, azdClient *azdext.AzdClient) (ParameterValues, error) { +func promptForYamlParameterValues(ctx context.Context, parameters map[string]agent_yaml.Parameter, azdClient *azdext.AzdClient) (ParameterValues, error) { paramValues := make(ParameterValues) - for _, param := range parameters { - fmt.Printf("Parameter: %s\n", param.Name) - if param.Description != "" { - fmt.Printf(" Description: %s\n", param.Description) + for paramName, param := range parameters { + fmt.Printf("Parameter: %s\n", paramName) + if param.Description != nil && *param.Description != "" { + fmt.Printf(" Description: %s\n", *param.Description) } // Get default value - defaultValue := param.Default + var defaultValue interface{} + if param.Schema.Default != nil { + defaultValue = *param.Schema.Default + } // Get enum values if available var enumValues []string - if len(param.Enum) > 0 { - enumValues = make([]string, len(param.Enum)) - for i, val := range param.Enum { + if param.Schema.Enum != nil && len(*param.Schema.Enum) > 0 { + enumValues = make([]string, len(*param.Schema.Enum)) + for i, val := range *param.Schema.Enum { enumValues[i] = fmt.Sprintf("%v", val) } } @@ -186,19 +181,20 @@ func promptForYamlParameterValues(ctx context.Context, parameters []agent_yaml.P // Prompt for value var value interface{} var err error + isRequired := param.Required != nil && *param.Required if len(enumValues) > 0 { // Use selection for enum parameters - value, err = promptForEnumValue(ctx, param.Name, enumValues, defaultValue, azdClient) + value, err = promptForEnumValue(ctx, paramName, enumValues, defaultValue, azdClient) } else { // Use text input for other parameters - value, err = promptForTextValue(ctx, param.Name, defaultValue, param.Required, azdClient) + value, err = promptForTextValue(ctx, paramName, defaultValue, isRequired, azdClient) } if err != nil { - return nil, fmt.Errorf("failed to get value for parameter %s: %w", param.Name, err) + return nil, fmt.Errorf("failed to get value for parameter %s: %w", paramName, err) } - paramValues[param.Name] = value + paramValues[paramName] = value } return paramValues, nil @@ -219,12 +215,12 @@ func injectParameterValuesIntoManifest(manifest *agent_yaml.AgentManifest, param } // Convert back to AgentManifest - var processedManifest agent_yaml.AgentManifest - if err := json.Unmarshal(processedBytes, &processedManifest); err != nil { - return nil, fmt.Errorf("failed to unmarshal processed manifest: %w", err) + processedManifest, err := agent_yaml.LoadAndValidateAgentManifest(processedBytes) + if err != nil { + return nil, fmt.Errorf("failed to reload processed manifest: %w", err) } - return &processedManifest, nil + return processedManifest, nil } // promptForEnumValue prompts the user to select from enumerated values @@ -312,6 +308,9 @@ func injectParameterValues(template json.RawMessage, paramValues ParameterValues placeholder := fmt.Sprintf("{{%s}}", paramName) valueStr := fmt.Sprintf("%v", paramValue) templateStr = strings.ReplaceAll(templateStr, placeholder, valueStr) + + placeholder = fmt.Sprintf("{{ %s }}", paramName) + templateStr = strings.ReplaceAll(templateStr, placeholder, valueStr) } // Check for any remaining unreplaced placeholders diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/project/parser.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/project/parser.go index 7fd1366ff9a..cb449410ec7 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/project/parser.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/project/parser.go @@ -26,6 +26,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/graphsdk" + "github.com/braydonk/yaml" "github.com/google/uuid" ) @@ -58,12 +59,21 @@ func shouldRun(ctx context.Context, project *azdext.ProjectConfig) (bool, error) return false, fmt.Errorf("failed to read agent yaml file: %w", err) } - agent, err := agent_yaml.LoadAndValidateAgentManifest(content) + manifest, err := agent_yaml.LoadAndValidateAgentManifest(content) if err != nil { return false, fmt.Errorf("failed to validate agent yaml file: %w", err) } - return agent.Agent.Kind == agent_yaml.AgentKindYamlContainerApp, nil + agentDefBytes, err := yaml.Marshal(manifest.Template) + if err != nil { + return false, fmt.Errorf("failed to marshal agent definition when updating project: %w", err) + } + var agentDef agent_yaml.AgentDefinition + if err := yaml.Unmarshal(agentDefBytes, &agentDef); err != nil { + return false, fmt.Errorf("failed to unmarshal agent definition when updating project: %w", err) + } + + return agentDef.Kind == agent_yaml.AgentKindYamlContainerApp, nil } } } diff --git a/cli/azd/extensions/azure.foundry.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.foundry.ai.agents/internal/project/service_target_agent.go index 40cdc9b5ea4..030f5d7f287 100644 --- a/cli/azd/extensions/azure.foundry.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.foundry.ai.agents/internal/project/service_target_agent.go @@ -5,6 +5,7 @@ package project import ( "context" + "encoding/json" "errors" "fmt" "os" @@ -269,14 +270,37 @@ func (p *AgentServiceTargetProvider) Deploy( return nil, fmt.Errorf("failed to create Azure credential: %w", err) } - // Determine agent type and delegate to appropriate deployment method - switch agent_api.AgentKind(agentManifest.Agent.Kind) { - case agent_api.AgentKindPrompt: + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return nil, fmt.Errorf("failed to marshal agent template to JSON: %w", err) + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return nil, fmt.Errorf("failed to unmarshal agent template from JSON: %w", err) + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return nil, fmt.Errorf("failed to marshal templateDict to JSON: %w", err) + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) + } + + switch agentDef.Kind { + case agent_yaml.AgentKindPrompt: return p.deployPromptAgent(ctx, cred, agentManifest, azdEnv) - case agent_api.AgentKindHosted: + case agent_yaml.AgentKindHosted: return p.deployHostedAgent(ctx, cred, serviceContext, progress, agentManifest, azdEnv) default: - return nil, fmt.Errorf("unsupported agent kind: %s", agentManifest.Agent.Kind) + return nil, fmt.Errorf("unsupported agent kind: %s", agentDef.Kind) } } @@ -292,7 +316,31 @@ func (p *AgentServiceTargetProvider) isContainerAgent() bool { return false } - return agentManifest.Agent.Kind == agent_yaml.AgentKind(agent_api.AgentKindHosted) + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return false + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return false + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return false + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return false + } + + return agentDef.Kind == agent_yaml.AgentKindHosted } // deployPromptAgent handles deployment of prompt-based agents @@ -307,11 +355,35 @@ func (p *AgentServiceTargetProvider) deployPromptAgent( return nil, fmt.Errorf("AZURE_AI_PROJECT_ENDPOINT environment variable is required") } + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return nil, fmt.Errorf("failed to marshal agent template to JSON: %w", err) + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return nil, fmt.Errorf("failed to unmarshal agent template from JSON: %w", err) + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return nil, fmt.Errorf("failed to marshal templateDict to JSON: %w", err) + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) + } + fmt.Fprintf(os.Stderr, "Deploying Prompt Agent\n") fmt.Fprintf(os.Stderr, "======================\n") fmt.Fprintf(os.Stderr, "Loaded configuration from: %s\n", p.agentDefinitionPath) fmt.Fprintf(os.Stderr, "Using endpoint: %s\n", azdEnv["AZURE_AI_PROJECT_ENDPOINT"]) - fmt.Fprintf(os.Stderr, "Agent Name: %s\n", agentManifest.Agent.Name) + fmt.Fprintf(os.Stderr, "Agent Name: %s\n", agentDef.Name) // Create agent request (no image URL needed for prompt agents) request, err := agent_yaml.CreateAgentAPIRequestFromManifest(*agentManifest) @@ -379,9 +451,33 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( return nil, errors.New("published container artifact not found") } + // Convert the template to bytes + templateBytes, err := json.Marshal(agentManifest.Template) + if err != nil { + return nil, fmt.Errorf("failed to marshal agent template to JSON: %w", err) + } + + // Convert the bytes to a dictionary + var templateDict map[string]interface{} + if err := json.Unmarshal(templateBytes, &templateDict); err != nil { + return nil, fmt.Errorf("failed to unmarshal agent template from JSON: %w", err) + } + + // Convert the dictionary to bytes + dictJsonBytes, err := json.Marshal(templateDict) + if err != nil { + return nil, fmt.Errorf("failed to marshal templateDict to JSON: %w", err) + } + + // Convert the bytes to an Agent Definition + var agentDef agent_yaml.AgentDefinition + if err := json.Unmarshal(dictJsonBytes, &agentDef); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON to AgentDefinition: %w", err) + } + fmt.Fprintf(os.Stderr, "Loaded configuration from: %s\n", p.agentDefinitionPath) fmt.Fprintf(os.Stderr, "Using endpoint: %s\n", azdEnv["AZURE_AI_PROJECT_ENDPOINT"]) - fmt.Fprintf(os.Stderr, "Agent Name: %s\n", agentManifest.Agent.Name) + fmt.Fprintf(os.Stderr, "Agent Name: %s\n", agentDef.Name) // Step 2: Create agent request with image URL request, err := agent_yaml.CreateAgentAPIRequestFromManifest(*agentManifest, agent_yaml.WithImageURL(fullImageURL)) @@ -478,18 +574,20 @@ func (p *AgentServiceTargetProvider) startAgentContainer( maxReplicas := int32(1) // Check if the agent definition has scale configuration - if containerAgent, ok := interface{}(agentManifest.Agent).(agent_yaml.ContainerAgent); ok { + if containerAgent, ok := interface{}(agentManifest.Template).(agent_yaml.ContainerAgent); ok { // For ContainerAgent, check if Options contains scale information - if options, exists := containerAgent.Options["scale"]; exists { - if scaleMap, ok := options.(map[string]interface{}); ok { - if minReplicasFloat, exists := scaleMap["minReplicas"]; exists { - if minReplicasVal, ok := minReplicasFloat.(float64); ok { - minReplicas = int32(minReplicasVal) + if containerAgent.Options != nil { + if options, exists := (*containerAgent.Options)["scale"]; exists { + if scaleMap, ok := options.(map[string]interface{}); ok { + if minReplicasFloat, exists := scaleMap["minReplicas"]; exists { + if minReplicasVal, ok := minReplicasFloat.(float64); ok { + minReplicas = int32(minReplicasVal) + } } - } - if maxReplicasFloat, exists := scaleMap["maxReplicas"]; exists { - if maxReplicasVal, ok := maxReplicasFloat.(float64); ok { - maxReplicas = int32(maxReplicasVal) + if maxReplicasFloat, exists := scaleMap["maxReplicas"]; exists { + if maxReplicasVal, ok := maxReplicasFloat.(float64); ok { + maxReplicas = int32(maxReplicasVal) + } } } }