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
- projectconfig
- SNAPPROCESS
- structpb
- subtest
Expand Down
3 changes: 3 additions & 0 deletions cli/azd/docs/extensions/extension-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -2484,6 +2484,7 @@ Builds a service's container image.
- Contains:
- `service_name` (string): Name of the service to build
- `service_context` (ServiceContext): Current service context with artifacts
- `service_path` (string): Optional project-relative path used only for this operation
- **Response:** _ContainerBuildResponse_
- Contains:
- `result` (ServiceBuildResult): Build result with artifacts
Expand Down Expand Up @@ -2517,6 +2518,7 @@ Packages a service's container for deployment.
- Contains:
- `service_name` (string): Name of the service to package
- `service_context` (ServiceContext): Current service context with artifacts
- `service_path` (string): Optional project-relative path used only for this operation
- **Response:** _ContainerPackageResponse_
- Contains:
- `result` (ServicePackageResult): Package result with artifacts
Expand All @@ -2529,6 +2531,7 @@ Publishes a container service to a registry.
- Contains:
- `service_name` (string): Name of the service to publish
- `service_context` (ServiceContext): Current service context with artifacts
- `service_path` (string): Optional project-relative path used only for this operation
- **Response:** _ContainerPublishResponse_
- Contains:
- `result` (ServicePublishResult): Publish result with artifacts
Expand Down
3 changes: 2 additions & 1 deletion cli/azd/extensions/azure.ai.agents/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ description: Ship agents with Microsoft Foundry from your terminal. (Beta)
usage: azd ai agent <command> [options]
# NOTE: Make sure version.txt is in sync with this version.
version: 1.0.0-beta.5
requiredAzdVersion: ">=1.27.1"
# Container service_path support starts in azd 1.28.0.
requiredAzdVersion: ">=1.28.0"
dependencies:
- id: azure.ai.inspector
version: "~1.0.0-beta.1"
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 @@ -22,6 +23,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 @@ -706,6 +708,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 @@ -718,6 +721,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 @@ -729,8 +746,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 @@ -745,10 +782,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 @@ -517,3 +517,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"])
}
113 changes: 93 additions & 20 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,30 @@ func configureExtensionHost(host *azdext.ExtensionHost) {
}

func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azdext.ProjectEventArgs) error {
deployments, err := collectProjectDeployments(args.Project.Services)
deployments, err := collectProjectDeploymentsAtRoot(
args.Project.Services,
args.Project.Path,
)
if err != nil {
return err
}
connections, err := collectConnections(args.Project.Services)
connections, err := collectConnectionsAtRoot(
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 := populateContainerSettings(
ctx,
azdClient,
svc,
args.Project.Path,
); err != nil {
return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err)
}
if err := envUpdate(ctx, azdClient, args.Project, svc, deployments, connections); err != nil {
Expand Down Expand Up @@ -194,16 +205,27 @@ func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *az
warnDuplicateAgentNames(args.Project)
})

deployments, err := collectProjectDeployments(args.Project.Services)
deployments, err := collectProjectDeploymentsAtRoot(
args.Project.Services,
args.Project.Path,
)
if err != nil {
return err
}
connections, err := collectConnections(args.Project.Services)
connections, err := collectConnectionsAtRoot(
args.Project.Services,
args.Project.Path,
)
if err != nil {
return err
}

if err := populateContainerSettings(ctx, azdClient, svc); err != nil {
if err := populateContainerSettings(
Comment thread
huimiu marked this conversation as resolved.
ctx,
azdClient,
svc,
args.Project.Path,
); err != nil {
return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err)
}
if err := envUpdate(ctx, azdClient, args.Project, svc, deployments, connections); err != nil {
Expand Down Expand Up @@ -716,10 +738,62 @@ 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 populateContainerSettings(
ctx context.Context,
azdClient *azdext.AzdClient,
svc *azdext.ServiceConfig,
projectRoot string,
) error {
persisted, err := prepareContainerSettings(svc, projectRoot)
if err != nil {
return err
}
if persisted == nil {
return nil
}

if err := project.AddServiceSerialized(
ctx,
azdClient,
persisted,
); err != nil {
return fmt.Errorf("adding agent service to project: %w", err)
}

return nil
}

func prepareContainerSettings(
svc *azdext.ServiceConfig,
projectRoot string,
) (*azdext.ServiceConfig, 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 nil, fmt.Errorf(
"failed to resolve agent config: %w",
err,
)
}
} else if err := project.NormalizeServiceConfigInPlace(svc); err != nil {
return nil, 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 nil, fmt.Errorf(
"failed to parse foundry agent config: %w",
err,
)
}

// Resolve the container resources, applying defaults when unset.
Expand All @@ -738,20 +812,19 @@ 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)
if err := project.SetAgentContainerSettings(
svc,
&project.ContainerSettings{Resources: result},
); err != nil {
return nil, 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 {
return fmt.Errorf("adding agent service to project: %w", err)
if hasRootFileRef {
return nil, nil
}

return nil
return svc, nil
}

// resolveToolboxEnvVars resolves ${VAR} references in toolbox name, description,
Expand Down
Loading
Loading