diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs index f31c570508..0d3639b614 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClient.cs @@ -64,13 +64,27 @@ internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentRecord a internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentVersion agentVersion, ChatOptions? chatOptions) : this( aiProjectClient, - new AgentReference(Throw.IfNull(agentVersion).Name, agentVersion.Version), + CreateAgentReference(Throw.IfNull(agentVersion)), (agentVersion.Definition as PromptAgentDefinition)?.Model, chatOptions) { this._agentVersion = agentVersion; } + /// + /// Creates an from an . + /// Uses the agent version's version if available, otherwise defaults to "latest". + /// + /// The agent version to create a reference from. + /// An for the specified agent version. + private static AgentReference CreateAgentReference(AgentVersion agentVersion) + { + // If the version is null, empty, or whitespace, use "latest" as the default. + // This handles cases where hosted agents (like MCP agents) may not have a version assigned. + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; + return new AgentReference(agentVersion.Name, version); + } + /// public override object? GetService(Type serviceType, object? serviceKey = null) { diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs index 37ee7fa82c..18a5ba3b72 100644 --- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AzureAIProjectChatClientExtensions.cs @@ -543,9 +543,16 @@ private static ChatClientAgentOptions CreateChatClientAgentOptions(AgentVersion } } + // Use the agent version's ID if available, otherwise generate one from name and version. + // This handles cases where hosted agents (like MCP agents) may not have an ID assigned. + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; + var agentId = string.IsNullOrWhiteSpace(agentVersion.Id) + ? $"{agentVersion.Name}:{version}" + : agentVersion.Id; + var agentOptions = new ChatClientAgentOptions() { - Id = agentVersion.Id, + Id = agentId, Name = agentVersion.Name, Description = agentVersion.Description, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs index da65d53c30..447c195c83 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AzureAIProjectChatClientExtensionsTests.cs @@ -2384,6 +2384,134 @@ public async Task GetAIAgentAsync_WithUseProvidedChatClientAsIs_PreservesSetting #endregion + #region Empty Version and ID Handling Tests + + /// + /// Verify that GetAIAgentAsync handles an agent with empty version by using "latest" as fallback. + /// + [Fact] + public async Task GetAIAgentAsync_WithEmptyVersion_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { Instructions = "Test" } + }; + + // Act + ChatClientAgent agent = await client.GetAIAgentAsync(options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentRecord handles empty version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentRecordEmptyVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); + AgentRecord agentRecord = this.CreateTestAgentRecordWithEmptyVersion(); + + // Act + var agent = client.AsAIAgent(agentRecord); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles empty version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentVersionEmptyVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithEmptyVersion(); + AgentVersion agentVersion = this.CreateTestAgentVersionWithEmptyVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that GetAIAgentAsync handles an agent with whitespace-only version by using "latest" as fallback. + /// + [Fact] + public async Task GetAIAgentAsync_WithWhitespaceVersion_CreatesAgentSuccessfullyAsync() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); + var options = new ChatClientAgentOptions + { + Name = "test-agent", + ChatOptions = new ChatOptions { Instructions = "Test" } + }; + + // Act + ChatClientAgent agent = await client.GetAIAgentAsync(options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + // Verify the agent ID is generated from server-returned name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentRecord handles whitespace-only version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentRecordWhitespaceVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); + AgentRecord agentRecord = this.CreateTestAgentRecordWithWhitespaceVersion(); + + // Act + var agent = client.AsAIAgent(agentRecord); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent record name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + /// + /// Verify that AsAIAgent with AgentVersion handles whitespace-only version by using "latest" as fallback. + /// + [Fact] + public void AsAIAgent_WithAgentVersionWhitespaceVersion_CreatesAgentWithGeneratedId() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClientWithWhitespaceVersion(); + AgentVersion agentVersion = this.CreateTestAgentVersionWithWhitespaceVersion(); + + // Act + var agent = client.AsAIAgent(agentVersion); + + // Assert + Assert.NotNull(agent); + // Verify the agent ID is generated from agent version name ("agent_abc123") and "latest" + Assert.Equal("agent_abc123:latest", agent.Id); + } + + #endregion + #region ApplyToolsToAgentDefinition Tests /// @@ -2678,6 +2806,54 @@ private AgentRecord CreateTestAgentRecord(AgentDefinition? agentDefinition = nul return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJson(agentDefinition: agentDefinition)))!; } + /// + /// Creates a test AIProjectClient with empty version fields for testing hosted MCP agents. + /// + private FakeAgentClient CreateTestAgentClientWithEmptyVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, useEmptyVersion: true); + } + + /// + /// Creates a test AgentRecord with empty version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithEmptyVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithEmptyVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with empty version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithEmptyVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion()))!; + } + + /// + /// Creates a test AIProjectClient with whitespace-only version fields for testing hosted MCP agents. + /// + private FakeAgentClient CreateTestAgentClientWithWhitespaceVersion(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + { + return new FakeAgentClient(agentName, instructions, description, agentDefinitionResponse, versionMode: VersionMode.Whitespace); + } + + /// + /// Creates a test AgentRecord with whitespace-only version for testing hosted MCP agents. + /// + private AgentRecord CreateTestAgentRecordWithWhitespaceVersion(AgentDefinition? agentDefinition = null) + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(agentDefinition: agentDefinition)))!; + } + + /// + /// Creates a test AgentVersion with whitespace-only version for testing hosted MCP agents. + /// + private AgentVersion CreateTestAgentVersionWithWhitespaceVersion() + { + return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion()))!; + } + private const string OpenAPISpec = """ { "openapi": "3.0.3", @@ -2716,14 +2892,26 @@ private AgentVersion CreateTestAgentVersion() return ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!; } + /// + /// Specifies the version mode for test data generation. + /// + private enum VersionMode + { + Normal, + Empty, + Whitespace + } + /// /// Fake AIProjectClient for testing. /// private sealed class FakeAgentClient : AIProjectClient { - public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + public FakeAgentClient(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, bool useEmptyVersion = false, VersionMode versionMode = VersionMode.Normal) { - this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse); + // Handle backward compatibility with bool parameter + var effectiveVersionMode = useEmptyVersion ? VersionMode.Empty : versionMode; + this.Agents = new FakeAIProjectAgentsOperations(agentName, instructions, description, agentDefinitionResponse, effectiveVersionMode); } public override ClientConnection GetConnection(string connectionId) @@ -2739,60 +2927,82 @@ private sealed class FakeAIProjectAgentsOperations : AIProjectAgentsOperations private readonly string? _instructions; private readonly string? _description; private readonly AgentDefinition? _agentDefinition; + private readonly VersionMode _versionMode; - public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null) + public FakeAIProjectAgentsOperations(string? agentName = null, string? instructions = null, string? description = null, AgentDefinition? agentDefinitionResponse = null, VersionMode versionMode = VersionMode.Normal) { this._agentName = agentName; this._instructions = instructions; this._description = description; this._agentDefinition = agentDefinitionResponse; + this._versionMode = versionMode; + } + + private string GetAgentResponseJson() + { + return this._versionMode switch + { + VersionMode.Empty => TestDataUtil.GetAgentResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + VersionMode.Whitespace => TestDataUtil.GetAgentResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + _ => TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) + }; + } + + private string GetAgentVersionResponseJson() + { + return this._versionMode switch + { + VersionMode.Empty => TestDataUtil.GetAgentVersionResponseJsonWithEmptyVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + VersionMode.Whitespace => TestDataUtil.GetAgentVersionResponseJsonWithWhitespaceVersion(this._agentName, this._agentDefinition, this._instructions, this._description), + _ => TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description) + }; } public override ClientResult GetAgent(string agentName, RequestOptions options) { - var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))); } public override ClientResult GetAgent(string agentName, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); } public override Task GetAgentAsync(string agentName, RequestOptions options) { - var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)))); } public override Task> GetAgentAsync(string agentName, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); } public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson))); } public override ClientResult CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200)); } public override Task CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200, BinaryData.FromString(responseJson)))); } public override Task> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default) { - var responseJson = TestDataUtil.GetAgentVersionResponseJson(this._agentName, this._agentDefinition, this._instructions, this._description); + var responseJson = this.GetAgentVersionResponseJson(); return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(responseJson))!, new MockPipelineResponse(200))); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs index c65d10de43..8471ddbcf1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs +++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/TestDataUtil.cs @@ -52,6 +52,70 @@ public static string GetAgentVersionResponseJson(string? agentName = null, Agent return json; } + /// + /// Gets the agent version response JSON with empty version and ID fields for testing hosted agents like MCP agents. + /// + public static string GetAgentVersionResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentVersionResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Remove the version and id fields to simulate hosted agents without version + json = json.Replace("\"version\": \"1\",", "\"version\": \"\","); + json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\","); + return json; + } + + /// + /// Gets the agent response JSON with empty version and ID fields in the latest version for testing hosted agents like MCP agents. + /// + public static string GetAgentResponseJsonWithEmptyVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Remove the version and id fields to simulate hosted agents without version + json = json.Replace("\"version\": \"1\",", "\"version\": \"\","); + json = json.Replace("\"id\": \"agent_abc123:1\",", "\"id\": \"\","); + return json; + } + + /// + /// Gets the agent version response JSON with whitespace-only version and ID fields for testing hosted agents like MCP agents. + /// + public static string GetAgentVersionResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentVersionResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Use whitespace-only version and id fields to simulate hosted agents without version + return json + .Replace("\"version\": \"1\",", "\"version\": \" \",") + .Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \","); + } + + /// + /// Gets the agent response JSON with whitespace-only version and ID fields in the latest version for testing hosted agents like MCP agents. + /// + public static string GetAgentResponseJsonWithWhitespaceVersion(string? agentName = null, AgentDefinition? agentDefinition = null, string? instructions = null, string? description = null) + { + var json = s_agentResponseJson; + json = ApplyAgentName(json, agentName); + json = ApplyAgentDefinition(json, agentDefinition); + json = ApplyInstructions(json, instructions); + json = ApplyDescription(json, description); + // Use whitespace-only version and id fields to simulate hosted agents without version + return json + .Replace("\"version\": \"1\",", "\"version\": \" \",") + .Replace("\"id\": \"agent_abc123:1\",", "\"id\": \" \","); + } + /// /// Gets the OpenAI default response JSON with optional placeholder replacements applied. ///