Skip to content
Open
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
2 changes: 2 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ words:
- protoimpl
- protojson
- protoreflect
- protowire

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be moved into the extension specific cspell, will remove the need for azd core codeowners approval

- projectconfig
- SNAPPROCESS
- structpb
- subtest
Expand Down
64 changes: 63 additions & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"log"
"maps"
"net/http"
"net/url"
"os"
Expand All @@ -23,6 +24,7 @@ import (
"azureaiagent/internal/pkg/agents/agent_api"
"azureaiagent/internal/pkg/agents/agent_yaml"
"azureaiagent/internal/pkg/paths"
"azureaiagent/internal/pkg/projectconfig"
projectpkg "azureaiagent/internal/project"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
Expand Down Expand Up @@ -871,6 +873,7 @@ type ServiceRunContext struct {
ServiceName string // the resolved service name (from azure.yaml)
ProjectDir string // absolute path to the service source directory
StartupCommand string // startupCommand from AdditionalProperties (may be empty)
Environment map[string]string
// Definition is the resolved agent definition (from the inline azure.yaml
// entry or a legacy agent.yaml). It is nil when no definition can be resolved.
Definition *agent_yaml.ContainerAgent
Expand All @@ -883,6 +886,20 @@ func resolveServiceRunContext(ctx context.Context, azdClient *azdext.AzdClient,
if err != nil {
return nil, err
}
if err := projectpkg.ResolveServiceConfigInPlace(
svc,
project.Path,
); err != nil {
return nil, exterrors.Validation(
exterrors.CodeInvalidServiceConfig,
fmt.Sprintf(
"failed to resolve agent service %s: %s",
svc.Name,
err,
),
"fix the agent service configuration in azure.yaml",
)
}

projectDir, err := paths.JoinAllowRoot(project.Path, svc.RelativePath)
if err != nil {
Expand All @@ -894,8 +911,28 @@ func resolveServiceRunContext(ctx context.Context, azdClient *azdext.AzdClient,
}

var startupCmd string
if agentConfig, cfgErr := projectpkg.LoadServiceTargetAgentConfig(svc); cfgErr == nil {
serviceEnv := map[string]string{}
if agentConfig, cfgErr := projectpkg.LoadServiceTargetAgentConfig(
svc,
); cfgErr == nil {
startupCmd = agentConfig.StartupCommand
maps.Copy(serviceEnv, agentConfig.Environment)
}
serviceEnv, err = loadServiceRunEnvironment(
project.Path,
svc,
serviceEnv,
)
if err != nil {
return nil, exterrors.Validation(
exterrors.CodeInvalidServiceConfig,
fmt.Sprintf(
"failed to load environment for %s: %s",
svc.Name,
err,
),
"fix the service env configuration in azure.yaml",
)
}

var definition *agent_yaml.ContainerAgent
Expand All @@ -910,10 +947,35 @@ func resolveServiceRunContext(ctx context.Context, azdClient *azdext.AzdClient,
ServiceName: svc.Name,
ProjectDir: projectDir,
StartupCommand: startupCmd,
Environment: serviceEnv,
Definition: definition,
}, nil
}

func loadServiceRunEnvironment(
projectRoot string,
svc *azdext.ServiceConfig,
base map[string]string,
) (map[string]string, error) {
env := maps.Clone(base)
if env == nil {
env = map[string]string{}
}
raw, err := projectconfig.LoadServiceEnvironment(
projectRoot,
svc.GetName(),
)
if err != nil {
return nil, err
}
if raw == nil {
maps.Copy(env, svc.GetEnvironment())
} else {
maps.Copy(env, raw)
}
return env, nil
}

// toServiceKey converts a service name into the env var key format (uppercase, underscores).
func toServiceKey(serviceName string) string {
key := strings.ReplaceAll(serviceName, " ", "_")
Expand Down
38 changes: 38 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -838,3 +838,41 @@ func TestResolveAgentProtocol_MultipleServicesPromptsOnce(t *testing.T) {
require.Equal(t, int32(1), promptServer.selectCalls.Load(),
"resolveAgentProtocol should trigger exactly one prompt")
}

func TestLoadServiceRunEnvironmentUsesRawValues(t *testing.T) {
t.Parallel()

root := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(root, "azure.yaml"),
[]byte(`services:
agent:
host: azure.ai.agent
env:
PROJECT: ${{project.endpoint}}
ENABLED: true
SHARED: direct
`),
0o600,
))
svc := &azdext.ServiceConfig{
Name: "agent",
Environment: map[string]string{
"PROJECT": "",
"ENABLED": "",
"SHARED": "expanded",
},
}

env, err := loadServiceRunEnvironment(
root,
svc,
map[string]string{"CONFIG_ONLY": "config", "SHARED": "config"},
)

require.NoError(t, err)
require.Equal(t, "${{project.endpoint}}", env["PROJECT"])
require.Equal(t, "true", env["ENABLED"])
require.Equal(t, "direct", env["SHARED"])
require.Equal(t, "config", env["CONFIG_ONLY"])
}
79 changes: 60 additions & 19 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,25 @@ func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, args
ctx,
azdClient,
args.Project.Services,
args.Project.Path,
); err != nil {
return err
}
connections, err := collectConnections(args.Project.Services)
connections, err := collectConnections(
args.Project.Services,
args.Project.Path,
)
if err != nil {
return err
}

for _, svc := range args.Project.Services {
switch svc.Host {
case AiAgentHost:
if err := populateContainerSettings(ctx, azdClient, svc); err != nil {
if err := prepareContainerSettings(
svc,
args.Project.Path,
); err != nil {
Comment thread
huimiu marked this conversation as resolved.
return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err)
}
if err := envUpdate(
Expand Down Expand Up @@ -167,8 +174,12 @@ func updateLegacyProjectDeployments(
ctx context.Context,
azdClient *azdext.AzdClient,
services map[string]*azdext.ServiceConfig,
projectRoot string,
) error {
deployments, err := collectLegacyProjectDeployments(services)
deployments, err := collectLegacyProjectDeployments(
services,
projectRoot,
)
if err != nil {
return err
}
Expand Down Expand Up @@ -221,15 +232,22 @@ func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *az
ctx,
azdClient,
args.Project.Services,
args.Project.Path,
); err != nil {
return err
}
connections, err := collectConnections(args.Project.Services)
connections, err := collectConnections(
args.Project.Services,
args.Project.Path,
)
if err != nil {
return err
}

if err := populateContainerSettings(ctx, azdClient, svc); err != nil {
if err := prepareContainerSettings(
svc,
args.Project.Path,
); err != nil {
return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err)
}
if err := envUpdate(
Expand Down Expand Up @@ -738,10 +756,37 @@ func setEnvVar(ctx context.Context, azdClient *azdext.AzdClient, envName string,
return nil
}

func populateContainerSettings(ctx context.Context, azdClient *azdext.AzdClient, svc *azdext.ServiceConfig) error {
func prepareContainerSettings(
svc *azdext.ServiceConfig,
projectRoot string,
) error {
rawAdditional := svc.GetAdditionalProperties()
rawConfig := svc.GetConfig()
hasRootFileRef := rawAdditional != nil &&
rawAdditional.GetFields()["$ref"] != nil ||
rawConfig != nil && rawConfig.GetFields()["$ref"] != nil
if hasRootFileRef {
if err := project.ResolveServiceConfigInPlace(
svc,
projectRoot,
); err != nil {
return fmt.Errorf(
"failed to resolve agent config: %w",
err,
)
}
} else if err := project.NormalizeServiceConfigInPlace(svc); err != nil {
Comment thread
trangevi marked this conversation as resolved.
return fmt.Errorf(
"failed to normalize agent config: %w",
err,
)
}
foundryAgentConfig, err := project.LoadServiceTargetAgentConfig(svc)
if err != nil {
return fmt.Errorf("failed to parse foundry agent config: %w", err)
return fmt.Errorf(
"failed to parse foundry agent config: %w",
err,
)
}

// Resolve the container resources, applying defaults when unset.
Expand All @@ -760,19 +805,15 @@ func populateContainerSettings(ctx context.Context, azdClient *azdext.AzdClient,
result.Cpu = project.DefaultCpu
}

// Persist the resolved container settings back onto the service's inline
// properties, preserving the agent definition and other config keys.
if err := project.SetAgentContainerSettings(svc, &project.ContainerSettings{Resources: result}); err != nil {
return fmt.Errorf("failed to update agent container settings: %w", err)
}

// Need to add the service config back to the project for use further down the pipeline
req := &azdext.AddServiceRequest{Service: svc}

if _, err := azdClient.Project().AddService(ctx, req); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is no longer necessary?

return fmt.Errorf("adding agent service to project: %w", err)
if err := project.SetAgentContainerSettings(
svc,
&project.ContainerSettings{Resources: result},
); err != nil {
return fmt.Errorf(
"failed to update agent container settings: %w",
err,
)
}

return nil
}

Expand Down
Loading
Loading