Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type InitAction struct {
models *modelSelector

deploymentDetails []project.Deployment
containerSettings *project.ContainerSettings
httpClient *http.Client
}

Expand Down Expand Up @@ -475,6 +476,23 @@ func (a *InitAction) Run(ctx context.Context) error {
return fmt.Errorf("configuring model choice: %w", err)
}

// For hosted agents, prompt for container resources before writing agent.yaml
// so the selected values are persisted into the definition file.
if hostedAgent, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok {
containerSettings, err := a.populateContainerSettings(ctx, hostedAgent.Resources)
if err != nil {
return fmt.Errorf("failed to populate container settings: %w", err)
}
a.containerSettings = containerSettings

// Update the agent definition with the selected resources
hostedAgent.Resources = &agent_yaml.ContainerResources{
Cpu: containerSettings.Resources.Cpu,
Memory: containerSettings.Resources.Memory,
}
agentManifest.Template = hostedAgent
}

// Write the final agent.yaml to disk (after deployment names have been injected)
if err := writeAgentDefinitionFile(targetDir, agentManifest); err != nil {
return fmt.Errorf("writing agent definition: %w", err)
Expand Down Expand Up @@ -1226,12 +1244,8 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
}
}

// Prompt user for container settings
containerSettings, err := a.populateContainerSettings(ctx)
if err != nil {
return fmt.Errorf("failed to populate container settings: %w", err)
}
agentConfig.Container = containerSettings
// Use container settings that were already populated before writing agent.yaml
agentConfig.Container = a.containerSettings
}

agentConfig.Deployments = a.deploymentDetails
Expand Down Expand Up @@ -1282,7 +1296,10 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
return nil
}

func (a *InitAction) populateContainerSettings(ctx context.Context) (*project.ContainerSettings, error) {
func (a *InitAction) populateContainerSettings(
ctx context.Context,
manifestResources *agent_yaml.ContainerResources,
) (*project.ContainerSettings, error) {
choices := make([]*azdext.SelectChoice, len(project.ResourceTiers))
for i, t := range project.ResourceTiers {
choices[i] = &azdext.SelectChoice{
Expand All @@ -1292,6 +1309,15 @@ func (a *InitAction) populateContainerSettings(ctx context.Context) (*project.Co
}

defaultIndex := int32(0)
if manifestResources != nil {
for i, t := range project.ResourceTiers {
if t.Cpu == manifestResources.Cpu && t.Memory == manifestResources.Memory {
defaultIndex = int32(i)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low] Resource tier pre-selection uses exact string equality

t.Cpu == manifestResources.Cpu && t.Memory == manifestResources.Memory requires exact format match. Manually edited manifests with variant representations (e.g., 0.250 vs 0.25, 512Mi vs 0.5Gi) won't pre-select the matching tier. Falls back gracefully to first tier — not a bug, just a robustness note.

break
}
}
}

resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "Select container resource allocation (CPU and Memory) for your agent. You can adjust these settings later in the azure.yaml file if needed.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (a *InitFromCodeAction) Run(ctx context.Context) error {
fmt.Printf(" %s %s\n", color.GreenString("+"), color.GreenString("%s/agent.yaml", srcDir))
}

fmt.Println("\nYou can customize environment variables, cpu, memory, and replica settings in the agent.yaml.")
fmt.Println("\nYou can customize environment variables and other settings in the agent.yaml.")
if projectID, _ := a.azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
EnvName: a.environment.Name,
Key: "AZURE_AI_PROJECT_ID",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package agent_yaml
import (
"strings"
"testing"

"go.yaml.in/yaml/v3"
)

// TestExtractAgentDefinition_WithTemplateField tests parsing YAML with a template field (manifest format)
Expand Down Expand Up @@ -40,6 +42,107 @@ template:
}
}

