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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ const (
// template. Entries with any other (or empty) templateType belong to the
// standard awesome-azd gallery and are filtered out.
templateTypeExtensionAIAgent = "extension.ai.agent"

// featuredTag is the extensionTags value that marks a template for the
// curated starter list. These templates are shown first; the user can
// expand to see the full catalog.
featuredTag = "featured"

// recommendedTag is the extensionTags value that identifies the default
// pre-selected template in the featured list.
recommendedTag = "recommended"

// seeAllSentinel is the SelectChoice.Value used for the "See all
// templates..." option appended to the featured list.
seeAllSentinel = "__see_all__"
)

// AgentTemplate represents an agent template entry from the remote JSON catalog.
Expand Down Expand Up @@ -189,9 +202,25 @@ func fetchAgentTemplatesFromURL(
return filtered, nil
}

// isFeatured reports whether the template carries the "featured" extensionTag,
// which marks it for the curated starter list.
func (t *AgentTemplate) isFeatured() bool {
return slices.Contains(t.ExtensionTags, featuredTag)
}

// isRecommended reports whether the template carries the "recommended"
// extensionTag, which marks it as the default pre-selected template.
func (t *AgentTemplate) isRecommended() bool {
return slices.Contains(t.ExtensionTags, recommendedTag)
}

// promptAgentTemplate guides the user through language selection and template selection.
// Returns the selected AgentTemplate. The caller should check EffectiveType() to determine
// whether to use the agent.yaml manifest flow or the full azd template flow.
//
// Templates tagged "featured" are shown first in a curated list. The template
// tagged "recommended" gets a (Recommended) suffix in the label and is
// pre-selected. A "See all templates..." option expands to the full catalog.
func promptAgentTemplate(
ctx context.Context,
azdClient *azdext.AzdClient,
Expand Down Expand Up @@ -240,37 +269,130 @@ func promptAgentTemplate(
selectedLanguage := languageChoices[*langResp.Value].Value

// Filter templates by selected language (entries can declare multiple).
filtered := make([]AgentTemplate, 0, len(templates))
langFiltered := make([]AgentTemplate, 0, len(templates))
for _, t := range templates {
if slices.Contains(t.Languages, selectedLanguage) {
filtered = append(filtered, t)
langFiltered = append(langFiltered, t)
}
}

if len(filtered) == 0 {
return nil, fmt.Errorf("no agent templates available for %s", languageChoices[*langResp.Value].Label)
if len(langFiltered) == 0 {
return nil, fmt.Errorf(
"no agent templates available for %s",
languageChoices[*langResp.Value].Label,
)
}

// Sort templates alphabetically by title
slices.SortFunc(filtered, func(a, b AgentTemplate) int {
// Partition into featured vs rest.
featured, rest := partitionFeatured(langFiltered)

// When there are both featured and non-featured templates, show the
// curated featured list first with a "See all templates…" escape hatch.
// When all templates are featured (len(rest) == 0) or none are
// (len(featured) == 0), skip the curated step and show the full list
// directly — a curated list that equals the full list adds no value.
if len(featured) > 0 && len(rest) > 0 {
Comment thread
Nathandrake229 marked this conversation as resolved.
defaultIdx := findRecommendedIndex(featured)

selected, err := promptSelectTemplate(
ctx, azdClient, featured,
"Select a starter template", &defaultIdx, true,
)
if err != nil {
return nil, err
}

if selected != nil {
return selected, nil
}
// User chose "See all templates…" → fall through to full list.
}

// Show the complete catalog (featured + rest, sorted alphabetically).
allSorted := slices.Clone(langFiltered)
slices.SortFunc(allSorted, func(a, b AgentTemplate) int {
return strings.Compare(a.Title, b.Title)
})

// Build template choices with framework in label
templateChoices := make([]*azdext.SelectChoice, len(filtered))
for i, t := range filtered {
label := fmt.Sprintf("%s (%s)", t.Title, t.ExtensionFramework)
templateChoices[i] = &azdext.SelectChoice{
Label: label,
// Pre-select the recommended template in the full list too.
recommendedIdx := findRecommendedIndex(allSorted)

return promptSelectTemplate(
ctx, azdClient, allSorted,
"Select an agent template", &recommendedIdx, false,
)
}

// partitionFeatured splits templates into featured (tagged "featured") and
// the rest. Both slices are sorted alphabetically by title.
func partitionFeatured(templates []AgentTemplate) (featured, rest []AgentTemplate) {
for _, t := range templates {
if t.isFeatured() {
featured = append(featured, t)
} else {
rest = append(rest, t)
}
}

sortByTitle := func(a, b AgentTemplate) int {
return strings.Compare(a.Title, b.Title)
}
slices.SortFunc(featured, sortByTitle)
slices.SortFunc(rest, sortByTitle)

return featured, rest
}

// findRecommendedIndex returns the index of the recommended default template
// in the given list. It looks for a template tagged "recommended"; if none
// is found it returns 0 (first item in the list).
func findRecommendedIndex(templates []AgentTemplate) int32 {
for i, t := range templates {
if t.isRecommended() {
return int32(i) //nolint:gosec // template list length is always small
}
}
return 0
}

// promptSelectTemplate presents a select prompt for the given templates.
// defaultIdx, when non-nil, pre-selects that index in the list.
// When includeSeeAll is true, a "See all templates…" option is appended;
// selecting it causes the function to return (nil, nil) so the caller can
// re-prompt with the full list.
func promptSelectTemplate(
ctx context.Context,
azdClient *azdext.AzdClient,
templates []AgentTemplate,
message string,
defaultIdx *int32,
includeSeeAll bool,
) (*AgentTemplate, error) {
choices := make([]*azdext.SelectChoice, len(templates))
for i, t := range templates {
choices[i] = &azdext.SelectChoice{
Label: t.Title,
Value: fmt.Sprintf("%d", i),
}
}

templateResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "Select an agent template",
Choices: templateChoices,
},
if includeSeeAll {
choices = append(choices, &azdext.SelectChoice{
Label: "See all templates...",
Value: seeAllSentinel,
})
}

opts := &azdext.SelectOptions{
Message: message,
Choices: choices,
}
if defaultIdx != nil {
opts.SelectedIndex = defaultIdx
}

resp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
Options: opts,
})
if err != nil {
if exterrors.IsCancellation(err) {
Expand All @@ -279,8 +401,12 @@ func promptAgentTemplate(
return nil, fmt.Errorf("failed to prompt for template: %w", err)
}

selectedTemplate := filtered[*templateResp.Value]
return &selectedTemplate, nil
selected := choices[*resp.Value]
if selected.Value == seeAllSentinel {
return nil, nil
}

return &templates[*resp.Value], nil
}

// findAgentManifest searches the directory tree rooted at dir for the first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,158 @@ func TestEffectiveType(t *testing.T) {
}
}

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

tests := []struct {
name string
tags []string
expected bool
}{
{name: "tagged featured", tags: []string{"featured", "Responses Protocol"}, expected: true},
{name: "not tagged featured", tags: []string{"MCP", "Responses Protocol"}, expected: false},
{name: "nil tags", tags: nil, expected: false},
{name: "empty tags", tags: []string{}, expected: false},
{name: "featured only", tags: []string{"featured"}, expected: true},
{name: "example tag is not featured", tags: []string{"example"}, expected: false},
{name: "template tag is not featured", tags: []string{"template"}, expected: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmpl := &AgentTemplate{ExtensionTags: tt.tags}
require.Equal(t, tt.expected, tmpl.isFeatured())
})
}
}

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

tests := []struct {
name string
tags []string
expected bool
}{
{name: "tagged recommended", tags: []string{"featured", "recommended"}, expected: true},
{name: "not tagged recommended", tags: []string{"featured"}, expected: false},
{name: "nil tags", tags: nil, expected: false},
{name: "empty tags", tags: []string{}, expected: false},
{name: "recommended without featured", tags: []string{"recommended"}, expected: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmpl := &AgentTemplate{ExtensionTags: tt.tags}
require.Equal(t, tt.expected, tmpl.isRecommended())
})
}
}

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

