-
Notifications
You must be signed in to change notification settings - Fork 341
feat(ai-agents): add --inspector-port flag to azd ai agent run #9366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ import ( | |
| "time" | ||
|
|
||
| "azureaiagent/internal/cmd/nextstep" | ||
| "azureaiagent/internal/exterrors" | ||
| "azureaiagent/internal/pkg/agents/agent_yaml" | ||
| "azureaiagent/internal/project" | ||
|
|
||
|
|
@@ -36,15 +37,27 @@ import ( | |
| const ( | ||
| agentInspectorExtensionID = "azure.ai.inspector" | ||
| agentInspectorReadyPollPeriod = 250 * time.Millisecond | ||
| // defaultInspectorUIPort mirrors the default UI port of the | ||
| // azure.ai.inspector extension. It is referenced only in help text: when | ||
| // --inspector-port is unset we do not forward the flag, so the inspector | ||
| // extension remains the single source of truth for the actual default. | ||
| defaultInspectorUIPort = 8087 | ||
| ) | ||
|
|
||
| type runFlags struct { | ||
| port int | ||
| name string | ||
| startCommand string | ||
| noInspector bool | ||
| noClient bool | ||
| channel string | ||
| port int | ||
| // inspectorPort is the port the Agent Inspector UI listens on. When | ||
| // inspectorPortSet is false the flag was not supplied and | ||
| // --inspector-port is not forwarded to the inspector. | ||
| inspectorPort int | ||
| // inspectorPortSet records whether --inspector-port was explicitly | ||
| // supplied, so an explicit (and invalid) 0 is not mistaken for unset. | ||
| inspectorPortSet bool | ||
| name string | ||
| startCommand string | ||
| noInspector bool | ||
| noClient bool | ||
| channel string | ||
| } | ||
|
|
||
| type environmentEntry struct { | ||
|
|
@@ -83,6 +96,9 @@ Playground for activity agents. Use --no-client to skip this.`, | |
| # Start on a custom port | ||
| azd ai agent run --port 9090 | ||
|
|
||
| # Start a second agent with its own Agent Inspector UI port | ||
| azd ai agent run --port 9091 --inspector-port 9002 | ||
|
|
||
| # Start without opening a local client | ||
| azd ai agent run --no-client | ||
|
|
||
|
|
@@ -93,12 +109,15 @@ Playground for activity agents. Use --no-client to skip this.`, | |
| if len(args) > 0 { | ||
| flags.name = args[0] | ||
| } | ||
| flags.inspectorPortSet = cmd.Flags().Changed("inspector-port") | ||
| ctx := azdext.WithAccessToken(cmd.Context()) | ||
| return runRun(ctx, flags, extCtx.NoPrompt) | ||
| }, | ||
| } | ||
|
|
||
| cmd.Flags().IntVarP(&flags.port, "port", "p", DefaultPort, "Port to listen on") | ||
| cmd.Flags().IntVar(&flags.inspectorPort, "inspector-port", 0, | ||
| fmt.Sprintf("Port the Agent Inspector UI listens on (default: %d)", defaultInspectorUIPort)) | ||
| cmd.Flags().StringVarP(&flags.startCommand, "start-command", "c", "", | ||
| "Explicit startup command (overrides azure.yaml and auto-detection)") | ||
| cmd.Flags().BoolVar(&flags.noInspector, "no-inspector", false, "Do not open the local client (Agent Inspector or Playground)") | ||
|
|
@@ -115,6 +134,10 @@ Playground for activity agents. Use --no-client to skip this.`, | |
| } | ||
|
|
||
| func runRun(ctx context.Context, flags *runFlags, noPrompt bool) error { | ||
| if err := validateInspectorPort(flags.inspectorPort, flags.inspectorPortSet); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| azdClient, err := azdext.NewAzdClient() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create azd client: %w", err) | ||
|
|
@@ -309,6 +332,7 @@ func runRun(ctx context.Context, flags *runFlags, noPrompt bool) error { | |
| ctx, | ||
| azdClient.Workflow(), | ||
| flags.port, | ||
| flags.inspectorPort, | ||
| suppressClient, | ||
| inspectorInstalled, | ||
| inspectorInstallErr, | ||
|
|
@@ -366,6 +390,7 @@ func handleInspectorAutoLaunch( | |
| ctx context.Context, | ||
| workflow azdext.WorkflowServiceClient, | ||
| agentPort int, | ||
| inspectorPort int, | ||
| noInspector bool, | ||
| inspectorInstalled bool, | ||
| inspectorInstallErr error, | ||
|
|
@@ -386,6 +411,7 @@ func handleInspectorAutoLaunch( | |
| ctx, | ||
| workflow, | ||
| agentPort, | ||
| inspectorPort, | ||
| agentInspectorReadyPollPeriod, | ||
| stderr, | ||
| ) | ||
|
|
@@ -395,6 +421,7 @@ func startInspectorAfterAgentReadyWithOptions( | |
| ctx context.Context, | ||
| workflow azdext.WorkflowServiceClient, | ||
| agentPort int, | ||
| inspectorPort int, | ||
| pollPeriod time.Duration, | ||
| stderr io.Writer, | ||
| ) { | ||
|
|
@@ -411,7 +438,7 @@ func startInspectorAfterAgentReadyWithOptions( | |
| return | ||
| } | ||
|
|
||
| if err := launchInspector(ctx, workflow, agentPort); err != nil && !isContextCancellation(err) { | ||
| if err := launchInspector(ctx, workflow, agentPort, inspectorPort); err != nil && !isContextCancellation(err) { | ||
| fmt.Fprintln(stderr, inspectorLaunchWarning(err)) | ||
| } | ||
| }() | ||
|
|
@@ -441,21 +468,33 @@ func waitForLocalPort(ctx context.Context, port int, pollPeriod time.Duration) e | |
| } | ||
| } | ||
|
|
||
| func launchInspector(ctx context.Context, workflow azdext.WorkflowServiceClient, agentPort int) error { | ||
| func launchInspector( | ||
| ctx context.Context, | ||
| workflow azdext.WorkflowServiceClient, | ||
| agentPort int, | ||
| inspectorPort int, | ||
| ) error { | ||
| args := []string{ | ||
| "ai", | ||
| "inspector", | ||
| "launch", | ||
| "--port", | ||
| strconv.Itoa(agentPort), | ||
| } | ||
| // Only forward --inspector-port when the user asked for a specific UI port, | ||
| // so the inspector extension keeps applying its own default otherwise. | ||
| if inspectorPort > 0 { | ||
| args = append(args, "--inspector-port", strconv.Itoa(inspectorPort)) | ||
| } | ||
| args = append(args, "--silent") | ||
|
|
||
| _, err := workflow.Run(ctx, &azdext.RunWorkflowRequest{ | ||
| Workflow: &azdext.Workflow{ | ||
| Name: "launch-agent-inspector", | ||
| Steps: []*azdext.WorkflowStep{ | ||
| { | ||
| Command: &azdext.WorkflowCommand{ | ||
| Args: []string{ | ||
| "ai", | ||
| "inspector", | ||
| "launch", | ||
| "--port", | ||
| strconv.Itoa(agentPort), | ||
| "--silent", | ||
| }, | ||
| Args: args, | ||
| }, | ||
| }, | ||
| }, | ||
|
|
@@ -464,6 +503,23 @@ func launchInspector(ctx context.Context, workflow azdext.WorkflowServiceClient, | |
| return err | ||
| } | ||
|
|
||
| // validateInspectorPort rejects out-of-range --inspector-port values. When the | ||
| // flag was not supplied (set is false) the inspector extension applies its own | ||
| // default UI port. An explicitly supplied zero is out of range and rejected. | ||
| // Validating here keeps an invalid value from being silently dropped or failing | ||
| // later inside the inspector with a less obvious message. | ||
| func validateInspectorPort(inspectorPort int, set bool) error { | ||
| if !set || (inspectorPort >= 1 && inspectorPort <= 65535) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider rejecting Passing the agent port into this function would change the signature the new tests use, so a separate check in |
||
| return nil | ||
| } | ||
|
|
||
| return exterrors.Validation( | ||
| exterrors.CodeInvalidParameter, | ||
| fmt.Sprintf("--inspector-port must be between 1 and 65535, got %d", inspectorPort), | ||
| "pass a free TCP port, for example --inspector-port 9002", | ||
| ) | ||
| } | ||
|
|
||
| func isInspectorExtensionInstalled(ctx context.Context, azdClient *azdext.AzdClient) (bool, error) { | ||
| configHelper, err := azdext.NewConfigHelper(azdClient) | ||
| if err != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validateInspectorPortruns before we know whether the inspector will launch at all, so two paths accept--inspector-portand then drop it:--no-client(or the deprecated--no-inspector) makeshandleInspectorAutoLaunchreturn early, so the port never reaches the workflow.handlePlaygroundAutoLaunchbranch instead, which has no inspector port at all.The doc comment on
validateInspectorPortsays the reason to validate here is to stop a value from being silently dropped, so the suppressed-client case looks like it deserves the same treatment.init.goalready usesexterrors.CodeConflictingArgumentswithcmd.Flags().Changed(...)for this shape of conflict.The activity agent case is murkier, since the user can't always predict which branch they'll land on, so a stderr warning may fit better there than a hard error.