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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
const DefaultAgentAPIVersion = "2025-05-15-preview"
const DefaultVNextAgentAPIVersion = "2025-11-15-preview"

// ConversationsAPIVersion is the API version used by the Foundry Conversations protocol.
const ConversationsAPIVersion = "v1"

// AgentContext holds the common properties of a hosted agent.
type AgentContext struct {
ProjectEndpoint string
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func resolveConversationID(
agentName string,
explicit string,
forceNew bool,
endpoint string,
projectEndpoint string,
bearerToken string,
) (string, error) {
if explicit != "" {
Expand All @@ -386,7 +386,7 @@ func resolveConversationID(
}

// Create and persist a new conversation for multi-turn memory.
newConvID, err := createConversation(ctx, endpoint, bearerToken)
newConvID, err := createConversation(ctx, projectEndpoint, agentName, bearerToken)
if err != nil {
return "", fmt.Errorf("failed to create conversation: %w", err)
}
Expand Down
15 changes: 9 additions & 6 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error {
return fmt.Errorf("agent name is required; provide as the first argument or define an azure.ai.agent service in azure.yaml")
}

endpoint, err := resolveAgentEndpoint(ctx, "", "")
projectEndpoint, err := resolveAgentEndpoint(ctx, "", "")
if err != nil {
return err
}
Expand Down Expand Up @@ -417,7 +417,7 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error {
name,
a.flags.conversation,
a.flags.newConversation,
endpoint,
projectEndpoint,
token.Token,
)
if err != nil {
Expand All @@ -440,10 +440,10 @@ func (a *InvokeAction) responsesRemote(ctx context.Context) error {
if vnext {
url = fmt.Sprintf(
"%s/agents/%s/endpoint/protocols/openai/responses?api-version=%s",
endpoint, name, DefaultAgentAPIVersion,
projectEndpoint, name, DefaultAgentAPIVersion,
)
} else {
url = fmt.Sprintf("%s/openai/responses?api-version=%s", endpoint, DefaultAgentAPIVersion)
url = fmt.Sprintf("%s/openai/responses?api-version=%s", projectEndpoint, DefaultAgentAPIVersion)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
Expand Down Expand Up @@ -911,8 +911,11 @@ func handleInvocationLRO(
}

// createConversation creates a new Foundry conversation for multi-turn memory.
func createConversation(ctx context.Context, endpoint string, bearerToken string) (string, error) {
url := fmt.Sprintf("%s/openai/conversations?api-version=%s", endpoint, DefaultAgentAPIVersion)
func createConversation(ctx context.Context, projectEndpoint, agentName, bearerToken string) (string, error) {
url := fmt.Sprintf(
"%s/agents/%s/endpoint/protocols/openai/conversations?api-version=%s",
projectEndpoint, agentName, ConversationsAPIVersion,
)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader([]byte("{}")))
Comment thread
therealjohn marked this conversation as resolved.
if err != nil {
return "", err
Expand Down
124 changes: 124 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 @@ -828,3 +828,127 @@ type pollStep struct {
retryAfter string
repeat bool
}

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

tests := []struct {
name string
agentName string
statusCode int
body string
wantID string
wantErr bool
errContain string
}{
{
name: "success returns conversation ID",
agentName: "my-agent",
statusCode: 200,
body: `{"id":"conv-abc123"}`,
wantID: "conv-abc123",
},
{
name: "HTTP 400 returns error",
agentName: "my-agent",
statusCode: 400,
body: `{"error":"bad request"}`,
wantErr: true,
errContain: "failed with HTTP 400",
},
{
name: "HTTP 500 returns error",
agentName: "my-agent",
statusCode: 500,
body: `{"error":"internal"}`,
wantErr: true,
errContain: "failed with HTTP 500",
},
{
name: "response missing id field",
agentName: "my-agent",
statusCode: 200,
body: `{"status":"ok"}`,
wantErr: true,
errContain: "missing 'id' field",
},
{
name: "response with non-string id",
agentName: "my-agent",
statusCode: 200,
body: `{"id":12345}`,
wantErr: true,
errContain: "missing 'id' field",
},
{
name: "invalid JSON response",
agentName: "my-agent",
statusCode: 200,
body: `not-json`,
wantErr: true,
errContain: "invalid character",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request method
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}

// Verify path includes the agent name and conversations endpoint
wantPath := "/agents/" + tt.agentName +
"/endpoint/protocols/openai/conversations"
if r.URL.Path != wantPath {
t.Errorf("path = %s, want %s", r.URL.Path, wantPath)
}

// Verify api-version query parameter uses the constant
if got := r.URL.Query().Get("api-version"); got != ConversationsAPIVersion {
t.Errorf("api-version = %q, want %q", got, ConversationsAPIVersion)
}

// Verify auth header
if got := r.Header.Get("Authorization"); got != "Bearer test-token" {
t.Errorf("Authorization = %q, want %q", got, "Bearer test-token")
}

// Verify content type
if got := r.Header.Get("Content-Type"); got != "application/json" {
t.Errorf("Content-Type = %q, want %q", got, "application/json")
}

w.WriteHeader(tt.statusCode)
_, _ = w.Write([]byte(tt.body))
}))
defer srv.Close()

id, err := createConversation(
t.Context(), srv.URL, tt.agentName, "test-token",
)

if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errContain != "" &&
!strings.Contains(err.Error(), tt.errContain) {
t.Errorf("error = %q, want substring %q",
err.Error(), tt.errContain)
}
return
}

if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if id != tt.wantID {
t.Errorf("id = %q, want %q", id, tt.wantID)
}
})
}
}
Loading