Skip to content

Commit 586f547

Browse files
committed
Enhance agent initialization by adding service name collision resolution and improving project detection messages
1 parent 3ae2d8e commit 586f547

File tree

1 file changed

+214
-10
lines changed
  • cli/azd/extensions/azure.ai.agents/internal/cmd

1 file changed

+214
-10
lines changed

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

Lines changed: 214 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ type InitAction struct {
6868
flags *initFlags
6969
models *modelSelector
7070

71-
deploymentDetails []project.Deployment
72-
containerSettings *project.ContainerSettings
73-
httpClient *http.Client
71+
deploymentDetails []project.Deployment
72+
containerSettings *project.ContainerSettings
73+
httpClient *http.Client
74+
serviceNameOverride string // when set, addToProject uses this instead of the manifest name
7475
}
7576

7677
// modelSelector encapsulates the dependencies needed for model selection and
@@ -574,6 +575,22 @@ func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdC
574575
}
575576

576577
fmt.Println()
578+
} else if projectResponse.Project != nil {
579+
// An existing azd project was found — tell the user so the skipped template
580+
// download isn't a mystery. Also warn if the project lacks an infra/ directory,
581+
// since deployment may require infrastructure scaffolding.
582+
fmt.Println(output.WithGrayFormat(
583+
"Found existing azd project at %q. Adding agent to it.", projectResponse.Project.Path,
584+
))
585+
586+
infraDir := filepath.Join(projectResponse.Project.Path, "infra")
587+
if _, statErr := os.Stat(infraDir); os.IsNotExist(statErr) {
588+
fmt.Printf("%s", output.WithWarningFormat(
589+
"No infra/ directory found in the project. If you need Azure infrastructure "+
590+
"for deployment, run 'azd init -t Azure-Samples/azd-ai-starter-basic' in an empty "+
591+
"directory first, then re-run this command from there.\n",
592+
))
593+
}
577594
}
578595

