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
3 changes: 2 additions & 1 deletion cli/azd/extensions/azure.ai.rle/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release History

## 0.4.0-preview (Unreleased)
## 0.4.1-preview (Unreleased)

- Align environment discovery and remote invocation with the refreshed RLE service routes and cursor-based response contracts.
- Use `/rl_environments` consistently for environment and instance lifecycle APIs.
Expand All @@ -10,6 +10,7 @@
- Delete the temporary instance and group on exit with Ctrl+C-independent cleanup and concise terminal status.
- Persist the environment name as `environmentName` while continuing to read legacy `name` state files.
- Authenticate and API-version OpenEnv gateway requests on the configured Foundry project origin, wait for runtime health before reporting readiness, and route the browser playground through an authenticated local proxy.
- Initialize a required local folder by interactively selecting and sparsely downloading an environment from the RLE samples repository.

## 0.3.0-preview

Expand Down
22 changes: 15 additions & 7 deletions cli/azd/extensions/azure.ai.rle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install:
- Azure CLI (`az`): https://learn.microsoft.com/cli/azure/install-azure-cli
- Docker Desktop: https://www.docker.com/products/docker-desktop/
- Go, if building from source: https://go.dev/doc/install
- Git, if building from source: https://git-scm.com/downloads
- Git, required by `azd ai rle init` to download samples: https://git-scm.com/downloads

Verify:

Expand Down Expand Up @@ -81,23 +81,31 @@ az acr login --name <registry>

### 1. Initialize an environment session

Default echo session:
Select a sample and use its name for the local folder and RLE environment:

```powershell
azd ai rle init
cd .\echo_env
```

The default echo session downloads the Hugging Face `OpenEnv` repo, copies `envs/echo_env` into the session folder, and writes `.azd-rle.json` with the local `environmentName`. Existing state files that use the legacy `name` property remain supported.
`init` reads the available environments from
[rle-samples](https://github.com/sujit-kamireddy/rle-samples) and prompts you to select one.
Only the selected sample is downloaded. For example, selecting `code_rl` copies it into `.\code_rl`
and stores `code_rl` as the RLE environment name in `.azd-rle.json`.

The copied session does not keep `.git` metadata from the upstream repository.
To use a different local folder and RLE environment name, provide it before selecting a sample:

Name the copied echo session:
```powershell
azd ai rle init my_environment
```

When prompts are disabled, the required positional name selects the sample and is also used for the folder:

```powershell
azd ai rle init code_rl
azd ai rle init code_rl --no-prompt
```

The copied session does not keep `.git` metadata from the sample repository.

For an existing source folder, skip `init` and run commands directly from that folder.

### 2. Run locally
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/extensions/azure.ai.rle/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ tags:
- ai
- rle
usage: azd ai rle <command> [options]
version: 0.4.0-preview
version: 0.4.1-preview
examples:
- name: init
description: Copy the OpenEnv echo sample into a local RLE environment.
usage: azd ai rle init
description: Select an RLE sample and copy it into a new local folder.
usage: azd ai rle init [folder-name]
- name: publish
description: Build, push, and create or update the RLE environment.
usage: azd ai rle publish --version-bump major
Expand Down
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.rle/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/azure/azure-dev/cli/azd v1.25.0
github.com/fatih/color v1.18.0
github.com/gorilla/websocket v1.5.3
github.com/spf13/cobra v1.10.1
)

Expand Down
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.ai.rle/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
Expand Down
155 changes: 133 additions & 22 deletions cli/azd/extensions/azure.ai.rle/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package cmd