// TestExtractAgentDefinition_WithResources tests that resources (cpu/memory) are parsed and round-tripped
func TestExtractAgentDefinition_WithResources(t *testing.T) {
yamlContent := []byte(`
name: echo-agent
template:
kind: hosted
name: echo-agent
description: A simple echo agent
protocols:
- protocol: invocations
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
`)

agent, err := ExtractAgentDefinition(yamlContent)
if err != nil {
t.Fatalf("ExtractAgentDefinition failed: %v", err)
}

containerAgent, ok := agent.(ContainerAgent)
if !ok {
t.Fatalf("Expected ContainerAgent, got %T", agent)
}

if containerAgent.Resources == nil {
t.Fatal("Expected Resources to be set, got nil")
}
if containerAgent.Resources.Cpu != "0.25" {
t.Errorf("Expected cpu '0.25', got '%s'", containerAgent.Resources.Cpu)
}
if containerAgent.Resources.Memory != "0.5Gi" {
t.Errorf("Expected memory '0.5Gi', got '%s'", containerAgent.Resources.Memory)
}

// Verify YAML round-trip: marshal the agent and check resources are preserved
marshaled, err := yaml.Marshal(containerAgent)
if err != nil {
t.Fatalf("Failed to marshal ContainerAgent: %v", err)
}

marshaledStr := string(marshaled)
if !strings.Contains(marshaledStr, "cpu:") || !strings.Contains(marshaledStr, "memory:") {
t.Errorf("Marshaled YAML should contain cpu and memory under resources, got:\n%s", marshaledStr)
}

// Unmarshal back and verify
var roundTripped ContainerAgent
if err := yaml.Unmarshal(marshaled, &roundTripped); err != nil {
t.Fatalf("Failed to unmarshal ContainerAgent: %v", err)
}

if roundTripped.Resources == nil {
t.Fatal("Expected Resources after round-trip, got nil")
}
if roundTripped.Resources.Cpu != "0.25" {
t.Errorf("Round-trip cpu: expected '0.25', got '%s'", roundTripped.Resources.Cpu)
}
if roundTripped.Resources.Memory != "0.5Gi" {
t.Errorf("Round-trip memory: expected '0.5Gi', got '%s'", roundTripped.Resources.Memory)
}
}

// TestExtractAgentDefinition_WithoutResources tests that ContainerAgent without resources still parses
func TestExtractAgentDefinition_WithoutResources(t *testing.T) {
yamlContent := []byte(`
name: test-manifest
template:
kind: hosted
name: test-agent
protocols:
- protocol: responses
version: v1
`)

agent, err := ExtractAgentDefinition(yamlContent)
if err != nil {
t.Fatalf("ExtractAgentDefinition failed: %v", err)
}

containerAgent, ok := agent.(ContainerAgent)
if !ok {
t.Fatalf("Expected ContainerAgent, got %T", agent)
}

if containerAgent.Resources != nil {
t.Errorf("Expected Resources to be nil when not specified, got %+v", containerAgent.Resources)
}

// Verify marshaling omits resources when nil
marshaled, err := yaml.Marshal(containerAgent)
if err != nil {
t.Fatalf("Failed to marshal ContainerAgent: %v", err)
}

if strings.Contains(string(marshaled), "resources:") {
t.Errorf("Marshaled YAML should not contain resources when nil, got:\n%s", string(marshaled))
}
}

// TestExtractAgentDefinition_EmptyTemplateField tests that an empty or null template field returns an error
func TestExtractAgentDefinition_EmptyTemplateField(t *testing.T) {
testCases := []struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,19 @@ type Workflow struct {
Trigger *map[string]any `json:"trigger,omitempty" yaml:"trigger,omitempty"`
}

// ContainerResources represents the resource allocation for a containerized agent.
type ContainerResources struct {
Cpu string `json:"cpu" yaml:"cpu"`
Memory string `json:"memory" yaml:"memory"`
}

// ContainerAgent This 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 ContainerAgent struct {
AgentDefinition `json:",inline" yaml:",inline"`
Protocols []ProtocolVersionRecord `json:"protocols" yaml:"protocols"`
Resources *ContainerResources `json:"resources,omitempty" yaml:"resources,omitempty"`
EnvironmentVariables *[]EnvironmentVariable `json:"environmentVariables,omitempty" yaml:"environment_variables,omitempty"`
}

Expand Down
Loading