Skip to content

Commit 27ec913

Browse files
committed
feat: implement agent template initialization and selection process
1 parent ba9f7ab commit 27ec913

File tree

2 files changed

+313
-7
lines changed

2 files changed

+313
-7
lines changed

cli/azd/extensions/azure.ai.agents/internal/cmd/init.go

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,134 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command {
195195
return err
196196
}
197197
} else {
198-
action := &InitFromCodeAction{
199-
azdClient: azdClient,
200-
flags: flags,
201-
httpClient: httpClient,
202-
}
203-
204-
if err := action.Run(ctx); err != nil {
198+
// No manifest provided - prompt user for init mode
199+
initMode, err := promptInitMode(ctx, azdClient)
200+
if err != nil {
205201
if exterrors.IsCancellation(err) {
206202
return exterrors.Cancelled("initialization was cancelled")
207203
}
208204
return err
209205
}
206+
207+
switch initMode {
208+
case initModeTemplate:
209+
// User chose to start from a template - select one
210+
selectedTemplate, err := promptAgentTemplate(ctx, azdClient, httpClient)
211+
if err != nil {
212+
if exterrors.IsCancellation(err) {
213+
return exterrors.Cancelled("initialization was cancelled")
214+
}
215+
return err
216+
}
217+
218+
switch selectedTemplate.EffectiveType() {
219+
case TemplateTypeAzd:
220+
// Full azd template - dispatch azd init -t <repo>
221+
initArgs := []string{"init", "-t", selectedTemplate.Source}
222+
if flags.env != "" {
223+
initArgs = append(initArgs, "--environment", flags.env)
224+
} else {
225+
cwd, err := os.Getwd()
226+
if err == nil {
227+
sanitizedDirectoryName := sanitizeAgentName(filepath.Base(cwd))
228+
initArgs = append(initArgs, "--environment", sanitizedDirectoryName+"-dev")
229+
}
230+
}
231+
232+
workflow := &azdext.Workflow{
233+
Name: "init",
234+
Steps: []*azdext.WorkflowStep{
235+
{Command: &azdext.WorkflowCommand{Args: initArgs}},
236+
},
237+
}
238+
239+
_, err := azdClient.Workflow().Run(ctx, &azdext.RunWorkflowRequest{
240+
Workflow: workflow,
241+
})
242+
if err != nil {
243+
if exterrors.IsCancellation(err) {
244+
return exterrors.Cancelled("initialization was cancelled")
245+
}
246+
return exterrors.Dependency(
247+
exterrors.CodeProjectInitFailed,
248+
fmt.Sprintf("failed to initialize project from template: %s", err),
249+
"",
250+
)
251+
}
252+
253+
fmt.Printf("\nProject initialized from template: %s\n", selectedTemplate.Title)
254+
255+
default:
256+
// Agent manifest template - use existing -m flow
257+
flags.manifestPointer = selectedTemplate.Source
258+
259+
azureContext, projectConfig, environment, err := ensureAzureContext(ctx, flags, azdClient)
260+
if err != nil {
261+
if exterrors.IsCancellation(err) {
262+
return exterrors.Cancelled("initialization was cancelled")
263+
}
264+
return err
265+
}
266+
267+
credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{
268+
TenantID: azureContext.Scope.TenantId,
269+
AdditionallyAllowedTenants: []string{"*"},
270+
})
271+
if err != nil {
272+
return exterrors.Auth(
273+
exterrors.CodeCredentialCreationFailed,
274+
fmt.Sprintf("failed to create Azure credential: %s", err),
275+
"run 'azd auth login' to authenticate",
276+
)
277+
}
278+
279+
console := input.NewConsole(
280+
false, // noPrompt
281+
true, // isTerminal
282+
input.Writers{Output: os.Stdout},
283+
input.ConsoleHandles{
284+
Stderr: os.Stderr,
285+
Stdin: os.Stdin,
286+
Stdout: os.Stdout,
287+
},
288+
nil, // formatter
289+
nil, // externalPromptCfg
290+
)
291+
292+
action := &InitAction{
293+
azdClient: azdClient,
294+
azureContext: azureContext,
295+
console: console,
296+
credential: credential,
297+
projectConfig: projectConfig,
298+
environment: environment,
299+
flags: flags,
300+
httpClient: httpClient,
301+
}
302+
303+
if err := action.Run(ctx); err != nil {
304+
if exterrors.IsCancellation(err) {
305+
return exterrors.Cancelled("initialization was cancelled")
306+
}
307+
return err
308+
}
309+
}
310+
311+
default:
312+
// initModeFromCode - use existing code in current directory
313+
action := &InitFromCodeAction{
314+
azdClient: azdClient,
315+
flags: flags,
316+
httpClient: httpClient,
317+
}
318+
319+
if err := action.Run(ctx); err != nil {
320+
if exterrors.IsCancellation(err) {
321+
return exterrors.Cancelled("initialization was cancelled")
322+
}
323+
return err
324+
}
325+
}
210326
}
211327