import (
"context"
"fmt"
"os"
"runtime"
"slices"
"strings"

"azure.ai.rle/internal/project"
Expand All @@ -20,34 +22,50 @@ type rleInitFlags struct {
}

type initAction struct {
cmd *cobra.Command
flags *rleInitFlags
envNameOverride string
cmd *cobra.Command
flags *rleInitFlags
folderName string
noPrompt bool
}

var checkoutOpenEnvEchoSampleFunc = project.CheckoutOpenEnvEchoSample
type rleSampleCatalog interface {
SampleNames() []string
Copy(sampleName string, folderName string, dest string, force bool) (string, error)
Close() error
}

var loadRleSampleCatalogFunc = func() (rleSampleCatalog, error) {
return project.LoadRleSampleCatalog()
}

func newInitCommand() *cobra.Command {
var selectRleSampleFunc = selectRleSample

func newInitCommand(noPrompt *bool) *cobra.Command {
flags := &rleInitFlags{}

cmd := &cobra.Command{
Use: "init",
Short: "Initialize a local RLE environment",
Use: "init [folder-name]",
Short: "Initialize a local RLE environment from a sample",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
envNameOverride := ""
folderName := ""
if len(args) == 1 {
envNameOverride = args[0]
folderName = args[0]
}
return (&initAction{cmd: cmd, flags: flags, envNameOverride: envNameOverride}).Run()
return (&initAction{
cmd: cmd,
flags: flags,
folderName: folderName,
noPrompt: noPrompt != nil && *noPrompt,
}).Run()
},
}

cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
var help strings.Builder
help.WriteString("Initialize a local RLE environment\n")
help.WriteString("Initialize a local RLE environment from a sample\n")
help.WriteString("Usage:\n")
help.WriteString(" rle init [environment-name] [flags]\n")
help.WriteString(" rle init [folder-name] [flags]\n")
help.WriteString("Flags:\n")
help.WriteString(" --force Overwrite generated files in an existing non-empty session directory\n")
help.WriteString(" -h, --help help for init\n")
Expand All @@ -62,32 +80,125 @@ func newInitCommand() *cobra.Command {
}

func (a *initAction) Run() error {
envName := firstNonEmpty(a.envNameOverride, "echo_env")
var err error
envName, err = project.ValidateEnvironmentName(envName)
if err != nil {
if a.noPrompt && a.folderName == "" {
return &azdext.LocalError{
Message: err.Error(),
Code: "rle_invalid_environment_name",
Message: "A sample name is required when prompts are disabled.",
Code: "rle_sample_name_required",
Category: azdext.LocalErrorCategoryUser,
Suggestion: "Use snake_case starting with a letter, for example code_rl.",
Suggestion: "Run azd ai rle init <sample-name> --no-prompt.",
}
}
folderName := a.folderName
if folderName != "" {
var err error
folderName, err = validateRleFolderName(folderName)
if err != nil {
return err
}
}

sessionDir, err := checkoutOpenEnvEchoSampleFunc(envName, ".", a.flags.force)
catalog, err := loadRleSampleCatalogFunc()
if err != nil {
return err
}
defer func() {
_ = catalog.Close()
}()
requestedSample := ""
if a.noPrompt {
requestedSample = folderName
}
sampleName, err := resolveRleSample(a.cmd.Context(), requestedSample, catalog.SampleNames())
if err != nil {
return err
}
if folderName == "" {
folderName, err = validateRleFolderName(sampleName)
if err != nil {
return err
}
}
sessionDir, err := catalog.Copy(sampleName, folderName, ".", a.flags.force)
if err != nil {
return err
}

if err := saveRleStateIn(sessionDir, defaultRleState(envName)); err != nil {
if err := saveRleStateIn(sessionDir, defaultRleState(folderName)); err != nil {
return err
}

displayDir := "." + string(os.PathSeparator) + sessionDir
if _, err := fmt.Fprintf(a.cmd.OutOrStdout(), "Copied RLE sample %q.\n", sampleName); err != nil {
return err
}
_, err = fmt.Fprint(a.cmd.OutOrStdout(), initNextSteps(displayDir, runtime.GOOS, os.Getenv("SHELL")))
return err
}

func validateRleFolderName(folderName string) (string, error) {
validated, err := project.ValidateEnvironmentName(folderName)
if err == nil {
return validated, nil
}
return "", &azdext.LocalError{
Message: err.Error(),
Code: "rle_invalid_environment_name",
Category: azdext.LocalErrorCategoryUser,
Suggestion: "Use snake_case starting with a letter, for example code_rl.",
}
}

func resolveRleSample(ctx context.Context, requestedSample string, sampleNames []string) (string, error) {
if requestedSample == "" {
return selectRleSampleFunc(ctx, sampleNames)
}
if slices.Contains(sampleNames, requestedSample) {
return requestedSample, nil
}
return "", &azdext.LocalError{
Message: fmt.Sprintf("RLE sample %q was not found.", requestedSample),
Code: "rle_sample_not_found",
Category: azdext.LocalErrorCategoryUser,
Suggestion: fmt.Sprintf("Choose one of the available samples: %s.", strings.Join(sampleNames, ", ")),
}
}

func selectRleSample(ctx context.Context, sampleNames []string) (string, error) {
if len(sampleNames) == 0 {
return "", &azdext.LocalError{
Message: "No RLE samples are available.",
Code: "rle_samples_empty",
Category: azdext.LocalErrorCategoryUser,
Suggestion: "Add a sample to the RLE samples repository, then retry.",
}
}
choices := make([]*azdext.SelectChoice, len(sampleNames))
for index, sampleName := range sampleNames {
choices[index] = &azdext.SelectChoice{Label: sampleName, Value: sampleName}
}
azdClient, err := azdext.NewAzdClient()
if err != nil {
return "", fmt.Errorf("create azd client for sample selection: %w", err)
}
defer azdClient.Close()
response, err := azdClient.Prompt().Select(azdext.WithAccessToken(ctx), &azdext.SelectRequest{
Options: &azdext.SelectOptions{
Message: "Select an RLE sample",
Choices: choices,
DisplayNumbers: new(true),
EnableFiltering: new(true),
},
})
Comment thread
farhann1 marked this conversation as resolved.
if err != nil {
return "", fmt.Errorf("select RLE sample: %w", err)
}
selectedIndex := int(response.GetValue())
if selectedIndex < 0 || selectedIndex >= len(sampleNames) {
return "", fmt.Errorf("invalid RLE sample selection index: %d", selectedIndex)
}
return sampleNames[selectedIndex], nil
}

func initNextSteps(displayDir string, goos string, shell string) string {
projectEndpoint := `https://<account>.services.ai.azure.com/api/projects/<project>`
registryEndpoint := `<registry>.azurecr.io`
Expand All @@ -111,7 +222,7 @@ func initNextSteps(displayDir string, goos string, shell string) string {
}

return fmt.Sprintf(
"Created OpenEnv-style environment at: %s\n"+
"Created RLE environment at: %s\n"+
"\nRun locally:\n"+
" cd \"%s\"\n"+
" azd ai rle run\n"+
Expand Down
Loading
Loading