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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"

"azure.ai.toolboxes/internal/exterrors"
Expand All @@ -15,6 +16,7 @@ import (

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/azure/azure-dev/cli/azd/pkg/foundry"
"google.golang.org/protobuf/types/known/structpb"
)

// aiToolboxHost is the azure.yaml service host kind owned by this extension. A
Expand Down Expand Up @@ -126,7 +128,10 @@ func (p *toolboxServiceTarget) Deploy(
targetResource *azdext.TargetResource,
progress azdext.ProgressReporter,
) (*azdext.ServiceDeployResult, error) {
cfg, err := parseToolboxServiceConfig(serviceConfig)
cfg, err := p.parseToolboxServiceConfig(
ctx,
serviceConfig,
)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -305,15 +310,62 @@ func (p *toolboxServiceTarget) buildToolEntries(
// back to the deprecated config: shape for azure.yaml files written before the
// per-resource service split.
func parseToolboxServiceConfig(svc *azdext.ServiceConfig) (*toolboxServiceConfig, error) {
props := svc.GetAdditionalProperties()
if props == nil || len(props.GetFields()) == 0 {
props = svc.GetConfig()
return parseToolboxServiceConfigAtRoot(svc, "")
}

func (p *toolboxServiceTarget) parseToolboxServiceConfig(
ctx context.Context,
svc *azdext.ServiceConfig,
) (*toolboxServiceConfig, error) {
props := toolboxServiceProps(svc)
if props == nil || !containsToolboxFileRef(props.AsMap()) {
return parseToolboxServiceConfig(svc)
}
project, err := p.azdClient.Project().Get(
ctx,
&azdext.EmptyRequest{},
)
if err != nil {
return nil, fmt.Errorf(
"resolving project root for toolbox %q: %w",
svc.GetName(),
err,
)
}
if project.GetProject() == nil {
return nil, fmt.Errorf(
"resolving project root for toolbox %q: empty project",
svc.GetName(),
)
}
return parseToolboxServiceConfigAtRoot(
svc,
project.GetProject().GetPath(),
)
}

func parseToolboxServiceConfigAtRoot(
svc *azdext.ServiceConfig,
projectRoot string,
) (*toolboxServiceConfig, error) {
props := toolboxServiceProps(svc)
cfg := &toolboxServiceConfig{}
if props == nil {
return cfg, nil
}
b, err := json.Marshal(props.AsMap())
values := props.AsMap()
if projectRoot != "" {
resolved, err := foundry.ResolveFileRefs(values, projectRoot)
if err != nil {
return nil, fmt.Errorf(
"resolving toolbox service %q config: %w",
svc.GetName(),
err,
)
Comment on lines +360 to +364
}
values = resolved
}
b, err := json.Marshal(values)
if err != nil {
return nil, fmt.Errorf("encoding toolbox service %q config: %w", svc.GetName(), err)
}
Expand All @@ -323,6 +375,35 @@ func parseToolboxServiceConfig(svc *azdext.ServiceConfig) (*toolboxServiceConfig
return cfg, nil
}

func toolboxServiceProps(
svc *azdext.ServiceConfig,
) *structpb.Struct {
props := svc.GetAdditionalProperties()
if props == nil || len(props.GetFields()) == 0 {
return svc.GetConfig()
}
return props
}

func containsToolboxFileRef(value any) bool {
switch typed := value.(type) {
case map[string]any:
if _, exists := typed["$ref"]; exists {
return true
}
for _, child := range typed {
if containsToolboxFileRef(child) {
return true
}
}
case []any:
if slices.ContainsFunc(typed, containsToolboxFileRef) {
return true
}
}
return false
}

// currentEnvValues loads all key-value pairs from the active azd environment, used to
// resolve ${VAR} references in tool fields at deploy time.
func (p *toolboxServiceTarget) currentEnvValues(ctx context.Context) (map[string]string, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package cmd

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
Expand Down Expand Up @@ -50,6 +52,39 @@ func TestParseToolboxServiceConfig_ServiceLevel(t *testing.T) {
assert.Equal(t, "github-mcp", cfg.Tools[1]["connection"])
}

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

root := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(root, "toolbox.yaml"),
[]byte(
"description: referenced tools\n"+
"tools:\n"+
" - type: web_search\n",
),
0o600,
))
props, err := structpb.NewStruct(map[string]any{
"$ref": "./toolbox.yaml",
})
require.NoError(t, err)

cfg, err := parseToolboxServiceConfigAtRoot(
&azdext.ServiceConfig{
Name: "referenced",
Host: aiToolboxHost,
AdditionalProperties: props,
},
root,
)

require.NoError(t, err)
assert.Equal(t, "referenced tools", cfg.Description)
require.Len(t, cfg.Tools, 1)
assert.Equal(t, "web_search", cfg.Tools[0]["type"])
}

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

Expand Down
Loading