templates := []AgentTemplate{
{Title: "MCP Tools Agent", ExtensionTags: []string{"MCP"}},
{Title: "Basic Agent", ExtensionTags: []string{"featured"}},
{Title: "Workflow Agent", ExtensionTags: []string{"workflows"}},
{Title: "Hello World", ExtensionTags: []string{"featured"}},
}

featured, rest := partitionFeatured(templates)

require.Len(t, featured, 2)
require.Equal(t, "Basic Agent", featured[0].Title)
require.Equal(t, "Hello World", featured[1].Title)

require.Len(t, rest, 2)
require.Equal(t, "MCP Tools Agent", rest[0].Title)
require.Equal(t, "Workflow Agent", rest[1].Title)
}

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

templates := []AgentTemplate{
{Title: "B Agent", ExtensionTags: []string{"featured"}},
{Title: "A Agent", ExtensionTags: []string{"featured"}},
}

featured, rest := partitionFeatured(templates)

require.Len(t, featured, 2)
require.Equal(t, "A Agent", featured[0].Title)
require.Equal(t, "B Agent", featured[1].Title)
require.Empty(t, rest)
}

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

featured, rest := partitionFeatured(nil)
require.Empty(t, featured)
require.Empty(t, rest)

featured2, rest2 := partitionFeatured([]AgentTemplate{})
require.Empty(t, featured2)
require.Empty(t, rest2)
}

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

templates := []AgentTemplate{
{Title: "MCP Tools Agent", ExtensionTags: []string{"MCP"}},
{Title: "Workflow Agent", ExtensionTags: []string{"workflows"}},
}

featured, rest := partitionFeatured(templates)

require.Empty(t, featured)
require.Len(t, rest, 2)
}

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

t.Run("finds recommended tag", func(t *testing.T) {
t.Parallel()
templates := []AgentTemplate{
{Title: "Hello World", ExtensionTags: []string{"featured"}},
{Title: "Basic Agent", ExtensionTags: []string{"featured", "recommended"}},
{Title: "MCP Agent", ExtensionTags: []string{"featured"}},
}
require.Equal(t, int32(1), findRecommendedIndex(templates))
})

t.Run("returns first when multiple recommended", func(t *testing.T) {
t.Parallel()
templates := []AgentTemplate{
{Title: "Hello World", ExtensionTags: []string{"featured"}},
{Title: "Agent A", ExtensionTags: []string{"featured", "recommended"}},
{Title: "Agent B", ExtensionTags: []string{"featured", "recommended"}},
}
require.Equal(t, int32(1), findRecommendedIndex(templates))
})

t.Run("returns 0 when no recommended tag", func(t *testing.T) {
t.Parallel()
templates := []AgentTemplate{
{Title: "Hello World", ExtensionTags: []string{"featured"}},
{Title: "Basic Agent", ExtensionTags: []string{"featured"}},
}
require.Equal(t, int32(0), findRecommendedIndex(templates))
})

t.Run("returns 0 for empty list", func(t *testing.T) {
t.Parallel()
require.Equal(t, int32(0), findRecommendedIndex(nil))
require.Equal(t, int32(0), findRecommendedIndex([]AgentTemplate{}))
})
}

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

Expand Down
Loading