579596
if projectResponse.Project == nil {
@@ -630,8 +647,25 @@ func manifestHasModelResources(manifest *agent_yaml.AgentManifest) bool {
630647
func (a *InitAction) configureModelChoice(
631648
ctx context.Context, agentManifest *agent_yaml.AgentManifest,
632649
) (*agent_yaml.AgentManifest, error) {
633-
// If --project-id is provided, validate the ARM format and extract the subscription ID
634-
// so ensureSubscription can skip the prompt and just resolve the tenant
650+
// When no --project-id flag was given, check whether the azd environment already
651+
// has a Foundry project configured from a previous init. If so, reuse it so the
652+
// user isn't prompted to select a project they already chose.
653+
if a.flags.projectResourceId == "" {
654+
if existing, err := a.azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
655+
EnvName: a.environment.Name,
656+
Key: "AZURE_AI_PROJECT_ID",
657+
}); err == nil && existing.Value != "" {
658+
a.flags.projectResourceId = existing.Value
659+
log.Printf("Reusing existing Foundry project from environment: %s", existing.Value)
660+
fmt.Println(output.WithGrayFormat(
661+
"Using Foundry project from environment: %s", existing.Value,
662+
))
663+
}
664+
}
665+
666+
// If --project-id is provided (or reused from environment), validate the ARM
667+
// format and extract the subscription ID so ensureSubscription can skip the
668+
// prompt and just resolve the tenant.
635669
if a.flags.projectResourceId != "" {
636670
projectDetails, err := extractProjectDetails(a.flags.projectResourceId)
637671
if err != nil {
@@ -1213,12 +1247,28 @@ func (a *InitAction) downloadAgentYaml(
12131247
fmt.Println(output.WithGrayFormat("✓ Manifest validated successfully"))
12141248

12151249
agentId := agentManifest.Name
1250+
serviceName := strings.ReplaceAll(agentId, " ", "")
12161251

12171252
// Use targetDir if provided, otherwise default to "src/{agentId}"
1218-
if targetDir == "" {
1253+
autoDir := targetDir == ""
1254+
if autoDir {
12191255
targetDir = filepath.Join("src", agentId)
12201256
}
12211257

1258+
// When the target directory was auto-computed (no --src flag), check for
1259+
// collisions with an existing directory or an existing azure.yaml service.
1260+
// If a collision is found, prompt for a new service name (or auto-suffix
1261+
// in no-prompt mode).
1262+
if autoDir {
1263+
targetDir, serviceName, err = a.resolveCollisions(
1264+
ctx, agentId, targetDir, serviceName,
1265+
)
1266+
if err != nil {
1267+
return nil, "", err
1268+
}
1269+
}
1270+
a.serviceNameOverride = serviceName
1271+
12221272
// Safety checks for local container-based agents should happen before prompting for model SKU, etc.
12231273
if a.isLocalFilePath(manifestPointer) {
12241274
if _, isContainerAgent := agentManifest.Template.(agent_yaml.ContainerAgent); isContainerAgent {
@@ -1396,7 +1446,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
13961446
}
13971447

13981448
serviceConfig := &azdext.ServiceConfig{
1399-
Name: strings.ReplaceAll(agentDef.Name, " ", ""),
1449+
Name: a.serviceNameOverride,
14001450
RelativePath: targetDir,
14011451
Host: AiAgentHost,
14021452
Language: "docker",
@@ -1416,18 +1466,172 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
14161466
return fmt.Errorf("adding agent service to project: %w", err)
14171467
}
14181468

1419-
fmt.Printf("\nAdded your agent as a service entry named '%s' under the file azure.yaml.\n", agentDef.Name)
1469+
fmt.Printf(
1470+
"\nAdded your agent as a service entry named '%s' under the file azure.yaml.\n",
1471+
a.serviceNameOverride,
1472+
)
14201473
if projectID, _ := a.azdClient.Environment().GetValue(ctx, &azdext.GetEnvRequest{
14211474
EnvName: a.environment.Name,
14221475
Key: "AZURE_AI_PROJECT_ID",
14231476
}); projectID != nil && projectID.Value != "" {
1424-
fmt.Printf("To deploy your agent, use %s.\n", color.HiBlueString("azd deploy %s", agentDef.Name))
1477+
fmt.Printf("To deploy your agent, use %s.\n",
1478+
color.HiBlueString("azd deploy %s", a.serviceNameOverride))
14251479
} else {
1426-
fmt.Printf("To provision and deploy the whole solution, use %s.\n", color.HiBlueString("azd up"))
1480+
fmt.Printf(
1481+
"To provision and deploy the whole solution, use %s.\n",
1482+
color.HiBlueString("azd up"),
1483+
)
14271484
}
14281485
return nil
14291486
}
14301487

1488+
// resolveCollisions checks whether the auto-computed target directory or
1489+
// service name already exist. When a collision is detected, the user is
1490+
// prompted for a new name (or a numeric suffix is appended in no-prompt
1491+
// mode). Returns the (possibly adjusted) targetDir and serviceName.
1492+
func (a *InitAction) resolveCollisions(
1493+
ctx context.Context,
1494+
agentId string,
1495+
targetDir string,
1496+
serviceName string,
1497+
) (string, string, error) {
1498+
dirExists := fileExists(targetDir)
1499+
1500+
serviceExists := false
1501+
if a.projectConfig != nil {
1502+
for _, svc := range a.projectConfig.Services {
1503+
if svc.Name == serviceName {
1504+
serviceExists = true
1505+
break
1506+
}
1507+
}
1508+
}
1509+
1510+
if !dirExists && !serviceExists {
1511+
return targetDir, serviceName, nil
1512+
}
1513+
1514+
// Find the next available name for use as the default suggestion
1515+
// (interactive) or the final answer (no-prompt).
1516+
suggestion, suggestionDir, suggestionSvc, err :=
1517+
a.nextAvailableName(agentId)
1518+
if err != nil {
1519+
return "", "", err
1520+
}
1521+
1522+
if a.flags.NoPrompt {
1523+
log.Printf(
1524+
"Collision on %q; using %q", agentId, suggestion,
1525+
)
1526+
return suggestionDir, suggestionSvc, nil
1527+
}
1528+
1529+
// Interactive mode: let the user choose.
1530+
choices := []*azdext.SelectChoice{
1531+
{
1532+
Label: "Overwrite existing",
1533+
Value: "overwrite",
1534+
},
1535+
{
1536+
Label: "Use a different service name",
1537+
Value: "rename",
1538+
},
1539+
}
1540+
1541+
defaultIdx := int32(1)
1542+
resp, err := a.azdClient.Prompt().Select(ctx, &azdext.SelectRequest{
1543+
Options: &azdext.SelectOptions{
1544+
Message: fmt.Sprintf(
1545+
"An agent named '%s' already exists in your azure.yaml."+
1546+
" Overwrite it or use a different name?",
1547+
serviceName,
1548+
),
1549+
Choices: choices,
1550+
SelectedIndex: &defaultIdx,
1551+
},
1552+
})
1553+
if err != nil {
1554+
if exterrors.IsCancellation(err) {
1555+
return "", "", exterrors.Cancelled(
1556+
"initialization was cancelled",
1557+
)
1558+
}
1559+
return "", "", fmt.Errorf(
1560+
"prompting for collision resolution: %w", err,
1561+
)
1562+
}
1563+
1564+
if choices[*resp.Value].Value == "overwrite" {
1565+
return targetDir, serviceName, nil
1566+
}
1567+
1568+
// Prompt for a new name — default to the next available suffix.
1569+
nameResp, err := a.azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
1570+
Options: &azdext.PromptOptions{
1571+
Message: "Enter a new service name for this agent",
1572+
DefaultValue: suggestion,
1573+
IgnoreHintKeys: true,
1574+
},
1575+
})
1576+
if err != nil {
1577+
if exterrors.IsCancellation(err) {
1578+
return "", "", exterrors.Cancelled(
1579+
"initialization was cancelled",
1580+
)
1581+
}
1582+
return "", "", fmt.Errorf(
1583+
"prompting for new service name: %w", err,
1584+
)
1585+
}
1586+
1587+
newName := strings.TrimSpace(nameResp.Value)
1588+
if newName == "" {
1589+
newName = suggestion
1590+
}
1591+
1592+
newDir := filepath.Join("src", newName)
1593+
newSvc := strings.ReplaceAll(newName, " ", "")
1594+
return newDir, newSvc, nil
1595+
}
1596+
1597+
// nextAvailableName finds the next unused name by appending -2, -3, etc.
1598+
// Returns the candidate name, directory, and service name.
1599+
func (a *InitAction) nextAvailableName(
1600+
agentId string,
1601+
) (string, string, string, error) {
1602+
const maxAttempts = 100
1603+
for i := 2; i <= maxAttempts; i++ {
1604+
candidate := fmt.Sprintf("%s-%d", agentId, i)
1605+
candidateDir := filepath.Join("src", candidate)
1606+
candidateSvc := strings.ReplaceAll(candidate, " ", "")
1607+
1608+
if fileExists(candidateDir) {
1609+
continue
1610+
}
1611+
1612+
svcTaken := false
1613+
if a.projectConfig != nil {
1614+
for _, svc := range a.projectConfig.Services {
1615+
if svc.Name == candidateSvc {
1616+
svcTaken = true
1617+
break
1618+
}
1619+
}
1620+
}
1621+
if svcTaken {
1622+
continue
1623+
}
1624+
1625+
return candidate, candidateDir, candidateSvc, nil
1626+
}
1627+
1628+
return "", "", "", fmt.Errorf(
1629+
"could not find a unique name after %d attempts "+
1630+
"(tried %s-2 through %s-%d)",
1631+
maxAttempts-1, agentId, agentId, maxAttempts,
1632+
)
1633+
}
1634+
14311635
func (a *InitAction) populateContainerSettings(
14321636
ctx context.Context,
14331637
manifestResources *agent_yaml.ContainerResources,

0 commit comments

Comments
 (0)