212328
return nil
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package cmd
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
"strings"
13+
14+
"azureaiagent/internal/exterrors"
15+
16+
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
17+
)
18+
19+
const agentTemplatesURL = "https://raw.githubusercontent.com/therealjohn/awesome-azd/agent-templates/website/static/agent-templates.json"
20+
21+
// Template type constants
22+
const (
23+
// TemplateTypeAgent is a template that points to an agent.yaml manifest file.
24+
TemplateTypeAgent = "agent"
25+
26+
// TemplateTypeAzd is a full azd template repository.
27+
TemplateTypeAzd = "azd"
28+
)
29+
30+
// AgentTemplate represents an agent template entry from the remote JSON catalog.
31+
type AgentTemplate struct {
32+
Title string `json:"title"`
33+
Description string `json:"description"`
34+
Language string `json:"language"`
35+
Framework string `json:"framework"`
36+
Source string `json:"source"`
37+
Tags []string `json:"tags"`
38+
}
39+
40+
// EffectiveType determines the template type by inspecting the source URL.
41+
// If it ends with agent.yaml or agent.manifest.yaml, it's an agent manifest.
42+
// Otherwise, it's treated as a full azd template repo.
43+
func (t *AgentTemplate) EffectiveType() string {
44+
lower := strings.ToLower(t.Source)
45+
if strings.HasSuffix(lower, "/agent.yaml") ||
46+
strings.HasSuffix(lower, "/agent.manifest.yaml") ||
47+
lower == "agent.yaml" ||
48+
lower == "agent.manifest.yaml" {
49+
return TemplateTypeAgent
50+
}
51+
return TemplateTypeAzd
52+
}
53+
54+
const (
55+
initModeFromCode = "from_code"
56+
initModeTemplate = "template"
57+
)
58+
59+
// promptInitMode asks the user whether to use existing code or start from a template.
60+
// Returns initModeFromCode or initModeTemplate.
61+
func promptInitMode(ctx context.Context, azdClient *azdext.AzdClient) (string, error) {
62+
choices := []*azdext.SelectChoice{
63+
{Label: "Use the code in the current directory", Value: initModeFromCode},
64+
{Label: "Start new from a template", Value: initModeTemplate},
65+
}
66+
67+
resp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
68+
Options: &azdext.SelectOptions{
69+
Message: "How do you want to initialize your agent?",
70+
Choices: choices,
71+
},
72+
})
73+
if err != nil {
74+
if exterrors.IsCancellation(err) {
75+
return "", exterrors.Cancelled("initialization mode selection was cancelled")
76+
}
77+
return "", fmt.Errorf("failed to prompt for initialization mode: %w", err)
78+
}
79+
80+
return choices[*resp.Value].Value, nil
81+
}
82+
83+
// fetchAgentTemplates retrieves the agent template catalog from the remote JSON URL.
84+
func fetchAgentTemplates(ctx context.Context, httpClient *http.Client) ([]AgentTemplate, error) {
85+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, agentTemplatesURL, nil)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to create request: %w", err)
88+
}
89+
90+
resp, err := httpClient.Do(req)
91+
if err != nil {
92+
return nil, fmt.Errorf("failed to fetch agent templates: %w", err)
93+
}
94+
defer resp.Body.Close()
95+
96+
if resp.StatusCode != http.StatusOK {
97+
return nil, fmt.Errorf("failed to fetch agent templates: HTTP %d", resp.StatusCode)
98+
}
99+
100+
body, err := io.ReadAll(resp.Body)
101+
if err != nil {
102+
return nil, fmt.Errorf("failed to read agent templates response: %w", err)
103+
}
104+
105+
var templates []AgentTemplate
106+
if err := json.Unmarshal(body, &templates); err != nil {
107+
return nil, fmt.Errorf("failed to parse agent templates: %w", err)
108+
}
109+
110+
return templates, nil
111+
}
112+
113+
// promptAgentTemplate guides the user through language selection and template selection.
114+
// Returns the selected AgentTemplate. The caller should check EffectiveType() to determine
115+
// whether to use the agent.yaml manifest flow or the full azd template flow.
116+
func promptAgentTemplate(
117+
ctx context.Context,
118+
azdClient *azdext.AzdClient,
119+
httpClient *http.Client,
120+
) (*AgentTemplate, error) {
121+
fmt.Println("Retrieving agent templates...")
122+
123+
templates, err := fetchAgentTemplates(ctx, httpClient)
124+
if err != nil {
125+
return nil, fmt.Errorf("failed to retrieve agent templates: %w", err)
126+
}
127+
128+
if len(templates) == 0 {
129+
return nil, fmt.Errorf("no agent templates available")
130+
}
131+
132+
// Prompt for language
133+
languageChoices := []*azdext.SelectChoice{
134+
{Label: "Python", Value: "python"},
135+
{Label: "C#", Value: "csharp"},
136+
}
137+
138+
langResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
139+
Options: &azdext.SelectOptions{
140+
Message: "Select a language:",
141+
Choices: languageChoices,
142+
},
143+
})
144+
if err != nil {
145+
if exterrors.IsCancellation(err) {
146+
return nil, exterrors.Cancelled("language selection was cancelled")
147+
}
148+
return nil, fmt.Errorf("failed to prompt for language: %w", err)
149+
}
150+
151+
selectedLanguage := languageChoices[*langResp.Value].Value
152+
153+
// Filter templates by selected language
154+
var filtered []AgentTemplate
155+
for _, t := range templates {
156+
if t.Language == selectedLanguage {
157+
filtered = append(filtered, t)
158+
}
159+
}
160+
161+
if len(filtered) == 0 {
162+
return nil, fmt.Errorf("no agent templates available for %s", languageChoices[*langResp.Value].Label)
163+
}
164+
165+
// Build template choices with framework in label
166+
templateChoices := make([]*azdext.SelectChoice, len(filtered))
167+
for i, t := range filtered {
168+
label := fmt.Sprintf("%s (%s)", t.Title, t.Framework)
169+
templateChoices[i] = &azdext.SelectChoice{
170+
Label: label,
171+
Value: fmt.Sprintf("%d", i),
172+
}
173+
}
174+
175+
templateResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
176+
Options: &azdext.SelectOptions{
177+
Message: "Select an agent template:",
178+
Choices: templateChoices,
179+
},
180+
})
181+
if err != nil {
182+
if exterrors.IsCancellation(err) {
183+
return nil, exterrors.Cancelled("template selection was cancelled")
184+
}
185+
return nil, fmt.Errorf("failed to prompt for template: %w", err)
186+
}
187+
188+
selectedTemplate := filtered[*templateResp.Value]
189+
return &selectedTemplate, nil
190+
}

0 commit comments

Comments
 (0)