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
48 changes: 43 additions & 5 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type invokeFlags struct {
newSession bool
conversation string
newConversation bool
protocol string
}

type InvokeAction struct {
Expand Down Expand Up @@ -70,6 +71,9 @@ session automatically. Pass --new-session to force a reset.`,
# Invoke a specific remote agent by name
azd ai agent invoke my-agent "Hello!"

# Invoke using a specific protocol
azd ai agent invoke --protocol invocations "Hello!"

# Invoke with a file as the request body
azd ai agent invoke -f request.json

Expand Down Expand Up @@ -125,13 +129,28 @@ session automatically. Pass --new-session to force a reset.`,
)
}

if flags.protocol != "" {
switch agent_api.AgentProtocol(flags.protocol) {
case agent_api.AgentProtocolResponses,
agent_api.AgentProtocolInvocations:
// valid
default:
return exterrors.Validation(
exterrors.CodeInvalidParameter,
fmt.Sprintf("unsupported protocol %q", flags.protocol),
"supported protocols are: responses, invocations",
)
}
}

action := &InvokeAction{flags: flags}
return action.Run(ctx)
},
}

cmd.Flags().BoolVarP(&flags.local, "local", "l", false, "Invoke on localhost instead of Foundry")
cmd.Flags().StringVarP(&flags.inputFile, "input-file", "f", "", "Path to a file whose contents are sent as the request body")
cmd.Flags().StringVarP(&flags.protocol, "protocol", "p", "", "Protocol to use: responses (default) or invocations")
cmd.Flags().IntVar(&flags.port, "port", DefaultPort, "Local server port")
cmd.Flags().IntVarP(&flags.timeout, "timeout", "t", 120, "Request timeout in seconds (0 for no timeout)")
cmd.Flags().StringVarP(&flags.session, "session-id", "s", "", "Explicit session ID override")
Expand All @@ -143,8 +162,9 @@ session automatically. Pass --new-session to force a reset.`,
}

func (a *InvokeAction) Run(ctx context.Context) error {
protocol := a.resolveProtocol(ctx)

if a.flags.local {
protocol := a.resolveLocalProtocol(ctx)
switch protocol {
case agent_api.AgentProtocolInvocations:
return a.invocationsLocal(ctx)
Expand All @@ -154,15 +174,33 @@ func (a *InvokeAction) Run(ctx context.Context) error {
}

// Remote: only allow the invocations protocol when vnext is enabled.
if isVNextEnabled(ctx) {
protocol := a.resolveRemoteProtocol(ctx)
if protocol == agent_api.AgentProtocolInvocations {
return a.invocationsRemote(ctx)
if protocol == agent_api.AgentProtocolInvocations {
if !isVNextEnabled(ctx) {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"invocations protocol for remote agents requires vnext to be enabled",
"enable vnext or use --protocol responses",
)
}
return a.invocationsRemote(ctx)
}
Comment thread
glharper marked this conversation as resolved.
return a.responsesRemote(ctx)
}

// resolveProtocol returns the protocol to use for this invocation.
// The explicit --protocol flag takes priority; otherwise the protocol
// is auto-detected from agent.yaml (local or remote).
func (a *InvokeAction) resolveProtocol(ctx context.Context) agent_api.AgentProtocol {
if a.flags.protocol != "" {
return agent_api.AgentProtocol(a.flags.protocol)
}

if a.flags.local {
return a.resolveLocalProtocol(ctx)
}
return a.resolveRemoteProtocol(ctx)
}

// resolveRemoteProtocol determines the protocol for remote invocation from agent.yaml.
func (a *InvokeAction) resolveRemoteProtocol(ctx context.Context) agent_api.AgentProtocol {
azdClient, err := azdext.NewAzdClient()
Expand Down
88 changes: 88 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"strings"
"testing"
"time"

"azureaiagent/internal/pkg/agents/agent_api"
)

func TestReadSSEStream(t *testing.T) {
Expand Down Expand Up @@ -251,6 +253,92 @@ func TestHttpTimeout(t *testing.T) {
}
}

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

tests := []struct {
name string
protocol string
want agent_api.AgentProtocol
}{
{
name: "explicit invocations",
protocol: "invocations",
want: agent_api.AgentProtocolInvocations,
},
{
name: "explicit responses",
protocol: "responses",
want: agent_api.AgentProtocolResponses,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
action := &InvokeAction{
flags: &invokeFlags{protocol: tt.protocol},
}
// resolveProtocol with an explicit flag should return it directly
// without trying to read agent.yaml (which would fail in tests).
got := action.resolveProtocol(t.Context())
if got != tt.want {
t.Errorf("resolveProtocol() = %q, want %q", got, tt.want)
}
})
}
}

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

tests := []struct {
name string
args []string
wantErr bool
errSub string
}{
{
name: "valid responses",
args: []string{"--protocol", "responses", "hello"},
wantErr: false,
},
{
name: "valid invocations",
args: []string{"--protocol", "invocations", "hello"},
wantErr: false,
},
{
name: "invalid protocol",
args: []string{"--protocol", "bogus", "hello"},
wantErr: true,
errSub: "unsupported protocol",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
cmd := newInvokeCommand()
cmd.SetArgs(tt.args)
err := cmd.Execute()
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), tt.errSub) {
t.Errorf("error %q should contain %q", err.Error(), tt.errSub)
}
}
// For valid protocols the command will still fail (no azd host),
// but the error should NOT be about an invalid protocol.
if !tt.wantErr && err != nil && strings.Contains(err.Error(), "unsupported protocol") {
t.Errorf("unexpected validation error: %v", err)
}
})
}
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ template:
- full
model:
id: gpt-4o
publisher: azure
provider: azure
options:
temperature: 0.8
maxTokens: 4000
maxOutputTokens: 4000
topP: 0.95
instructions: |
You are a helpful testing assistant.
Expand Down
Loading