From 6dc65dbaa194fa57688ed9bfc149e95a335c395a Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:32:45 +0000 Subject: [PATCH 1/5] .NET: Increase credential timeout for Integration Tests (#4472) * Increase credential timeout for Integration Tests * Fix format error. * Update further tests * Fix comment * Rename credentials file and class. * Fix broken reference. --- dotnet/agent-framework-dotnet.slnx | 4 +++ dotnet/eng/MSBuild/Shared.props | 3 ++ .../README.md | 9 ++++++ .../TestAzureCliCredentials.cs | 28 +++++++++++++++++++ .../AIProjectClientCreateTests.cs | 3 +- .../AIProjectClientFixture.cs | 5 ++-- .../AzureAI.IntegrationTests.csproj | 1 + ...AIAgentsPersistent.IntegrationTests.csproj | 1 + .../AzureAIAgentsPersistentCreateTests.cs | 3 +- .../AzureAIAgentsPersistentFixture.cs | 3 +- ...nts.AI.DurableTask.IntegrationTests.csproj | 1 + .../TestHelper.cs | 4 +-- .../FoundryMemoryProviderTests.cs | 3 +- ...s.AI.FoundryMemory.IntegrationTests.csproj | 1 + .../Agents/FunctionToolAgentProvider.cs | 3 +- .../Agents/MarketingAgentProvider.cs | 3 +- .../Agents/MathChatAgentProvider.cs | 3 +- .../Agents/PoemAgentProvider.cs | 3 +- .../Agents/TestAgentProvider.cs | 3 +- .../Agents/VisionAgentProvider.cs | 3 +- .../AzureAgentProviderTest.cs | 4 +-- .../Framework/IntegrationTest.cs | 3 +- .../MediaInputTest.cs | 4 +-- ...kflows.Declarative.IntegrationTests.csproj | 1 + 24 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md create mode 100644 dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 9801ccc105..d5773ee9d9 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -413,6 +413,10 @@ + + + + diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props index 9b4771a64e..94ac5b417b 100644 --- a/dotnet/eng/MSBuild/Shared.props +++ b/dotnet/eng/MSBuild/Shared.props @@ -8,6 +8,9 @@ + + + diff --git a/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md b/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md new file mode 100644 index 0000000000..e26295ed7f --- /dev/null +++ b/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md @@ -0,0 +1,9 @@ +# Integration Tests Azure Credentials + +Adds a helper for loading Azure credentials in integration tests. + +```xml + + true + +``` diff --git a/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs b/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs new file mode 100644 index 0000000000..f1c83ce1f2 --- /dev/null +++ b/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0005 // This is required in some projects and not in others. +using System; +#pragma warning restore IDE0005 +using Azure.Identity; + +namespace Shared.IntegrationTests; + +/// +/// Provides credential instances for integration tests with +/// increased timeouts to avoid CI pipeline authentication failures. +/// +internal static class TestAzureCliCredentials +{ + /// + /// The default timeout for Azure CLI credential operations. + /// Increased from the default (~13s) to accommodate CI pipeline latency. + /// + private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(60); + + /// + /// Creates a new with an increased process timeout + /// suitable for CI environments. + /// + public static AzureCliCredential CreateAzureCliCredential() => + new(new AzureCliCredentialOptions { ProcessTimeout = s_processTimeout }); +} diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs index ec4103f6a8..a6691a41bd 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests.Support; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Files; @@ -17,7 +16,7 @@ namespace AzureAI.IntegrationTests; public class AIProjectClientCreateTests { - private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); [Theory] [InlineData("CreateWithChatClientAgentOptionsAsync")] diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs index 64a8e86c8a..2485176cd3 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs @@ -8,7 +8,6 @@ using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Responses; @@ -168,13 +167,13 @@ public class AIProjectClientFixture : IChatClientAgentFixture public virtual async Task InitializeAsync() { - this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); } public async Task InitializeAsync(ChatClientAgentOptions options) { - this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(options); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj index 83f65051d2..bbe03693ea 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj +++ b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj @@ -2,6 +2,7 @@ True + True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj index 4078342410..9cd72a7e77 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj @@ -2,6 +2,7 @@ True + True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs index f750b5a8e7..6b29bb4b08 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading.Tasks; using AgentConformance.IntegrationTests.Support; using Azure.AI.Agents.Persistent; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ namespace AzureAIAgentsPersistent.IntegrationTests; public class AzureAIAgentsPersistentCreateTests { - private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential()); + private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); [Theory] [InlineData("CreateWithChatClientAgentOptionsAsync")] diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs index 5de4192557..ff5e96c4f1 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs @@ -6,7 +6,6 @@ using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Azure; using Azure.AI.Agents.Persistent; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; @@ -96,7 +95,7 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture public async Task InitializeAsync() { - this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential()); + this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj index ac4f52e3eb..adc184e510 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj @@ -3,6 +3,7 @@ $(TargetFrameworksCore) enable + True diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs index 295277021b..ba73c7fbe4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs @@ -2,7 +2,6 @@ using Azure; using Azure.AI.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; @@ -14,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenAI.Chat; +using Shared.IntegrationTests; using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; @@ -166,7 +166,7 @@ internal sealed class TestHelper : IDisposable AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey) ? new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureKeyCredential(azureOpenAiKey)) - : new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureCliCredential()); + : new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); return client.GetChatClient(azureOpenAiDeploymentName); } diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs index 4b1838335c..9b3c95c5c2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using Azure.AI.Projects; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.IntegrationTests; @@ -41,7 +40,7 @@ public sealed class FoundryMemoryProviderTests : IDisposable if (!string.IsNullOrWhiteSpace(endpoint) && !string.IsNullOrWhiteSpace(memoryStoreName)) { - this._client = new AIProjectClient(new Uri(endpoint), new AzureCliCredential()); + this._client = new AIProjectClient(new Uri(endpoint), TestAzureCliCredentials.CreateAzureCliCredential()); this._memoryStoreName = memoryStoreName; this._deploymentName = deploymentName ?? "gpt-4.1-mini"; } diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj index 4bf96a5b35..af184142ca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj @@ -2,6 +2,7 @@ True + True diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs index 8198618b65..98243dc4d3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using OpenAI.Responses; @@ -25,7 +24,7 @@ internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AIFunctionFactory.Create(menuPlugin.GetItemPrice), ]; - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs index f84a40ae23..693d99b638 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class MarketingAgentProvider(IConfiguration configuration) : Age { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs index 92cea7d76a..91d63404bd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class MathChatAgentProvider(IConfiguration configuration) : Agen { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs index 8882709a03..1b79e4e25e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentPro { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs index 03b201d440..dcb09a4798 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class TestAgentProvider(IConfiguration configuration) : AgentPro { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs index 1c09ea9247..0d95342264 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class VisionAgentProvider(IConfiguration configuration) : AgentP { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs index da3f6f2fd5..7ec01b6588 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs @@ -2,9 +2,9 @@ using System.Linq; using System.Threading.Tasks; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; +using Shared.IntegrationTests; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -15,7 +15,7 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati public async Task ConversationTestAsync() { // Arrange - AzureAgentProvider provider = new(this.TestEndpoint, new AzureCliCredential()); + AzureAgentProvider provider = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); // Act string conversationId = await provider.CreateConversationAsync(); // Assert diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs index 517dba9e4e..6cabd4983b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; @@ -68,7 +67,7 @@ public abstract class IntegrationTest : IDisposable protected async ValueTask CreateOptionsAsync(bool externalConversation, IMcpToolHandler? mcpToolProvider, params IEnumerable functionTools) { AzureAgentProvider agentProvider = - new(this.TestEndpoint, new AzureCliCredential()) + new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()) { Functions = functionTools, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index da30db6f98..244e4f0eb3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -4,11 +4,11 @@ using System; using System.IO; using System.Threading.Tasks; using Azure.AI.Projects; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; using OpenAI.Files; +using Shared.IntegrationTests; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -77,7 +77,7 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o { // Arrange byte[] fileData = ReadLocalFile(fileSource); - AIProjectClient client = new(this.TestEndpoint, new AzureCliCredential()); + AIProjectClient client = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); using MemoryStream contentStream = new(fileData); OpenAIFileClient fileClient = client.GetProjectOpenAIClient().GetOpenAIFileClient(); OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, documentName, FileUploadPurpose.Assistants); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj index 92e09fcebb..d37dd58c8c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj @@ -5,6 +5,7 @@ true true true + True From 56bba795cbc873b3d5961a15c9f4b2528884aafd Mon Sep 17 00:00:00 2001 From: Leo Yao Date: Thu, 5 Mar 2026 03:43:24 -0800 Subject: [PATCH 2/5] .NET: Add foundry extension samples for python and dotnet (#4359) * Add foundry extension samples for python and dotnet * Align foundry extension samples with existing hosted agent patterns - Fix Python multiagent indentation bug (from_agent_framework ran in both modes) - Remove hardcoded personal endpoint from appsettings.Development.json - Rename .NET folders/projects to PascalCase (FoundryMultiAgent, FoundrySingleAgent) - Upgrade .NET multiagent from net9.0 to net10.0 - Add ManagePackageVersionsCentrally=false and analyzer blocks to .csproj files - Replace wildcard package versions with fixed versions - Use alpine Docker images and standard build pattern - Align agent.yaml structure (template nesting, displayName, resources, authors) - Convert .NET multiagent from namespace/class to top-level statements - Add run-requests.http for multiagent sample - Fix Python requirements.txt (remove dev deps, add agent-framework) - Add proper copyright headers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align foundry samples: fix builds, upgrade AgentServer to beta.8 - Fix TargetFrameworks (plural) to override inherited net472 from Directory.Build.props - Upgrade Azure.AI.AgentServer.AgentFramework to 1.0.0-beta.8 (latest) - Bump OpenTelemetry packages to 1.12.0 (required by beta.8) - Fix Roslynator/format errors (imports ordering, BOM, sealed record, target-typed new) - Verified with docker dotnet format (matching CI pipeline) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor hosted samples to use AIProjectClient.CreateAIAgentAsync Replace PersistentAgentsClient and manual AzureOpenAIClient setup with AIProjectClient.CreateAIAgentAsync() from Microsoft.Agents.AI.AzureAI. - FoundryMultiAgent: Remove Azure.AI.Agents.Persistent, use CreateAIAgentAsync for Writer and Reviewer agents with cleanup in finally block - FoundrySingleAgent: Remove manual GetConnection/AzureOpenAIClient chain, use CreateAIAgentAsync with hotel search tool - Update csproj: add Microsoft.Agents.AI.AzureAI, remove unused packages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update READMEs to reflect AIProjectClient.CreateAIAgentAsync usage - Reference Microsoft.Agents.AI.AzureAI and Microsoft.Agents.AI.Workflows packages - Add Azure AI Developer role requirement for agents/write data action - Replace PersistentAgentsClient references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add HostedAgents READMEs and Foundry samples to solution - Create dotnet/samples/05-end-to-end/HostedAgents/README.md with sample index - Create python/samples/05-end-to-end/hosted_agents/README.md with sample index - Add FoundryMultiAgent and FoundrySingleAgent to agent-framework-dotnet.slnx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python linting: reorder imports before load_dotenv, remove trailing whitespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update uv.lock to match latest package versions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix trailing whitespace in foundry_single_agent agent.yaml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Exclude dotnet.microsoft.com from link checker This domain intermittently times out in CI, causing flaky markdown link check failures unrelated to PR changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align env vars to AZURE_AI_PROJECT_ENDPOINT and default model to gpt-4o-mini Addresses PR review feedback: - Rename PROJECT_ENDPOINT to AZURE_AI_PROJECT_ENDPOINT across all Foundry samples (dotnet + python) to match existing samples - Change default model from gpt-4.1-mini to gpt-4o-mini consistently Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Skip flaky test CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync Tracked in #4398 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove Python foundry samples from PR scope Python hosted agent samples need further alignment with the azure-ai package conventions. Removing from this PR to ship .NET samples first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Narrow linkspector exclusion to dotnet.microsoft.com/download only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Leo Yao Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/.linkspector.yml | 1 + dotnet/agent-framework-dotnet.slnx | 2 + .../HostedAgents/FoundryMultiAgent/Dockerfile | 20 +++ .../FoundryMultiAgent.csproj | 76 ++++++++ .../HostedAgents/FoundryMultiAgent/Program.cs | 49 +++++ .../HostedAgents/FoundryMultiAgent/README.md | 168 ++++++++++++++++++ .../HostedAgents/FoundryMultiAgent/agent.yaml | 31 ++++ .../appsettings.Development.json | 4 + .../FoundryMultiAgent/run-requests.http | 34 ++++ .../FoundrySingleAgent/Dockerfile | 20 +++ .../FoundrySingleAgent.csproj | 67 +++++++ .../FoundrySingleAgent/Program.cs | 128 +++++++++++++ .../HostedAgents/FoundrySingleAgent/README.md | 167 +++++++++++++++++ .../FoundrySingleAgent/agent.yaml | 32 ++++ .../FoundrySingleAgent/run-requests.http | 52 ++++++ .../05-end-to-end/HostedAgents/README.md | 6 +- .../ObservabilityTests.cs | 2 +- 17 files changed, 856 insertions(+), 3 deletions(-) create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml create mode 100644 dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml index eb365c2982..c0da7d36b2 100644 --- a/.github/.linkspector.yml +++ b/.github/.linkspector.yml @@ -20,6 +20,7 @@ ignorePatterns: - pattern: "https://your-resource.openai.azure.com/" - pattern: "http://host.docker.internal" - pattern: "https://openai.github.io/openai-agents-js/openai/agents/classes/" + - pattern: "https:\/\/dotnet.microsoft.com\/download" # excludedDirs: # Folders which include links to localhost, since it's not ignored with regular expressions baseUrl: https://github.com/microsoft/agent-framework/ diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d5773ee9d9..0f105d4a80 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -286,6 +286,8 @@ + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile new file mode 100644 index 0000000000..fc3d3a1a5b --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Dockerfile @@ -0,0 +1,20 @@ +# Build the application +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# Copy files from the current directory on the host to the working directory in the container +COPY . . + +RUN dotnet restore +RUN dotnet build -c Release --no-restore +RUN dotnet publish -c Release --no-build -o /app -f net10.0 + +# Run the application +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app + +# Copy everything needed to run the app from the "build" stage. +COPY --from=build /app . + +EXPOSE 8088 +ENTRYPOINT ["dotnet", "FoundryMultiAgent.dll"] diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj new file mode 100644 index 0000000000..b2fb41ac5e --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/FoundryMultiAgent.csproj @@ -0,0 +1,76 @@ + + + Exe + net10.0 + enable + enable + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + PreserveNewest + + + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs new file mode 100644 index 0000000000..138efb0096 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/Program.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a multi-agent workflow with Writer and Reviewer agents +// using Azure AI Foundry AIProjectClient and the Agent Framework WorkflowBuilder. + +using Azure.AI.AgentServer.AgentFramework.Extensions; +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +Console.WriteLine($"Using Azure AI endpoint: {endpoint}"); +Console.WriteLine($"Using model deployment: {deploymentName}"); + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential()); + +// Create Foundry agents +AIAgent writerAgent = await aiProjectClient.CreateAIAgentAsync( + name: "Writer", + model: deploymentName, + instructions: "You are an excellent content writer. You create new content and edit contents based on the feedback."); + +AIAgent reviewerAgent = await aiProjectClient.CreateAIAgentAsync( + name: "Reviewer", + model: deploymentName, + instructions: "You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content. Provide the feedback in the most concise manner possible."); + +try +{ + var workflow = new WorkflowBuilder(writerAgent) + .AddEdge(writerAgent, reviewerAgent) + .Build(); + + Console.WriteLine("Starting Writer-Reviewer Workflow Agent Server on http://localhost:8088"); + await workflow.AsAgent().RunAIAgentAsync(); +} +finally +{ + // Cleanup server-side agents + await aiProjectClient.Agents.DeleteAgentAsync(writerAgent.Name); + await aiProjectClient.Agents.DeleteAgentAsync(reviewerAgent.Name); +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md new file mode 100644 index 0000000000..314320880b --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/README.md @@ -0,0 +1,168 @@ +**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). + +Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. + +Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. + +Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. + +# What this sample demonstrates + +This sample demonstrates a **key advantage of code-based hosted agents**: + +- **Multi-agent workflows** - Orchestrate multiple agents working together + +Code-based agents can execute **any C# code** you write. This sample includes a Writer-Reviewer workflow where two agents collaborate: a Writer creates content and a Reviewer provides feedback. + +The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/) and can be deployed to Microsoft Foundry. + +## How It Works + +### Multi-Agent Workflow + +In [Program.cs](Program.cs), the sample creates two agents using `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package: + +- **Writer** - An agent that creates and edits content based on feedback +- **Reviewer** - An agent that provides actionable feedback on the content + +The `WorkflowBuilder` from the [Microsoft.Agents.AI.Workflows](https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/) package connects these agents in a sequential flow: + +1. The Writer receives the initial request and generates content +2. The Reviewer evaluates the content and provides feedback +3. Both agent responses are output to the user + +### Agent Hosting + +The agent is hosted using the [Azure AI AgentServer SDK](https://www.nuget.org/packages/Azure.AI.AgentServer.AgentFramework/), +which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Running the Agent Locally + +### Prerequisites + +Before running this sample, ensure you have: + +1. **Azure AI Foundry Project** + - Project created. + - Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`) + - Note your project endpoint URL and model deployment name + > **Note**: You can right-click the project in the Microsoft Foundry VS Code extension and select `Copy Project Endpoint URL` to get the endpoint. + +2. **Azure CLI** + - Installed and authenticated + - Run `az login` and verify with `az account show` + - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) + +3. **.NET 10.0 SDK or later** + - Verify your version: `dotnet --version` + - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) + +### Environment Variables + +Set the following environment variables: + +**PowerShell:** + +```powershell +# Replace with your actual values +$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +**Bash:** + +```bash +export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +### Running the Sample + +To run the agent, execute the following command in your terminal: + +```bash +dotnet restore +dotnet build +dotnet run +``` + +This will start the hosted agent locally on `http://localhost:8088/`. + +### Interacting with the Agent + +**VS Code:** + +1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. +2. Execute the following commands to start the containerized hosted agent. + ```bash + dotnet restore + dotnet build + dotnet run + ``` +3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "Create a slogan for a new electric SUV that is affordable and fun to drive." +4. Review the agent's response in the playground interface. + +> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. + +**PowerShell (Windows):** + +```powershell +$body = @{ + input = "Create a slogan for a new electric SUV that is affordable and fun to drive" + stream = $false +} | ConvertTo-Json + +Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" +``` + +**Bash/curl (Linux/macOS):** + +```bash +curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ + -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive","stream":false}' +``` + +You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. + +The Writer agent will generate content based on your prompt, and the Reviewer agent will provide feedback on the output. + +## Deploying the Agent to Microsoft Foundry + +**Preparation (required)** + +Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. + +To deploy the hosted agent: + +1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. + +2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. + +3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. + +**What the deploy flow does for you:** + +- Creates or obtains an Azure Container Registry for the target project. +- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). +- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). +- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. + +## MSI Configuration in the Azure Portal + +This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. + +To configure the Managed Identity: + +1. In the Azure Portal, open the Foundry Project. +2. Select "Access control (IAM)" from the left-hand menu. +3. Click "Add" and choose "Add role assignment". +4. In the role selection, search for and select "Azure AI User", then click "Next". +5. For "Assign access to", choose "Managed identity". +6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". +7. Click "Review + assign" to complete the assignment. +8. Allow a few minutes for the role assignment to propagate before running the application. + +## Additional Resources + +- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) +- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml new file mode 100644 index 0000000000..70b82abf7c --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/agent.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml + +name: FoundryMultiAgent +displayName: "Foundry Multi-Agent Workflow" +description: > + A multi-agent workflow featuring a Writer and Reviewer that collaborate + to create and refine content using Azure AI Foundry PersistentAgentsClient. +metadata: + authors: + - Microsoft Agent Framework Team + tags: + - Azure AI AgentServer + - Microsoft Agent Framework + - Multi-Agent Workflow + - Writer-Reviewer + - Content Creation +template: + kind: hosted + name: FoundryMultiAgent + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: MODEL_DEPLOYMENT_NAME + value: gpt-4o-mini +resources: + - name: "gpt-4o-mini" + kind: model + id: gpt-4o-mini diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json new file mode 100644 index 0000000000..b6b1c77b85 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/appsettings.Development.json @@ -0,0 +1,4 @@ +{ + "AZURE_AI_PROJECT_ENDPOINT": "https://.services.ai.azure.com/api/projects/", + "MODEL_DEPLOYMENT_NAME": "gpt-4o-mini" +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http new file mode 100644 index 0000000000..2fcdb2499e --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundryMultiAgent/run-requests.http @@ -0,0 +1,34 @@ +@host = http://localhost:8088 +@endpoint = {{host}}/responses + +### Health Check +GET {{host}}/readiness + +### Simple string input - Content creation request +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "Create a slogan for a new electric SUV that is affordable and fun to drive", + "stream": false +} + +### Explicit input format +POST {{endpoint}} +Content-Type: application/json + +{ + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Write a short product description for a smart water bottle that tracks hydration" + } + ] + } + ], + "stream": false +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile new file mode 100644 index 0000000000..0d1141cc69 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Dockerfile @@ -0,0 +1,20 @@ +# Build the application +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build +WORKDIR /src + +# Copy files from the current directory on the host to the working directory in the container +COPY . . + +RUN dotnet restore +RUN dotnet build -c Release --no-restore +RUN dotnet publish -c Release --no-build -o /app -f net10.0 + +# Run the application +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final +WORKDIR /app + +# Copy everything needed to run the app from the "build" stage. +COPY --from=build /app . + +EXPOSE 8088 +ENTRYPOINT ["dotnet", "FoundrySingleAgent.dll"] diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj new file mode 100644 index 0000000000..756f3d30ee --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/FoundrySingleAgent.csproj @@ -0,0 +1,67 @@ + + + Exe + net10.0 + enable + enable + + + false + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs new file mode 100644 index 0000000000..759636bcc0 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/Program.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle. +// Uses Microsoft Agent Framework with Azure AI Foundry. +// Ready for deployment to Foundry Hosted Agent service. + +using System.ComponentModel; +using System.Globalization; +using System.Text; + +using Azure.AI.AgentServer.AgentFramework.Extensions; +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +// Get configuration from environment variables +var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; +Console.WriteLine($"Project Endpoint: {endpoint}"); +Console.WriteLine($"Model Deployment: {deploymentName}"); +// Simulated hotel data for Seattle +var seattleHotels = new[] +{ + new Hotel("Contoso Suites", 189, 4.5, "Downtown"), + new Hotel("Fabrikam Residences", 159, 4.2, "Pike Place Market"), + new Hotel("Alpine Ski House", 249, 4.7, "Seattle Center"), + new Hotel("Margie's Travel Lodge", 219, 4.4, "Waterfront"), + new Hotel("Northwind Inn", 139, 4.0, "Capitol Hill"), + new Hotel("Relecloud Hotel", 99, 3.8, "University District"), +}; + +[Description("Get available hotels in Seattle for the specified dates. This simulates a call to a hotel availability API.")] +string GetAvailableHotels( + [Description("Check-in date in YYYY-MM-DD format")] string checkInDate, + [Description("Check-out date in YYYY-MM-DD format")] string checkOutDate, + [Description("Maximum price per night in USD (optional, defaults to 500)")] int maxPrice = 500) +{ + try + { + // Parse dates + if (!DateTime.TryParseExact(checkInDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkIn)) + { + return "Error parsing check-in date. Please use YYYY-MM-DD format."; + } + + if (!DateTime.TryParseExact(checkOutDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var checkOut)) + { + return "Error parsing check-out date. Please use YYYY-MM-DD format."; + } + + // Validate dates + if (checkOut <= checkIn) + { + return "Error: Check-out date must be after check-in date."; + } + + var nights = (checkOut - checkIn).Days; + + // Filter hotels by price + var availableHotels = seattleHotels.Where(h => h.PricePerNight <= maxPrice).ToList(); + + if (availableHotels.Count == 0) + { + return $"No hotels found in Seattle within your budget of ${maxPrice}/night."; + } + + // Build response + var result = new StringBuilder(); + result.AppendLine($"Available hotels in Seattle from {checkInDate} to {checkOutDate} ({nights} nights):"); + result.AppendLine(); + + foreach (var hotel in availableHotels) + { + var totalCost = hotel.PricePerNight * nights; + result.AppendLine($"**{hotel.Name}**"); + result.AppendLine($" Location: {hotel.Location}"); + result.AppendLine($" Rating: {hotel.Rating}/5"); + result.AppendLine($" ${hotel.PricePerNight}/night (Total: ${totalCost})"); + result.AppendLine(); + } + + return result.ToString(); + } + catch (Exception ex) + { + return $"Error processing request. Details: {ex.Message}"; + } +} + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +AIProjectClient aiProjectClient = new(new Uri(endpoint), new DefaultAzureCredential()); + +// Create Foundry agent with hotel search tool +AIAgent agent = await aiProjectClient.CreateAIAgentAsync( + name: "SeattleHotelAgent", + model: deploymentName, + instructions: """ + You are a helpful travel assistant specializing in finding hotels in Seattle, Washington. + + When a user asks about hotels in Seattle: + 1. Ask for their check-in and check-out dates if not provided + 2. Ask about their budget preferences if not mentioned + 3. Use the GetAvailableHotels tool to find available options + 4. Present the results in a friendly, informative way + 5. Offer to help with additional questions about the hotels or Seattle + + Be conversational and helpful. If users ask about things outside of Seattle hotels, + politely let them know you specialize in Seattle hotel recommendations. + """, + tools: [AIFunctionFactory.Create(GetAvailableHotels)]); + +try +{ + Console.WriteLine("Seattle Hotel Agent Server running on http://localhost:8088"); + await agent.RunAIAgentAsync(telemetrySourceName: "Agents"); +} +finally +{ + // Cleanup server-side agent + await aiProjectClient.Agents.DeleteAgentAsync(agent.Name); +} + +// Hotel record for simulated data +internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location); diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md new file mode 100644 index 0000000000..31f3fc1a9d --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/README.md @@ -0,0 +1,167 @@ +**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md). + +Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct. + +Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates. + +Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output. + +# What this sample demonstrates + +This sample demonstrates a **key advantage of code-based hosted agents**: + +- **Local C# tool execution** - Run custom C# methods as agent tools + +Code-based agents can execute **any C# code** you write. This sample includes a Seattle Hotel Agent with a `GetAvailableHotels` tool that searches for available hotels based on check-in/check-out dates and budget preferences. + +The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme) and can be deployed to Microsoft Foundry. + +## How It Works + +### Local Tools Integration + +In [Program.cs](Program.cs), the agent uses `AIProjectClient.CreateAIAgentAsync()` from the [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) package to create a Foundry agent with a local C# method (`GetAvailableHotels`) that simulates a hotel availability API. This demonstrates how code-based agents can execute custom server-side logic that prompt agents cannot access. + +The tool accepts: + +- **checkInDate** - Check-in date in YYYY-MM-DD format +- **checkOutDate** - Check-out date in YYYY-MM-DD format +- **maxPrice** - Maximum price per night in USD (optional, defaults to $500) + +### Agent Hosting + +The agent is hosted using the [Azure AI AgentServer SDK](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/ai.agentserver.agentframework-readme), +which provisions a REST API endpoint compatible with the OpenAI Responses protocol. + +## Running the Agent Locally + +### Prerequisites + +Before running this sample, ensure you have: + +1. **Azure AI Foundry Project** + - Project created. + - Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`) + - Note your project endpoint URL and model deployment name + +2. **Azure CLI** + - Installed and authenticated + - Run `az login` and verify with `az account show` + - Your identity needs the **Azure AI Developer** role on the Foundry resource (for `agents/write` data action required by `CreateAIAgentAsync`) + +3. **.NET 10.0 SDK or later** + - Verify your version: `dotnet --version` + - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) + +### Environment Variables + +Set the following environment variables (matching `agent.yaml`): + +- `AZURE_AI_PROJECT_ENDPOINT` - Your Azure AI Foundry project endpoint URL (required) +- `MODEL_DEPLOYMENT_NAME` - The deployment name for your chat model (defaults to `gpt-4o-mini`) + +**PowerShell:** + +```powershell +# Replace with your actual values +$env:AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +$env:MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +**Bash:** + +```bash +export AZURE_AI_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export MODEL_DEPLOYMENT_NAME="gpt-4o-mini" +``` + +### Running the Sample + +To run the agent, execute the following command in your terminal: + +```bash +dotnet restore +dotnet build +dotnet run +``` + +This will start the hosted agent locally on `http://localhost:8088/`. + +### Interacting with the Agent + +**VS Code:** + +1. Open the Visual Studio Code Command Palette and execute the `Microsoft Foundry: Open Container Agent Playground Locally` command. +2. Execute the following commands to start the containerized hosted agent. + + ```bash + dotnet restore + dotnet build + dotnet run + ``` + +3. Submit a request to the agent through the playground interface. For example, you may enter a prompt such as: "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night." +4. The agent will use the GetAvailableHotels tool to search for available hotels matching your criteria. + +> **Note**: Open the local playground before starting the container agent to ensure the visualization functions correctly. + +**PowerShell (Windows):** + +```powershell +$body = @{ + input = "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under `$200 per night" + stream = $false +} | ConvertTo-Json + +Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json" +``` + +**Bash/curl (Linux/macOS):** + +```bash +curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \ + -d '{"input": "Find me hotels in Seattle for March 20-23, 2025 under $200 per night","stream":false}' +``` + +You can also use the `run-requests.http` file in this directory with the VS Code REST Client extension. + +The agent will use the `GetAvailableHotels` tool to search for available hotels matching your criteria. + +## Deploying the Agent to Microsoft Foundry + +**Preparation (required)** + +Please check the environment_variables section in [agent.yaml](agent.yaml) and ensure the variables there are set in your target Microsoft Foundry Project. + +To deploy the hosted agent: + +1. Open the VS Code Command Palette and run the `Microsoft Foundry: Deploy Hosted Agent` command. +2. Follow the interactive deployment prompts. The extension will help you select or create the container files it needs. +3. After deployment completes, the hosted agent appears under the `Hosted Agents (Preview)` section of the extension tree. You can select the agent there to view details and test it using the integrated playground. + +**What the deploy flow does for you:** + +- Creates or obtains an Azure Container Registry for the target project. +- Builds and pushes a container image from your workspace (the build packages the workspace respecting `.dockerignore`). +- Creates an agent version in Microsoft Foundry using the built image. If a `.env` file exists at the workspace root, the extension will parse it and include its key/value pairs as the hosted agent's environment variables in the create request (these variables will be available to the agent runtime). +- Starts the agent container on the project's capability host. If the capability host is not provisioned, the extension will prompt you to enable it and will guide you through creating it. + +## MSI Configuration in the Azure Portal + +This sample requires the Microsoft Foundry Project to authenticate using a Managed Identity when running remotely in Azure. Grant the project's managed identity the required permissions by assigning the built-in [Azure AI User](https://aka.ms/foundry-ext-project-role) role. + +To configure the Managed Identity: + +1. In the Azure Portal, open the Foundry Project. +2. Select "Access control (IAM)" from the left-hand menu. +3. Click "Add" and choose "Add role assignment". +4. In the role selection, search for and select "Azure AI User", then click "Next". +5. For "Assign access to", choose "Managed identity". +6. Click "Select members", locate the managed identity associated with your Foundry Project (you can search by the project name), then click "Select". +7. Click "Review + assign" to complete the assignment. +8. Allow a few minutes for the role assignment to propagate before running the application. + +## Additional Resources + +- [Microsoft Agents Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) +- [Managed Identities for Azure Resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/) diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml new file mode 100644 index 0000000000..100defd112 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/agent.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml + +name: FoundrySingleAgent +displayName: "Foundry Single Agent with Local Tools" +description: > + A travel assistant agent that helps users find hotels in Seattle. + Demonstrates local C# tool execution - a key advantage of code-based + hosted agents over prompt agents. +metadata: + authors: + - Microsoft Agent Framework Team + tags: + - Azure AI AgentServer + - Microsoft Agent Framework + - Local Tools + - Travel Assistant + - Hotel Search +template: + kind: hosted + name: FoundrySingleAgent + protocols: + - protocol: responses + version: v1 + environment_variables: + - name: AZURE_AI_PROJECT_ENDPOINT + value: ${AZURE_AI_PROJECT_ENDPOINT} + - name: MODEL_DEPLOYMENT_NAME + value: gpt-4o-mini +resources: + - name: "gpt-4o-mini" + kind: model + id: gpt-4o-mini \ No newline at end of file diff --git a/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http new file mode 100644 index 0000000000..4f2e87e097 --- /dev/null +++ b/dotnet/samples/05-end-to-end/HostedAgents/FoundrySingleAgent/run-requests.http @@ -0,0 +1,52 @@ +@host = http://localhost:8088 +@endpoint = {{host}}/responses + +### Health Check +GET {{host}}/readiness + +### Simple hotel search - budget under $200 +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night", + "stream": false +} + +### Hotel search with higher budget +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "Find me hotels in Seattle for March 20-23, 2025 under $250 per night", + "stream": false +} + +### Ask for recommendations without dates (agent should ask for clarification) +POST {{endpoint}} +Content-Type: application/json + +{ + "input": "What hotels do you recommend in Seattle?", + "stream": false +} + +### Explicit input format +POST {{endpoint}} +Content-Type: application/json + +{ + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "I'm looking for a hotel in Seattle from 2025-04-01 to 2025-04-05, my budget is $150 per night maximum" + } + ] + } + ], + "stream": false +} diff --git a/dotnet/samples/05-end-to-end/HostedAgents/README.md b/dotnet/samples/05-end-to-end/HostedAgents/README.md index f7a3bdc94b..f2d32f3c4d 100644 --- a/dotnet/samples/05-end-to-end/HostedAgents/README.md +++ b/dotnet/samples/05-end-to-end/HostedAgents/README.md @@ -12,6 +12,8 @@ These samples demonstrate how to build and host AI agents using the [Azure AI Ag | [`AgentWithHostedMCP`](./AgentWithHostedMCP/) | Hosted MCP server tool (Microsoft Learn search) | | [`AgentWithTextSearchRag`](./AgentWithTextSearchRag/) | RAG with `TextSearchProvider` (Contoso Outdoors) | | [`AgentsInWorkflows`](./AgentsInWorkflows/) | Sequential workflow pipeline (translation chain) | +| [`FoundryMultiAgent`](./FoundryMultiAgent/) | Multi-agent Writer-Reviewer workflow using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | +| [`FoundrySingleAgent`](./FoundrySingleAgent/) | Single agent with local C# tool execution (hotel search) using `AIProjectClient.CreateAIAgentAsync()` from [Microsoft.Agents.AI.AzureAI](https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI/) | ## Common Prerequisites @@ -38,9 +40,9 @@ Most samples require one or more of these environment variables: |----------|---------|-------------| | `AZURE_OPENAI_ENDPOINT` | Most samples | Your Azure OpenAI resource endpoint URL | | `AZURE_OPENAI_DEPLOYMENT_NAME` | Most samples | Chat model deployment name (defaults to `gpt-4o-mini`) | -| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools | Azure AI Foundry project endpoint | +| `AZURE_AI_PROJECT_ENDPOINT` | AgentWithTools, AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Azure AI Foundry project endpoint | | `MCP_TOOL_CONNECTION_ID` | AgentWithTools | Foundry MCP tool connection name | -| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools | Chat model deployment name (defaults to `gpt-4o-mini`) | +| `MODEL_DEPLOYMENT_NAME` | AgentWithLocalTools, FoundryMultiAgent, FoundrySingleAgent | Chat model deployment name (defaults to `gpt-4o-mini`) | See each sample's README for the specific variables required. diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs index 4c0aeef5bb..40e79f8af5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs @@ -133,7 +133,7 @@ public sealed class ObservabilityTests : IDisposable activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event"); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync() { await this.TestWorkflowEndToEndActivitiesAsync("Default"); From 3fb90a501a14c24dc09a0ef33fee9e1dd191f232 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:14:33 +0000 Subject: [PATCH 3/5] .NET: CI Build time end to end improvement (#4208) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * .NET: Upgrade to XUnit 3 and Microsoft Testing Platform (#4176) * Fix copilot studio integration tests failure (#4209) * Fix anthropic integration tests and skip reason (#4211) * Remove accidental add of code coverage for integration tests (#4219) * Add solution filtered parallel test run (#4226) * Fix build paths (#4228) * Fix coverage settings path and trait filter (#4229) * Add project name filter to solution (#4231) * Increase Integration Test Parallelism (#4241) * Increase integration tests threads to 4x (#4242) * Separate build and test into parallel jobs (#4243) * Filter src by framework for tests build (#4244) * Separate build and test into parallel jobs * Filter source projects by framework for tests build * Pre-build samples via tests to avoid timeouts (#4245) * Separate build from run for console sample validation (#4251) * Address PR comments (#4255) * Merge and move scripts (#4308) * .NET: Add Microsoft Fabric sample #3674 (#4230) Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> * Python: Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference (#4207) * Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference Add embedding client implementations to existing provider packages: - OllamaEmbeddingClient: Text embeddings via Ollama's embed API - BedrockEmbeddingClient: Text embeddings via Amazon Titan on Bedrock - AzureAIInferenceEmbeddingClient: Text and image embeddings via Azure AI Inference, supporting Content | str input with separate model IDs for text (AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID) and image (AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID) endpoints Additional changes: - Rename EmbeddingCoT -> EmbeddingT, EmbeddingOptionsCoT -> EmbeddingOptionsT - Add otel_provider_name passthrough to all embedding clients - Register integration pytest marker in all packages - Add lazy-loading namespace exports for Ollama and Bedrock embeddings - Add image embedding sample using Cohere-embed-v3-english - Add azure-ai-inference dependency to azure-ai package Part of #1188 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix mypy duplicate name and ruff lint issues - Rename second 'vector' variable to 'img_vector' in image embedding loop - Combine nested with statements in tests - Remove unused result assignments in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updates from feedback * Fix CI failures in embedding usage handling - Fix Azure AI embedding mypy issues by normalizing vectors to list[float], safely accumulating optional usage token fields, and filtering None entries before constructing GeneratedEmbeddings - Avoid Bandit false positive by initializing usage details as an empty dict - Update OpenAI embedding tests to assert canonical usage keys (input_token_count/total_token_count) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [Purview] Mark responses as responses and fix epoch bug for python long overflow (#4225) * .NET: Support InvokeMcpTool for declarative workflows (#4204) * Initial implementation of InvokeMcpTool in declarative workflow * Cleaned up sample implementation * Updated sample comments. * Added missing executor routing attribute * Fix PR comments. * Updated based on PR comments. * Updated based on PR comments. * Removed unnecessary using statement. * Update Python package versions to rc2 (#4258) - Bump core and azure-ai to 1.0.0rc2 - Bump preview packages to 1.0.0b260225 - Update dependencies to >=1.0.0rc2 - Add CHANGELOG entries for changes since rc1 - Update uv.lock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * .NET: Fixing issue where OpenTelemetry span is never exported in .NET in-process workflow execution (#4196) * 1. Add reproduction test for issue #4155: workflow.run Activity never stopped in streaming OffThread path The WorkflowRunActivity_IsStopped_Streaming_OffThread test demonstrates that the workflow.run OpenTelemetry Activity created in StreamingRunEventStream.RunLoopAsync is started but never stopped when using the OffThread/Default streaming execution. The background run loop keeps running after event consumption completes, so the using Activity? declaration never disposes until explicit StopAsync() is called. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 2. Fix workflow.run Activity never stopped in streaming OffThread execution (#4155) The workflow.run OpenTelemetry Activity in StreamingRunEventStream.RunLoopAsync was scoped to the method lifetime via 'using'. Since the run loop only exits on cancellation, the Activity was never stopped/exported until explicit disposal. Fix: Remove 'using' and explicitly dispose the Activity when the workflow reaches Idle status (all supersteps complete). A safety-net disposal in the finally block handles cancellation and error paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add root-level workflow.session activity spanning run loop lifetime\n\nImplements two-level telemetry hierarchy per PR feedback from lokitoth:\n- workflow.session: spans the entire run loop / stream lifetime\n- workflow_invoke: per input-to-halt cycle, nested within the session\n\nThis ensures the session activity stays open across multiple turns,\nwhile individual run activities are created and disposed per cycle.\n\nAlso fixes linkedSource CancellationTokenSource disposal leak in\nStreamingRunEventStream (added using declaration)." * Address Copilot review: fix Activity/CTS disposal, rename activity, add error tag\n\n1. LockstepRunEventStream: Remove 'using' from Activity in async iterator\n and manually dispose in finally block (fixes #4155 pattern). Also dispose\n linkedSource CTS in finally to prevent leak.\n2. Tags.cs: Add ErrorMessage (\"error.message\") tag for runtime errors,\n distinct from BuildErrorMessage (\"build.error.message\").\n3. ActivityNames: Rename WorkflowRun from \"workflow_invoke\" to \"workflow.run\"\n for cross-language consistency.\n4. WorkflowTelemetryContext: Fix XML doc to say \"outer/parent span\" instead\n of \"root-level span\".\n5. ObservabilityTests: Assert WorkflowSession absence when DisableWorkflowRun\n is true.\n6. WorkflowRunActivityStopTests: Fix streaming test race by disposing\n StreamingRun before asserting activities are stopped.\n7. StreamingRunEventStream/LockstepRunEventStream: Use Tags.ErrorMessage\n instead of Tags.BuildErrorMessage for runtime error events." * Review fixes: revert workflow_invoke rename, use 'using' for linkedSource, move SessionStarted earlier\n\n- Revert ActivityNames.WorkflowRun back to \"workflow_invoke\" (OTEL semantic convention contract)\n- Use 'using' declaration for linkedSource CTS in LockstepRunEventStream (no timing sensitivity)\n- Move SessionStarted event before WaitForInputAsync in StreamingRunEventStream to match Lockstep behavior" * Improve naming and comments in WorkflowRunActivityStopTests" * Prevent session Activity.Current leak in lockstep mode, add nesting test Save and restore Activity.Current in LockstepRunEventStream.Start() so the session activity doesn't leak into caller code via AsyncLocal. Re-establish Activity.Current = sessionActivity before creating the run activity in TakeEventStreamAsync to preserve parent-child nesting. Add test verifying app activities after RunAsync are not parented under the session, and that the workflow_invoke activity nests under the session." * Fix stale XML doc: WorkflowRun -> WorkflowInvoke in ObservabilityTests --------- Co-authored-by: alliscode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python / .NET Samples - Restructure and Improve Samples (Feature Branc… (#4092) * Python: .NET Samples - Restructure and Improve Samples (Feature Branch) (#4091) * Moved by agent (#4094) * Fix readme links * .NET Samples - Create `04-hosting` learning path step (#4098) * Agent move * Agent reorderd * Remove A2A section from README Removed A2A section from the Getting Started README. * Agent fixed links * Fix broken sample links in durable-agents README (#4101) * Initial plan * Fix broken internal links in documentation Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Revert template link changes; keep only durable-agents README fix Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * .NET Samples - Create `03-workflows` learning path step (#4102) * Fix solution project path * Python: Fix broken markdown links to repo resources (outside /docs) (#4105) * Initial plan * Fix broken markdown links to repo resources Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Update README to rename .NET Workflows Samples section --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * .NET Samples - Create `02-agents` learning path step (#4107) * .NET: Fix broken relative link in GroupChatToolApproval README (#4108) * Initial plan * Fix broken link in GroupChatToolApproval README Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Update labeler configuration for workflow samples * .NET - Reorder Agents samples to start from Step01 instead of Step04 (#4110) * Fix solution * Resolve new sample paths * Move new AgentSkills and AgentWithMemory_Step04 samples * Fix link * Fix readme path * fix: update stale dotnet/samples/Durable path reference in AGENTS.md Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Moved new sample * Update solution * Resolve merge (new sample) * Sync to new sample - FoundryAgents_Step21_BingCustomSearch * Updated README * .NET Samples - Configuration Naming Update (#4149) * .NET: Restore AzureFunctions index parity with ConsoleApps under DurableAgents samples (#4221) * Clean-up `05_host_your_agent` * Config setting consistency * Refine samples * AGENTS.md * Move new samples * Re-order samples * Move new project and fixup solution * Fixup model config * Fix up new UT project --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> * Python: Fix Bedrock embedding test stub missing meta attribute (#4287) * Fix Bedrock embedding test stub missing meta attribute * Increase test coverage so gate passes * Python: (ag-ui): fix approval payloads being re-processed on subsequent conversation turns (#4232) * Fix ag-ui tool call issue * Safe json fix * Python: Update workflow orchestration samples to use AzureOpenAIResponsesClient (#4285) * Update workflow orchestration samples to use AzureOpenAIResponsesClient * Fix broken link * Move scripts to scripts folder --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rishabh Chawla Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: Ben Thomas Co-authored-by: alliscode Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> * Fix encoding (#4309) * Disable Parallelization for WorkflowRunActivityStopTests (#4313) * Revert parallel disable (#4324) * .NET: Disable flakey Workflow Observability tests (#4416) * Disable flakey OffThread test * Disable additional OffThread test * Disable a further test * Disable all observability tests --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rishabh Chawla Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: Ben Thomas Co-authored-by: alliscode Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com> --- .github/workflows/dotnet-build-and-test.yml | 157 ++++++++++++------ dotnet/.github/skills/build-and-test/SKILL.md | 55 +++++- dotnet/Directory.Packages.props | 10 +- dotnet/agent-framework-dotnet.slnx | 5 +- dotnet/eng/scripts/New-FilteredSolution.ps1 | 145 ++++++++++++++++ .../eng/scripts}/dotnet-check-coverage.ps1 | 0 dotnet/global.json | 3 + .../AgentTests.cs | 10 +- ...opicChatCompletion.IntegrationTests.csproj | 1 + ...pletionChatClientAgentRunStreamingTests.cs | 21 +-- ...icChatCompletionChatClientAgentRunTests.cs | 21 +-- .../AnthropicChatCompletionFixture.cs | 13 +- ...nthropicChatCompletionRunStreamingTests.cs | 28 +--- .../AnthropicChatCompletionRunTests.cs | 28 +--- .../AnthropicSkillsIntegrationTests.cs | 8 +- .../AIProjectClientAgentRunStreamingTests.cs | 8 +- .../AIProjectClientAgentRunTests.cs | 8 +- ...jectClientAgentStructuredOutputRunTests.cs | 29 ++-- ...tClientChatClientAgentRunStreamingTests.cs | 4 +- .../AIProjectClientChatClientAgentRunTests.cs | 4 +- .../AIProjectClientFixture.cs | 10 +- .../AzureAI.IntegrationTests.csproj | 1 + ...AIAgentsPersistent.IntegrationTests.csproj | 1 + .../AzureAIAgentsPersistentFixture.cs | 11 +- ...gentsPersistentStructuredOutputRunTests.cs | 24 ++- .../CopilotStudio.IntegrationTests.csproj | 1 + .../CopilotStudioFixture.cs | 28 +++- .../CopilotStudioRunStreamingTests.cs | 40 +++-- .../CopilotStudioRunTests.cs | 40 +++-- dotnet/tests/Directory.Build.props | 13 +- .../CosmosChatHistoryProviderTests.cs | 70 ++++---- .../CosmosCheckpointStoreTests.cs | 42 ++--- ...oft.Agents.AI.CosmosNoSql.UnitTests.csproj | 1 - .../AgentEntityTests.cs | 1 - .../ConsoleAppSamplesValidation.cs | 42 ++++- .../ExternalClientTests.cs | 1 - .../Logging/TestLogger.cs | 1 - .../Logging/TestLoggerProvider.cs | 1 - .../OrchestrationTests.cs | 1 - .../TestHelper.cs | 1 - .../TimeToLiveTests.cs | 1 - .../ToolCallingTests.cs | 1 - .../SamplesValidation.cs | 42 ++++- .../AzureAgentProviderTest.cs | 1 - .../DeclarativeCodeGenTest.cs | 1 - .../DeclarativeWorkflowTest.cs | 1 - .../Framework/IntegrationTest.cs | 1 - .../Framework/TestOutputAdapter.cs | 1 - .../Framework/WorkflowTest.cs | 1 - .../FunctionCallingWorkflowTest.cs | 1 - .../InvokeToolWorkflowTest.cs | 1 - .../MediaInputTest.cs | 1 - .../AddConversationMessageTemplateTest.cs | 1 - .../CodeGen/BreakLoopTemplateTest.cs | 1 - .../CodeGen/ClearAllVariablesTemplateTest.cs | 1 - .../CodeGen/ConditionGroupTemplateTest.cs | 1 - .../CodeGen/ContinueLoopTemplateTest.cs | 1 - .../CopyConversationMessagesTemplateTest.cs | 1 - .../CodeGen/CreateConversationTemplateTest.cs | 1 - .../CodeGen/DeclarativeEjectionTest.cs | 1 - .../CodeGen/EdgeTemplateTest.cs | 1 - .../CodeGen/EndConversationTest.cs | 1 - .../CodeGen/EndDialogTest.cs | 1 - .../CodeGen/ForeachTemplateTest.cs | 1 - .../CodeGen/GotoTemplateTest.cs | 1 - .../CodeGen/InvokeAzureAgentTemplateTest.cs | 1 - .../CodeGen/ProviderTemplateTest.cs | 1 - .../CodeGen/ResetVariableTemplateTest.cs | 1 - ...RetrieveConversationMessageTemplateTest.cs | 1 - ...etrieveConversationMessagesTemplateTest.cs | 1 - .../SetMultipleVariablesTemplateTest.cs | 1 - .../CodeGen/SetTextVariableTemplateTest.cs | 1 - .../CodeGen/SetVariableTemplateTest.cs | 1 - .../CodeGen/WorkflowActionTemplateTest.cs | 1 - .../DeclarativeWorkflowExceptionTest.cs | 1 - .../DeclarativeWorkflowTest.cs | 1 - .../Entities/EntityExtractionResultTest.cs | 1 - .../Entities/EntityExtractorTest.cs | 1 - .../Events/EventTest.cs | 1 - .../Events/ExternalInputRequestTest.cs | 1 - .../Events/ExternalInputResponseTest.cs | 1 - .../Interpreter/WorkflowModelTest.cs | 1 - .../AddConversationMessageExecutorTest.cs | 1 - .../ClearAllVariablesExecutorTest.cs | 1 - .../ObjectModel/ConditionGroupExecutorTest.cs | 1 - .../CopyConversationMessagesExecutorTest.cs | 1 - .../CreateConversationExecutorTest.cs | 1 - .../ObjectModel/DefaultActionExecutorTest.cs | 1 - .../ObjectModel/EditTableExecutorTest.cs | 1 - .../ObjectModel/EditTableV2ExecutorTest.cs | 1 - .../ObjectModel/ForeachExecutorTest.cs | 1 - .../InvokeFunctionToolExecutorTest.cs | 1 - .../ObjectModel/InvokeMcpToolExecutorTest.cs | 1 - .../ObjectModel/ParseValueExecutorTest.cs | 1 - .../ObjectModel/QuestionExecutorTest.cs | 1 - .../RequestExternalInputExecutorTest.cs | 1 - .../ObjectModel/ResetVariableExecutorTest.cs | 1 - ...RetrieveConversationMessageExecutorTest.cs | 1 - ...etrieveConversationMessagesExecutorTest.cs | 1 - .../ObjectModel/SendActivityExecutorTest.cs | 1 - .../SetMultipleVariablesExecutorTest.cs | 1 - .../SetTextVariableExecutorTest.cs | 1 - .../ObjectModel/SetVariableExecutorTest.cs | 1 - .../ObjectModel/WorkflowActionExecutorTest.cs | 1 - .../PowerFx/RecalcEngineFactoryTests.cs | 1 - .../PowerFx/RecalcEngineTest.cs | 1 - .../PowerFx/TemplateExtensionsTests.cs | 1 - .../PowerFx/WorkflowExpressionEngineTests.cs | 1 - .../TestOutputAdapter.cs | 1 - .../WorkflowTest.cs | 1 - .../ObservabilityTests.cs | 32 ++-- .../WorkflowRunActivityStopTests.cs | 12 +- .../OpenAIAssistantFixture.cs | 11 +- .../OpenAIChatCompletionFixture.cs | 10 +- ...esponseChatClientAgentRunStreamingTests.cs | 16 +- .../OpenAIResponseChatClientAgentRunTests.cs | 16 +- .../OpenAIResponseFixture.cs | 8 +- .../OpenAIResponseRunStreamingTests.cs | 17 +- .../OpenAIResponseRunTests.cs | 17 +- dotnet/tests/coverage.runsettings | 21 +++ 120 files changed, 732 insertions(+), 427 deletions(-) create mode 100644 dotnet/eng/scripts/New-FilteredSolution.ps1 rename {.github/workflows => dotnet/eng/scripts}/dotnet-check-coverage.ps1 (100%) create mode 100644 dotnet/tests/coverage.runsettings diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 22047407a7..3bdb43dabf 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -59,20 +59,20 @@ jobs: if: steps.filter.outputs.dotnet != 'true' run: echo "NOT dotnet file" - dotnet-build-and-test: + # Build the full solution (including samples) on all TFMs. No tests. + dotnet-build: needs: paths-filter if: needs.paths-filter.outputs.dotnetChanges == 'true' strategy: fail-fast: false matrix: include: - - { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" } + - { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release } - { targetFramework: "net9.0", os: "windows-latest", configuration: Debug } - { targetFramework: "net8.0", os: "ubuntu-latest", configuration: Release } - - { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" } + - { targetFramework: "net472", os: "windows-latest", configuration: Release } runs-on: ${{ matrix.os }} - environment: ${{ matrix.environment }} steps: - uses: actions/checkout@v6 with: @@ -84,16 +84,6 @@ jobs: python workflow-samples - # Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened) - - name: Start Azure Cosmos DB Emulator - if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }} - shell: pwsh - run: | - Write-Host "Launching Azure Cosmos DB Emulator" - Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" - Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" - echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV - - name: Setup dotnet uses: actions/setup-dotnet@v5.1.0 with: @@ -140,25 +130,98 @@ jobs: popd rm -rf "$TEMP_DIR" - - name: Run Unit Tests - shell: bash - run: | - export UT_PROJECTS=$(find ./dotnet -type f -name "*.UnitTests.csproj" | tr '\n' ' ') - for project in $UT_PROJECTS; do - # Query the project's target frameworks using MSBuild with the current configuration - target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r') + # Build src+tests only (no samples) for a single TFM and run tests. + dotnet-test: + needs: paths-filter + if: needs.paths-filter.outputs.dotnetChanges == 'true' + strategy: + fail-fast: false + matrix: + include: + - { targetFramework: "net10.0", os: "ubuntu-latest", configuration: Release, integration-tests: true, environment: "integration" } + - { targetFramework: "net472", os: "windows-latest", configuration: Release, integration-tests: true, environment: "integration" } - # Check if the project supports the target framework - if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then - if [[ "${{ matrix.targetFramework }}" == "${{ env.COVERAGE_FRAMEWORK }}" ]]; then - dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --collect:"XPlat Code Coverage" --results-directory:"TestResults/Coverage/" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByAttribute=GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute - else - dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx - fi - else - echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)" - fi - done + runs-on: ${{ matrix.os }} + environment: ${{ matrix.environment }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + sparse-checkout: | + . + .github + dotnet + python + workflow-samples + + # Start Cosmos DB Emulator for all integration tests and only for unit tests when CosmosDB changes happened) + - name: Start Azure Cosmos DB Emulator + if: ${{ runner.os == 'Windows' && (needs.paths-filter.outputs.cosmosDbChanges == 'true' || (github.event_name != 'pull_request' && matrix.integration-tests)) }} + shell: pwsh + run: | + Write-Host "Launching Azure Cosmos DB Emulator" + Import-Module "$env:ProgramFiles\Azure Cosmos DB Emulator\PSModules\Microsoft.Azure.CosmosDB.Emulator" + Start-CosmosDbEmulator -NoUI -Key "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" + echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV + + - name: Setup dotnet + uses: actions/setup-dotnet@v5.1.0 + with: + global-json-file: ${{ github.workspace }}/dotnet/global.json + + - name: Generate test solution (no samples) + shell: pwsh + run: | + ./dotnet/eng/scripts/New-FilteredSolution.ps1 ` + -Solution dotnet/agent-framework-dotnet.slnx ` + -TargetFramework ${{ matrix.targetFramework }} ` + -Configuration ${{ matrix.configuration }} ` + -ExcludeSamples ` + -OutputPath dotnet/filtered.slnx ` + -Verbose + + - name: Build src and tests + shell: bash + run: dotnet build dotnet/filtered.slnx -c ${{ matrix.configuration }} -f ${{ matrix.targetFramework }} --warnaserror + + - name: Generate test-type filtered solutions + shell: pwsh + run: | + $commonArgs = @{ + Solution = "dotnet/filtered.slnx" + TargetFramework = "${{ matrix.targetFramework }}" + Configuration = "${{ matrix.configuration }}" + Verbose = $true + } + ./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs ` + -TestProjectNameFilter "*UnitTests*" ` + -OutputPath dotnet/filtered-unit.slnx + ./dotnet/eng/scripts/New-FilteredSolution.ps1 @commonArgs ` + -TestProjectNameFilter "*IntegrationTests*" ` + -OutputPath dotnet/filtered-integration.slnx + + - name: Run Unit Tests + shell: pwsh + working-directory: dotnet + run: | + $coverageSettings = Join-Path $PWD "tests/coverage.runsettings" + $coverageArgs = @() + if ("${{ matrix.targetFramework }}" -eq "${{ env.COVERAGE_FRAMEWORK }}") { + $coverageArgs = @( + "--coverage", + "--coverage-output-format", "cobertura", + "--coverage-settings", $coverageSettings, + "--results-directory", "../TestResults/Coverage/" + ) + } + + dotnet test --solution ./filtered-unit.slnx ` + -f ${{ matrix.targetFramework }} ` + -c ${{ matrix.configuration }} ` + --no-build -v Normal ` + --report-xunit-trx ` + --ignore-exit-code 8 ` + @coverageArgs env: # Cosmos DB Emulator connection settings COSMOSDB_ENDPOINT: https://localhost:8081 @@ -185,21 +248,19 @@ jobs: id: azure-functions-setup - name: Run Integration Tests - shell: bash + shell: pwsh + working-directory: dotnet if: github.event_name != 'pull_request' && matrix.integration-tests run: | - export INTEGRATION_TEST_PROJECTS=$(find ./dotnet -type f -name "*IntegrationTests.csproj" | tr '\n' ' ') - for project in $INTEGRATION_TEST_PROJECTS; do - # Query the project's target frameworks using MSBuild with the current configuration - target_frameworks=$(dotnet msbuild $project -getProperty:TargetFrameworks -p:Configuration=${{ matrix.configuration }} -nologo 2>/dev/null | tr -d '\r') - - # Check if the project supports the target framework - if [[ "$target_frameworks" == *"${{ matrix.targetFramework }}"* ]]; then - dotnet test -f ${{ matrix.targetFramework }} -c ${{ matrix.configuration }} $project --no-build -v Normal --logger trx --filter "Category!=IntegrationDisabled" - else - echo "Skipping $project - does not support target framework ${{ matrix.targetFramework }} (supports: $target_frameworks)" - fi - done + dotnet test --solution ./filtered-integration.slnx ` + -f ${{ matrix.targetFramework }} ` + -c ${{ matrix.configuration }} ` + --no-build -v Normal ` + --report-xunit-trx ` + --ignore-exit-code 8 ` + --filter-not-trait "Category=IntegrationDisabled" ` + --parallel-algorithm aggressive ` + --max-threads 2.0x env: # Cosmos DB Emulator connection settings COSMOSDB_ENDPOINT: https://localhost:8081 @@ -222,7 +283,7 @@ jobs: if: matrix.targetFramework == env.COVERAGE_FRAMEWORK uses: danielpalme/ReportGenerator-GitHub-Action@5.5.1 with: - reports: "./TestResults/Coverage/**/coverage.cobertura.xml" + reports: "./TestResults/Coverage/**/*.cobertura.xml" targetdir: "./TestResults/Reports" reporttypes: "HtmlInline;JsonSummary" @@ -236,13 +297,13 @@ jobs: - name: Check coverage if: matrix.targetFramework == env.COVERAGE_FRAMEWORK shell: pwsh - run: .github/workflows/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD + run: ./dotnet/eng/scripts/dotnet-check-coverage.ps1 -JsonReportPath "TestResults/Reports/Summary.json" -CoverageThreshold $env:COVERAGE_THRESHOLD # This final job is required to satisfy the merge queue. It must only run (or succeed) if no tests failed dotnet-build-and-test-check: if: always() runs-on: ubuntu-latest - needs: [dotnet-build-and-test] + needs: [dotnet-build, dotnet-test] steps: - name: Get Date shell: bash diff --git a/dotnet/.github/skills/build-and-test/SKILL.md b/dotnet/.github/skills/build-and-test/SKILL.md index 60492fe135..1009e2c5b7 100644 --- a/dotnet/.github/skills/build-and-test/SKILL.md +++ b/dotnet/.github/skills/build-and-test/SKILL.md @@ -17,14 +17,17 @@ dotnet format # Auto-fix formatting for all projects # Build/test/format a specific project (preferred for isolated/internal changes) dotnet build src/Microsoft.Agents.AI. --tl:off -dotnet test tests/Microsoft.Agents.AI..UnitTests +dotnet test --project tests/Microsoft.Agents.AI..UnitTests dotnet format src/Microsoft.Agents.AI. # Run a single test -dotnet test --filter "FullyQualifiedName~Namespace.TestClassName.TestMethodName" +# Replace the filter values with the appropriate assembly, namespace, class, and method names for the test you want to run and use * as a wildcard elsewhere, e.g. "/*/*/HttpClientTests/GetAsync_ReturnsSuccessStatusCode" +# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for some projects +dotnet test --filter-query "////" --ignore-exit-code 8 # Run unit tests only -dotnet test --filter FullyQualifiedName\~UnitTests +# Use `--ignore-exit-code 8` to avoid failing the build when no tests are found for integration test projects +dotnet test --filter-query "/*UnitTests*/*/*/*" --ignore-exit-code 8 ``` Use `--tl:off` when building to avoid flickering when running commands in the agent. @@ -56,7 +59,7 @@ Example: Running tests for a single project using .NET 10. ```bash # From dotnet/ directory -dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 +dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 ``` Example: Running a single test in a specific project using .NET 10. @@ -64,7 +67,7 @@ Provide the full namespace, class name, and method name for the test you want to ```bash # From dotnet/ directory -dotnet test ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter "FullyQualifiedName~Microsoft.Agents.AI.Abstractions.UnitTests.AgentRunOptionsTests.CloningConstructorCopiesProperties" +dotnet test --project ./tests/Microsoft.Agents.AI.Abstractions.UnitTests -f net10.0 --filter-query "/*/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests/CloningConstructorCopiesProperties" ``` ### Multi-target framework tip @@ -83,3 +86,45 @@ Just remember to run `dotnet restore` after pulling changes, making changes to p Unit tests target both .NET Framework as well as .NET Core. When running on Linux, only the .NET Core tests can be run, as .NET Framework is not supported on Linux. To run only the .NET Core tests, use the `-f net10.0` option with `dotnet test`. + +### Microsoft Testing Platform (MTP) + +Tests use the [Microsoft Testing Platform](https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-intro) via xUnit v3. Key differences from the legacy VSTest runner: + +- **`dotnet test` requires `--project`** to specify a test project directly (positional arguments are no longer supported). +- **Test output** uses the MTP format (e.g., `[✓112/x0/↓0]` progress and `Test run summary: Passed!`). +- **TRX reports** use `--report-xunit-trx` instead of `--logger trx`. +- **Code coverage** uses `Microsoft.Testing.Extensions.CodeCoverage` with `--coverage --coverage-output-format cobertura`. +- **Running a test project directly** is supported via `dotnet run --project `. This bypasses the `dotnet test` infrastructure and runs the test executable directly with the MTP command line. + +- **Running tests across the solution** with a filter may cause some projects to match zero tests, which MTP treats as a failure (exit code 8). Use `--ignore-exit-code 8` to suppress this: + +```bash +# Run all unit tests across the solution, ignoring projects with no matching tests +dotnet test --solution ./agent-framework-dotnet.slnx --no-build -f net10.0 --ignore-exit-code 8 +``` + +- **Running tests with `--solution` for a specific TFM** requires all projects in the solution to support that TFM. Not all projects target every framework (e.g., some are `net10.0`-only). Use `./dotnet/eng/scripts/New-FilteredSolution.ps1` to generate a filtered solution: + +```powershell +# Generate a filtered solution for net472 and run tests +$filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472 +dotnet test --solution $filtered --no-build -f net472 --ignore-exit-code 8 + +# Exclude samples and keep only unit test projects +./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -ExcludeSamples -TestProjectNameFilter "*UnitTests*" -OutputPath dotnet/filtered-unit.slnx +``` + +```bash +# Run tests via dotnet test (uses MTP under the hood) +dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 + +# Run tests with code coverage (Cobertura format) +dotnet test --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 --coverage --coverage-output-format cobertura --coverage-settings ./tests/coverage.runsettings + +# Run tests directly via dotnet run (MTP native command line) +dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 + +# Show MTP command line help +dotnet run --project ./tests/Microsoft.Agents.AI.UnitTests -f net10.0 -- -? +``` diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a44a4d420e..255d8fe94f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -140,12 +140,10 @@ - - - - - - + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 0f105d4a80..75888768fa 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -313,7 +313,6 @@ - @@ -350,6 +349,10 @@ + + + + diff --git a/dotnet/eng/scripts/New-FilteredSolution.ps1 b/dotnet/eng/scripts/New-FilteredSolution.ps1 new file mode 100644 index 0000000000..de6a8f9d1d --- /dev/null +++ b/dotnet/eng/scripts/New-FilteredSolution.ps1 @@ -0,0 +1,145 @@ +#!/usr/bin/env pwsh +# Copyright (c) Microsoft. All rights reserved. + +<# +.SYNOPSIS + Generates a filtered .slnx solution file by removing projects that don't match the specified criteria. + +.DESCRIPTION + Parses a .slnx solution file and applies one or more filters: + - Removes projects that don't support the specified target framework (via MSBuild query). + - Optionally removes all sample projects (under samples/). + - Optionally filters test projects by name pattern (e.g., only *UnitTests*). + Writes the filtered solution to the specified output path and prints the path. + +.PARAMETER Solution + Path to the source .slnx solution file. + +.PARAMETER TargetFramework + The target framework to filter by (e.g., net10.0, net472). + +.PARAMETER Configuration + Optional MSBuild configuration used when querying TargetFrameworks. Defaults to Debug. + +.PARAMETER TestProjectNameFilter + Optional wildcard pattern to filter test project names (e.g., *UnitTests*, *IntegrationTests*). + When specified, only test projects whose filename matches this pattern are kept. + +.PARAMETER ExcludeSamples + When specified, removes all projects under the samples/ directory from the solution. + +.PARAMETER OutputPath + Optional output path for the filtered .slnx file. If not specified, a temp file is created. + +.EXAMPLE + # Generate a filtered solution and run tests + $filtered = ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472 + dotnet test --solution $filtered --no-build -f net472 + +.EXAMPLE + # Generate a solution with only unit test projects + ./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net10.0 -TestProjectNameFilter "*UnitTests*" -OutputPath filtered-unit.slnx + +.EXAMPLE + # Inline usage with dotnet test (PowerShell) + dotnet test --solution (./dotnet/eng/scripts/New-FilteredSolution.ps1 -Solution dotnet/agent-framework-dotnet.slnx -TargetFramework net472) --no-build -f net472 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string]$Solution, + + [Parameter(Mandatory)] + [string]$TargetFramework, + + [string]$Configuration = "Debug", + + [string]$TestProjectNameFilter, + + [switch]$ExcludeSamples, + + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" + +# Resolve the solution path +$solutionPath = Resolve-Path $Solution +$solutionDir = Split-Path $solutionPath -Parent + +if (-not $OutputPath) { + $OutputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "filtered-$(Split-Path $solutionPath -Leaf)") +} + +# Parse the .slnx XML +[xml]$slnx = Get-Content $solutionPath -Raw + +$removed = @() +$kept = @() + +# Remove sample projects if requested +if ($ExcludeSamples) { + $sampleProjects = $slnx.SelectNodes("//Project[contains(@Path, 'samples/')]") + foreach ($proj in $sampleProjects) { + $projRelPath = $proj.GetAttribute("Path") + Write-Verbose "Removing (sample): $projRelPath" + $removed += $projRelPath + $proj.ParentNode.RemoveChild($proj) | Out-Null + } + Write-Host "Removed $($sampleProjects.Count) sample project(s)." -ForegroundColor Yellow +} + +# Filter all remaining projects by target framework +$allProjects = $slnx.SelectNodes("//Project") + +foreach ($proj in $allProjects) { + $projRelPath = $proj.GetAttribute("Path") + $projFullPath = Join-Path $solutionDir $projRelPath + $projFileName = Split-Path $projRelPath -Leaf + $isTestProject = $projRelPath -like "*tests/*" + + # Filter test projects by name pattern if specified + if ($isTestProject -and $TestProjectNameFilter -and ($projFileName -notlike $TestProjectNameFilter)) { + Write-Verbose "Removing (name filter): $projRelPath" + $removed += $projRelPath + $proj.ParentNode.RemoveChild($proj) | Out-Null + continue + } + + if (-not (Test-Path $projFullPath)) { + Write-Verbose "Project not found, keeping in solution: $projRelPath" + $kept += $projRelPath + continue + } + + # Query the project's target frameworks using MSBuild + $targetFrameworks = & dotnet msbuild $projFullPath -getProperty:TargetFrameworks -p:Configuration=$Configuration -nologo 2>$null + $targetFrameworks = $targetFrameworks.Trim() + + if ($targetFrameworks -like "*$TargetFramework*") { + Write-Verbose "Keeping: $projRelPath (targets: $targetFrameworks)" + $kept += $projRelPath + } + else { + Write-Verbose "Removing: $projRelPath (targets: $targetFrameworks, missing: $TargetFramework)" + $removed += $projRelPath + $proj.ParentNode.RemoveChild($proj) | Out-Null + } +} + +# Write the filtered solution +$slnx.Save($OutputPath) + +# Report results to stderr so stdout is clean for piping +Write-Host "Filtered solution written to: $OutputPath" -ForegroundColor Green +if ($removed.Count -gt 0) { + Write-Host "Removed $($removed.Count) project(s):" -ForegroundColor Yellow + foreach ($r in $removed) { + Write-Host " - $r" -ForegroundColor Yellow + } +} +Write-Host "Kept $($kept.Count) project(s)." -ForegroundColor Green + +# Output the path for piping +Write-Output $OutputPath diff --git a/.github/workflows/dotnet-check-coverage.ps1 b/dotnet/eng/scripts/dotnet-check-coverage.ps1 similarity index 100% rename from .github/workflows/dotnet-check-coverage.ps1 rename to dotnet/eng/scripts/dotnet-check-coverage.ps1 diff --git a/dotnet/global.json b/dotnet/global.json index 54533bf771..482aa6b8d3 100644 --- a/dotnet/global.json +++ b/dotnet/global.json @@ -3,5 +3,8 @@ "version": "10.0.100", "rollForward": "minor", "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } \ No newline at end of file diff --git a/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs index 353b4a36ba..1dc8fa2bcd 100644 --- a/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs +++ b/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs @@ -15,11 +15,15 @@ public abstract class AgentTests(Func createAgentF { protected TAgentFixture Fixture { get; private set; } = default!; - public Task InitializeAsync() + public async ValueTask InitializeAsync() { this.Fixture = createAgentFixture(); - return this.Fixture.InitializeAsync(); + await this.Fixture.InitializeAsync(); } - public Task DisposeAsync() => this.Fixture.DisposeAsync(); + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + await this.Fixture.DisposeAsync(); + } } diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj index 929eafe998..ac59cff3fd 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj @@ -1,6 +1,7 @@ + $(NoWarn);CS8793 True diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs index 992db5380b..86b07a30f9 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -1,26 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading.Tasks; using AgentConformance.IntegrationTests; namespace AnthropicChatCompletion.IntegrationTests; -public abstract class SkipAllChatClientRunStreaming(Func func) : ChatClientAgentRunStreamingTests(func) -{ - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() - => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); +public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() - => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); -} +public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); -public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: true)); +public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); -public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: true)); - -public class AnthropicChatCompletionChatClientAgentRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: false, useBeta: false)); - -public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: false)); +public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs index e2ce6e5d04..db150a2605 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -1,30 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading.Tasks; using AgentConformance.IntegrationTests; namespace AnthropicChatCompletion.IntegrationTests; -public abstract class SkipAllChatClientAgentRun(Func func) : ChatClientAgentRunTests(func) -{ - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() - => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() - => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); -} - public class AnthropicBetaChatCompletionChatClientAgentRunTests() - : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: true)); + : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests() - : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: true)); + : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionChatClientAgentRunTests() - : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: false)); + : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionChatClientAgentReasoningRunTests() - : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: false)); + : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs index bdaaeb85f6..af98629237 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -102,9 +103,15 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture // Chat Completion does not require/support deleting sessions, so this is a no-op. Task.CompletedTask; - public async Task InitializeAsync() => + public async ValueTask InitializeAsync() + { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); this._agent = await this.CreateChatClientAgentAsync(); + } - public Task DisposeAsync() => - Task.CompletedTask; + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return default; + } } diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs index 4ed6d39edb..ee39281ba6 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -1,37 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading.Tasks; using AgentConformance.IntegrationTests; namespace AnthropicChatCompletion.IntegrationTests; -public abstract class SkipAllRunStreaming(Func func) : RunStreamingTests(func) -{ - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task SessionMaintainsHistoryAsync() => base.SessionMaintainsHistoryAsync(); -} - public class AnthropicBetaChatCompletionRunStreamingTests() - : SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: true)); + : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionReasoningRunStreamingTests() - : SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: true)); + : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionRunStreamingTests() - : SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: false)); + : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunStreamingTests() - : SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: false)); + : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs index 06f2a15804..6cf514e695 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -1,37 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Threading.Tasks; using AgentConformance.IntegrationTests; namespace AnthropicChatCompletion.IntegrationTests; -public abstract class SkipAllRun(Func func) : RunTests(func) -{ - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); - - [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] - public override Task SessionMaintainsHistoryAsync() => base.SessionMaintainsHistoryAsync(); -} - public class AnthropicBetaChatCompletionRunTests() - : SkipAllRun(() => new(useReasoningChatModel: false, useBeta: true)); + : RunTests(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionReasoningRunTests() - : SkipAllRun(() => new(useReasoningChatModel: true, useBeta: true)); + : RunTests(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionRunTests() - : SkipAllRun(() => new(useReasoningChatModel: false, useBeta: false)); + : RunTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunTests() - : SkipAllRun(() => new(useReasoningChatModel: true, useBeta: false)); + : RunTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicSkillsIntegrationTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicSkillsIntegrationTests.cs index aada9025fe..452b0c6cf2 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicSkillsIntegrationTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicSkillsIntegrationTests.cs @@ -22,9 +22,11 @@ public sealed class AnthropicSkillsIntegrationTests // All tests for Anthropic are intended to be ran locally as the CI pipeline for Anthropic is not setup. private const string SkipReason = "Integrations tests for local execution only"; - [Fact(Skip = SkipReason)] + [Fact] public async Task CreateAgentWithPptxSkillAsync() { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); + // Arrange AnthropicClient anthropicClient = new() { ApiKey = TestConfiguration.GetRequiredValue(TestSettings.AnthropicApiKey) }; string model = TestConfiguration.GetRequiredValue(TestSettings.AnthropicChatModelName); @@ -51,9 +53,11 @@ public sealed class AnthropicSkillsIntegrationTests Assert.NotEmpty(response.Text); } - [Fact(Skip = SkipReason)] + [Fact] public async Task ListAnthropicManagedSkillsAsync() { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); + // Arrange AnthropicClient anthropicClient = new() { ApiKey = TestConfiguration.GetRequiredValue(TestSettings.AnthropicApiKey) }; diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs index 50ced1e64d..870dda648c 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentRunStreamingTests.cs @@ -9,10 +9,10 @@ namespace AzureAI.IntegrationTests; public class AIProjectClientAgentRunStreamingPreviousResponseTests() : RunStreamingTests(() => new()) { - [Fact(Skip = "No messages is not supported")] public override Task RunWithNoMessageDoesNotFailAsync() { - return Task.CompletedTask; + Assert.Skip("No messages is not supported"); + return base.RunWithNoMessageDoesNotFailAsync(); } } @@ -24,9 +24,9 @@ public class AIProjectClientAgentRunStreamingConversationTests() : RunTests(() => new()) { - [Fact(Skip = "No messages is not supported")] public override Task RunWithNoMessageDoesNotFailAsync() { - return Task.CompletedTask; + Assert.Skip("No messages is not supported"); + return base.RunWithNoMessageDoesNotFailAsync(); } } @@ -24,9 +24,9 @@ public class AIProjectClientAgentRunConversationTests() : RunTests - base.RunWithGenericTypeReturnsExpectedResultAsync(); + public override Task RunWithGenericTypeReturnsExpectedResultAsync() + { + Assert.Skip(NotSupported); + return base.RunWithGenericTypeReturnsExpectedResultAsync(); + } - [Fact(Skip = NotSupported)] - public override Task RunWithResponseFormatReturnsExpectedResultAsync() => - base.RunWithResponseFormatReturnsExpectedResultAsync(); + public override Task RunWithResponseFormatReturnsExpectedResultAsync() + { + Assert.Skip(NotSupported); + return base.RunWithResponseFormatReturnsExpectedResultAsync(); + } - [Fact(Skip = NotSupported)] - public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() => - base.RunWithPrimitiveTypeReturnsExpectedResultAsync(); + public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() + { + Assert.Skip(NotSupported); + return base.RunWithPrimitiveTypeReturnsExpectedResultAsync(); + } } /// @@ -84,7 +89,7 @@ public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRu /// public class AIProjectClientStructuredOutputFixture : AIProjectClientFixture { - public override Task InitializeAsync() + public override async ValueTask InitializeAsync() { var agentOptions = new ChatClientAgentOptions { @@ -94,6 +99,6 @@ public class AIProjectClientStructuredOutputFixture : AIProjectClientFixture }, }; - return this.InitializeAsync(agentOptions); + await this.InitializeAsync(agentOptions); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs index befa409d80..3b0c1c27b4 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunStreamingTests.cs @@ -7,9 +7,9 @@ namespace AzureAI.IntegrationTests; public class AIProjectClientChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new()) { - [Fact(Skip = "No messages is not supported")] public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() { - return Task.CompletedTask; + Assert.Skip("No messages is not supported"); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs index 1af12606cb..1e47d0a970 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientChatClientAgentRunTests.cs @@ -7,9 +7,9 @@ namespace AzureAI.IntegrationTests; public class AIProjectClientChatClientAgentRunTests() : ChatClientAgentRunTests(() => new()) { - [Fact(Skip = "No messages is not supported")] public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() { - return Task.CompletedTask; + Assert.Skip("No messages is not supported"); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs index 2485176cd3..6356bb6e01 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs @@ -155,17 +155,19 @@ public class AIProjectClientFixture : IChatClientAgentFixture } } - public Task DisposeAsync() + public ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (this._client is not null && this._agent is not null) { - return this._client.Agents.DeleteAgentAsync(this._agent.Name); + return new ValueTask(this._client.Agents.DeleteAgentAsync(this._agent.Name)); } - return Task.CompletedTask; + return default; } - public virtual async Task InitializeAsync() + public virtual async ValueTask InitializeAsync() { this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); diff --git a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj index bbe03693ea..2703360cb2 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj +++ b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj @@ -1,6 +1,7 @@ + $(NoWarn);CS8793 True True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj index 9cd72a7e77..0913d484e5 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj @@ -1,6 +1,7 @@ + $(NoWarn);CS8793 True True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs index ff5e96c4f1..e6446be1cf 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading.Tasks; using AgentConformance.IntegrationTests; @@ -83,17 +84,19 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture return Task.CompletedTask; } - public Task DisposeAsync() + public ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (this._persistentAgentsClient is not null && this._agent is not null) { - return this._persistentAgentsClient.Administration.DeleteAgentAsync(this._agent.Id); + return new ValueTask(this._persistentAgentsClient.Administration.DeleteAgentAsync(this._agent.Id)); } - return Task.CompletedTask; + return default; } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs index a56917c515..0fa20f18ac 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs @@ -9,15 +9,21 @@ public class AzureAIAgentsPersistentStructuredOutputRunTests() : StructuredOutpu { private const string SkipReason = "Fails intermittently on the build agent/CI"; - [Fact(Skip = SkipReason)] - public override Task RunWithResponseFormatReturnsExpectedResultAsync() => - base.RunWithResponseFormatReturnsExpectedResultAsync(); + public override Task RunWithResponseFormatReturnsExpectedResultAsync() + { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); + return base.RunWithResponseFormatReturnsExpectedResultAsync(); + } - [Fact(Skip = SkipReason)] - public override Task RunWithGenericTypeReturnsExpectedResultAsync() => - base.RunWithGenericTypeReturnsExpectedResultAsync(); + public override Task RunWithGenericTypeReturnsExpectedResultAsync() + { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); + return base.RunWithGenericTypeReturnsExpectedResultAsync(); + } - [Fact(Skip = SkipReason)] - public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() => - base.RunWithPrimitiveTypeReturnsExpectedResultAsync(); + public override Task RunWithPrimitiveTypeReturnsExpectedResultAsync() + { + Assert.SkipWhen(SkipReason is not null, SkipReason ?? string.Empty); + return base.RunWithPrimitiveTypeReturnsExpectedResultAsync(); + } } diff --git a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj index 5f535eb7bd..312a322989 100644 --- a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj +++ b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj @@ -1,6 +1,7 @@ + $(NoWarn);CS8793 True true diff --git a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioFixture.cs b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioFixture.cs index f2f0ce5eb3..c8db0c77d7 100644 --- a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioFixture.cs +++ b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioFixture.cs @@ -28,16 +28,24 @@ public class CopilotStudioFixture : IAgentFixture // Chat Completion does not require/support deleting threads, so this is a no-op. Task.CompletedTask; - public Task InitializeAsync() + public ValueTask InitializeAsync() { const string CopilotStudioHttpClientName = nameof(CopilotStudioAgent); - var settings = new CopilotStudioConnectionSettings( - TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioTenantId), - TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioAgentAppId)) + CopilotStudioConnectionSettings? settings = null; + try { - DirectConnectUrl = TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioDirectConnectUrl), - }; + settings = new CopilotStudioConnectionSettings( + TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioTenantId), + TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioAgentAppId)) + { + DirectConnectUrl = TestConfiguration.GetRequiredValue(TestSettings.CopilotStudioDirectConnectUrl), + }; + } + catch (InvalidOperationException ex) + { + Assert.Skip("CopilotStudio configuration could not be loaded. Error:" + ex.Message); + } ServiceCollection services = new(); @@ -56,8 +64,12 @@ public class CopilotStudioFixture : IAgentFixture this.Agent = new CopilotStudioAgent(client); - return Task.CompletedTask; + return default; } - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return default; + } } diff --git a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunStreamingTests.cs b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunStreamingTests.cs index 076512252b..cd482ee748 100644 --- a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunStreamingTests.cs +++ b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunStreamingTests.cs @@ -10,23 +10,33 @@ public class CopilotStudioRunStreamingTests() : RunStreamingTests - Task.CompletedTask; + public override Task SessionMaintainsHistoryAsync() + { + Assert.Skip("Copilot Studio does not support session history retrieval, so this test is not applicable."); + return base.SessionMaintainsHistoryAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithChatMessageReturnsExpectedResultAsync() => - base.RunWithChatMessageReturnsExpectedResultAsync(); + public override Task RunWithChatMessageReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithChatMessageReturnsExpectedResultAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithChatMessagesReturnsExpectedResultAsync() => - base.RunWithChatMessagesReturnsExpectedResultAsync(); + public override Task RunWithChatMessagesReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithChatMessagesReturnsExpectedResultAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithNoMessageDoesNotFailAsync() => - base.RunWithNoMessageDoesNotFailAsync(); + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithNoMessageDoesNotFailAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithStringReturnsExpectedResultAsync() => - base.RunWithStringReturnsExpectedResultAsync(); + public override Task RunWithStringReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithStringReturnsExpectedResultAsync(); + } } diff --git a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunTests.cs b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunTests.cs index bf7bcfcd64..b927b1bfc5 100644 --- a/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunTests.cs +++ b/dotnet/tests/CopilotStudio.IntegrationTests/CopilotStudioRunTests.cs @@ -10,23 +10,33 @@ public class CopilotStudioRunTests() : RunTests(() => new( // Set to null to run the tests. private const string ManualVerification = "For manual verification"; - [Fact(Skip = "Copilot Studio does not support session history retrieval, so this test is not applicable.")] - public override Task SessionMaintainsHistoryAsync() => - Task.CompletedTask; + public override Task SessionMaintainsHistoryAsync() + { + Assert.Skip("Copilot Studio does not support session history retrieval, so this test is not applicable."); + return base.SessionMaintainsHistoryAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); + public override Task RunWithChatMessageReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithChatMessageReturnsExpectedResultAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithChatMessagesReturnsExpectedResultAsync() => + public override Task RunWithChatMessagesReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithChatMessagesReturnsExpectedResultAsync(); + } - base.RunWithChatMessagesReturnsExpectedResultAsync(); + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithNoMessageDoesNotFailAsync(); + } - [Fact(Skip = ManualVerification)] - public override Task RunWithNoMessageDoesNotFailAsync() => - base.RunWithNoMessageDoesNotFailAsync(); - - [Fact(Skip = ManualVerification)] - public override Task RunWithStringReturnsExpectedResultAsync() => - base.RunWithStringReturnsExpectedResultAsync(); + public override Task RunWithStringReturnsExpectedResultAsync() + { + Assert.SkipWhen(ManualVerification is not null, ManualVerification ?? string.Empty); + return base.RunWithStringReturnsExpectedResultAsync(); + } } diff --git a/dotnet/tests/Directory.Build.props b/dotnet/tests/Directory.Build.props index e3bdd6745d..c4bfc0b0b5 100644 --- a/dotnet/tests/Directory.Build.props +++ b/dotnet/tests/Directory.Build.props @@ -6,22 +6,25 @@ false true false + Exe net10.0;net472 b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - $(NoWarn);Moq1410;xUnit2023;MAAI001 + true + true + $(NoWarn);Moq1410;xUnit1051;MAAI001 - + - - + + - + diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs index 56d6293a58..4b62e549c0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosChatHistoryProviderTests.cs @@ -58,7 +58,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable private bool _preserveContainer; private CosmosClient? _setupClient; // Only used for test setup/cleanup - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { // Fail fast if emulator is not available this.SkipIfEmulatorNotAvailable(); @@ -100,8 +100,10 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable } } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (this._setupClient != null && this._emulatorAvailable) { try @@ -143,12 +145,12 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable // Locally: Skip if emulator connection check failed var ciEmulatorAvailable = string.Equals(Environment.GetEnvironmentVariable("COSMOSDB_EMULATOR_AVAILABLE"), bool.TrueString, StringComparison.OrdinalIgnoreCase); - Xunit.Skip.If(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available"); + Assert.SkipWhen(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available"); } #region Constructor Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void StateKeys_ReturnsDefaultKey_WhenNoStateKeyProvided() { @@ -163,7 +165,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Contains("CosmosChatHistoryProvider", provider.StateKeys); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void StateKeys_ReturnsCustomKey_WhenSetViaConstructor() { @@ -179,7 +181,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Contains("custom-key", provider.StateKeys); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithConnectionString_ShouldCreateInstance() { @@ -196,7 +198,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(TestContainerId, provider.ContainerId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithNullConnectionString_ShouldThrowArgumentException() { @@ -206,7 +208,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable _ => new CosmosChatHistoryProvider.State("test-conversation"))); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithNullStateInitializer_ShouldThrowArgumentNullException() { @@ -221,7 +223,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region InvokedAsync Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_WithSingleMessage_ShouldAddMessageAsync() { @@ -286,7 +288,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(ChatRole.User, messageList[0].Role); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_WithMultipleMessages_ShouldAddAllMessagesAsync() { @@ -329,7 +331,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region InvokingAsync Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokingAsync_WithNoMessages_ShouldReturnEmptyAsync() { @@ -347,7 +349,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Empty(messages); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokingAsync_WithConversationIsolation_ShouldOnlyReturnMessagesForConversationAsync() { @@ -391,7 +393,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region Integration Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task FullWorkflow_AddAndGet_ShouldWorkCorrectlyAsync() { @@ -442,7 +444,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region Disposal Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Dispose_AfterUse_ShouldNotThrow() { @@ -455,7 +457,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable provider.Dispose(); // Should not throw } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Dispose_MultipleCalls_ShouldNotThrow() { @@ -473,7 +475,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region Hierarchical Partitioning Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithHierarchicalConnectionString_ShouldCreateInstance() { @@ -490,7 +492,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(HierarchicalTestContainerId, provider.ContainerId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithHierarchicalEndpoint_ShouldCreateInstance() { @@ -508,7 +510,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(HierarchicalTestContainerId, provider.ContainerId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void Constructor_WithHierarchicalCosmosClient_ShouldCreateInstance() { @@ -525,7 +527,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(HierarchicalTestContainerId, provider.ContainerId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void State_WithEmptyConversationId_ShouldThrowArgumentException() { @@ -534,7 +536,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable new CosmosChatHistoryProvider.State("")); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public void State_WithWhitespaceConversationId_ShouldThrowArgumentException() { @@ -543,7 +545,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable new CosmosChatHistoryProvider.State(" ")); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_WithHierarchicalPartitioning_ShouldAddMessageWithMetadataAsync() { @@ -597,7 +599,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(SessionId, (string)document!.sessionId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_WithHierarchicalMultipleMessages_ShouldAddAllMessagesAsync() { @@ -636,7 +638,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Third hierarchical message", messageList[2].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokingAsync_WithHierarchicalPartitionIsolation_ShouldIsolateMessagesByUserIdAsync() { @@ -682,7 +684,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Message from user 2", messageList2[0].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task StateBag_WithHierarchicalPartitioning_ShouldPreserveStateAcrossProviderInstancesAsync() { @@ -717,7 +719,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(HierarchicalTestContainerId, newStore.ContainerId); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task HierarchicalAndSimplePartitioning_ShouldCoexistAsync() { @@ -759,7 +761,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Hierarchical partitioning message", hierarchicalMessageList[0].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task MaxMessagesToRetrieve_ShouldLimitAndReturnMostRecentAsync() { @@ -800,7 +802,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Message 10", messageList[4].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task MaxMessagesToRetrieve_Null_ShouldReturnAllMessagesAsync() { @@ -836,7 +838,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Message 10", messageList[9].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task GetMessageCountAsync_WithMessages_ShouldReturnCorrectCountAsync() { @@ -868,7 +870,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(5, count); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task GetMessageCountAsync_WithNoMessages_ShouldReturnZeroAsync() { @@ -887,7 +889,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal(0, count); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task ClearMessagesAsync_WithMessages_ShouldDeleteAndReturnCountAsync() { @@ -935,7 +937,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Empty(retrievedMessages); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task ClearMessagesAsync_WithNoMessages_ShouldReturnZeroAsync() { @@ -958,7 +960,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable #region Message Filter Tests - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_DefaultFilter_ExcludesChatHistoryMessagesFromStorageAsync() { @@ -993,7 +995,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Response", messages[2].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync() { @@ -1031,7 +1033,7 @@ public sealed class CosmosChatHistoryProviderTests : IAsyncLifetime, IDisposable Assert.Equal("Response", messages[1].Text); } - [SkippableFact] + [Fact] [Trait("Category", "CosmosDB")] public async Task InvokingAsync_RetrievalOutputFilter_FiltersRetrievedMessagesAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosCheckpointStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosCheckpointStoreTests.cs index 4fa013b8d1..301b58bc49 100644 --- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosCheckpointStoreTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/CosmosCheckpointStoreTests.cs @@ -55,7 +55,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable return options; } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { // Fail fast if emulator is not available this.SkipIfEmulatorNotAvailable(); @@ -88,8 +88,10 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable } } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (this._cosmosClient != null && this._emulatorAvailable) { try @@ -124,12 +126,12 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable // Locally: Skip if emulator connection check failed var ciEmulatorAvailable = string.Equals(Environment.GetEnvironmentVariable("COSMOSDB_EMULATOR_AVAILABLE"), bool.TrueString, StringComparison.OrdinalIgnoreCase); - Xunit.Skip.If(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available"); + Assert.SkipWhen(!ciEmulatorAvailable && !this._emulatorAvailable, "Cosmos DB Emulator is not available"); } #region Constructor Tests - [SkippableFact] + [Fact] public void Constructor_WithCosmosClient_SetsProperties() { // Arrange @@ -143,7 +145,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Equal(TestContainerId, store.ContainerId); } - [SkippableFact] + [Fact] public void Constructor_WithConnectionString_SetsProperties() { // Arrange @@ -157,7 +159,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Equal(TestContainerId, store.ContainerId); } - [SkippableFact] + [Fact] public void Constructor_WithNullCosmosClient_ThrowsArgumentNullException() { // Act & Assert @@ -165,7 +167,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable new CosmosCheckpointStore((CosmosClient)null!, s_testDatabaseId, TestContainerId)); } - [SkippableFact] + [Fact] public void Constructor_WithNullConnectionString_ThrowsArgumentException() { // Act & Assert @@ -177,7 +179,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable #region Checkpoint Operations Tests - [SkippableFact] + [Fact] public async Task CreateCheckpointAsync_NewCheckpoint_CreatesSuccessfullyAsync() { this.SkipIfEmulatorNotAvailable(); @@ -197,7 +199,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.NotEmpty(checkpointInfo.CheckpointId); } - [SkippableFact] + [Fact] public async Task RetrieveCheckpointAsync_ExistingCheckpoint_ReturnsCorrectValueAsync() { this.SkipIfEmulatorNotAvailable(); @@ -218,7 +220,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Equal("Hello, World!", messageProp.GetString()); } - [SkippableFact] + [Fact] public async Task RetrieveCheckpointAsync_NonExistentCheckpoint_ThrowsInvalidOperationExceptionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -233,7 +235,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable store.RetrieveCheckpointAsync(sessionId, fakeCheckpointInfo).AsTask()); } - [SkippableFact] + [Fact] public async Task RetrieveIndexAsync_EmptyStore_ReturnsEmptyCollectionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -250,7 +252,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Empty(index); } - [SkippableFact] + [Fact] public async Task RetrieveIndexAsync_WithCheckpoints_ReturnsAllCheckpointsAsync() { this.SkipIfEmulatorNotAvailable(); @@ -275,7 +277,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Contains(index, c => c.CheckpointId == checkpoint3.CheckpointId); } - [SkippableFact] + [Fact] public async Task CreateCheckpointAsync_WithParent_CreatesHierarchyAsync() { this.SkipIfEmulatorNotAvailable(); @@ -295,7 +297,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable Assert.Equal(sessionId, childCheckpoint.SessionId); } - [SkippableFact] + [Fact] public async Task RetrieveIndexAsync_WithParentFilter_ReturnsFilteredResultsAsync() { this.SkipIfEmulatorNotAvailable(); @@ -331,7 +333,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable #region Run Isolation Tests - [SkippableFact] + [Fact] public async Task CheckpointOperations_DifferentRuns_IsolatesDataAsync() { this.SkipIfEmulatorNotAvailable(); @@ -361,7 +363,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable #region Error Handling Tests - [SkippableFact] + [Fact] public async Task CreateCheckpointAsync_WithNullSessionId_ThrowsArgumentExceptionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -375,7 +377,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable store.CreateCheckpointAsync(null!, checkpointValue).AsTask()); } - [SkippableFact] + [Fact] public async Task CreateCheckpointAsync_WithEmptySessionId_ThrowsArgumentExceptionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -389,7 +391,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable store.CreateCheckpointAsync("", checkpointValue).AsTask()); } - [SkippableFact] + [Fact] public async Task RetrieveCheckpointAsync_WithNullCheckpointInfo_ThrowsArgumentNullExceptionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -407,7 +409,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable #region Disposal Tests - [SkippableFact] + [Fact] public async Task Dispose_AfterDisposal_ThrowsObjectDisposedExceptionAsync() { this.SkipIfEmulatorNotAvailable(); @@ -424,7 +426,7 @@ public class CosmosCheckpointStoreTests : IAsyncLifetime, IDisposable store.CreateCheckpointAsync("test-run", checkpointValue).AsTask()); } - [SkippableFact] + [Fact] public void Dispose_MultipleCalls_DoesNotThrow() { this.SkipIfEmulatorNotAvailable(); diff --git a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj index 78072b8b6a..0103c23028 100644 --- a/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.CosmosNoSql.UnitTests/Microsoft.Agents.AI.CosmosNoSql.UnitTests.csproj @@ -17,7 +17,6 @@ - diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs index fe20b2e843..e8c17cdfc9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/AgentEntityTests.cs @@ -9,7 +9,6 @@ using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Entities; using Microsoft.Extensions.Configuration; using OpenAI.Chat; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ConsoleAppSamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ConsoleAppSamplesValidation.cs index d49614868f..af14a4c8f4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ConsoleAppSamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ConsoleAppSamplesValidation.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; @@ -30,7 +29,7 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) private readonly ITestOutputHelper _outputHelper = outputHelper; - async Task IAsyncLifetime.InitializeAsync() + async ValueTask IAsyncLifetime.InitializeAsync() { if (!s_infrastructureStarted) { @@ -39,7 +38,7 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) } } - async Task IAsyncLifetime.DisposeAsync() + async ValueTask IAsyncDisposable.DisposeAsync() { // Nothing to clean up await Task.CompletedTask; @@ -736,6 +735,9 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) private async Task RunSampleTestAsync(string samplePath, Func, Task> testAction) { + // Build the sample project first (it may not have been built as part of the solution) + await this.BuildSampleAsync(samplePath); + // Generate a unique TaskHub name for this sample test to prevent cross-test interference // when multiple tests run together and share the same DTS emulator. string uniqueTaskHubName = $"sample-{Guid.NewGuid().ToString("N").Substring(0, 6)}"; @@ -814,12 +816,44 @@ public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) return null; } + private async Task BuildSampleAsync(string samplePath) + { + this._outputHelper.WriteLine($"Building sample at {samplePath}..."); + + ProcessStartInfo buildInfo = new() + { + FileName = "dotnet", + Arguments = $"build --framework {s_dotnetTargetFramework}", + WorkingDirectory = samplePath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using Process buildProcess = new() { StartInfo = buildInfo }; + buildProcess.Start(); + + // Read both streams asynchronously to avoid deadlocks from filled pipe buffers + Task stdoutTask = buildProcess.StandardOutput.ReadToEndAsync(); + Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); + await buildProcess.WaitForExitAsync(); + + string stderr = await stderrTask; + if (buildProcess.ExitCode != 0) + { + string stdout = await stdoutTask; + throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); + } + + this._outputHelper.WriteLine($"Build completed for {samplePath}."); + } + private Process StartConsoleApp(string samplePath, BlockingCollection logs, string taskHubName) { ProcessStartInfo startInfo = new() { FileName = "dotnet", - Arguments = $"run --framework {s_dotnetTargetFramework}", + Arguments = $"run --no-build --framework {s_dotnetTargetFramework}", WorkingDirectory = samplePath, UseShellExecute = false, RedirectStandardOutput = true, diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs index d48e8c0c28..0e35d29750 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs @@ -9,7 +9,6 @@ using Microsoft.DurableTask.Client; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using OpenAI.Chat; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLogger.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLogger.cs index ca80b8cf7b..764d9cb24c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLogger.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLogger.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLoggerProvider.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLoggerProvider.cs index 7019852e5e..57fbc4e4db 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLoggerProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Logging/TestLoggerProvider.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs index 641cb57dc8..753d57f160 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs @@ -7,7 +7,6 @@ using Microsoft.DurableTask.Client; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using OpenAI.Chat; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs index ba73c7fbe4..d9350cec59 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenAI.Chat; using Shared.IntegrationTests; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs index f9f008c1c2..4c21817a6d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TimeToLiveTests.cs @@ -7,7 +7,6 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.Configuration; using OpenAI.Chat; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs index d512af28cd..3da741851d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/ToolCallingTests.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs index 173cea189f..c7004e6ba5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests; @@ -36,7 +35,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi private readonly ITestOutputHelper _outputHelper = outputHelper; - async Task IAsyncLifetime.InitializeAsync() + async ValueTask IAsyncLifetime.InitializeAsync() { if (!s_infrastructureStarted) { @@ -45,7 +44,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi } } - async Task IAsyncLifetime.DisposeAsync() + async ValueTask IAsyncDisposable.DisposeAsync() { // Nothing to clean up await Task.CompletedTask; @@ -793,6 +792,9 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi private async Task RunSampleTestAsync(string samplePath, Func, Task> testAction) { + // Build the sample project first (it may not have been built as part of the solution) + await this.BuildSampleAsync(samplePath); + // Start the Azure Functions app List logsContainer = []; using Process funcProcess = this.StartFunctionApp(samplePath, logsContainer); @@ -812,12 +814,44 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi private sealed record OutputLog(DateTime Timestamp, LogLevel Level, string Message); + private async Task BuildSampleAsync(string samplePath) + { + this._outputHelper.WriteLine($"Building sample at {samplePath}..."); + + ProcessStartInfo buildInfo = new() + { + FileName = "dotnet", + Arguments = $"build -f {s_dotnetTargetFramework}", + WorkingDirectory = samplePath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + using Process buildProcess = new() { StartInfo = buildInfo }; + buildProcess.Start(); + + // Read both streams asynchronously to avoid deadlocks from filled pipe buffers + Task stdoutTask = buildProcess.StandardOutput.ReadToEndAsync(); + Task stderrTask = buildProcess.StandardError.ReadToEndAsync(); + await buildProcess.WaitForExitAsync(); + + string stderr = await stderrTask; + if (buildProcess.ExitCode != 0) + { + string stdout = await stdoutTask; + throw new InvalidOperationException($"Failed to build sample at {samplePath}:\n{stdout}\n{stderr}"); + } + + this._outputHelper.WriteLine($"Build completed for {samplePath}."); + } + private Process StartFunctionApp(string samplePath, List logs) { ProcessStartInfo startInfo = new() { FileName = "dotnet", - Arguments = $"run -f {s_dotnetTargetFramework} --port {AzureFunctionsPort}", + Arguments = $"run --no-build -f {s_dotnetTargetFramework} --port {AzureFunctionsPort}", WorkingDirectory = samplePath, UseShellExecute = false, RedirectStandardOutput = true, diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs index 7ec01b6588..4749289f5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; using Shared.IntegrationTests; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeCodeGenTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeCodeGenTest.cs index 03f07758c0..0efb0c19c4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeCodeGenTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeCodeGenTest.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs index 17fe4041cf..eb1d0f55a2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs index 6cabd4983b..6be840ce48 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Shared.IntegrationTests; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs index e1a0857c85..5acc3e5c02 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/TestOutputAdapter.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Text; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs index 151e9fc70c..0333bf4d1c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/WorkflowTest.cs @@ -8,7 +8,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.Extensions.AI; -using Xunit.Abstractions; using Xunit.Sdk; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs index 63e052481a..17b9514ee4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/FunctionCallingWorkflowTest.cs @@ -11,7 +11,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs index 359d9389a6..9d5efa6b6d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/InvokeToolWorkflowTest.cs @@ -12,7 +12,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.Mcp; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index 244e4f0eb3..7c3aef758c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; using OpenAI.Files; using Shared.IntegrationTests; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/AddConversationMessageTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/AddConversationMessageTemplateTest.cs index d62bb8556c..786563d688 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/AddConversationMessageTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/AddConversationMessageTemplateTest.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/BreakLoopTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/BreakLoopTemplateTest.cs index a3e202b60a..2960718256 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/BreakLoopTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/BreakLoopTemplateTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ClearAllVariablesTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ClearAllVariablesTemplateTest.cs index a7abb63ee4..be7ea25eab 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ClearAllVariablesTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ClearAllVariablesTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ConditionGroupTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ConditionGroupTemplateTest.cs index 0d3c47089e..af0166c44e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ConditionGroupTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ConditionGroupTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ContinueLoopTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ContinueLoopTemplateTest.cs index 19e4a41d2c..9210460701 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ContinueLoopTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ContinueLoopTemplateTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CopyConversationMessagesTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CopyConversationMessagesTemplateTest.cs index 438f793b0e..5f005b6b3b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CopyConversationMessagesTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CopyConversationMessagesTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CreateConversationTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CreateConversationTemplateTest.cs index 9991a1a827..c4c0fd4458 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CreateConversationTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/CreateConversationTemplateTest.cs @@ -5,7 +5,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs index 6f87f77fb4..0c6ac9efe7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/DeclarativeEjectionTest.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Threading.Tasks; using Shared.Code; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EdgeTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EdgeTemplateTest.cs index ead2ca742a..10633f4581 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EdgeTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EdgeTemplateTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndConversationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndConversationTest.cs index c38036e777..75d2cc7b80 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndConversationTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndConversationTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndDialogTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndDialogTest.cs index 59065665c3..aea9b76833 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndDialogTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/EndDialogTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ForeachTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ForeachTemplateTest.cs index aaafa5bfb3..d6e924c262 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ForeachTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ForeachTemplateTest.cs @@ -4,7 +4,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/GotoTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/GotoTemplateTest.cs index b4aefadb68..1c9c2c26ad 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/GotoTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/GotoTemplateTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs index 34acf37702..8642270726 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/InvokeAzureAgentTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ProviderTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ProviderTemplateTest.cs index fcaabcb4a1..28ae9a0314 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ProviderTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ProviderTemplateTest.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ResetVariableTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ResetVariableTemplateTest.cs index 1ffd3e16ef..b34126c5be 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ResetVariableTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/ResetVariableTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessageTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessageTemplateTest.cs index 093a43ffa5..153cb95ea4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessageTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessageTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessagesTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessagesTemplateTest.cs index 1c3f5c20f5..30988ef019 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessagesTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/RetrieveConversationMessagesTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetMultipleVariablesTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetMultipleVariablesTemplateTest.cs index 5dd05c8bac..91387705e0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetMultipleVariablesTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetMultipleVariablesTemplateTest.cs @@ -4,7 +4,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetTextVariableTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetTextVariableTemplateTest.cs index 4638ee0c8b..9a503394de 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetTextVariableTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetTextVariableTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetVariableTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetVariableTemplateTest.cs index c71c57486e..64f8a1b6a8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetVariableTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/SetVariableTemplateTest.cs @@ -4,7 +4,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.CodeGen; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/WorkflowActionTemplateTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/WorkflowActionTemplateTest.cs index 2f6cedb6dd..6ae2a4b45e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/WorkflowActionTemplateTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/CodeGen/WorkflowActionTemplateTest.cs @@ -3,7 +3,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowExceptionTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowExceptionTest.cs index cbe3ac0a81..099c09c27d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowExceptionTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowExceptionTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index 09c984ca05..6c61d6cb7d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -12,7 +12,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Moq; -using Xunit.Abstractions; using Xunit.Sdk; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractionResultTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractionResultTest.cs index 50cff90b3e..d2c545516e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractionResultTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractionResultTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Entities; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Entities; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractorTest.cs index b03700d215..4a677eb362 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Entities/EntityExtractorTest.cs @@ -4,7 +4,6 @@ using System; using Microsoft.Agents.AI.Workflows.Declarative.Entities; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Entities; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs index a4965ebc61..9133471553 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/EventTest.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text.Json; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs index d1165d84d4..cebdc60cb9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputRequestTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Events; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs index b1fb358727..384664a68c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/ExternalInputResponseTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Events; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs index 95d738f8f0..03a5bb670f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Interpreter; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs index a7f2ba48f6..2f89de4dee 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs index 70e4ac0a02..cc18bcb463 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ConditionGroupExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ConditionGroupExecutorTest.cs index caf7344467..910af1ca64 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ConditionGroupExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ConditionGroupExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs index cb818fec15..c0a2fdf659 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CreateConversationExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CreateConversationExecutorTest.cs index a8c8f799b2..5c00fbcdda 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CreateConversationExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CreateConversationExecutorTest.cs @@ -6,7 +6,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/DefaultActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/DefaultActionExecutorTest.cs index 0e7f0a4558..e10f0b0d92 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/DefaultActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/DefaultActionExecutorTest.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs index 6c422247f1..ad9d51c2fe 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableExecutorTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs index 5eb723ae0e..bb4442507c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs index 44989ad8a1..7840910d5b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeFunctionToolExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeFunctionToolExecutorTest.cs index 4a07ba3002..b00339ea3b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeFunctionToolExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeFunctionToolExecutorTest.cs @@ -6,7 +6,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs index 2cad0029ff..45b0b3c7b7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/InvokeMcpToolExecutorTest.cs @@ -10,7 +10,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Moq; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs index 22854c90e8..01c6944654 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs index b2713037bc..dbe056f891 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/QuestionExecutorTest.cs @@ -12,7 +12,6 @@ using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; using Moq; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs index 778a6dd7b7..1e11f1a0ae 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RequestExternalInputExecutorTest.cs @@ -12,7 +12,6 @@ using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; using Moq; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs index 9059780751..022d84bbfe 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessageExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessageExecutorTest.cs index e3812100ee..622b54d1b2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessageExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessageExecutorTest.cs @@ -6,7 +6,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessagesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessagesExecutorTest.cs index cbdfc2056d..7b726ccb23 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessagesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/RetrieveConversationMessagesExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs index 32cadc6c4e..8ae95d0eb5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SendActivityExecutorTest.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs index 037ee5b94a..467a20044e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs index 0bc850e9ce..f15a315eab 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs index dddfab6365..4f4bb39856 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 6c87668bbf..de5487c79b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -10,7 +10,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; using Xunit.Sdk; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs index 976ad796b9..d158ca552b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.PowerFx; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs index eeaefaf669..c509259fe1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineTest.cs @@ -2,7 +2,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.PowerFx; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs index 9bbbc39f42..de7f045052 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index 2aaa016141..ebaaf5d046 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -8,7 +8,6 @@ using Microsoft.Agents.ObjectModel; using Microsoft.Agents.ObjectModel.Abstractions; using Microsoft.Agents.ObjectModel.Exceptions; using Microsoft.PowerFx.Types; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/TestOutputAdapter.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/TestOutputAdapter.cs index 72da232da9..e4d756a24a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/TestOutputAdapter.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/TestOutputAdapter.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Text; using Microsoft.Extensions.Logging; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/WorkflowTest.cs index c8805b606c..1e6704b1f6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/WorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/WorkflowTest.cs @@ -3,7 +3,6 @@ using System; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; -using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs index 40e79f8af5..36c43076ed 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ObservabilityTests.cs @@ -133,31 +133,31 @@ public sealed class ObservabilityTests : IDisposable activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event"); } - [Fact(Skip = "Flaky test - temporarily disabled")] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_DefaultAsync() { await this.TestWorkflowEndToEndActivitiesAsync("Default"); } - [Fact(Skip = "Flaky test - temporarily disabled. Tracked in #12345")] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_OffThreadAsync() { await this.TestWorkflowEndToEndActivitiesAsync("OffThread"); } - [Fact(Skip = "Flaky test - temporarily disabled")] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_ConcurrentAsync() { await this.TestWorkflowEndToEndActivitiesAsync("Concurrent"); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task CreatesWorkflowEndToEndActivities_WithCorrectName_LockstepAsync() { await this.TestWorkflowEndToEndActivitiesAsync("Lockstep"); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task CreatesWorkflowActivities_WithCorrectNameAsync() { // Arrange @@ -182,7 +182,7 @@ public sealed class ObservabilityTests : IDisposable tags.Should().ContainKey(Tags.WorkflowDefinition); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task TelemetryDisabledByDefault_CreatesNoActivitiesAsync() { // Arrange @@ -200,7 +200,7 @@ public sealed class ObservabilityTests : IDisposable capturedActivities.Should().BeEmpty("No activities should be created when telemetry is disabled (default)."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task WithOpenTelemetry_UsesProvidedActivitySourceAsync() { // Arrange @@ -235,7 +235,7 @@ public sealed class ObservabilityTests : IDisposable "All activities should come from the user-provided ActivitySource."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task DisableWorkflowBuild_PreventsWorkflowBuildActivityAsync() { // Arrange @@ -255,7 +255,7 @@ public sealed class ObservabilityTests : IDisposable "WorkflowBuild activity should be disabled."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync() { // Arrange @@ -285,7 +285,7 @@ public sealed class ObservabilityTests : IDisposable "Other activities should still be created."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync() { // Arrange @@ -312,7 +312,7 @@ public sealed class ObservabilityTests : IDisposable "Other activities should still be created."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task DisableEdgeGroupProcess_PreventsEdgeGroupProcessActivityAsync() { // Arrange @@ -333,7 +333,7 @@ public sealed class ObservabilityTests : IDisposable "Other activities should still be created."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task DisableMessageSend_PreventsMessageSendActivityAsync() { // Arrange @@ -382,7 +382,7 @@ public sealed class ObservabilityTests : IDisposable return builder.WithOpenTelemetry(configure: opts => opts.DisableMessageSend = true).Build(); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task EnableSensitiveData_LogsExecutorInputAndOutputAsync() { // Arrange @@ -413,7 +413,7 @@ public sealed class ObservabilityTests : IDisposable tags[Tags.ExecutorOutput].Should().Contain("HELLO", "Output should contain the transformed value."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task EnableSensitiveData_Disabled_DoesNotLogInputOutputAsync() { // Arrange @@ -442,7 +442,7 @@ public sealed class ObservabilityTests : IDisposable tags.Should().NotContainKey(Tags.ExecutorOutput, "Output should NOT be logged when EnableSensitiveData is false."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task EnableSensitiveData_LogsMessageSendContentAsync() { // Arrange @@ -474,7 +474,7 @@ public sealed class ObservabilityTests : IDisposable tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should be logged."); } - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task EnableSensitiveData_Disabled_DoesNotLogMessageContentAsync() { // Arrange diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowRunActivityStopTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowRunActivityStopTests.cs index a296af8095..112961c609 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowRunActivityStopTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowRunActivityStopTests.cs @@ -67,7 +67,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// Bug: The Activity created by LockstepRunEventStream.TakeEventStreamAsync is never /// disposed because yield break in async iterators does not trigger using disposal. /// - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task WorkflowRunActivity_IsStopped_LockstepAsync() { // Arrange @@ -111,7 +111,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// Verifies that the workflow_invoke Activity is stopped when using the OffThread (Default) /// execution environment (StreamingRunEventStream). /// - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task WorkflowRunActivity_IsStopped_OffThreadAsync() { // Arrange @@ -156,7 +156,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// (StreamingRun.WatchStreamAsync) with the OffThread execution environment. /// This matches the exact usage pattern described in the issue. /// - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task WorkflowRunActivity_IsStopped_Streaming_OffThreadAsync() { // Arrange @@ -203,7 +203,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// streaming invocation, even when using the same workflow in a multi-turn pattern, /// and that each session gets its own session activity. /// - [Fact(Skip = "Flaky test - temporarily disabled")] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task WorkflowRunActivity_IsStopped_Streaming_OffThread_MultiTurnAsync() { // Arrange @@ -264,7 +264,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// Verifies that all started activities (not just workflow_invoke) are properly stopped. /// This ensures no spans are "leaked" without being exported. /// - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task AllActivities_AreStopped_AfterWorkflowCompletionAsync() { // Arrange @@ -305,7 +305,7 @@ public sealed class WorkflowRunActivityStopTests : IDisposable /// be parented under the workflow session span. The run activity should /// still nest correctly under the session. /// - [Fact] + [Fact(Skip = "Flaky test - temporarily disabled.")] public async Task Lockstep_SessionActivity_DoesNotLeak_IntoCaller_ActivityCurrentAsync() { // Arrange diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs index b2ae9b81e8..f679da04aa 100644 --- a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs +++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantFixture.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Threading.Tasks; using AgentConformance.IntegrationTests; @@ -77,7 +78,7 @@ public class OpenAIAssistantFixture : IChatClientAgentFixture return Task.CompletedTask; } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { var client = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)); this._assistantClient = client.GetAssistantClient(); @@ -85,13 +86,15 @@ public class OpenAIAssistantFixture : IChatClientAgentFixture this._agent = await this.CreateChatClientAgentAsync(); } - public Task DisposeAsync() + public ValueTask DisposeAsync() { + GC.SuppressFinalize(this); + if (this._assistantClient is not null && this._agent is not null) { - return this._assistantClient.DeleteAssistantAsync(this._agent.Id); + return new ValueTask(this._assistantClient.DeleteAssistantAsync(this._agent.Id)); } - return Task.CompletedTask; + return default; } } diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs index b8a9388b27..4e3bd7e3b0 100644 --- a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -63,9 +64,12 @@ public class OpenAIChatCompletionFixture : IChatClientAgentFixture // Chat Completion does not require/support deleting threads, so this is a no-op. Task.CompletedTask; - public async Task InitializeAsync() => + public async ValueTask InitializeAsync() => this._agent = await this.CreateChatClientAgentAsync(); - public Task DisposeAsync() => - Task.CompletedTask; + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return default; + } } diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunStreamingTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunStreamingTests.cs index 80a148d7fc..737abd2561 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunStreamingTests.cs @@ -9,16 +9,20 @@ public class OpenAIResponseStoreTrueChatClientAgentRunStreamingTests() : ChatCli { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => - Task.CompletedTask; + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip(SkipReason); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } } public class OpenAIResponseStoreFalseChatClientAgentRunStreamingTests() : ChatClientAgentRunStreamingTests(() => new(store: false)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => - Task.CompletedTask; + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip(SkipReason); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } } diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunTests.cs index 8b742e2964..58463212bd 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunTests.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseChatClientAgentRunTests.cs @@ -9,16 +9,20 @@ public class OpenAIResponseStoreTrueChatClientAgentRunTests() : ChatClientAgentR { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => - Task.CompletedTask; + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip(SkipReason); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } } public class OpenAIResponseStoreFalseChatClientAgentRunTests() : ChatClientAgentRunTests(() => new(store: false)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => - Task.CompletedTask; + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + { + Assert.Skip(SkipReason); + return base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); + } } diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs index 515703c21c..74c7ef9041 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs @@ -94,7 +94,7 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture // Chat Completion does not require/support deleting threads, so this is a no-op. Task.CompletedTask; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { this._openAIResponseClient = new OpenAIClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIApiKey)) .GetResponsesClient(TestConfiguration.GetRequiredValue(TestSettings.OpenAIChatModelName)); @@ -102,5 +102,9 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture this._agent = await this.CreateChatClientAgentAsync(); } - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return default; + } } diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunStreamingTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunStreamingTests.cs index c12f8f2db5..75c337bd5a 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunStreamingTests.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunStreamingTests.cs @@ -8,16 +8,21 @@ namespace ResponseResult.IntegrationTests; public class OpenAIResponseStoreTrueRunStreamingTests() : RunStreamingTests(() => new(store: true)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => - Task.CompletedTask; + + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.Skip(SkipReason); + return base.RunWithNoMessageDoesNotFailAsync(); + } } public class OpenAIResponseStoreFalseRunStreamingTests() : RunStreamingTests(() => new(store: false)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => - Task.CompletedTask; + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.Skip(SkipReason); + return base.RunWithNoMessageDoesNotFailAsync(); + } } diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunTests.cs index 423ac583c7..df4962b640 100644 --- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunTests.cs +++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseRunTests.cs @@ -8,16 +8,21 @@ namespace ResponseResult.IntegrationTests; public class OpenAIResponseStoreTrueRunTests() : RunTests(() => new(store: true)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => - Task.CompletedTask; + + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.Skip(SkipReason); + return base.RunWithNoMessageDoesNotFailAsync(); + } } public class OpenAIResponseStoreFalseRunTests() : RunTests(() => new(store: false)) { private const string SkipReason = "ResponseResult does not support empty messages"; - [Fact(Skip = SkipReason)] - public override Task RunWithNoMessageDoesNotFailAsync() => - Task.CompletedTask; + public override Task RunWithNoMessageDoesNotFailAsync() + { + Assert.Skip(SkipReason); + return base.RunWithNoMessageDoesNotFailAsync(); + } } diff --git a/dotnet/tests/coverage.runsettings b/dotnet/tests/coverage.runsettings new file mode 100644 index 0000000000..c59039e263 --- /dev/null +++ b/dotnet/tests/coverage.runsettings @@ -0,0 +1,21 @@ + + + + + + + + + + + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Runtime\.CompilerServices\.CompilerGeneratedAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + + + + From 4a043c6c669e03666c86a245d737c12b3b41511b Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:42:46 +0000 Subject: [PATCH 4/5] .NET: Switch auth sample to use Singletons (#4454) * Switch auth sample to use Singletons * Address PR comments * Add comment to warn users to choose the appropriate lifetime for their service --- .../Service/Program.cs | 11 +++- .../Service/UserContext.cs | 64 ++++++++++++++----- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs index b4a5d00a9a..1d89296a2e 100644 --- a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs +++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/Program.cs @@ -75,10 +75,15 @@ string apiKey = builder.Configuration["OPENAI_API_KEY"] ?? throw new InvalidOperationException("Set the OPENAI_API_KEY environment variable."); string model = builder.Configuration["OPENAI_MODEL"] ?? "gpt-4.1-mini"; +// Here we are using Singleton lifetime, since none of the services, function tools and user context classes in the sample have state that are per request. +// You should evaluate the appropriate lifetime for your own services and tools based on their behavior and dependencies. +// E.g. if any of the service instances or tools maintain state that is specific to a user, and each request may be from a different user, +// you should use Scoped lifetime instead, so that a new instance is created for each request. +// Note that if you use Scoped lifetime for any dependencies, you must also use Scoped lifetime for any class that uses it, including the agent itself. builder.Services.AddHttpContextAccessor(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(sp => +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => { var expenseService = sp.GetRequiredService(); diff --git a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs index 34f4fe8956..3c621f0207 100644 --- a/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs +++ b/dotnet/samples/05-end-to-end/AspNetAgentAuthorization/Service/UserContext.cs @@ -27,43 +27,73 @@ public interface IUserContext /// Keycloak uses sub for the user ID, preferred_username /// for the login name, given_name/family_name for the /// display name, and scope (space-delimited) for granted scopes. -/// Registered as a scoped service so it is resolved once per request. +/// Registered as a singleton — claims are parsed once per request and +/// cached in . /// public sealed class KeycloakUserContext : IUserContext { - public string UserId { get; } + private static readonly object s_cacheKey = new(); - public string UserName { get; } - - public string DisplayName { get; } - - public IReadOnlySet Scopes { get; } + private readonly IHttpContextAccessor _httpContextAccessor; public KeycloakUserContext(IHttpContextAccessor httpContextAccessor) { - ClaimsPrincipal? user = httpContextAccessor.HttpContext?.User; + this._httpContextAccessor = httpContextAccessor; + } - this.UserId = user?.FindFirstValue(ClaimTypes.NameIdentifier) - ?? user?.FindFirstValue("sub") - ?? "anonymous"; + public string UserId => this.GetOrCreateCachedInfo().UserId; - this.UserName = user?.FindFirstValue("preferred_username") - ?? user?.FindFirstValue(ClaimTypes.Name) - ?? "unknown"; + public string UserName => this.GetOrCreateCachedInfo().UserName; + + public string DisplayName => this.GetOrCreateCachedInfo().DisplayName; + + public IReadOnlySet Scopes => this.GetOrCreateCachedInfo().Scopes; + + private CachedUserInfo GetOrCreateCachedInfo() + { + HttpContext? httpContext = this._httpContextAccessor.HttpContext; + if (httpContext is not null && httpContext.Items.TryGetValue(s_cacheKey, out object? cached) && cached is CachedUserInfo info) + { + return info; + } + + info = ParseClaims(httpContext?.User); + + if (httpContext is not null) + { + httpContext.Items[s_cacheKey] = info; + } + + return info; + } + + private static CachedUserInfo ParseClaims(ClaimsPrincipal? user) + { + string userId = user?.FindFirstValue(ClaimTypes.NameIdentifier) + ?? user?.FindFirstValue("sub") + ?? "anonymous"; + + string userName = user?.FindFirstValue("preferred_username") + ?? user?.FindFirstValue(ClaimTypes.Name) + ?? "unknown"; string? givenName = user?.FindFirstValue("given_name") ?? user?.FindFirstValue(ClaimTypes.GivenName); string? familyName = user?.FindFirstValue("family_name") ?? user?.FindFirstValue(ClaimTypes.Surname); - this.DisplayName = (givenName, familyName) switch + string displayName = (givenName, familyName) switch { (not null, not null) => $"{givenName} {familyName}", (not null, null) => givenName, (null, not null) => familyName, - _ => this.UserName, + _ => userName, }; string? scopeClaim = user?.FindFirstValue("scope"); - this.Scopes = scopeClaim is not null + IReadOnlySet scopes = scopeClaim is not null ? new HashSet(scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase) : new HashSet(StringComparer.OrdinalIgnoreCase); + + return new CachedUserInfo(userId, userName, displayName, scopes); } + + private sealed record CachedUserInfo(string UserId, string UserName, string DisplayName, IReadOnlySet Scopes); } From 55ddd841b710b2ab5f5bb59fdc804ece34d30515 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 5 Mar 2026 16:32:24 +0100 Subject: [PATCH 5/5] Python: Fix Python pyright package scoping and typing remediation (#4426) * Fix Python pyright package scoping and typing remediation Implements issue #4407 by removing the root pyright include, adding package-level pyright includes, and resolving pyright/mypy typing issues across Python packages. Also cleans unnecessary casts and applies line-level, rule-specific ignores where external libraries are too dynamic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reduce pyright cost in handoff cloning Simplify cloned_options construction in HandoffAgentExecutor to avoid expensive TypedDict narrowing/inference in _handoff.py, which was causing pyright to spend a long time in orchestrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix types * Fix lint and type-check regressions Resolve current Python package check failures across lint, pyright, and mypy after recent code changes, including purview/declarative pyright issues and multiple ruff simplification findings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fixed hooks * Stabilize package tests and test tasks Resolve cross-package non-integration test failures, simplify streaming type flow, harden locale/culture handling, and standardize package test poe tasks to exclude integration tests where applicable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * lots of small fixes * Fix current Python test regressions Address current failing unit tests in azure-ai, bedrock, and azure-cosmos while keeping Bedrock parsing logic inline (no new static helper methods). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * small fixes * small fixes * removed pydantic from json * final updates * fix core * fix tests * fix obser --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/CODING_STANDARD.md | 21 + .../a2a/agent_framework_a2a/_agent.py | 33 +- python/packages/a2a/pyproject.toml | 3 +- python/packages/ag-ui/pyproject.toml | 3 +- .../agent_framework_anthropic/_chat_client.py | 31 +- python/packages/anthropic/pyproject.toml | 2 +- .../_context_provider.py | 8 +- .../packages/azure-ai-search/pyproject.toml | 3 +- .../agent_framework_azure_ai/_chat_client.py | 73 ++- .../agent_framework_azure_ai/_client.py | 76 +-- .../_embedding_client.py | 2 +- .../_project_provider.py | 2 +- .../agent_framework_azure_ai/_shared.py | 49 +- python/packages/azure-ai/pyproject.toml | 3 +- .../_history_provider.py | 28 +- python/packages/azure-cosmos/pyproject.toml | 3 +- .../samples/cosmos_history_provider.py | 4 +- .../tests/test_cosmos_history_provider.py | 12 +- .../agent_framework_azurefunctions/_app.py | 61 ++- .../_serialization.py | 17 +- .../_workflow.py | 32 +- python/packages/azurefunctions/pyproject.toml | 3 +- .../agent_framework_bedrock/__init__.py | 4 +- .../agent_framework_bedrock/_chat_client.py | 131 +++-- .../_embedding_client.py | 24 +- python/packages/bedrock/pyproject.toml | 5 +- python/packages/chatkit/pyproject.toml | 3 +- .../claude/agent_framework_claude/_agent.py | 81 +-- python/packages/claude/pyproject.toml | 3 +- .../agent_framework_copilotstudio/_agent.py | 20 +- python/packages/copilotstudio/pyproject.toml | 3 +- .../packages/core/agent_framework/__init__.py | 6 +- .../packages/core/agent_framework/_agents.py | 63 ++- .../packages/core/agent_framework/_clients.py | 9 +- .../core/agent_framework/_middleware.py | 52 +- .../core/agent_framework/_serialization.py | 20 +- .../core/agent_framework/_sessions.py | 11 +- .../core/agent_framework/_settings.py | 4 +- .../packages/core/agent_framework/_skills.py | 5 +- .../packages/core/agent_framework/_tools.py | 437 ++++------------ .../packages/core/agent_framework/_types.py | 347 ++++++------- .../_workflows/_agent_executor.py | 15 +- .../_workflows/_function_executor.py | 2 +- .../_workflows/_runner_context.py | 10 +- .../_workflows/_typing_utils.py | 26 +- .../azure/_assistants_client.py | 33 +- .../agent_framework/azure/_chat_client.py | 47 +- .../azure/_embedding_client.py | 19 +- .../azure/_responses_client.py | 26 +- .../core/agent_framework/azure/_shared.py | 3 + .../agent_framework/declarative/__init__.pyi | 2 - .../core/agent_framework/observability.py | 130 +++-- .../openai/_assistant_provider.py | 43 +- .../openai/_assistants_client.py | 74 ++- .../agent_framework/openai/_chat_client.py | 51 +- .../openai/_embedding_client.py | 23 +- .../openai/_responses_client.py | 39 +- .../core/agent_framework/openai/_shared.py | 8 +- python/packages/core/pyproject.toml | 5 +- .../packages/core/tests/core/test_skills.py | 8 +- python/packages/core/tests/core/test_tools.py | 466 +----------------- python/packages/core/tests/core/test_types.py | 20 +- .../openai/test_openai_embedding_client.py | 9 +- .../tests/workflow/test_agent_executor.py | 51 +- .../core/tests/workflow/test_agent_utils.py | 27 +- .../packages/core/tests/workflow/test_edge.py | 3 +- .../core/tests/workflow/test_executor.py | 127 ++--- .../tests/workflow/test_workflow_agent.py | 36 +- .../tests/workflow/test_workflow_kwargs.py | 90 +++- .../tests/workflow/test_workflow_states.py | 8 +- .../agent_framework_declarative/_loader.py | 13 +- .../_workflows/_declarative_base.py | 82 +-- .../_workflows/_declarative_builder.py | 7 +- .../_workflows/_executors_agents.py | 4 +- .../_workflows/_executors_basic.py | 83 ++-- .../_workflows/_executors_tools.py | 13 +- .../_workflows/_powerfx_functions.py | 32 +- .../_workflows/_state.py | 9 +- python/packages/declarative/pyproject.toml | 2 +- .../tests/test_declarative_loader.py | 8 +- .../tests/test_powerfx_yaml_compatibility.py | 32 +- .../devui/agent_framework_devui/__init__.py | 4 +- .../agent_framework_devui/_conversations.py | 31 +- .../agent_framework_devui/_deployment.py | 19 +- .../devui/agent_framework_devui/_discovery.py | 78 +-- .../devui/agent_framework_devui/_executor.py | 185 ++++--- .../devui/agent_framework_devui/_mapper.py | 129 +++-- .../_openai/_executor.py | 56 ++- .../devui/agent_framework_devui/_server.py | 121 +++-- .../devui/agent_framework_devui/_session.py | 43 +- .../devui/agent_framework_devui/_utils.py | 43 +- .../models/_discovery_models.py | 7 +- python/packages/devui/pyproject.toml | 2 +- .../agent_framework_durabletask/_entities.py | 4 +- .../_response_utils.py | 4 +- python/packages/durabletask/pyproject.toml | 5 +- .../_foundry_local_client.py | 9 +- python/packages/foundry_local/pyproject.toml | 3 +- .../agent_framework_github_copilot/_agent.py | 24 +- python/packages/github_copilot/pyproject.toml | 3 +- .../lab/gaia/agent_framework_lab_gaia/gaia.py | 118 +++-- python/packages/lab/pyproject.toml | 9 +- .../_message_utils.py | 2 +- .../agent_framework_lab_tau2/_tau2_utils.py | 76 ++- .../tau2/agent_framework_lab_tau2/runner.py | 14 +- .../agent_framework_mem0/_context_provider.py | 2 +- python/packages/mem0/pyproject.toml | 3 +- .../agent_framework_ollama/_chat_client.py | 4 +- .../_embedding_client.py | 16 +- python/packages/ollama/pyproject.toml | 3 +- .../_handoff.py | 105 ++-- python/packages/orchestrations/pyproject.toml | 3 +- .../agent_framework_purview/_client.py | 66 ++- .../agent_framework_purview/_middleware.py | 4 +- .../agent_framework_purview/_models.py | 78 +-- .../agent_framework_purview/_processor.py | 11 +- python/packages/purview/pyproject.toml | 3 +- .../_context_provider.py | 25 +- .../_history_provider.py | 6 +- python/packages/redis/pyproject.toml | 3 +- python/pyproject.toml | 3 +- python/uv.lock | 28 +- 122 files changed, 2328 insertions(+), 2407 deletions(-) diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 21d87e5b8c..ccb8e058e3 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -27,6 +27,12 @@ Public modules must include a module-level docstring, including `__init__.py` fi ## Type Annotations +We use typing as a helper, it is not a goal in and of itself, so be pragmatic about where and when to strictly type, versus when to use a targetted cast or ignore. +In general, the public interfaces of our classes, are important to get right, internally it is okay to have loosely typed code, as long as tests cover the code itself. +This includes making a conscious choice when to program defensively, you can always do `getattr(item, 'attribute')` but that might end up causing you issues down the road +because the type of `item` in this case, should have that attribute and if it doesn't it points to a larger issue, so if the type is expected to have that attribute, you should +use `item.attribute` to ensure it fails at that point, rather then somewhere downstream where a value is expected but none was found. + ### Future Annotations > **Note:** This convention is being adopted. See [#3578](https://github.com/microsoft/agent-framework/issues/3578) for progress. @@ -79,6 +85,21 @@ def process_config(config: MutableMapping[str, Any]) -> None: ... ``` +### Typing Ignore and Cast Policy + +Use typing as a helper first and suppressions as a last resort: + +- **Prefer explicit typing before suppression**: Start with clearer type annotations, helper types, overloads, + protocols, or refactoring dynamic code into typed helpers. Prioritize performance over completeness of typing, but make a good-faith effort to reduce uncertainty with typing before ignoring. Prefer to use a cast over a typeguard function since that does add overhead. +- **Avoid redundant casts**: Do not add `cast(...)` if the type already matches; casts should be reserved for + unavoidable narrowing where the runtime contract is known, we will use mypy's check on redundant casts to enforce this. +- **Avoid multiple assignments**: Avoid assigning multiple variables just to get typing to pass, that has performance impact while typing should not have that. +- **Line-level pyright ignores only**: If suppression is still required, use a line-level rule-specific ignore + (`# pyright: ignore[reportGeneralTypeIssues]`), file-level is allowed if there is a compelling reason for it, that should be documented right beneath the ignore. + Never change the global suppression flags for mypy and pyright unless the dev team okays it. +- **Private usage boundary**: Accessing private members across `agent_framework*` packages can be acceptable for this + codebase, but private member usage for non-Agent Framework dependencies should remain flagged. + ## Function Parameter Guidelines To make the code easier to use and maintain: diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index 2eec8a41db..31fac386b3 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -7,7 +7,7 @@ import json import re import uuid from collections.abc import AsyncIterable, Awaitable, Sequence -from typing import Any, Final, Literal, overload +from typing import Any, Final, Literal, TypeAlias, overload import httpx from a2a.client import Client, ClientConfig, ClientFactory, minimal_agent_card @@ -19,9 +19,11 @@ from a2a.types import ( FileWithBytes, FileWithUri, Task, + TaskArtifactUpdateEvent, TaskIdParams, TaskQueryParams, TaskState, + TaskStatusUpdateEvent, TextPart, TransportProtocol, ) @@ -70,6 +72,9 @@ IN_PROGRESS_TASK_STATES = [ TaskState.auth_required, ] +A2AClientEvent: TypeAlias = tuple[Task, TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None] +A2AStreamItem: TypeAlias = A2AMessage | A2AClientEvent + def _get_uri_data(uri: str) -> str: match = URI_PATTERN.match(uri) @@ -260,7 +265,9 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): When stream=True: A ResponseStream of AgentResponseUpdate items. """ if continuation_token is not None: - a2a_stream: AsyncIterable[Any] = self.client.resubscribe(TaskIdParams(id=continuation_token["task_id"])) + a2a_stream: AsyncIterable[A2AStreamItem] = self.client.resubscribe( + TaskIdParams(id=continuation_token["task_id"]) + ) else: normalized_messages = normalize_messages(messages) a2a_message = self._prepare_message_for_a2a(normalized_messages[-1]) @@ -276,7 +283,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): async def _map_a2a_stream( self, - a2a_stream: AsyncIterable[Any], + a2a_stream: AsyncIterable[A2AStreamItem], *, background: bool = False, ) -> AsyncIterable[AgentResponseUpdate]: @@ -300,14 +307,12 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): response_id=str(getattr(item, "message_id", uuid.uuid4())), raw_representation=item, ) - elif isinstance(item, tuple) and len(item) == 2: # ClientEvent = (Task, UpdateEvent) + elif isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], Task): task, _update_event = item - if isinstance(task, Task): - for update in self._updates_from_task(task, background=background): - yield update + for update in self._updates_from_task(task, background=background): + yield update else: - msg = f"Only Message and Task responses are supported from A2A agents. Received: {type(item)}" - raise NotImplementedError(msg) + raise NotImplementedError("Only Message and Task responses are supported") # ------------------------------------------------------------------ # Task helpers @@ -396,6 +401,8 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): for content in message.contents: match content.type: case "text": + if content.text is None: + raise ValueError("Text content requires a non-null text value") parts.append( A2APart( root=TextPart( @@ -414,6 +421,8 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): ) ) case "uri": + if content.uri is None: + raise ValueError("URI content requires a non-null uri value") parts.append( A2APart( root=FilePart( @@ -426,11 +435,13 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): ) ) case "data": + if content.uri is None: + raise ValueError("Data content requires a non-null uri value") parts.append( A2APart( root=FilePart( file=FileWithBytes( - bytes=_get_uri_data(content.uri), # type: ignore[arg-type] + bytes=_get_uri_data(content.uri), mime_type=content.media_type, ), metadata=content.additional_properties, @@ -438,6 +449,8 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): ) ) case "hosted_file": + if content.file_id is None: + raise ValueError("Hosted file content requires a non-null file_id value") parts.append( A2APart( root=FilePart( diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index b537b0a30d..b7bfdb9275 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_a2a"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a" -test = "pytest --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index 74d9fcbd2e..044d7d935a 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -64,6 +64,7 @@ warn_unused_configs = true disallow_untyped_defs = false [tool.pyright] +include = ["agent_framework_ag_ui"] exclude = ["tests", "tests/ag_ui", "examples"] typeCheckingMode = "basic" @@ -73,4 +74,4 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ag_ui" -test = "pytest --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui" +test = "pytest -m \"not integration\" --cov=agent_framework_ag_ui --cov-report=term-missing:skip-covered -n auto --dist worksteal tests/ag_ui" diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 8ec2943181..5cda4991c8 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import sys -from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence +from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence from typing import Any, ClassVar, Final, Generic, Literal, TypedDict from agent_framework import ( @@ -302,15 +302,18 @@ class AnthropicClient( env_file_encoding=env_file_encoding, ) + api_key_secret = anthropic_settings.get("api_key") + model_id_setting = anthropic_settings.get("chat_model_id") + if anthropic_client is None: - if not anthropic_settings["api_key"]: + if api_key_secret is None: raise ValueError( "Anthropic API key is required. Set via 'api_key' parameter " "or 'ANTHROPIC_API_KEY' environment variable." ) anthropic_client = AsyncAnthropic( - api_key=anthropic_settings["api_key"].get_secret_value(), + api_key=api_key_secret.get_secret_value(), default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT}, ) @@ -324,7 +327,7 @@ class AnthropicClient( # Initialize instance variables self.anthropic_client = anthropic_client self.additional_beta_flags = additional_beta_flags or [] - self.model_id = anthropic_settings["chat_model_id"] + self.model_id = model_id_setting # streaming requires tracking the last function call ID, name, and content type self._last_call_id_name: tuple[str, str] | None = None self._last_call_content_type: str | None = None @@ -785,18 +788,22 @@ class AnthropicClient( "description": tool.description, "input_schema": tool.parameters(), }) - elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp": + elif isinstance(tool, Mapping) and tool.get("type") == "mcp": # type: ignore[reportUnknownMemberType] # MCP servers must be routed to separate mcp_servers parameter server_def: dict[str, Any] = { "type": "url", - "name": tool.get("server_label", ""), - "url": tool.get("server_url", ""), + "name": tool.get("server_label", ""), # type: ignore[reportUnknownMemberType] + "url": tool.get("server_url", ""), # type: ignore[reportUnknownMemberType] } - if allowed_tools := tool.get("allowed_tools"): - server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)} - headers = tool.get("headers") - if isinstance(headers, dict) and (auth := headers.get("authorization")): - server_def["authorization_token"] = auth + allowed_tools = tool.get("allowed_tools") # type: ignore[reportUnknownMemberType] + if isinstance(allowed_tools, Sequence) and not isinstance(allowed_tools, str): + server_def["tool_configuration"] = { + "allowed_tools": [str(item) for item in allowed_tools] # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType] + } + headers = tool.get("headers") # type: ignore[reportUnknownMemberType] + authorization = headers.get("authorization") if isinstance(headers, Mapping) else None # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] + if isinstance(authorization, str): + server_def["authorization_token"] = authorization mcp_server_list.append(server_def) else: # Pass through all other tools (dicts, SDK types) unchanged diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index ed31c4800a..51631bdd30 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -87,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic" -test = "pytest --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" +test = "pytest -m \"not integration\" --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py index ff245817b7..b2eb41e03f 100644 --- a/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py +++ b/python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py @@ -456,10 +456,10 @@ class AzureAISearchContextProvider(BaseContextProvider): elif self.embedding_function: if isinstance(self.embedding_function, SupportsGetEmbeddings): embeddings = await self.embedding_function.get_embeddings([query]) # type: ignore[reportUnknownVariableType] - query_vector: list[float] = embeddings[0].vector # type: ignore[reportUnknownVariableType] + query_vector = embeddings[0].vector # type: ignore[reportUnknownVariableType] else: - query_vector = await self.embedding_function(query) - vector_queries = [VectorizedQuery(vector=query_vector, k=vector_k, fields=self.vector_field_name)] + query_vector = await self.embedding_function(query) # type: ignore[reportUnknownVariableType] + vector_queries = [VectorizedQuery(vector=query_vector, k=vector_k, fields=self.vector_field_name)] # type: ignore[reportUnknownArgumentType] search_params: dict[str, Any] = {"search_text": query, "top": self.top_k} if vector_queries: @@ -632,6 +632,8 @@ class AzureAISearchContextProvider(BaseContextProvider): image=KnowledgeBaseMessageImageContentImage(url=content.uri), ) ) + case _: + pass elif msg.text: kb_content.append(KnowledgeBaseMessageTextContent(text=msg.text)) if kb_content: diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index a4bdc5e978..0827c2d816 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -62,6 +62,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_azure_ai_search"] exclude = ['tests'] [tool.mypy] @@ -88,7 +89,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search" -test = "pytest --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 2c0498b1e4..a0c9d9046c 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -9,7 +9,7 @@ import os import re import sys from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence -from typing import Any, ClassVar, Generic, TypedDict +from typing import Any, ClassVar, Generic, TypedDict, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -77,9 +77,9 @@ from azure.ai.agents.models import ( RunStatus, RunStep, RunStepDeltaChunk, - RunStepDeltaCodeInterpreterDetailItemObject, RunStepDeltaCodeInterpreterImageOutput, RunStepDeltaCodeInterpreterLogOutput, + RunStepDeltaToolCall, SubmitToolApprovalAction, SubmitToolOutputsAction, ThreadMessageOptions, @@ -704,7 +704,7 @@ class AzureAIAgentClient( args["tool_approvals"] = tool_approvals await self.agents_client.runs.submit_tool_outputs_stream(**args) # type: ignore[reportUnknownMemberType] # Pass the handler to the stream to continue processing - stream = handler # type: ignore + stream = handler final_thread_id = thread_run.thread_id else: # Handle thread creation or cancellation @@ -881,7 +881,7 @@ class AzureAIAgentClient( azure_search_tool_calls: list[dict[str, Any]] = [] response_stream = await stream.__aenter__() if isinstance(stream, AsyncAgentRunStream) else stream # type: ignore[no-untyped-call] try: - async for event_type, event_data, _ in response_stream: # type: ignore + async for event_type, event_data, _ in response_stream: match event_data: case MessageDeltaChunk(): # only one event_type: AgentStreamEvent.THREAD_MESSAGE_DELTA @@ -997,21 +997,16 @@ class AzureAIAgentClient( role="assistant", ) case RunStepDeltaChunk(): # type: ignore - if ( - event_data.delta.step_details is not None - and event_data.delta.step_details.type == "tool_calls" - and event_data.delta.step_details.tool_calls is not None # type: ignore[attr-defined] - ): - for tool_call in event_data.delta.step_details.tool_calls: # type: ignore[attr-defined] - if tool_call.type == "code_interpreter" and isinstance( - tool_call.code_interpreter, - RunStepDeltaCodeInterpreterDetailItemObject, - ): + step_details = event_data.delta.step_details + if step_details is not None and step_details.type == "tool_calls": + tool_calls = cast(list[RunStepDeltaToolCall], step_details.tool_calls) # type: ignore + for tool_call in tool_calls: + if tool_call.type == "code_interpreter" and tool_call.code_interpreter is not None: # type: ignore[attr-defined, reportUnknownMemberType] code_contents: list[Content] = [] - if tool_call.code_interpreter.input is not None: - logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}") - if tool_call.code_interpreter.outputs is not None: - for output in tool_call.code_interpreter.outputs: + if tool_call.code_interpreter.input is not None: # type: ignore[attr-defined, reportUnknownMemberType] + logger.debug(f"Code Interpreter Input: {tool_call.code_interpreter.input}") # type: ignore[attr-defined, reportUnknownMemberType] + if tool_call.code_interpreter.outputs is not None: # type: ignore[attr-defined, reportUnknownMemberType] + for output in tool_call.code_interpreter.outputs: # type: ignore[attr-defined, reportUnknownMemberType] if isinstance(output, RunStepDeltaCodeInterpreterLogOutput) and output.logs: code_contents.append(Content.from_text(text=output.logs)) if ( @@ -1027,7 +1022,7 @@ class AzureAIAgentClient( contents=code_contents, conversation_id=thread_id, message_id=response_id, - raw_representation=tool_call.code_interpreter, + raw_representation=tool_call.code_interpreter, # type: ignore[attr-defined, reportUnknownMemberType] response_id=response_id, ) case _: # ThreadMessage or string @@ -1056,17 +1051,15 @@ class AzureAIAgentClient( ) -> None: """Capture Azure AI Search tool call data from completed steps.""" try: - if ( - hasattr(step_data, "step_details") - and hasattr(step_data.step_details, "tool_calls") - and step_data.step_details.tool_calls - ): - for tool_call in step_data.step_details.tool_calls: - if hasattr(tool_call, "type") and tool_call.type == "azure_ai_search": + step_details = getattr(step_data, "step_details", None) + tool_calls = getattr(step_details, "tool_calls", None) if step_details is not None else None + if isinstance(tool_calls, list): + for tool_call in cast(list[object], tool_calls): + if getattr(tool_call, "type", None) == "azure_ai_search": # Store the complete tool call as a dictionary tool_call_dict = { "id": getattr(tool_call, "id", None), - "type": tool_call.type, + "type": getattr(tool_call, "type", None), "azure_ai_search": getattr(tool_call, "azure_ai_search", None), } azure_search_tool_calls.append(tool_call_dict) @@ -1219,19 +1212,18 @@ class AzureAIAgentClient( self, options: Mapping[str, Any] ) -> AgentsToolChoiceOptionMode | AgentsNamedToolChoice | None: """Prepare the tool choice mode for Azure AI Agents API.""" - tool_choice = options.get("tool_choice") + tool_choice = cast(str | dict[str, str] | None, options.get("tool_choice")) if tool_choice is None: return None - if tool_choice == "none": - return AgentsToolChoiceOptionMode.NONE - if tool_choice == "auto": - return AgentsToolChoiceOptionMode.AUTO - if isinstance(tool_choice, Mapping) and tool_choice.get("mode") == "required": + if isinstance(tool_choice, str) and tool_choice in {"none", "auto"}: + return AgentsToolChoiceOptionMode(tool_choice) + if isinstance(tool_choice, dict): + mode = tool_choice.get("mode") req_fn = tool_choice.get("required_function_name") - if req_fn: + if mode == "required" and req_fn is not None: return AgentsNamedToolChoice( type=AgentsNamedToolChoiceType.FUNCTION, - function=FunctionName(name=str(req_fn)), + function=FunctionName(name=req_fn), ) return None @@ -1369,14 +1361,9 @@ class AzureAIAgentClient( # SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.) tool_definitions.extend(tool.definitions) # Handle tool resources (MCP resources handled separately by _prepare_mcp_resources) - if ( - run_options is not None - and hasattr(tool, "resources") - and tool.resources - and "mcp" not in tool.resources - ): - if "tool_resources" not in run_options: - run_options["tool_resources"] = {} + resources = getattr(tool, "resources", None) + if run_options is not None and resources and isinstance(resources, Mapping) and "mcp" not in resources: + run_options.setdefault("tool_resources", {}) run_options["tool_resources"].update(tool.resources) else: # Pass through ToolDefinition, dict, and other types unchanged diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 61c4a09e94..df0340a8f1 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -6,7 +6,7 @@ import json import logging import re import sys -from collections.abc import Awaitable, Callable, Mapping, Sequence +from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence from contextlib import suppress from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar, cast @@ -304,7 +304,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ # Import Azure Monitor with proper error handling try: - from azure.monitor.opentelemetry import configure_azure_monitor + from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import] except ImportError as exc: raise ImportError( "azure-monitor-opentelemetry is required for Azure Monitor integration. " @@ -433,31 +433,36 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ """Extract comparable tool names from runtime tool payloads.""" if not isinstance(tools, Sequence) or isinstance(tools, str | bytes): return set() - return {self._get_tool_name(tool) for tool in tools} + tool_names: set[str] = set() + for tool_item in cast(Sequence[object], tools): + tool_names.add(self._get_tool_name(tool_item)) + return tool_names def _get_tool_name(self, tool: Any) -> str: """Get a stable name for a tool for runtime comparison.""" if isinstance(tool, FunctionTool): return tool.name + if isinstance(tool, Mapping): - tool_type = tool.get("type") + tool_type = tool.get("type") # type: ignore[reportUnknownMemberType] if tool_type == "function": - if isinstance(function_data := tool.get("function"), Mapping) and function_data.get("name"): - return str(function_data["name"]) - if tool.get("name"): - return str(tool["name"]) - if tool.get("name"): - return str(tool["name"]) - if tool.get("server_label"): - return f"mcp:{tool['server_label']}" + function_data = tool.get("function") # type: ignore[reportUnknownMemberType] + if isinstance(function_data, Mapping) and (function_name := function_data.get("name")): # type: ignore[assignment] + return function_name # type: ignore[no-any-return] + if tool_name := tool.get("name"): # type: ignore[reportUnknownMemberType] + return tool_name # type: ignore[no-any-return] + if server_label := tool.get("server_label"): # type: ignore[reportUnknownMemberType] + return f"mcp:{server_label}" if tool_type: - return str(tool_type) - if getattr(tool, "name", None): - return str(tool.name) - if getattr(tool, "server_label", None): - return f"mcp:{tool.server_label}" - if getattr(tool, "type", None): - return str(tool.type) + return tool_type # type: ignore[no-any-return] + raise ValueError("Dict based tool definitions must include a 'name' property for runtime comparison.") + + if name_value := getattr(tool, "name", None): + return name_value # type: ignore[no-any-return] + if server_label_value := getattr(tool, "server_label", None): + return f"mcp:{server_label_value}" + if tool_type_value := getattr(tool, "type", None): + return tool_type_value # type: ignore[no-any-return] return type(tool).__name__ def _get_structured_output_signature(self, chat_options: Mapping[str, Any] | None) -> str | None: @@ -545,14 +550,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ return run_options @override - def _check_model_presence(self, run_options: dict[str, Any]) -> None: + def _check_model_presence(self, options: dict[str, Any]) -> None: # Skip model check for application endpoints - model is pre-configured on server if self._is_application_endpoint: return - if not run_options.get("model"): + if not options.get("model"): if not self.model_id: raise ValueError("model_deployment_name must be a non-empty string") - run_options["model"] = self.model_id + options["model"] = self.model_id def _transform_input_for_azure_ai(self, input_items: list[dict[str, Any]]) -> list[dict[str, Any]]: """Transform input items to match Azure AI Projects expected schema. @@ -575,15 +580,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ # Add 'annotations' only to output_text content items (assistant messages) # User messages (input_text) do NOT support annotations in Azure AI - if "content" in new_item and isinstance(new_item["content"], list): - new_content: list[dict[str, Any] | Any] = [] - for content_item in new_item["content"]: - if isinstance(content_item, dict): - new_content_item: dict[str, Any] = dict(content_item) + if (content := new_item.get("content")) and isinstance(content, list): + new_content: list[Any] = [] + for content_item in content: # type: ignore[list-item] + if isinstance(content_item, MutableMapping): # Only add annotations to output_text (assistant content) - if new_content_item.get("type") == "output_text" and "annotations" not in new_content_item: - new_content_item["annotations"] = [] - new_content.append(new_content_item) + if content_item.get("type") == "output_text" and "annotations" not in content_item: # type: ignore[reportUnknownMemberType] + content_item["annotations"] = [] + new_content.append(content_item) else: new_content.append(content_item) new_item["content"] = new_content @@ -721,9 +725,13 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ # Streaming "added" events send output as an empty list; skip. continue if output is not None: - urls = output.get("get_urls") if isinstance(output, dict) else output.get_urls - if urls and isinstance(urls, list): - get_urls.extend(urls) + urls = output.get("get_urls") if isinstance(output, Mapping) else getattr(output, "get_urls", None) # type: ignore + if isinstance(urls, list): + string_urls: list[str] = [] + for url_item in urls: # type: ignore[list-item] + if isinstance(url_item, str): + string_urls.append(url_item) + get_urls.extend(string_urls) return get_urls def _get_search_doc_url(self, citation_title: str | None, get_urls: list[str]) -> str | None: @@ -878,7 +886,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[ contents=contents_list, conversation_id=update.conversation_id, response_id=update.response_id, - role=update.role, + role=update.role, # type: ignore[union-attr] model_id=update.model_id, continuation_token=update.continuation_token, additional_properties=update.additional_properties, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_embedding_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_embedding_client.py index 7e6cdfc8b7..a243f77a38 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_embedding_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_embedding_client.py @@ -186,7 +186,7 @@ class RawAzureAIInferenceEmbeddingClient( values: Sequence[Content | str], *, options: AzureAIInferenceEmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[list[float]]: + ) -> GeneratedEmbeddings[list[float], AzureAIInferenceEmbeddingOptionsT]: """Generate embeddings for text and/or image inputs. Text inputs (``str`` or ``Content`` with ``type="text"``) are sent to the diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index d6b922db91..335a7f16ec 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -224,7 +224,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]): if isinstance(tool, MCPTool): mcp_tools.append(tool) elif isinstance(tool, (FunctionTool, MutableMapping)): - non_mcp_tools.append(tool) + non_mcp_tools.append(tool) # type: ignore[reportUnknownArgumentType] # Connect MCP tools and discover their functions BEFORE creating the agent # This is required because Azure AI Responses API doesn't accept tools at request time diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py index 6f7d39c3be..59289d2746 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -79,7 +79,7 @@ class AzureAISettings(TypedDict, total=False): model_deployment_name: str | None -def _extract_project_connection_id(additional_properties: dict[str, Any] | None) -> str | None: +def _extract_project_connection_id(additional_properties: Mapping[str, Any] | None) -> str | None: """Extract project_connection_id from tool additional_properties. Checks for both direct 'project_connection_id' key (programmatic usage) @@ -95,17 +95,18 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None) return None # Check for direct project_connection_id (programmatic usage) - project_connection_id = additional_properties.get("project_connection_id") - if isinstance(project_connection_id, str): - return project_connection_id + + if (proj_conn_id := additional_properties.get("project_connection_id")) and isinstance(proj_conn_id, str): + return proj_conn_id # type: ignore[no-any-return] # Check for connection.name structure (declarative/YAML usage) - if "connection" in additional_properties: - conn = additional_properties["connection"] - if isinstance(conn, dict): - name = conn.get("name") - if isinstance(name, str): - return name + if ( + (connection := additional_properties.get("connection")) + and isinstance(connection, Mapping) + and (name := connection.get("name")) # type: ignore + and isinstance(name, str) + ): + return name # type: ignore[no-any-return] return None @@ -189,9 +190,9 @@ def to_azure_ai_agent_tools( and tool.resources and "mcp" not in tool.resources ): - if "tool_resources" not in run_options: - run_options["tool_resources"] = {} - run_options["tool_resources"].update(tool.resources) + run_options.setdefault("tool_resources", {}) + if isinstance(tool.resources, Mapping): + run_options["tool_resources"].update(tool.resources) elif isinstance(tool, (dict, MutableMapping)): # Handle dict-based tools - pass through directly tool_dict = tool if isinstance(tool, dict) else dict(tool) @@ -422,9 +423,16 @@ def to_azure_ai_tools( elif isinstance(tool, Tool): # Pass through SDK Tool types directly (CodeInterpreterTool, FileSearchTool, etc.) azure_tools.append(tool) + elif isinstance(tool, MutableMapping): + # Convert mutable mappings into plain dicts for stable typing. + tool_dict: dict[str, Any] = dict(tool) + if tool_dict.get("type") == "mcp": + azure_tools.append(_prepare_mcp_tool_dict_for_azure_ai(tool_dict)) + else: + azure_tools.append(tool_dict) else: - # Pass through dict-based tools directly - azure_tools.append(dict(tool) if isinstance(tool, MutableMapping) else tool) # type: ignore[arg-type] + # Pass through any other supported tool objects unchanged. + azure_tools.append(tool) return azure_tools @@ -446,7 +454,16 @@ def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool: mcp["server_description"] = description # Check for project_connection_id - if project_connection_id := tool_dict.get("project_connection_id"): + project_connection_id = tool_dict.get("project_connection_id") + if not isinstance(project_connection_id, str): + additional_properties = tool_dict.get("additional_properties") + project_connection_id = ( + _extract_project_connection_id(additional_properties) # pyright: ignore[reportUnknownArgumentType] + if isinstance(additional_properties, Mapping) + else None + ) + + if project_connection_id: mcp["project_connection_id"] = project_connection_id elif headers := tool_dict.get("headers"): mcp["headers"] = headers diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index bdc898af8c..2bd51729c2 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_azure_ai"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai" -test = "pytest --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests" [tool.poe.tasks.integration-tests] cmd = """ diff --git a/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py b/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py index 5b802bde9f..35c4243c37 100644 --- a/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py +++ b/python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py @@ -124,7 +124,6 @@ class CosmosHistoryProvider(BaseHistoryProvider): self._database_client = self._cosmos_client.get_database_client(self.database_name) - async def get_messages(self, session_id: str | None, **kwargs: Any) -> list[Message]: """Retrieve stored messages for this session from Azure Cosmos DB.""" await self._ensure_container_proxy() @@ -146,8 +145,15 @@ class CosmosHistoryProvider(BaseHistoryProvider): messages: list[Message] = [] async for item in items: message_payload = item.get("message") - if isinstance(message_payload, dict): - messages.append(Message.from_dict(message_payload)) + if not isinstance(message_payload, dict): + logger.warning("Skipping Cosmos DB item with non-mapping message payload.") + continue + try: + msg = Message.from_dict(message_payload) # pyright: ignore[reportUnknownArgumentType] + except ValueError as e: + logger.warning("Failed to deserialize message from Cosmos DB item: %s", e) + continue + messages.append(msg) return messages @@ -205,12 +211,8 @@ class CosmosHistoryProvider(BaseHistoryProvider): async def list_sessions(self) -> list[str]: """List all session IDs stored in this provider's Cosmos container.""" await self._ensure_container_proxy() - query = ( - "SELECT DISTINCT VALUE c.session_id FROM c WHERE c.source_id = @source_id" - ) - parameters: list[dict[str, object]] = [ - {"name": "@source_id", "value": self.source_id} - ] + query = "SELECT DISTINCT VALUE c.session_id FROM c WHERE c.source_id = @source_id" + parameters: list[dict[str, object]] = [{"name": "@source_id", "value": self.source_id}] # without a partition key, it is automatically a cross-partition query items = self._container_proxy.query_items(query=query, parameters=parameters) # type: ignore[union-attr] @@ -249,11 +251,9 @@ class CosmosHistoryProvider(BaseHistoryProvider): if self._database_client is None: raise RuntimeError("Cosmos database client is not initialized.") - self._container_proxy = ( - await self._database_client.create_container_if_not_exists( - id=self.container_name, - partition_key=PartitionKey(path="/session_id"), - ) + self._container_proxy = await self._database_client.create_container_if_not_exists( + id=self.container_name, + partition_key=PartitionKey(path="/session_id"), ) @staticmethod diff --git a/python/packages/azure-cosmos/pyproject.toml b/python/packages/azure-cosmos/pyproject.toml index d053465fb1..cae3b3168c 100644 --- a/python/packages/azure-cosmos/pyproject.toml +++ b/python/packages/azure-cosmos/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_azure_cosmos"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -85,7 +86,7 @@ executor.type = "uv" include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_cosmos" -test = "pytest --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_azure_cosmos --cov-report=term-missing:skip-covered tests" integration-tests = "pytest tests/test_cosmos_history_provider.py -m integration" [build-system] diff --git a/python/packages/azure-cosmos/samples/cosmos_history_provider.py b/python/packages/azure-cosmos/samples/cosmos_history_provider.py index ea476f9837..ff6138c1e5 100644 --- a/python/packages/azure-cosmos/samples/cosmos_history_provider.py +++ b/python/packages/azure-cosmos/samples/cosmos_history_provider.py @@ -5,10 +5,11 @@ import asyncio import os from agent_framework.azure import AzureOpenAIResponsesClient -from agent_framework_azure_cosmos import CosmosHistoryProvider from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv +from agent_framework_azure_cosmos import CosmosHistoryProvider + # Load environment variables from .env file. load_dotenv() @@ -31,7 +32,6 @@ Optional: """ - async def main() -> None: """Run the Cosmos history provider sample with an Agent.""" project_endpoint = os.getenv("AZURE_AI_PROJECT_ENDPOINT") diff --git a/python/packages/azure-cosmos/tests/test_cosmos_history_provider.py b/python/packages/azure-cosmos/tests/test_cosmos_history_provider.py index 33d7bf2414..e3ac636aa6 100644 --- a/python/packages/azure-cosmos/tests/test_cosmos_history_provider.py +++ b/python/packages/azure-cosmos/tests/test_cosmos_history_provider.py @@ -9,15 +9,16 @@ from contextlib import suppress from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -import agent_framework_azure_cosmos._history_provider as history_provider_module import pytest from agent_framework import AgentResponse, Message from agent_framework._sessions import AgentSession, SessionContext from agent_framework.exceptions import SettingNotFoundError -from agent_framework_azure_cosmos._history_provider import CosmosHistoryProvider from azure.cosmos.aio import CosmosClient from azure.cosmos.exceptions import CosmosResourceNotFoundError +import agent_framework_azure_cosmos._history_provider as history_provider_module +from agent_framework_azure_cosmos._history_provider import CosmosHistoryProvider + skip_if_cosmos_integration_tests_disabled = pytest.mark.skipif( any( os.getenv(name, "") == "" @@ -357,9 +358,10 @@ class TestCosmosHistoryProviderClose: async def test_async_context_manager_preserves_original_exception(self, mock_container: MagicMock) -> None: provider = CosmosHistoryProvider(source_id="mem", container_client=mock_container) - with patch.object( - provider, "close", AsyncMock(side_effect=RuntimeError("close failed")) - ), pytest.raises(ValueError, match="inner error"): + with ( + patch.object(provider, "close", AsyncMock(side_effect=RuntimeError("close failed"))), + pytest.raises(ValueError, match="inner error"), + ): async with provider: raise ValueError("inner error") diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index c7d8552b24..01dcc102f4 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -274,10 +274,14 @@ class AgentFunctionApp(DFAppBase): """ from agent_framework._workflows._state import State - data = json.loads(inputData) - message_data = data["message"] + data_obj = json.loads(inputData) + if not isinstance(data_obj, dict): + raise ValueError("Activity inputData must decode to a JSON object") + data = cast(dict[str, Any], data_obj) + + message_data = data.get("message") shared_state_snapshot = data.get("shared_state_snapshot", {}) - source_executor_ids = data.get("source_executor_ids", [SOURCE_ORCHESTRATOR]) + source_executor_ids = cast(list[str], data.get("source_executor_ids", [SOURCE_ORCHESTRATOR])) if not self.workflow: raise RuntimeError("Workflow not initialized in AgentFunctionApp") @@ -299,15 +303,20 @@ class AgentFunctionApp(DFAppBase): shared_state = State() # Deserialize shared state values to reconstruct dataclasses/Pydantic models - deserialized_state = {k: deserialize_value(v) for k, v in (shared_state_snapshot or {}).items()} - original_snapshot = dict(deserialized_state) + deserialized_state: dict[str, Any] = { + str(k): deserialize_value(v) for k, v in shared_state_snapshot.items() + } + original_snapshot: dict[str, Any] = dict(deserialized_state) shared_state.import_state(deserialized_state) if is_hitl_response: # Handle HITL response by calling the executor's @response_handler + if not isinstance(message_data, dict): + raise ValueError("HITL message payload must be a JSON object") + await execute_hitl_response_handler( executor=executor, - hitl_message=message_data, + hitl_message=cast(dict[str, Any], message_data), shared_state=shared_state, runner_context=runner_context, ) @@ -323,11 +332,11 @@ class AgentFunctionApp(DFAppBase): # Commit pending state changes and export shared_state.commit() current_state = shared_state.export_state() - original_keys = set(original_snapshot.keys()) - current_keys = set(current_state.keys()) + original_keys: set[str] = set(original_snapshot.keys()) + current_keys: set[str] = set(current_state.keys()) # Deleted = was in original, not in current - deletes = original_keys - current_keys + deletes: set[str] = original_keys - current_keys # Updates = keys in current that are new or have different values updates = { @@ -348,7 +357,7 @@ class AgentFunctionApp(DFAppBase): pending_request_info_events = await runner_context.get_pending_request_info_events() # Serialize pending request info events for orchestrator - serialized_pending_requests = [] + serialized_pending_requests: list[dict[str, Any]] = [] for _request_id, event in pending_request_info_events.items(): serialized_pending_requests.append({ "request_id": event.request_id, @@ -361,7 +370,7 @@ class AgentFunctionApp(DFAppBase): }) # Serialize messages for JSON compatibility - serialized_sent_messages = [] + serialized_sent_messages: list[dict[str, Any]] = [] for _source_id, msg_list in sent_messages.items(): for msg in msg_list: serialized_sent_messages.append({ @@ -441,6 +450,9 @@ class AgentFunctionApp(DFAppBase): ) -> func.HttpResponse: """HTTP endpoint to get workflow status.""" instance_id = req.route_params.get("instanceId") + if not instance_id: + return self._build_error_response("Instance ID is required", status_code=400) + status = await client.get_status(instance_id) if not status: @@ -457,17 +469,23 @@ class AgentFunctionApp(DFAppBase): } # Add pending HITL requests info if available - custom_status = status.custom_status or {} - if isinstance(custom_status, dict) and custom_status.get("pending_requests"): + if ( + (custom_status := status.custom_status) + and isinstance(custom_status, dict) + and (pending_requests_dict := custom_status.get("pending_requests")) # type: ignore + and isinstance(pending_requests_dict, dict) + ): base_url = self._build_base_url(req.url) - pending_requests = [] - for req_id, req_data in custom_status["pending_requests"].items(): + pending_requests: list[dict[str, Any]] = [] + for req_id, req_data in pending_requests_dict.items(): # type: ignore + if not isinstance(req_data, dict): + continue pending_requests.append({ "requestId": req_id, - "sourceExecutor": req_data.get("source_executor_id"), - "requestData": req_data.get("data"), - "requestType": req_data.get("request_type"), - "responseType": req_data.get("response_type"), + "sourceExecutor": req_data.get("source_executor_id"), # type: ignore[reportUnknownMemberType] + "requestData": req_data.get("data"), # type: ignore[reportUnknownMemberType] + "requestType": req_data.get("request_type"), # type: ignore[reportUnknownMemberType] + "responseType": req_data.get("response_type"), # type: ignore[reportUnknownMemberType] "respondUrl": f"{base_url}/api/workflow/respond/{instance_id}/{req_id}", }) response["pendingHumanInputRequests"] = pending_requests @@ -515,6 +533,11 @@ class AgentFunctionApp(DFAppBase): mimetype="application/json", ) + # Ensure route handlers are registered (prevents unused function warnings) + _ = start_workflow_orchestration + _ = get_workflow_status + _ = send_hitl_response + def _build_status_url(self, request_url: str, instance_id: str) -> str: """Build the status URL for a workflow instance.""" base_url = self._build_base_url(request_url) diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py index 94263fa4ef..f48e55f5d5 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_serialization.py @@ -13,22 +13,24 @@ This module adds: - serialize_value / deserialize_value: convenience aliases for encode/decode - reconstruct_to_type: for HITL responses where external data (without type markers) needs to be reconstructed to a known type -- _resolve_type: resolves 'module:class' type keys to Python types +- resolve_type: resolves 'module:class' type keys to Python types """ from __future__ import annotations import importlib import logging +from contextlib import suppress from dataclasses import is_dataclass from typing import Any from agent_framework._workflows._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value +from pydantic import BaseModel logger = logging.getLogger(__name__) -def _resolve_type(type_key: str) -> type | None: +def resolve_type(type_key: str) -> type | None: """Resolve a 'module:class' type key to its Python type. Args: @@ -108,11 +110,9 @@ def reconstruct_to_type(value: Any, target_type: type) -> Any: if value is None: return None - try: + with suppress(TypeError): if isinstance(value, target_type): return value - except TypeError: - pass if not isinstance(value, dict): return value @@ -123,17 +123,18 @@ def reconstruct_to_type(value: Any, target_type: type) -> Any: return decoded # Try Pydantic model validation (for unmarked dicts, e.g., external HITL data) - if hasattr(target_type, "model_validate"): + if issubclass(target_type, BaseModel): try: return target_type.model_validate(value) except Exception: logger.debug("Could not validate Pydantic model %s", target_type) + return value # type: ignore[return-value] # Try dataclass construction (for unmarked dicts, e.g., external HITL data) - if is_dataclass(target_type) and isinstance(target_type, type): + if is_dataclass(target_type) and isinstance(target_type, type): # type: ignore try: return target_type(**value) except Exception: logger.debug("Could not construct dataclass %s", target_type) - return value + return value # type: ignore[return-value] diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_workflow.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_workflow.py index a0e0f04185..60c04ad66c 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_workflow.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_workflow.py @@ -44,12 +44,13 @@ from agent_framework._workflows._edge import ( SingleEdgeGroup, SwitchCaseEdgeGroup, ) +from agent_framework._workflows._state import State from agent_framework_durabletask import AgentSessionId, DurableAgentSession, DurableAIAgent from azure.durable_functions import DurableOrchestrationContext from ._context import CapturingRunnerContext from ._orchestration import AzureFunctionsAgentExecutor -from ._serialization import _resolve_type, deserialize_value, reconstruct_to_type, serialize_value +from ._serialization import deserialize_value, reconstruct_to_type, resolve_type, serialize_value logger = logging.getLogger(__name__) @@ -148,7 +149,7 @@ def _evaluate_edge_condition_sync(edge: Edge, message: Any) -> bool: True if the edge should be traversed, False otherwise """ # Access the internal condition directly since should_route is async - condition = edge._condition + condition = edge._condition # pyright: ignore[reportPrivateUsage] if condition is None: return True result = condition(message) @@ -322,7 +323,8 @@ def _prepare_activity_task( activity_input_json = json.dumps(activity_input) # Use the prefixed activity name that matches the registered function activity_name = f"dafx-{executor_id}" - return context.call_activity(activity_name, activity_input_json) + orchestration_context: Any = context + return orchestration_context.call_activity(activity_name, activity_input_json) # ============================================================================ @@ -346,13 +348,16 @@ def _process_agent_response( ExecutorResult containing the processed response """ response_text = agent_response.text if agent_response else None - structured_response = None + structured_response: dict[str, Any] | None = None if agent_response and agent_response.value is not None: - if hasattr(agent_response.value, "model_dump"): - structured_response = agent_response.value.model_dump() + model_dump = getattr(agent_response.value, "model_dump", None) + if callable(model_dump): + dumped = model_dump() + if isinstance(dumped, dict): + structured_response = dumped # type: ignore[assignment] elif isinstance(agent_response.value, dict): - structured_response = agent_response.value + structured_response = agent_response.value # type: ignore[assignment] output_message = build_agent_executor_response( executor_id=executor_id, @@ -726,7 +731,7 @@ def run_workflow_orchestrator( if winner == approval_task: # Cancel the timeout - timeout_task.cancel() + timeout_task.cancel() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] # Get the response raw_response = approval_task.result @@ -756,7 +761,7 @@ def run_workflow_orchestrator( ) else: # Timeout occurred — cancel the dangling external event listener - approval_task.cancel() + approval_task.cancel() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] logger.warning("HITL request %s timed out after %s hours", request_id, hitl_timeout_hours) raise TimeoutError( f"Human-in-the-loop request '{request_id}' timed out after {hitl_timeout_hours} hours." @@ -864,7 +869,8 @@ def _extract_message_content(message: Any) -> str: # Extract text from the last message in the request message_content = message.messages[-1].text or "" elif isinstance(message, dict): - logger.warning("Unexpected dict message in _extract_message_content. Keys: %s", list(message.keys())) + key_names = list(message.keys()) # type: ignore[union-attr] + logger.warning("Unexpected dict message in _extract_message_content. Keys: %s", key_names) # type: ignore elif isinstance(message, str): message_content = message @@ -879,7 +885,7 @@ def _extract_message_content(message: Any) -> str: async def execute_hitl_response_handler( executor: Any, hitl_message: dict[str, Any], - shared_state: Any, + shared_state: State, runner_context: CapturingRunnerContext, ) -> None: """Execute a HITL response handler on an executor. @@ -910,7 +916,7 @@ async def execute_hitl_response_handler( response = _deserialize_hitl_response(response_data, response_type_str) # Find the matching response handler - handler = executor._find_response_handler(original_request, response) + handler = executor._find_response_handler(original_request, response) # pyright: ignore[reportPrivateUsage] if handler is None: logger.warning( @@ -965,7 +971,7 @@ def _deserialize_hitl_response(response_data: Any, response_type_str: str | None # Try to deserialize using the type hint if response_type_str: - response_type = _resolve_type(response_type_str) + response_type = resolve_type(response_type_str) if response_type: logger.debug("Found response type %s, attempting reconstruction", response_type) result = reconstruct_to_type(response_data, response_type) diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 82fe4f32b5..0bb2ec9612 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -67,6 +67,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_azurefunctions"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -92,7 +93,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions" -test = "pytest --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/bedrock/agent_framework_bedrock/__init__.py b/python/packages/bedrock/agent_framework_bedrock/__init__.py index 3fbf5c15cf..b2dc511559 100644 --- a/python/packages/bedrock/agent_framework_bedrock/__init__.py +++ b/python/packages/bedrock/agent_framework_bedrock/__init__.py @@ -2,8 +2,8 @@ import importlib.metadata -from ._chat_client import BedrockChatClient, BedrockChatOptions, BedrockGuardrailConfig, BedrockSettings -from ._embedding_client import BedrockEmbeddingClient, BedrockEmbeddingOptions, BedrockEmbeddingSettings +from ._chat_client import BedrockChatClient, BedrockChatOptions, BedrockGuardrailConfig, BedrockSettings # type: ignore +from ._embedding_client import BedrockEmbeddingClient, BedrockEmbeddingOptions, BedrockEmbeddingSettings # type: ignore try: __version__ = importlib.metadata.version(__name__) diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index b0d87fe8cc..5bc9735846 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. - +# type: ignore +# Because the Bedrock client does not have typing, we are ignoring type issues in this module. from __future__ import annotations import asyncio @@ -288,14 +289,16 @@ class BedrockChatClient( env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) - if not settings.get("region"): - settings["region"] = DEFAULT_REGION + region = settings.get("region") or DEFAULT_REGION + chat_model_id = settings.get("chat_model_id") - if client is None: + if client: + self._bedrock_client = client + else: session = boto3_session or self._create_session(settings) - client = session.client( + self._bedrock_client = session.client( "bedrock-runtime", - region_name=settings["region"], + region_name=region, config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT), ) @@ -304,20 +307,28 @@ class BedrockChatClient( function_invocation_configuration=function_invocation_configuration, **kwargs, ) - self._bedrock_client = client - self.model_id = settings["chat_model_id"] - self.region = settings["region"] + self.model_id = chat_model_id + self.region = region @staticmethod def _create_session(settings: BedrockSettings) -> Boto3Session: session_kwargs: dict[str, Any] = {"region_name": settings.get("region") or DEFAULT_REGION} - if settings.get("access_key") and settings.get("secret_key"): - session_kwargs["aws_access_key_id"] = settings["access_key"].get_secret_value() # type: ignore[union-attr] - session_kwargs["aws_secret_access_key"] = settings["secret_key"].get_secret_value() # type: ignore[union-attr] - if settings.get("session_token"): - session_kwargs["aws_session_token"] = settings["session_token"].get_secret_value() # type: ignore[union-attr] + access_key = settings.get("access_key") + secret_key = settings.get("secret_key") + session_token = settings.get("session_token") + if access_key is not None and secret_key is not None: + session_kwargs["aws_access_key_id"] = access_key.get_secret_value() + session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value() + if session_token is not None: + session_kwargs["aws_session_token"] = session_token.get_secret_value() return Boto3Session(**session_kwargs) + def _invoke_converse(self, request: Mapping[str, Any]) -> dict[str, Any]: + response = self._bedrock_client.converse(**request) + if not isinstance(response, Mapping): + raise ChatClientInvalidResponseException("Bedrock converse response must be a mapping.") + return response + @override def _inner_get_response( self, @@ -332,16 +343,20 @@ class BedrockChatClient( if stream: # Streaming mode - simulate streaming by yielding a single update async def _stream() -> AsyncIterable[ChatResponseUpdate]: - response = await asyncio.to_thread(self._bedrock_client.converse, **request) + response = await asyncio.to_thread(self._invoke_converse, request) parsed_response = self._process_converse_response(response) contents = list(parsed_response.messages[0].contents if parsed_response.messages else []) if parsed_response.usage_details: contents.append(Content.from_usage(usage_details=parsed_response.usage_details)) # type: ignore[arg-type] + raw_finish_reason = ( + parsed_response.finish_reason if isinstance(parsed_response.finish_reason, str) else None + ) + finish_reason = self._map_finish_reason(raw_finish_reason) yield ChatResponseUpdate( response_id=parsed_response.response_id, contents=contents, model_id=parsed_response.model_id, - finish_reason=parsed_response.finish_reason, + finish_reason=finish_reason, raw_representation=parsed_response.raw_representation, ) @@ -349,7 +364,7 @@ class BedrockChatClient( # Non-streaming mode async def _get_response() -> ChatResponse: - raw_response = await asyncio.to_thread(self._bedrock_client.converse, **request) + raw_response = await asyncio.to_thread(self._invoke_converse, request) return self._process_converse_response(raw_response) return _get_response() @@ -529,25 +544,25 @@ class BedrockChatClient( def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]: prepared_result = result if isinstance(result, str) else FunctionTool.parse_result(result) try: - parsed_result = json.loads(prepared_result) + parsed_result: object = json.loads(prepared_result) except json.JSONDecodeError: return [{"text": prepared_result}] return self._convert_prepared_tool_result_to_blocks(parsed_result) - def _convert_prepared_tool_result_to_blocks(self, value: Any) -> list[dict[str, Any]]: - if isinstance(value, list): + def _convert_prepared_tool_result_to_blocks(self, value: object) -> list[dict[str, Any]]: + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): blocks: list[dict[str, Any]] = [] for item in value: blocks.extend(self._convert_prepared_tool_result_to_blocks(item)) return blocks or [{"text": ""}] return [self._normalize_tool_result_value(value)] - def _normalize_tool_result_value(self, value: Any) -> dict[str, Any]: + def _normalize_tool_result_value(self, value: object) -> dict[str, Any]: if isinstance(value, dict): return {"json": value} - if isinstance(value, (list, tuple)): - return {"json": list(value)} + if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): + return {"json": [item for item in value]} if isinstance(value, str): return {"text": value} if isinstance(value, (int, float, bool)) or value is None: @@ -586,12 +601,14 @@ class BedrockChatClient( return f"tool-call-{uuid4().hex}" def _process_converse_response(self, response: dict[str, Any]) -> ChatResponse: - output = response.get("output", {}) - message = output.get("message", {}) - content_blocks = message.get("content", []) or [] + """Convert Bedrock Converse API response to ChatResponse.""" + output = response.get("output") or {} + message = output.get("message") or {} + content_blocks = message.get("content") or [] contents = self._parse_message_contents(content_blocks) chat_message = Message(role="assistant", contents=contents, raw_representation=message) - usage_details = self._parse_usage(response.get("usage") or output.get("usage")) + usage_source = response.get("usage") or output.get("usage") + usage_details = self._parse_usage(usage_source) finish_reason = self._map_finish_reason(output.get("completionReason") or response.get("stopReason")) response_id = response.get("responseId") or message.get("id") model_id = response.get("modelId") or output.get("modelId") or self.model_id @@ -616,7 +633,7 @@ class BedrockChatClient( details["total_token_count"] = total_tokens return details - def _parse_message_contents(self, content_blocks: Sequence[MutableMapping[str, Any]]) -> list[Any]: + def _parse_message_contents(self, content_blocks: Sequence[dict[str, Any]]) -> list[Any]: contents: list[Any] = [] for block in content_blocks: if text_value := block.get("text"): @@ -625,32 +642,50 @@ class BedrockChatClient( if (json_value := block.get("json")) is not None: contents.append(Content.from_text(text=json.dumps(json_value), raw_representation=block)) continue - tool_use = block.get("toolUse") - if isinstance(tool_use, MutableMapping): - tool_name = tool_use.get("name") + tool_use_value = block.get("toolUse") + tool_use = ( + tool_use_value + if isinstance(tool_use_value, dict) + else dict(tool_use_value) + if isinstance(tool_use_value, Mapping) + else None + ) + if tool_use is not None: + tool_name_value = tool_use.get("name") + tool_name = tool_name_value if isinstance(tool_name_value, str) else None if not tool_name: raise ChatClientInvalidResponseException( "Bedrock response missing required tool name in toolUse block." ) + tool_use_id = tool_use.get("toolUseId") contents.append( Content.from_function_call( - call_id=tool_use.get("toolUseId") or self._generate_tool_call_id(), + call_id=tool_use_id if isinstance(tool_use_id, str) else self._generate_tool_call_id(), name=tool_name, arguments=tool_use.get("input"), raw_representation=block, ) ) continue - tool_result = block.get("toolResult") - if isinstance(tool_result, MutableMapping): - status = (tool_result.get("status") or "success").lower() + tool_result_value = block.get("toolResult") + tool_result = ( + tool_result_value + if isinstance(tool_result_value, dict) + else dict(tool_result_value) + if isinstance(tool_result_value, Mapping) + else None + ) + if tool_result is not None: + status_value = tool_result.get("status") + status = (status_value if isinstance(status_value, str) else "success").lower() exception = None if status not in {"success", "ok"}: exception = RuntimeError(f"Bedrock tool result status: {status}") result_value = self._convert_bedrock_tool_result_to_value(tool_result.get("content")) + tool_use_id = tool_result.get("toolUseId") contents.append( Content.from_function_result( - call_id=tool_result.get("toolUseId") or self._generate_tool_call_id(), + call_id=tool_use_id if isinstance(tool_use_id, str) else self._generate_tool_call_id(), result=result_value, exception=str(exception) if exception else None, # type: ignore[arg-type] raw_representation=block, @@ -673,24 +708,28 @@ class BedrockChatClient( """ return f"https://bedrock-runtime.{self.region}.amazonaws.com" - def _convert_bedrock_tool_result_to_value(self, content: Any) -> Any: + def _convert_bedrock_tool_result_to_value(self, content: object) -> object: if not content: return None if isinstance(content, Sequence) and not isinstance(content, (str, bytes, bytearray)): - values: list[Any] = [] + values: list[object] = [] for item in content: - if isinstance(item, MutableMapping): - if (text_value := item.get("text")) is not None: + item_dict = item if isinstance(item, dict) else dict(item) if isinstance(item, Mapping) else None + if item_dict is not None: + text_value = item_dict.get("text") + if isinstance(text_value, str): values.append(text_value) continue - if "json" in item: - values.append(item["json"]) + if "json" in item_dict: + values.append(item_dict["json"]) continue values.append(item) return values[0] if len(values) == 1 else values - if isinstance(content, MutableMapping): - if (text_value := content.get("text")) is not None: + content_dict = content if isinstance(content, dict) else dict(content) if isinstance(content, Mapping) else None + if content_dict is not None: + text_value = content_dict.get("text") + if isinstance(text_value, str): return text_value - if "json" in content: - return content["json"] + if "json" in content_dict: + return content_dict["json"] return content diff --git a/python/packages/bedrock/agent_framework_bedrock/_embedding_client.py b/python/packages/bedrock/agent_framework_bedrock/_embedding_client.py index 30be74eed9..d07bdee45c 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_embedding_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_embedding_client.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. - +# type: ignore +# Because the Bedrock client does not have typing, we are ignoring type issues in this module. from __future__ import annotations import asyncio @@ -122,25 +123,27 @@ class RawBedrockEmbeddingClient( ) resolved_region = settings.get("region") or DEFAULT_REGION - if client is None: + if client: + self._bedrock_client = client + else: if not boto3_session: session_kwargs: dict[str, Any] = {} if region := settings.get("region"): session_kwargs["region_name"] = region if (access_key := settings.get("access_key")) and (secret_key := settings.get("secret_key")): - session_kwargs["aws_access_key_id"] = access_key.get_secret_value() # type: ignore[union-attr] - session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value() # type: ignore[union-attr] + session_kwargs["aws_access_key_id"] = access_key.get_secret_value() + session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value() if session_token := settings.get("session_token"): - session_kwargs["aws_session_token"] = session_token.get_secret_value() # type: ignore[union-attr] + session_kwargs["aws_session_token"] = session_token.get_secret_value() boto3_session = Boto3Session(**session_kwargs) - client = boto3_session.client( + region_name = boto3_session.region_name + self._bedrock_client = boto3_session.client( "bedrock-runtime", - region_name=boto3_session.region_name or resolved_region, + region_name=region_name or resolved_region, config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT), ) - self._bedrock_client = client - self.model_id = settings["embedding_model_id"] # type: ignore[assignment] + self.model_id: str = settings["embedding_model_id"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess] self.region = resolved_region super().__init__(**kwargs) @@ -153,7 +156,7 @@ class RawBedrockEmbeddingClient( values: Sequence[str], *, options: BedrockEmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[list[float]]: + ) -> GeneratedEmbeddings[list[float], BedrockEmbeddingOptionsT]: """Call the Bedrock invoke_model API for embeddings. Uses the Amazon Titan Embeddings model format. Each value is embedded @@ -211,7 +214,6 @@ class RawBedrockEmbeddingClient( accept="application/json", body=json.dumps(body), ) - response_body = json.loads(response["body"].read()) embedding = Embedding( vector=response_body["embedding"], diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index 5cff0f4c69..b99ecb91ff 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -60,6 +60,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_bedrock"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -85,8 +86,8 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_bedrock" -test = "pytest --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_bedrock --cov-report=term-missing:skip-covered tests" [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index b4ecd81dff..74d7216da6 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_chatkit"] exclude = ['tests', 'chatkit-python', 'openai-chatkit-advanced-samples'] [tool.mypy] @@ -87,7 +88,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit" -test = "pytest --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index d764419214..127e3647ee 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -225,11 +225,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): description: str | None = None, context_providers: Sequence[BaseContextProvider] | None = None, middleware: Sequence[AgentMiddlewareTypes] | None = None, - tools: ToolTypes - | Callable[..., Any] - | str - | Sequence[ToolTypes | Callable[..., Any] | str] - | None = None, + tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None = None, default_options: OptionsT | MutableMapping[str, Any] | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, @@ -305,11 +301,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): def _normalize_tools( self, - tools: ToolTypes - | Callable[..., Any] - | str - | Sequence[ToolTypes | Callable[..., Any] | str] - | None, + tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None, ) -> None: """Separate built-in tools (strings) from custom tools. @@ -319,21 +311,17 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): if tools is None: return - # Normalize to sequence - if isinstance(tools, str): - tools_list: Sequence[Any] = [tools] - elif isinstance(tools, Sequence): - tools_list = list(tools) - else: - tools_list = [tools] - - for tool in tools_list: + non_builtin_tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] = [] + if not isinstance(tools, list): + tools = [tools] # type: ignore[assignment, reportUnknownVariableType] + for tool in tools: # type: ignore[reportUnknownVariableType] if isinstance(tool, str): self._builtin_tools.append(tool) else: - # Use normalize_tools for custom tools - normalized = normalize_tools(tool) - self._custom_tools.extend(normalized) + non_builtin_tools.append(tool) # type: ignore[union-attr, reportUnknownArgumentType] + if not non_builtin_tools: + return + self._custom_tools.extend(normalize_tools(non_builtin_tools)) # type: ignore[reportUnknownVariableType] async def __aenter__(self) -> RawClaudeAgent[OptionsT]: """Start the agent when entering async context.""" @@ -378,9 +366,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): session_id: The session ID to use, or None for a new session. """ needs_new_client = ( - not self._started - or self._client is None - or (session_id and session_id != self._current_session_id) + not self._started or self._client is None or (session_id and session_id != self._current_session_id) ) if needs_new_client: @@ -403,9 +389,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): self._client = None raise AgentException(f"Failed to start Claude SDK client: {ex}") from ex - def _prepare_client_options( - self, resume_session_id: str | None = None - ) -> SDKOptions: + def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions: """Prepare SDK options for client initialization. Args: @@ -445,9 +429,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): # Prepare custom tools (FunctionTool instances) custom_tools_server, custom_tool_names = ( - self._prepare_tools(self._custom_tools) - if self._custom_tools - else (None, []) + self._prepare_tools(self._custom_tools) if self._custom_tools else (None, []) ) # MCP servers - merge user-provided servers with custom tools server @@ -494,13 +476,9 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): if not sdk_tools: return None, [] - return create_sdk_mcp_server( - name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools - ), tool_names + return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names - def _function_tool_to_sdk_mcp_tool( - self, func_tool: FunctionTool - ) -> SdkMcpTool[Any]: + def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool) -> SdkMcpTool[Any]: """Convert a FunctionTool to an SDK MCP tool. Args: @@ -523,9 +501,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): return {"content": [{"type": "text", "text": f"Error: {e}"}]} # Get JSON schema from pydantic model - schema: dict[str, Any] = ( - func_tool.input_model.model_json_schema() if func_tool.input_model else {} - ) + schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {} input_schema: dict[str, Any] = { "type": "object", "properties": schema.get("properties", {}), @@ -586,9 +562,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): opts["instructions"] = system_prompt return opts - def _finalize_response( - self, updates: Sequence[AgentResponseUpdate] - ) -> AgentResponse[Any]: + def _finalize_response(self, updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]: """Build AgentResponse and propagate structured_output as value. Args: @@ -627,10 +601,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): stream: bool = False, session: AgentSession | None = None, **kwargs: Any, - ) -> ( - Awaitable[AgentResponse[Any]] - | ResponseStream[AgentResponseUpdate, AgentResponse[Any]] - ): + ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """Run the agent with the given messages. Args: @@ -696,11 +667,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): if text: yield AgentResponseUpdate( role="assistant", - contents=[ - Content.from_text( - text=text, raw_representation=message - ) - ], + contents=[Content.from_text(text=text, raw_representation=message)], raw_representation=message, ) elif delta_type == "thinking_delta": @@ -708,11 +675,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): if thinking: yield AgentResponseUpdate( role="assistant", - contents=[ - Content.from_text_reasoning( - text=thinking, raw_representation=message - ) - ], + contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)], raw_representation=message, ) elif isinstance(message, AssistantMessage): @@ -729,9 +692,7 @@ class RawClaudeAgent(BaseAgent, Generic[OptionsT]): "server_error": "Claude API server error", "unknown": "Unknown error from Claude API", } - error_msg = error_messages.get( - message.error, f"Claude API error: {message.error}" - ) + error_msg = error_messages.get(message.error, f"Claude API error: {message.error}") # Extract any error details from content blocks if message.content: for block in message.content: diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml index a3b009dcd5..f1891586f8 100644 --- a/python/packages/claude/pyproject.toml +++ b/python/packages/claude/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_claude"] exclude = ['tests'] [tool.mypy] @@ -87,7 +88,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude" -test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py index 91a07b58ff..edacb614a5 100644 --- a/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py +++ b/python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py @@ -133,43 +133,47 @@ class CopilotStudioAgent(BaseAgent): env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) + resolved_environment_id = copilot_studio_settings.get("environmentid") + resolved_agent_identifier = copilot_studio_settings.get("schemaname") + resolved_client_id = copilot_studio_settings.get("agentappid") + resolved_tenant_id = copilot_studio_settings.get("tenantid") if not settings: - if not copilot_studio_settings["environmentid"]: + if not resolved_environment_id: raise ValueError( "Copilot Studio environment ID is required. Set via 'environment_id' parameter " "or 'COPILOTSTUDIOAGENT__ENVIRONMENTID' environment variable." ) - if not copilot_studio_settings["schemaname"]: + if not resolved_agent_identifier: raise ValueError( "Copilot Studio agent identifier/schema name is required. Set via 'agent_identifier' parameter " "or 'COPILOTSTUDIOAGENT__SCHEMANAME' environment variable." ) settings = ConnectionSettings( - environment_id=copilot_studio_settings["environmentid"], - agent_identifier=copilot_studio_settings["schemaname"], + environment_id=resolved_environment_id, + agent_identifier=resolved_agent_identifier, cloud=cloud, copilot_agent_type=agent_type, custom_power_platform_cloud=custom_power_platform_cloud, ) if not token: - if not copilot_studio_settings["agentappid"]: + if not resolved_client_id: raise ValueError( "Copilot Studio client ID is required. Set via 'client_id' parameter " "or 'COPILOTSTUDIOAGENT__AGENTAPPID' environment variable." ) - if not copilot_studio_settings["tenantid"]: + if not resolved_tenant_id: raise ValueError( "Copilot Studio tenant ID is required. Set via 'tenant_id' parameter " "or 'COPILOTSTUDIOAGENT__TENANTID' environment variable." ) token = acquire_token( - client_id=copilot_studio_settings["agentappid"], - tenant_id=copilot_studio_settings["tenantid"], + client_id=resolved_client_id, + tenant_id=resolved_tenant_id, username=username, token_cache=token_cache, scopes=scopes, diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index 02fa708f20..c37fa71ecf 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_copilotstudio"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio" -test = "pytest --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index 1cbcc7a8cb..ef03652898 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -205,9 +205,6 @@ __all__ = [ "AgentResponseUpdate", "AgentRunInputs", "AgentSession", - "Skill", - "SkillResource", - "SkillsProvider", "Annotation", "BaseAgent", "BaseChatClient", @@ -272,6 +269,9 @@ __all__ = [ "SecretString", "SessionContext", "SingleEdgeGroup", + "Skill", + "SkillResource", + "SkillsProvider", "SubWorkflowRequestMessage", "SubWorkflowResponseMessage", "SupportsAgentRun", diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index a0c998757c..3aaf9f1419 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -83,10 +83,13 @@ OptionsCoT = TypeVar( def _get_tool_name(tool: Any) -> str | None: """Extract a tool's name from either an object with a .name attribute or a dict tool definition.""" - if isinstance(tool, dict): - func = tool.get("function") - if isinstance(func, dict): - return func.get("name") + if isinstance(tool, Mapping): + tool_mapping = cast(Mapping[str, Any], tool) + func = tool_mapping.get("function") + if isinstance(func, Mapping): + func_mapping = cast(Mapping[str, Any], func) + name = func_mapping.get("name") + return name if isinstance(name, str) else None return None return getattr(tool, "name", None) @@ -164,12 +167,12 @@ def _sanitize_agent_name(agent_name: str | None) -> str | None: class _RunContext(TypedDict): session: AgentSession | None session_context: SessionContext - input_messages: list[Message] - session_messages: list[Message] + input_messages: Sequence[Message] + session_messages: Sequence[Message] agent_name: str - chat_options: dict[str, Any] - filtered_kwargs: dict[str, Any] - finalize_kwargs: dict[str, Any] + chat_options: MutableMapping[str, Any] + filtered_kwargs: Mapping[str, Any] + finalize_kwargs: Mapping[str, Any] # region Agent Protocol @@ -770,10 +773,9 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] should check if there is already an agent name defined, and if not set it to this value. """ - if hasattr(self.client, "_update_agent_name_and_description") and callable( - self.client._update_agent_name_and_description - ): # type: ignore[reportAttributeAccessIssue, attr-defined] - self.client._update_agent_name_and_description(self.name, self.description) # type: ignore[reportAttributeAccessIssue, attr-defined] + update_fn = getattr(self.client, "_update_agent_name_and_description", None) + if callable(update_fn): + update_fn(self.name, self.description) @overload def run( @@ -860,11 +862,14 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] options=options, kwargs=kwargs, ) - response = await self.client.get_response( # type: ignore[call-overload] - messages=ctx["session_messages"], - stream=False, - options=ctx["chat_options"], - **ctx["filtered_kwargs"], + response = cast( + ChatResponse[Any], + await self.client.get_response( # type: ignore + messages=ctx["session_messages"], + stream=False, + options=ctx["chat_options"], # type: ignore[reportArgumentType] + **ctx["filtered_kwargs"], + ), ) if not response: @@ -930,7 +935,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] ) await self._run_after_providers(session=ctx["session"], context=session_context) - async def _get_stream() -> ResponseStream[ChatResponseUpdate, ChatResponse]: + async def _get_stream() -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ctx_holder["ctx"] = await self._prepare_run_context( messages=messages, session=session, @@ -942,7 +947,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] return self.client.get_response( # type: ignore[call-overload, no-any-return] messages=ctx["session_messages"], stream=True, - options=ctx["chat_options"], + options=ctx["chat_options"], # type: ignore[reportArgumentType] **ctx["filtered_kwargs"], ) @@ -965,12 +970,12 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] rf = ( ctx.get("chat_options", {}).get("response_format") if ctx - else (options.get("response_format") if options else None) + else (options.get("response_format") if options else None) # type: ignore[union-attr] ) return self._finalize_response_updates(updates, response_format=rf) return ( - ResponseStream + ResponseStream # type: ignore[reportUnknownMemberType] .from_awaitable(_get_stream()) .map( transform=partial( @@ -988,10 +993,13 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] updates: Sequence[AgentResponseUpdate], *, response_format: Any | None = None, - ) -> AgentResponse: + ) -> AgentResponse[Any]: """Finalize response updates into a single AgentResponse.""" output_format_type = response_format if isinstance(response_format, type) else None - return AgentResponse.from_updates(updates, output_format_type=output_format_type) + return AgentResponse.from_updates( # pyright: ignore[reportUnknownVariableType] + updates, + output_format_type=output_format_type, + ) @staticmethod def _extract_conversation_id_from_streaming_response(response: AgentResponse[Any]) -> str | None: @@ -1000,10 +1008,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] if raw is None: return None - raw_items: list[Any] = raw if isinstance(raw, list) else [raw] + raw_items: list[Any] = list(cast(Any, raw)) if isinstance(raw, list) else [raw] for item in reversed(raw_items): if isinstance(item, Mapping): - value = item.get("conversation_id") + mapped_item = cast(Mapping[str, Any], item) + value = mapped_item.get("conversation_id") if isinstance(value, str) and value: return value continue @@ -1074,7 +1083,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] # Merge runtime kwargs into additional_function_arguments so they're available # in function middleware context and tool invocation. - existing_additional_args = opts.pop("additional_function_arguments", None) or {} + existing_additional_args: dict[str, Any] = opts.pop("additional_function_arguments", None) or {} additional_function_arguments = {**kwargs, **existing_additional_args} # Include session so as_tool() wrappers with propagate_session=True can access it. if active_session is not None: diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index 278657a154..5dd049ecd3 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -317,10 +317,13 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]): updates: Sequence[ChatResponseUpdate], *, response_format: Any | None = None, - ) -> ChatResponse: + ) -> ChatResponse[Any]: """Finalize response updates into a single ChatResponse.""" output_format_type = response_format if isinstance(response_format, type) else None - return ChatResponse.from_updates(updates, output_format_type=output_format_type) + return ChatResponse.from_updates( # pyright: ignore[reportUnknownVariableType] + updates, + output_format_type=output_format_type, + ) def _build_response_stream( self, @@ -782,7 +785,7 @@ class BaseEmbeddingClient(SerializationMixin, ABC, Generic[EmbeddingInputT, Embe values: Sequence[EmbeddingInputT], *, options: EmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[EmbeddingT]: + ) -> GeneratedEmbeddings[EmbeddingT, EmbeddingOptionsT]: """Generate embeddings for the given values. Args: diff --git a/python/packages/core/agent_framework/_middleware.py b/python/packages/core/agent_framework/_middleware.py index 1f0f9e3338..7f3f3da13d 100644 --- a/python/packages/core/agent_framework/_middleware.py +++ b/python/packages/core/agent_framework/_middleware.py @@ -8,7 +8,7 @@ import sys from abc import ABC, abstractmethod from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence from enum import Enum -from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, overload +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, cast, overload from ._clients import SupportsChatGetResponse from ._types import ( @@ -170,9 +170,9 @@ class AgentContext: self.session = session self.options = options self.stream = stream - self.metadata = metadata if metadata is not None else {} + self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {} self.result = result - self.kwargs = kwargs if kwargs is not None else {} + self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {} self.stream_transform_hooks = list(stream_transform_hooks or []) self.stream_result_hooks = list(stream_result_hooks or []) self.stream_cleanup_hooks = list(stream_cleanup_hooks or []) @@ -231,9 +231,9 @@ class FunctionInvocationContext: """ self.function = function self.arguments = arguments - self.metadata = metadata if metadata is not None else {} + self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {} self.result = result - self.kwargs = kwargs if kwargs is not None else {} + self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {} class ChatContext: @@ -314,9 +314,9 @@ class ChatContext: self.messages = messages self.options = options self.stream = stream - self.metadata = metadata if metadata is not None else {} + self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {} self.result = result - self.kwargs = kwargs if kwargs is not None else {} + self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {} self.stream_transform_hooks = list(stream_transform_hooks or []) self.stream_result_hooks = list(stream_result_hooks or []) self.stream_cleanup_hooks = list(stream_cleanup_hooks or []) @@ -754,9 +754,11 @@ class AgentMiddlewarePipeline(BaseMiddlewarePipeline): if index >= len(self._middleware): async def final_wrapper() -> None: - context.result = final_handler(context) # type: ignore[assignment] - if inspect.isawaitable(context.result): - context.result = await context.result + result = final_handler(context) + if inspect.isawaitable(result): + context.result = await cast(Awaitable[AgentResponse], result) + else: + context.result = result return final_wrapper @@ -893,12 +895,17 @@ class ChatMiddlewarePipeline(BaseMiddlewarePipeline): The chat response after processing through all middleware. """ if not self._middleware: - context.result = final_handler(context) # type: ignore[assignment] - if isinstance(context.result, Awaitable): - context.result = await context.result - if context.stream and not isinstance(context.result, ResponseStream): + result = final_handler(context) + if inspect.isawaitable(result): + resolved_result: ChatResponse | ResponseStream[ChatResponseUpdate, ChatResponse] = await cast( + Awaitable[ChatResponse], result + ) + else: + resolved_result = result + context.result = resolved_result + if context.stream and not isinstance(resolved_result, ResponseStream): raise ValueError("Streaming agent middleware requires a ResponseStream result.") - return context.result + return resolved_result def create_next_handler(index: int) -> Callable[[], Awaitable[None]]: if index >= len(self._middleware): @@ -1038,7 +1045,10 @@ class ChatMiddlewareLayer(Generic[OptionsCoT]): # If result is ChatResponse (shouldn't happen for streaming), raise error raise ValueError("Expected ResponseStream for streaming, got ChatResponse") - return ResponseStream.from_awaitable(_execute_stream()) + return cast( + ResponseStream[ChatResponseUpdate, ChatResponse[Any]], + cast(Any, ResponseStream).from_awaitable(_execute_stream()), + ) # For non-streaming, return the coroutine directly return _execute() # type: ignore[return-value] @@ -1120,7 +1130,10 @@ class AgentMiddlewareLayer: ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """MiddlewareTypes-enabled unified run method.""" # Re-categorize self.middleware at runtime to support dynamic changes - base_middleware = getattr(self, "middleware", None) or [] + base_middleware_attr = getattr(self, "middleware", None) + base_middleware: Sequence[MiddlewareTypes] = ( + cast(Sequence[MiddlewareTypes], base_middleware_attr) if isinstance(base_middleware_attr, Sequence) else [] + ) base_middleware_list = categorize_middleware(base_middleware) run_middleware_list = categorize_middleware(middleware) pipeline = AgentMiddlewarePipeline(*base_middleware_list["agent"], *run_middleware_list["agent"]) @@ -1166,7 +1179,10 @@ class AgentMiddlewareLayer: # If result is AgentResponse (shouldn't happen for streaming), convert to stream raise ValueError("Expected ResponseStream for streaming, got AgentResponse") - return ResponseStream.from_awaitable(_execute_stream()) + return cast( + ResponseStream[AgentResponseUpdate, AgentResponse[Any]], + cast(Any, ResponseStream).from_awaitable(_execute_stream()), + ) # For non-streaming, return the coroutine directly return _execute() # type: ignore[return-value] diff --git a/python/packages/core/agent_framework/_serialization.py b/python/packages/core/agent_framework/_serialization.py index 7934477298..8dffdc0ce6 100644 --- a/python/packages/core/agent_framework/_serialization.py +++ b/python/packages/core/agent_framework/_serialization.py @@ -303,7 +303,7 @@ class SerializationMixin: # Handle lists containing SerializationProtocol objects if isinstance(value, list): value_as_list: list[Any] = [] - for item in value: + for item in value: # pyright: ignore[reportUnknownVariableType] if isinstance(item, SerializationProtocol): value_as_list.append(item.to_dict(exclude=exclude, exclude_none=exclude_none)) continue @@ -311,7 +311,7 @@ class SerializationMixin: value_as_list.append(item) continue logger.debug( - f"Skipping non-serializable item in list attribute '{key}' of type {type(item).__name__}" + f"Skipping non-serializable item in list attribute '{key}' of type {type(item).__name__}" # pyright: ignore[reportUnknownArgumentType] ) result[key] = value_as_list continue @@ -320,21 +320,22 @@ class SerializationMixin: from datetime import date, datetime, time serialized_dict: dict[str, Any] = {} - for k, v in value.items(): + for raw_key, v in value.items(): # pyright: ignore[reportUnknownVariableType] + dict_key = str(raw_key) # pyright: ignore[reportUnknownArgumentType] if isinstance(v, SerializationProtocol): - serialized_dict[k] = v.to_dict(exclude=exclude, exclude_none=exclude_none) + serialized_dict[dict_key] = v.to_dict(exclude=exclude, exclude_none=exclude_none) continue # Convert datetime objects to strings if isinstance(v, (datetime, date, time)): - serialized_dict[k] = str(v) + serialized_dict[dict_key] = str(v) continue # Check if the value is JSON serializable if is_serializable(v): - serialized_dict[k] = v + serialized_dict[dict_key] = v continue logger.debug( - f"Skipping non-serializable value for key '{k}' in dict attribute '{key}' " - f"of type {type(v).__name__}" + f"Skipping non-serializable value for key '{dict_key}' in dict attribute '{key}' " + f"of type {type(v).__name__}" # pyright: ignore[reportUnknownArgumentType] ) result[key] = serialized_dict continue @@ -505,7 +506,8 @@ class SerializationMixin: # Only apply if the instance matches if kwargs.get(field) == name and isinstance(dep_value, dict): # Apply instance-specific dependencies - for param_name, param_value in dep_value.items(): + for raw_param_name, param_value in dep_value.items(): # pyright: ignore[reportUnknownVariableType] + param_name = str(raw_param_name) # pyright: ignore[reportUnknownArgumentType] if param_name not in cls.INJECTABLE: logger.debug( f"Dependency '{param_name}' for type '{type_id}' is not in INJECTABLE set. " diff --git a/python/packages/core/agent_framework/_sessions.py b/python/packages/core/agent_framework/_sessions.py index aba90bc6e5..8c3457da26 100644 --- a/python/packages/core/agent_framework/_sessions.py +++ b/python/packages/core/agent_framework/_sessions.py @@ -16,7 +16,7 @@ import copy import uuid from abc import abstractmethod from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar, cast from ._types import AgentResponse, Message @@ -92,7 +92,7 @@ def _deserialize_value(value: Any) -> Any: from pydantic import BaseModel if issubclass(cls, BaseModel): - data = {k: v for k, v in value.items() if k != "type"} + data: dict[str, Any] = {str(k): v for k, v in value.items() if k != "type"} # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] return cls.model_validate(data) except ImportError: pass @@ -229,8 +229,11 @@ class SessionContext: tools: The tools to add. """ for tool in tools: - if hasattr(tool, "additional_properties") and isinstance(tool.additional_properties, dict): - tool.additional_properties["context_source"] = source_id + if hasattr(tool, "additional_properties"): + additional_properties_obj = tool.additional_properties + if isinstance(additional_properties_obj, dict): + additional_properties = cast(dict[str, Any], additional_properties_obj) + additional_properties["context_source"] = source_id self.tools.extend(tools) def get_messages( diff --git a/python/packages/core/agent_framework/_settings.py b/python/packages/core/agent_framework/_settings.py index e2b6af428c..4eecf3434d 100644 --- a/python/packages/core/agent_framework/_settings.py +++ b/python/packages/core/agent_framework/_settings.py @@ -215,9 +215,7 @@ def load_settings( raise FileNotFoundError(env_file_path) raw_dotenv_values = dotenv_values(dotenv_path=env_file_path, encoding=encoding) - loaded_dotenv_values = { - key: value for key, value in raw_dotenv_values.items() if key is not None and value is not None - } + loaded_dotenv_values = {key: value for key, value in raw_dotenv_values.items() if value is not None} # Filter out None overrides so defaults / env vars are preserved overrides = {k: v for k, v in overrides.items() if v is not None} diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 9e11ecbe96..49695c89e6 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -151,6 +151,7 @@ class Skill: content="Use this skill for DB tasks.", ) + @skill.resource def get_schema() -> str: return "CREATE TABLE ..." @@ -972,9 +973,7 @@ def _load_skills( if skills: for code_skill in skills: - error = _validate_skill_metadata( - code_skill.name, code_skill.description, "code skill" - ) + error = _validate_skill_metadata(code_skill.name, code_skill.description, "code skill") if error: logger.warning(error) continue diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 303699572c..3f11189fdc 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -27,7 +27,7 @@ from typing import ( Literal, TypeAlias, TypedDict, - Union, + cast, get_args, get_origin, overload, @@ -77,6 +77,7 @@ else: logger = logging.getLogger("agent_framework") + DEFAULT_MAX_ITERATIONS: Final[int] = 40 DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST: Final[int] = 3 SHELL_TOOL_KIND_VALUE: Final[str] = "shell" @@ -84,7 +85,7 @@ ChatClientT = TypeVar("ChatClientT", bound="SupportsChatGetResponse[Any]") # region Helpers -def _parse_inputs( +def _parse_inputs( # pyright: ignore[reportUnusedFunction] inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None, ) -> list[Content]: """Parse the inputs for a tool, ensuring they are of type Content. @@ -352,7 +353,8 @@ class FunctionTool(SerializationMixin): def declaration_only(self) -> bool: """Indicate whether the function is declaration only (i.e., has no implementation).""" # Check for explicit _declaration_only attribute first (used in tests) - if hasattr(self, "_declaration_only") and self._declaration_only: + declaration_flag = getattr(self, "_declaration_only", False) + if isinstance(declaration_flag, bool) and declaration_flag: return True return self.func is None @@ -430,10 +432,13 @@ class FunctionTool(SerializationMixin): ) self.invocation_count += 1 try: + func = self.func + if func is None: + raise ToolException(f"Function '{self.name}' has no implementation.") # If we have a bound instance, call the function with self if self._instance is not None: - return self.func(self._instance, *args, **kwargs) - return self.func(*args, **kwargs) # type:ignore[misc] + return func(self._instance, *args, **kwargs) + return func(*args, **kwargs) except Exception: self.invocation_exception_count += 1 raise @@ -600,9 +605,11 @@ class FunctionTool(SerializationMixin): from ._types import Content if isinstance(value, list): - return [FunctionTool._make_dumpable(item) for item in value] + list_value = cast(list[object], value) + return [FunctionTool._make_dumpable(item) for item in list_value] if isinstance(value, dict): - return {k: FunctionTool._make_dumpable(v) for k, v in value.items()} + dict_value = cast(dict[object, object], value) + return {key: FunctionTool._make_dumpable(item) for key, item in dict_value.items()} if isinstance(value, Content): return value.to_dict(exclude={"raw_representation", "additional_properties"}) if isinstance(value, BaseModel): @@ -661,7 +668,7 @@ class FunctionTool(SerializationMixin): return as_dict -ToolTypes: TypeAlias = FunctionTool | MCPTool | Mapping[str, Any] | Any +ToolTypes: TypeAlias = FunctionTool | MCPTool | Mapping[str, Any] | object def normalize_tools( @@ -679,27 +686,31 @@ def normalize_tools( if not tools: return [] - tool_items = ( - list(tools) - if isinstance(tools, Sequence) and not isinstance(tools, (str, bytes, bytearray, Mapping)) - else [tools] - ) + if isinstance(tools, (str, bytes, bytearray, Mapping)) or not isinstance(tools, Sequence): + tools = cast(list[ToolTypes | Callable[..., Any]], [tools]) + from ._mcp import MCPTool normalized: list[ToolTypes] = [] - for tool_item in tool_items: + for tool_item in tools: # type: ignore[reportUnknownVariableType] # check known types, these are also callable, so we need to do that first - if isinstance(tool_item, (FunctionTool, Mapping, MCPTool)): + if isinstance(tool_item, FunctionTool): normalized.append(tool_item) continue - if callable(tool_item): + if isinstance(tool_item, dict): + normalized.append(tool_item) # type: ignore[reportUnknownArgumentType] + continue + if isinstance(tool_item, MCPTool): + normalized.append(tool_item) + continue + if callable(tool_item): # type: ignore[reportUnknownArgumentType] normalized.append(tool(tool_item)) continue - normalized.append(tool_item) + normalized.append(tool_item) # type: ignore[reportUnknownArgumentType] return normalized -def _tools_to_dict( +def _tools_to_dict( # pyright: ignore[reportUnusedFunction] tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None, ) -> list[str | dict[str, Any]] | None: """Parse the tools to a dict. @@ -722,8 +733,8 @@ def _tools_to_dict( if isinstance(tool_item, SerializationMixin): results.append(tool_item.to_dict()) continue - if isinstance(tool_item, Mapping): - results.append(dict(tool_item)) + if isinstance(tool_item, dict): + results.append(tool_item) # type: ignore[reportUnknownArgumentType] continue logger.warning("Can't parse tool.") return results @@ -795,32 +806,28 @@ def _validate_arguments_against_schema( """Run lightweight argument checks for schema-supplied tools.""" parsed_arguments = dict(arguments) - required_raw = schema.get("required", []) - required_fields = [field for field in required_raw if isinstance(field, str)] + required_fields = [field for field in schema.get("required", []) if isinstance(field, str)] missing_fields = [field for field in required_fields if field not in parsed_arguments] if missing_fields: raise TypeError(f"Missing required argument(s) for '{tool_name}': {', '.join(sorted(missing_fields))}") - properties_raw = schema.get("properties") - properties = properties_raw if isinstance(properties_raw, Mapping) else {} - + properties: Mapping[str, Any] = schema.get("properties", {}) if schema.get("additionalProperties") is False: unexpected_fields = sorted(field for field in parsed_arguments if field not in properties) if unexpected_fields: raise TypeError(f"Unexpected argument(s) for '{tool_name}': {', '.join(unexpected_fields)}") for field_name, field_value in parsed_arguments.items(): - field_schema = properties.get(field_name) - if not isinstance(field_schema, Mapping): + if not isinstance(properties.get(field_name), dict): continue - enum_values = field_schema.get("enum") + enum_values = properties.get(field_name, {}).get("enum") # type: ignore if isinstance(enum_values, list) and enum_values and field_value not in enum_values: raise TypeError( f"Invalid value for '{field_name}' in '{tool_name}': {field_value!r} is not in {enum_values!r}" ) - schema_type = field_schema.get("type") + schema_type = properties.get(field_name, {}).get("type") # type: ignore if isinstance(schema_type, str): if not _matches_json_schema_type(field_value, schema_type): raise TypeError( @@ -830,7 +837,7 @@ def _validate_arguments_against_schema( continue if isinstance(schema_type, list): - allowed_types = [item for item in schema_type if isinstance(item, str)] + allowed_types: list[str] = [item for item in schema_type if isinstance(item, str)] # type: ignore[reportUnknownVariableType] if allowed_types and not any(_matches_json_schema_type(field_value, item) for item in allowed_types): raise TypeError( f"Invalid type for '{field_name}' in '{tool_name}': expected one of " @@ -840,240 +847,6 @@ def _validate_arguments_against_schema( return parsed_arguments -# Map JSON Schema types to Pydantic types -TYPE_MAPPING = { - "string": str, - "integer": int, - "number": float, - "boolean": bool, - "array": list, - "object": dict, - "null": type(None), -} - - -def _build_pydantic_model_from_json_schema( - model_name: str, - schema: Mapping[str, Any], -) -> type[BaseModel]: - """Creates a Pydantic model from JSON Schema with support for $refs, nested objects, and typed arrays. - - Args: - model_name: The name of the model to be created. - schema: The JSON Schema definition (should contain 'properties', 'required', '$defs', etc.). - - Returns: - The dynamically created Pydantic model class. - """ - properties = schema.get("properties") - required = schema.get("required", []) - definitions = schema.get("$defs", {}) - - # Check if 'properties' is missing or not a dictionary - if not properties: - return create_model(f"{model_name}_input") - - def _resolve_literal_type(prop_details: dict[str, Any]) -> type | None: - """Check if property should be a Literal type (const or enum). - - Args: - prop_details: The JSON Schema property details - - Returns: - Literal type if const or enum is present, None otherwise - """ - # const → Literal["value"] - if "const" in prop_details: - return Literal[prop_details["const"]] # type: ignore - - # enum → Literal["a", "b", ...] - if "enum" in prop_details and isinstance(prop_details["enum"], list): - enum_values = prop_details["enum"] - if enum_values: - return Literal[tuple(enum_values)] # type: ignore - - return None - - def _resolve_type(prop_details: dict[str, Any], parent_name: str = "") -> type: - """Resolve JSON Schema type to Python type, handling $ref, nested objects, and typed arrays. - - Args: - prop_details: The JSON Schema property details - parent_name: Name to use for creating nested models (for uniqueness) - - Returns: - Python type annotation (could be int, str, list[str], or a nested Pydantic model) - """ - # Handle oneOf + discriminator (polymorphic objects) - if "oneOf" in prop_details and "discriminator" in prop_details: - discriminator = prop_details["discriminator"] - disc_field = discriminator.get("propertyName") - - variants = [] - for variant in prop_details["oneOf"]: - if "$ref" in variant: - ref = variant["$ref"] - if ref.startswith("#/$defs/"): - def_name = ref.split("/")[-1] - resolved = definitions.get(def_name) - if resolved: - variant_model = _resolve_type( - resolved, - parent_name=f"{parent_name}_{def_name}", - ) - variants.append(variant_model) - - if variants and disc_field: - return Annotated[ - Union[tuple(variants)], # type: ignore - Field(discriminator=disc_field), - ] - - # Handle $ref by resolving the reference - if "$ref" in prop_details: - ref = prop_details["$ref"] - # Extract the reference path (e.g., "#/$defs/CustomerIdParam" -> "CustomerIdParam") - if ref.startswith("#/$defs/"): - def_name = ref.split("/")[-1] - if def_name in definitions: - # Resolve the reference and use its type - resolved = definitions[def_name] - return _resolve_type(resolved, def_name) - # If we can't resolve the ref, default to dict for safety - return dict - - # Map JSON Schema types to Python types - json_type = prop_details.get("type", "string") - match json_type: - case "integer": - return int - case "number": - return float - case "boolean": - return bool - case "array": - # Handle typed arrays - items_schema = prop_details.get("items") - if items_schema and isinstance(items_schema, dict): - # Recursively resolve the item type - item_type = _resolve_type(items_schema, f"{parent_name}_item") - # Return list[ItemType] instead of bare list - return list[item_type] # type: ignore - # If no items schema or invalid, return bare list - return list - case "object": - # Handle nested objects by creating a nested Pydantic model - nested_properties = prop_details.get("properties") - nested_required = prop_details.get("required", []) - - if nested_properties and isinstance(nested_properties, dict): - # Create the name for the nested model - nested_model_name = f"{parent_name}_nested" if parent_name else "NestedModel" - - # Recursively build field definitions for the nested model - nested_field_definitions: dict[str, Any] = {} - for nested_prop_name, nested_prop_details in nested_properties.items(): - nested_prop_details = ( - json.loads(nested_prop_details) - if isinstance(nested_prop_details, str) - else nested_prop_details - ) - - # Check for Literal types first (const/enum) - literal_type = _resolve_literal_type(nested_prop_details) - if literal_type is not None: - nested_python_type = literal_type - else: - nested_python_type = _resolve_type( - nested_prop_details, - f"{nested_model_name}_{nested_prop_name}", - ) - nested_description = nested_prop_details.get("description", "") - - # Build field kwargs for nested property - nested_field_kwargs: dict[str, Any] = {} - if nested_description: - nested_field_kwargs["description"] = nested_description - - # Create field definition - if nested_prop_name in nested_required: - nested_field_definitions[nested_prop_name] = ( - ( - nested_python_type, - Field(**nested_field_kwargs), - ) - if nested_field_kwargs - else (nested_python_type, ...) - ) - else: - nested_field_kwargs["default"] = nested_prop_details.get("default", None) - nested_field_definitions[nested_prop_name] = ( - nested_python_type, - Field(**nested_field_kwargs), - ) - - # Create and return the nested Pydantic model - return create_model(nested_model_name, **nested_field_definitions) # type: ignore - - # If no properties defined, return bare dict - return dict - case _: - return str # default - - field_definitions: dict[str, Any] = {} - for prop_name, prop_details in properties.items(): - prop_details = json.loads(prop_details) if isinstance(prop_details, str) else prop_details - - # Check for Literal types first (const/enum) - literal_type = _resolve_literal_type(prop_details) - if literal_type is not None: - python_type = literal_type - else: - python_type = _resolve_type(prop_details, f"{model_name}_{prop_name}") - description = prop_details.get("description", "") - - # Build field kwargs (description, etc.) - field_kwargs: dict[str, Any] = {} - if description: - field_kwargs["description"] = description - - # Create field definition for create_model - if prop_name in required: - if field_kwargs: - field_definitions[prop_name] = (python_type, Field(**field_kwargs)) - else: - field_definitions[prop_name] = (python_type, ...) - else: - default_value = prop_details.get("default", None) - field_kwargs["default"] = default_value - if field_kwargs and any(k != "default" for k in field_kwargs): - field_definitions[prop_name] = (python_type, Field(**field_kwargs)) - else: - field_definitions[prop_name] = (python_type, default_value) - - return create_model(f"{model_name}_input", **field_definitions) - - -def _create_model_from_json_schema(tool_name: str, schema_json: Mapping[str, Any]) -> type[BaseModel]: - """Creates a Pydantic model from a given JSON Schema. - - Args: - tool_name: The name of the model to be created. - schema_json: The JSON Schema definition. - - Returns: - The dynamically created Pydantic model class. - """ - # Validate that 'properties' exists and is a dict - if "properties" not in schema_json or not isinstance(schema_json["properties"], dict): - raise ValueError( - f"JSON schema for tool '{tool_name}' must contain a 'properties' key of type dict. " - f"Got: {schema_json.get('properties', None)}" - ) - - return _build_pydantic_model_from_json_schema(tool_name, schema_json) - - @overload def tool( func: Callable[..., Any], @@ -1348,8 +1121,6 @@ def normalize_function_invocation_configuration( raise ValueError("max_function_calls must be at least 1 or None.") if normalized["max_consecutive_errors_per_request"] < 0: raise ValueError("max_consecutive_errors_per_request must be 0 or more.") - if normalized["additional_tools"] is None: - normalized["additional_tools"] = [] return normalized @@ -1424,7 +1195,7 @@ async def _auto_invoke_function( if key not in {"_function_middleware_pipeline", "middleware", "conversation_id"} } try: - if not tool._schema_supplied and tool.input_model is not None: + if not cast(bool, getattr(tool, "_schema_supplied", False)) and tool.input_model is not None: args = tool.input_model.model_validate(parsed_args).model_dump(exclude_none=True) else: args = dict(parsed_args) @@ -1435,7 +1206,7 @@ async def _auto_invoke_function( ) except (TypeError, ValidationError) as exc: message = "Error: Argument parsing failed." - if config["include_detailed_errors"]: + if config.get("include_detailed_errors", False): message = f"{message} Exception: {exc}" return Content.from_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] @@ -1459,7 +1230,7 @@ async def _auto_invoke_function( ) except Exception as exc: message = "Error: Function failed." - if config["include_detailed_errors"]: + if config.get("include_detailed_errors", False): message = f"{message} Exception: {exc}" return Content.from_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] @@ -1505,7 +1276,7 @@ async def _auto_invoke_function( raise except Exception as exc: message = "Error: Function failed." - if config["include_detailed_errors"]: + if config.get("include_detailed_errors", False): message = f"{message} Exception: {exc}" return Content.from_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] @@ -1560,7 +1331,8 @@ async def _try_execute_function_calls( approval_tools, ) declaration_only = [tool_name for tool_name, tool in tool_map.items() if tool.declaration_only] - additional_tool_names = [tool.name for tool in config["additional_tools"]] if config["additional_tools"] else [] + configured_additional_tools = config.get("additional_tools") or [] + additional_tool_names = [tool.name for tool in configured_additional_tools] # check if any are calling functions that need approval # if so, we return approval request for all approval_needed = False @@ -1581,7 +1353,7 @@ async def _try_execute_function_calls( declaration_only_flag = True break if ( - config["terminate_on_unknown_calls"] and fcc.type == "function_call" and fcc.name not in tool_map # type: ignore[attr-defined] + config.get("terminate_on_unknown_calls", False) and fcc.type == "function_call" and fcc.name not in tool_map # type: ignore[attr-defined] ): raise KeyError(f'Error: Requested function "{fcc.name}" not found.') # type: ignore[attr-defined] if approval_needed: @@ -1598,7 +1370,7 @@ async def _try_execute_function_calls( if declaration_only_flag: # return the declaration only tools to the user, since we cannot execute them. # Mark as user_input_request so AgentExecutor emits request_info events and pauses the workflow. - declaration_only_calls = [] + declaration_only_calls: list[Content] = [] for fcc in function_calls: if fcc.type == "function_call": fcc.user_input_request = True @@ -1695,19 +1467,6 @@ def _update_conversation_id( options["conversation_id"] = conversation_id -async def _ensure_response_stream( - stream_like: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]], -) -> ResponseStream[Any, Any]: - from ._types import ResponseStream - - stream = await stream_like if isinstance(stream_like, Awaitable) else stream_like - if not isinstance(stream, ResponseStream): - raise ValueError("Streaming function invocation requires a ResponseStream result.") - if getattr(stream, "_stream", None) is None: - await stream - return stream - - def _extract_tools( options: dict[str, Any] | None, ) -> ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None: @@ -1776,7 +1535,7 @@ def _replace_approval_contents_with_results( } # Track approval requests that should be removed (duplicates) - contents_to_remove = [] + contents_to_remove: list[int] = [] for content_idx, content in enumerate(msg.contents): if content.type == "function_approval_request": @@ -2097,7 +1856,9 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): function_middleware_pipeline = FunctionMiddlewarePipeline( *(self.function_middleware), *(function_middleware or []) ) - max_errors: int = self.function_invocation_configuration["max_consecutive_errors_per_request"] # type: ignore[assignment] + max_errors = self.function_invocation_configuration.get( + "max_consecutive_errors_per_request", DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST + ) additional_function_arguments: dict[str, Any] = {} if options and (additional_opts := options.get("additional_function_arguments")): # type: ignore[attr-defined] additional_function_arguments = additional_opts # type: ignore @@ -2122,7 +1883,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): if not stream: - async def _get_response() -> ChatResponse: + async def _get_response() -> ChatResponse[Any]: nonlocal mutable_options nonlocal filtered_kwargs errors_in_a_row: int = 0 @@ -2130,13 +1891,11 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_function_calls: int | None = self.function_invocation_configuration.get("max_function_calls") prepped_messages = list(messages) fcc_messages: list[Message] = [] - response: ChatResponse | None = None + response: ChatResponse[Any] | None = None - for attempt_idx in range( - self.function_invocation_configuration["max_iterations"] - if self.function_invocation_configuration["enabled"] - else 0 - ): + loop_enabled = self.function_invocation_configuration.get("enabled", True) + max_iterations = self.function_invocation_configuration.get("max_iterations", DEFAULT_MAX_ITERATIONS) + for attempt_idx in range(max_iterations if loop_enabled else 0): approval_result = await _process_function_requests( response=None, prepped_messages=prepped_messages, @@ -2147,17 +1906,20 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_errors=max_errors, execute_function_calls=execute_function_calls, ) - if approval_result["action"] == "stop": + if approval_result.get("action") == "stop": response = ChatResponse(messages=prepped_messages) break - errors_in_a_row = approval_result["errors_in_a_row"] + errors_in_a_row = approval_result.get("errors_in_a_row", errors_in_a_row) total_function_calls += approval_result.get("function_call_count", 0) - response = await super_get_response( - messages=prepped_messages, - stream=False, - options=mutable_options, - **filtered_kwargs, + response = cast( + ChatResponse[Any], + await super_get_response( + messages=prepped_messages, + stream=False, + options=mutable_options, + **filtered_kwargs, + ), ) if response.conversation_id is not None: @@ -2174,10 +1936,10 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_errors=max_errors, execute_function_calls=execute_function_calls, ) - if result["action"] == "return": + if result.get("action") == "return": return response total_function_calls += result.get("function_call_count", 0) - if result["action"] == "stop": + if result.get("action") == "stop": # Error threshold reached: force a final non-tool turn so # function_call_output items are submitted before exit. mutable_options["tool_choice"] = "none" @@ -2190,7 +1952,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_function_calls, ) mutable_options["tool_choice"] = "none" - errors_in_a_row = result["errors_in_a_row"] + errors_in_a_row = result.get("errors_in_a_row", errors_in_a_row) # When tool_choice is 'required', reset tool_choice after one iteration to avoid infinite loops if mutable_options.get("tool_choice") == "required" or ( @@ -2213,17 +1975,20 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): # Make a final model call with tool_choice="none" so the model # produces a plain text answer instead of leaving orphaned # function_call items without matching results. - if response is not None and self.function_invocation_configuration["enabled"]: + if response is not None and self.function_invocation_configuration.get("enabled", True): logger.info( "Maximum iterations reached (%d). Requesting final response without tools.", - self.function_invocation_configuration["max_iterations"], + self.function_invocation_configuration.get("max_iterations", DEFAULT_MAX_ITERATIONS), ) mutable_options["tool_choice"] = "none" - response = await super_get_response( - messages=prepped_messages, - stream=False, - options=mutable_options, - **filtered_kwargs, + response = cast( + ChatResponse[Any], + await super_get_response( + messages=prepped_messages, + stream=False, + options=mutable_options, + **filtered_kwargs, + ), ) if fcc_messages: for msg in reversed(fcc_messages): @@ -2233,7 +1998,7 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): return _get_response() response_format = mutable_options.get("response_format") if mutable_options else None - output_format_type = response_format if isinstance(response_format, type) else None + output_format_type: type[BaseModel] | None = response_format if isinstance(response_format, type) else None stream_result_hooks: list[Callable[[ChatResponse], Any]] = [] async def _stream() -> AsyncIterable[ChatResponseUpdate]: @@ -2245,13 +2010,11 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_function_calls: int | None = self.function_invocation_configuration.get("max_function_calls") prepped_messages = list(messages) fcc_messages: list[Message] = [] - response: ChatResponse | None = None + response: ChatResponse[Any] | None = None - for attempt_idx in range( - self.function_invocation_configuration["max_iterations"] - if self.function_invocation_configuration["enabled"] - else 0 - ): + loop_enabled = self.function_invocation_configuration.get("enabled", True) + max_iterations = self.function_invocation_configuration.get("max_iterations", DEFAULT_MAX_ITERATIONS) + for attempt_idx in range(max_iterations if loop_enabled else 0): approval_result = await _process_function_requests( response=None, prepped_messages=prepped_messages, @@ -2262,20 +2025,22 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_errors=max_errors, execute_function_calls=execute_function_calls, ) - errors_in_a_row = approval_result["errors_in_a_row"] + errors_in_a_row = approval_result.get("errors_in_a_row", errors_in_a_row) total_function_calls += approval_result.get("function_call_count", 0) - if approval_result["action"] == "stop": + if approval_result.get("action") == "stop": mutable_options["tool_choice"] = "none" return - inner_stream = await _ensure_response_stream( + inner_stream = cast( + ResponseStream[ChatResponseUpdate, ChatResponse[Any]], super_get_response( messages=prepped_messages, stream=True, options=mutable_options, **filtered_kwargs, - ) + ), ) + await inner_stream # Collect result hooks from the inner stream to run later stream_result_hooks[:] = _get_result_hooks_from_stream(inner_stream) @@ -2308,18 +2073,18 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): max_errors=max_errors, execute_function_calls=execute_function_calls, ) - errors_in_a_row = result["errors_in_a_row"] + errors_in_a_row = result.get("errors_in_a_row", errors_in_a_row) total_function_calls += result.get("function_call_count", 0) - if role := result["update_role"]: + if role := result.get("update_role"): yield ChatResponseUpdate( - contents=result["function_call_results"] or [], + contents=result.get("function_call_results") or [], role=role, ) - if result["action"] == "stop": + if result.get("action") == "stop": # Error threshold reached: submit collected function_call_output # items once more with tools disabled. mutable_options["tool_choice"] = "none" - elif result["action"] != "continue": + elif result.get("action") != "continue": return elif max_function_calls is not None and total_function_calls >= max_function_calls: # Best-effort limit: checked after each batch of parallel calls completes, @@ -2352,26 +2117,28 @@ class FunctionInvocationLayer(Generic[OptionsCoT]): # Make a final model call with tool_choice="none" so the model # produces a plain text answer instead of leaving orphaned # function_call items without matching results. - if response is not None and self.function_invocation_configuration["enabled"]: + if response is not None and self.function_invocation_configuration.get("enabled", True): logger.info( "Maximum iterations reached (%d). Requesting final response without tools.", - self.function_invocation_configuration["max_iterations"], + self.function_invocation_configuration.get("max_iterations", DEFAULT_MAX_ITERATIONS), ) mutable_options["tool_choice"] = "none" - inner_stream = await _ensure_response_stream( + final_inner_stream = cast( + ResponseStream[ChatResponseUpdate, ChatResponse[Any]], super_get_response( messages=prepped_messages, stream=True, options=mutable_options, **filtered_kwargs, - ) + ), ) - async for update in inner_stream: + await final_inner_stream + async for update in final_inner_stream: yield update # Finalize the inner stream to trigger its hooks - await inner_stream.get_final_response() + await final_inner_stream.get_final_response() - def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse: + def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse[Any]: # Note: stream_result_hooks are already run via inner stream's get_final_response() # We don't need to run them again here return ChatResponse.from_updates(updates, output_format_type=output_format_type) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index ee0e813d27..7ae9dbaa3d 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -17,12 +17,15 @@ from collections.abc import ( Mapping, MutableMapping, Sequence, + Sized, ) from copy import deepcopy from datetime import datetime +from inspect import isawaitable from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, NewType, cast, overload from pydantic import BaseModel +from typing_extensions import TypedDict from ._serialization import SerializationMixin from ._tools import ToolTypes @@ -33,10 +36,6 @@ if sys.version_info >= (3, 13): from typing import TypeVar # pragma: no cover else: from typing_extensions import TypeVar # pragma: no cover -if sys.version_info >= (3, 11): - from typing import TypedDict # type: ignore # pragma: no cover -else: - from typing_extensions import TypedDict # type: ignore # pragma: no cover logger = logging.getLogger("agent_framework") @@ -194,7 +193,7 @@ def _get_data_bytes_as_str(content: Content) -> str | None: return data # type: ignore[return-value, no-any-return] -def _get_data_bytes(content: Content) -> bytes | None: +def _get_data_bytes(content: Content) -> bytes | None: # pyright: ignore[reportUnusedFunction] """Extract and decode binary data from data URI. Args: @@ -270,9 +269,9 @@ def _serialize_value(value: Any, exclude_none: bool) -> Any: if isinstance(value, Content): return value.to_dict(exclude_none=exclude_none) if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): - return [_serialize_value(item, exclude_none) for item in value] + return [_serialize_value(item, exclude_none) for item in cast(Iterable[Any], value)] if isinstance(value, Mapping): - return {k: _serialize_value(v, exclude_none) for k, v in value.items()} + return {k: _serialize_value(v, exclude_none) for k, v in value.items()} # type: ignore[reportUnknownVariableType] if hasattr(value, "to_dict"): return value.to_dict() # type: ignore[call-arg] return value @@ -376,7 +375,7 @@ ContentT = TypeVar("ContentT", bound="Content") # endregion -class UsageDetails(TypedDict, total=False): +class UsageDetails(TypedDict, total=False, extra_items=int): # type: ignore[call-arg] """A dictionary representing usage details. This is a non-closed dictionary, so any specific provider fields can be added as needed. @@ -397,6 +396,9 @@ class UsageDetails(TypedDict, total=False): def add_usage_details(usage1: UsageDetails | None, usage2: UsageDetails | None) -> UsageDetails: """Add two UsageDetails dictionaries by summing all numeric values. + If any of the two usage details contains a key with a non-int value, it will be skipped, + even if the other contains a int-value on that key. + Args: usage1: First usage details dictionary. usage2: Second usage details dictionary. @@ -420,22 +422,15 @@ def add_usage_details(usage1: UsageDetails | None, usage2: UsageDetails | None) return usage1 result = UsageDetails() - # Combine all keys from both dictionaries all_keys = set(usage1.keys()) | set(usage2.keys()) - for key in all_keys: - val1 = usage1.get(key) - val2 = usage2.get(key) - - # Sum if both present, otherwise use the non-None value - if val1 is not None and val2 is not None: - result[key] = val1 + val2 # type: ignore[literal-required, operator] - elif val1 is not None: - result[key] = val1 # type: ignore[literal-required] - elif val2 is not None: - result[key] = val2 # type: ignore[literal-required] - + if not isinstance((val1 := usage1.get(key, 0)), (int | None)) or not isinstance( + (val2 := usage2.get(key, 0)), (int | None) + ): + logger.warning("Non `int` value found in usage details, skipping.") + continue + result[key] = (val1 or 0) + (val2 or 0) # type: ignore[literal-required] return result @@ -465,7 +460,7 @@ class Content: error_code: str | None = None, error_details: str | None = None, # Usage content fields - usage_details: dict[str, Any] | UsageDetails | None = None, + usage_details: UsageDetails | None = None, # Function call/result fields call_id: str | None = None, name: str | None = None, @@ -1264,19 +1259,14 @@ class Content: return cls.from_data(remaining["data"], remaining["media_type"]) # Handle nested Content objects (e.g., function_call in function_approval_request) - if "function_call" in remaining and isinstance(remaining["function_call"], dict): - remaining["function_call"] = cls.from_dict(remaining["function_call"]) + if (function_call := remaining.get("function_call")) and isinstance(function_call, dict): + remaining["function_call"] = cls.from_dict(function_call) # type: ignore[reportUnknownArgumentType] # Handle list of Content objects (e.g., inputs in code_interpreter_tool_call) - if "inputs" in remaining and isinstance(remaining["inputs"], list): - remaining["inputs"] = [ - cls.from_dict(item) if isinstance(item, dict) else item for item in remaining["inputs"] - ] - - if "outputs" in remaining and isinstance(remaining["outputs"], list): - remaining["outputs"] = [ - cls.from_dict(item) if isinstance(item, dict) else item for item in remaining["outputs"] - ] + if (input_items := remaining.get("inputs")) and isinstance(input_items, list): + remaining["inputs"] = [cls.from_dict(item) if isinstance(item, dict) else item for item in input_items] # type: ignore[reportUnknownVariableType] + if (output_items := remaining.get("outputs")) and isinstance(output_items, list): + remaining["outputs"] = [cls.from_dict(item) if isinstance(item, dict) else item for item in output_items] # type: ignore[reportUnknownVariableType] return cls( type=content_type, @@ -1306,55 +1296,16 @@ class Content: def _add_text_content(self, other: Content) -> Content: """Add two TextContent instances.""" - # Merge raw representations - if self.raw_representation is None: - raw_representation = other.raw_representation - elif other.raw_representation is None: - raw_representation = self.raw_representation - else: - raw_representation = ( - self.raw_representation if isinstance(self.raw_representation, list) else [self.raw_representation] - ) + (other.raw_representation if isinstance(other.raw_representation, list) else [other.raw_representation]) - - # Merge annotations - if self.annotations is None: - annotations = other.annotations - elif other.annotations is None: - annotations = self.annotations - else: - annotations = self.annotations + other.annotations # type: ignore[operator] - return Content( "text", text=self.text + other.text, # type: ignore[attr-defined, operator] - annotations=annotations, - additional_properties={ - **(other.additional_properties or {}), - **(self.additional_properties or {}), - }, - raw_representation=raw_representation, + annotations=_combine_annotations(self.annotations, other.annotations), + additional_properties=_combine_additional_props(self.additional_properties, other.additional_properties), + raw_representation=_combine_raw_representations(self.raw_representation, other.raw_representation), ) def _add_text_reasoning_content(self, other: Content) -> Content: """Add two TextReasoningContent instances.""" - # Merge raw representations - if self.raw_representation is None: - raw_representation = other.raw_representation - elif other.raw_representation is None: - raw_representation = self.raw_representation - else: - raw_representation = ( - self.raw_representation if isinstance(self.raw_representation, list) else [self.raw_representation] - ) + (other.raw_representation if isinstance(other.raw_representation, list) else [other.raw_representation]) - - # Merge annotations - if self.annotations is None: - annotations = other.annotations - elif other.annotations is None: - annotations = self.annotations - else: - annotations = self.annotations + other.annotations # type: ignore[operator] - # Concatenate text, handling None values self_text = self.text or "" # type: ignore[attr-defined] other_text = other.text or "" # type: ignore[attr-defined] @@ -1367,12 +1318,9 @@ class Content: "text_reasoning", text=combined_text, protected_data=protected_data, - annotations=annotations, - additional_properties={ - **(other.additional_properties or {}), - **(self.additional_properties or {}), - }, - raw_representation=raw_representation, + annotations=_combine_annotations(self.annotations, other.annotations), + additional_properties=_combine_additional_props(self.additional_properties, other.additional_properties), + raw_representation=_combine_raw_representations(self.raw_representation, other.raw_representation), ) def _add_function_call_content(self, other: Content) -> Content: @@ -1396,64 +1344,23 @@ class Content: else: raise TypeError("Incompatible argument types") - # Merge raw representations - if self.raw_representation is None: - raw_representation: Any = other.raw_representation - elif other.raw_representation is None: - raw_representation = self.raw_representation - else: - raw_representation = ( - self.raw_representation if isinstance(self.raw_representation, list) else [self.raw_representation] - ) + (other.raw_representation if isinstance(other.raw_representation, list) else [other.raw_representation]) - return Content( "function_call", call_id=self_call_id, name=getattr(self, "name", getattr(other, "name", None)), arguments=arguments, exception=getattr(self, "exception", None) or getattr(other, "exception", None), - additional_properties={ - **(self.additional_properties or {}), - **(other.additional_properties or {}), - }, - raw_representation=raw_representation, + additional_properties=_combine_additional_props(self.additional_properties, other.additional_properties), + raw_representation=_combine_raw_representations(self.raw_representation, other.raw_representation), ) def _add_usage_content(self, other: Content) -> Content: """Add two UsageContent instances by combining their usage details.""" - self_details = getattr(self, "usage_details", {}) - other_details = getattr(other, "usage_details", {}) - - # Combine token counts - combined_details: dict[str, Any] = {} - for key in set(list(self_details.keys()) + list(other_details.keys())): - self_val = self_details.get(key) - other_val = other_details.get(key) - if isinstance(self_val, int) and isinstance(other_val, int): - combined_details[key] = self_val + other_val - elif self_val is not None: - combined_details[key] = self_val - elif other_val is not None: - combined_details[key] = other_val - - # Merge raw representations - if self.raw_representation is None: - raw_representation = other.raw_representation - elif other.raw_representation is None: - raw_representation = self.raw_representation - else: - raw_representation = ( - self.raw_representation if isinstance(self.raw_representation, list) else [self.raw_representation] - ) + (other.raw_representation if isinstance(other.raw_representation, list) else [other.raw_representation]) - return Content( "usage", - usage_details=combined_details, - additional_properties={ - **(self.additional_properties or {}), - **(other.additional_properties or {}), - }, - raw_representation=raw_representation, + usage_details=add_usage_details(self.usage_details, other.usage_details), + additional_properties=_combine_additional_props(self.additional_properties, other.additional_properties), + raw_representation=_combine_raw_representations(self.raw_representation, other.raw_representation), ) def has_top_level_media_type(self, top_level_media_type: Literal["application", "audio", "image", "text"]) -> bool: @@ -1530,6 +1437,42 @@ class Content: return self.arguments # type: ignore[return-value] +def _combine_additional_props( + self_additional_properties: dict[str, Any], other_additional_properties: dict[str, Any] +) -> dict[str, Any]: + """Combine additional properties for addition operations.""" + return { + **other_additional_properties, + **self_additional_properties, + } + + +def _combine_raw_representations( + self_repr: Any, + other_repr: Any, +) -> Any: + """Combine raw representations for addition operations.""" + if self_repr is None: + return other_repr + if other_repr is None: + return self_repr + self_list = self_repr if isinstance(self_repr, list) else [self_repr] # type: ignore[reportUnknownVariableType] + other_list = other_repr if isinstance(other_repr, list) else [other_repr] # type: ignore[reportUnknownVariableType] + return self_list + other_list # type: ignore[reportUnknownVariableType] + + +def _combine_annotations( + self_annotations: Sequence[Annotation] | None, + other_annotations: Sequence[Annotation] | None, +) -> Sequence[Annotation] | None: + """Combine annotations for addition operations.""" + if self_annotations is None: + return other_annotations + if other_annotations is None: + return self_annotations + return [*self_annotations, *other_annotations] + + # endregion @@ -1665,10 +1608,6 @@ class Message(SerializationMixin): Additional properties are used within Agent Framework, they are not sent to services. raw_representation: Optional raw representation of the chat message. """ - # Handle role conversion from legacy dict format - if isinstance(role, dict) and "value" in role: - role = role["value"] - # Handle contents conversion parsed_contents = [] if contents is None else _parse_content_list(contents) @@ -1836,14 +1775,14 @@ def _process_update(response: ChatResponse | AgentResponse, update: ChatResponse if update.created_at is not None: response.created_at = update.created_at if update.additional_properties is not None: - if response.additional_properties is None: - response.additional_properties = {} response.additional_properties.update(update.additional_properties) if response.raw_representation is None: response.raw_representation = [] if not isinstance(response.raw_representation, list): response.raw_representation = [response.raw_representation] - response.raw_representation.append(update.raw_representation) + raw_representation_value = cast(Any, getattr(response, "raw_representation", None)) + raw_representation_list = cast(list[Any], raw_representation_value) + raw_representation_list.append(update.raw_representation) if isinstance(response, ChatResponse) and isinstance(update, ChatResponseUpdate): if update.conversation_id is not None: response.conversation_id = update.conversation_id @@ -2026,9 +1965,6 @@ class ChatResponse(SerializationMixin, Generic[ResponseModelT]): self.conversation_id = conversation_id self.model_id = model_id self.created_at = created_at - # Handle legacy dict format for finish_reason - if isinstance(finish_reason, dict) and "value" in finish_reason: - finish_reason = finish_reason["value"] self.finish_reason = finish_reason self.usage_details = usage_details self._value: ResponseModelT | None = value @@ -2620,10 +2556,6 @@ class AgentResponseUpdate(SerializationMixin): processed_contents.append(c) self.contents = processed_contents - # Handle legacy dict format for role - if isinstance(role, dict) and "value" in role: - role = role["value"] - self.role: str | None = role self.author_name = author_name self.agent_id = agent_id @@ -2717,7 +2649,7 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): self._inner_stream: ResponseStream[Any, Any] | None = None self._inner_stream_source: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]] | None = None self._wrap_inner: bool = False - self._map_update: Callable[[Any], Any | Awaitable[Any]] | None = None + self._map_update: Callable[[Any], UpdateT | Awaitable[UpdateT]] | None = None def map( self, @@ -2757,11 +2689,11 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): ... AgentResponse.from_updates, ... ) """ - stream: ResponseStream[Any, Any] = ResponseStream(self, finalizer=finalizer) + stream: ResponseStream[OuterUpdateT, OuterFinalT] = ResponseStream(self, finalizer=finalizer) stream._inner_stream_source = self stream._wrap_inner = True stream._map_update = transform - return stream # type: ignore[return-value] + return stream def with_finalizer( self, @@ -2785,10 +2717,10 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): Example: >>> stream.with_finalizer(AgentResponse.from_updates) """ - stream: ResponseStream[Any, Any] = ResponseStream(self, finalizer=finalizer) + stream: ResponseStream[UpdateT, OuterFinalT] = ResponseStream(self, finalizer=finalizer) stream._inner_stream_source = self stream._wrap_inner = True - return stream # type: ignore[return-value] + return stream @classmethod def from_awaitable( @@ -2813,10 +2745,10 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): >>> async def get_stream() -> ResponseStream[Update, Response]: ... >>> stream = ResponseStream.from_awaitable(get_stream()) """ - stream: ResponseStream[Any, Any] = cls(awaitable) # type: ignore[arg-type] - stream._inner_stream_source = awaitable # type: ignore[assignment] + stream: ResponseStream[UpdateT, FinalT] = cls(cast(Awaitable[AsyncIterable[UpdateT]], awaitable)) + stream._inner_stream_source = awaitable stream._wrap_inner = True - return stream # type: ignore[return-value] + return stream async def _get_stream(self) -> AsyncIterable[UpdateT]: if self._stream is None: @@ -2826,10 +2758,10 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): if not iscoroutine(self._stream_source): self._stream = self._stream_source # type: ignore[assignment] else: - self._stream = await self._stream_source # type: ignore[assignment] + self._stream = await self._stream_source if isinstance(self._stream, ResponseStream) and self._wrap_inner: - self._inner_stream = self._stream - return self._stream + self._inner_stream = self._stream # type: ignore[assignment] + return self._inner_stream return self._stream # type: ignore[return-value] def __aiter__(self) -> ResponseStream[UpdateT, FinalT]: @@ -2840,7 +2772,7 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): stream = await self._get_stream() self._iterator = stream.__aiter__() try: - update = await self._iterator.__anext__() + update: UpdateT = await self._iterator.__anext__() except StopAsyncIteration: self._consumed = True await self._run_cleanup_hooks() @@ -2849,18 +2781,16 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): await self._run_cleanup_hooks() raise if self._map_update is not None: - mapped = self._map_update(update) - if isinstance(mapped, Awaitable): - update = await mapped - else: - update = mapped # type: ignore[assignment] + update = self._map_update(update) # type: ignore[assignment] + if isawaitable(update): + update = await update self._updates.append(update) for hook in self._transform_hooks: hooked = hook(update) - if isinstance(hooked, Awaitable): - update = await hooked - elif hooked is not None: - update = hooked # type: ignore[assignment] + if isawaitable(hooked): + hooked = await hooked + if hooked is not None: + update = hooked return update def __await__(self) -> Any: @@ -2903,58 +2833,71 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): # First, finalize the inner stream and run its result hooks # This ensures inner post-processing (e.g., context provider notifications) runs - if self._inner_stream._finalizer is not None: - inner_result: Any = self._inner_stream._finalizer(self._inner_stream._updates) - if isinstance(inner_result, Awaitable): + inner_stream = self._inner_stream + inner_result: Any + if inner_stream._finalizer is not None: + inner_finalizer = inner_stream._finalizer + inner_result = inner_finalizer(inner_stream._updates) + if isawaitable(inner_result): inner_result = await inner_result else: - inner_result = self._inner_stream._updates + inner_result = list(inner_stream._updates) + # Run inner stream's result hooks - for hook in self._inner_stream._result_hooks: - hooked = hook(inner_result) - if isinstance(hooked, Awaitable): - hooked = await hooked - if hooked is not None: - inner_result = hooked - self._inner_stream._final_result = inner_result - self._inner_stream._finalized = True + inner_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], inner_stream._result_hooks) + for hook in inner_hooks: + hooked_result = hook(inner_result) + if isawaitable(hooked_result): + hooked_result = await hooked_result + if hooked_result is not None: + inner_result = hooked_result + inner_stream._final_result = inner_result + inner_stream._finalized = True # Now finalize the outer stream with its own finalizer # If outer has no finalizer, use inner's result (preserves from_awaitable behavior) + outer_result: Any if self._finalizer is not None: - result: Any = self._finalizer(self._updates) - if isinstance(result, Awaitable): - result = await result + outer_result = self._finalizer(self._updates) + if isawaitable(outer_result): + outer_result = await outer_result else: # No outer finalizer - use inner's finalized result - result = inner_result + outer_result = inner_result + # Apply outer's result_hooks - for hook in self._result_hooks: - hooked = hook(result) - if isinstance(hooked, Awaitable): - hooked = await hooked - if hooked is not None: - result = hooked - self._final_result = result + outer_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks) + for hook in outer_hooks: + outer_hook_result = hook(outer_result) + if isawaitable(outer_hook_result): + outer_hook_result = await outer_hook_result + if outer_hook_result is not None: + outer_result = outer_hook_result + self._final_result = outer_result self._finalized = True return self._final_result # type: ignore[return-value] + if not self._finalized: if not self._consumed: async for _ in self: pass + # Use finalizer if configured, otherwise return collected updates + result: Any if self._finalizer is not None: result = self._finalizer(self._updates) - if isinstance(result, Awaitable): + if isawaitable(result): result = await result else: - result = self._updates - for hook in self._result_hooks: - hooked = hook(result) - if isinstance(hooked, Awaitable): - hooked = await hooked - if hooked is not None: - result = hooked + result = list(self._updates) + + final_hooks = cast(list[Callable[[Any], Any | Awaitable[Any] | None]], self._result_hooks) + for hook in final_hooks: + final_hook_result = hook(result) + if isawaitable(final_hook_result): + final_hook_result = await final_hook_result + if final_hook_result is not None: + result = final_hook_result self._final_result = result self._finalized = True return self._final_result # type: ignore[return-value] @@ -2991,7 +2934,7 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]): self._cleanup_run = True for hook in self._cleanup_hooks: result = hook() - if isinstance(result, Awaitable): + if isawaitable(result): await result @property @@ -3302,9 +3245,9 @@ def merge_chat_options( # Copy base values (shallow copy for simple values, dict copy for dicts) for key, value in base.items(): if isinstance(value, dict): - result[key] = dict(value) + result[key] = dict(value) # type: ignore[reportUnknownArgumentType] elif isinstance(value, list): - result[key] = list(value) + result[key] = list(value) # type: ignore[reportUnknownArgumentType] else: result[key] = value @@ -3326,19 +3269,19 @@ def merge_chat_options( if base_tools and value: # Add tools that aren't already present merged_tools = list(base_tools) - for tool in value if isinstance(value, list) else [value]: + for tool in value if isinstance(value, Iterable) else [value]: # type: ignore[reportUnknownVariableType] if tool not in merged_tools: merged_tools.append(tool) result["tools"] = merged_tools elif value: - result["tools"] = list(value) if isinstance(value, list) else [value] + result["tools"] = value if isinstance(value, list) else [value] elif key in ("logit_bias", "metadata", "additional_properties"): # Merge dicts base_dict = result.get(key) - if base_dict and isinstance(value, dict): + if base_dict and isinstance(base_dict, dict) and isinstance(value, dict): result[key] = {**base_dict, **value} elif value: - result[key] = dict(value) if isinstance(value, dict) else value + result[key] = dict(cast(Mapping[Any, Any], value)) if isinstance(value, dict) else value elif key == "tool_choice": # tool_choice from override takes precedence result["tool_choice"] = value if value else result.get("tool_choice") @@ -3424,8 +3367,8 @@ class Embedding(Generic[EmbeddingT]): """ if self._dimensions is not None: return self._dimensions - if isinstance(self.vector, (list, tuple, bytes)): - return len(self.vector) + if isinstance(self.vector, Sized) and not isinstance(self.vector, str): + return len(cast(Sized, self.vector)) return None diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 3d8024a35e..ac2ebcf56f 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -450,9 +450,9 @@ class AgentExecutor(Executor): options: dict[str, Any] = {} if options_from_workflow is not None: if isinstance(options_from_workflow, Mapping): - for key, value in options_from_workflow.items(): - if isinstance(key, str): - options[key] = value + options_from_workflow_map = cast(Mapping[str, Any], options_from_workflow) + for key, value in options_from_workflow_map.items(): + options[key] = value else: logger.warning( "Ignoring non-mapping workflow 'options' kwarg of type %s for AgentExecutor %s.", @@ -461,16 +461,17 @@ class AgentExecutor(Executor): ) existing_additional_args = options.get("additional_function_arguments") + additional_args: dict[str, Any] if isinstance(existing_additional_args, Mapping): - additional_args = {key: value for key, value in existing_additional_args.items() if isinstance(key, str)} + existing_additional_args_map = cast(Mapping[str, Any], existing_additional_args) + additional_args = {key: value for key, value in existing_additional_args_map.items()} else: additional_args = {} if workflow_additional_args is not None: if isinstance(workflow_additional_args, Mapping): - additional_args.update({ - key: value for key, value in workflow_additional_args.items() if isinstance(key, str) - }) + workflow_additional_args_map = cast(Mapping[str, Any], workflow_additional_args) + additional_args.update({key: value for key, value in workflow_additional_args_map.items()}) else: logger.warning( "Ignoring non-mapping workflow 'additional_function_arguments' kwarg of type %s for AgentExecutor %s.", # noqa: E501 diff --git a/python/packages/core/agent_framework/_workflows/_function_executor.py b/python/packages/core/agent_framework/_workflows/_function_executor.py index a27e250690..326145b6c4 100644 --- a/python/packages/core/agent_framework/_workflows/_function_executor.py +++ b/python/packages/core/agent_framework/_workflows/_function_executor.py @@ -119,7 +119,7 @@ class FunctionExecutor(Executor): # Determine if function has WorkflowContext parameter self._has_context = ctx_annotation is not None # Determine if the function is an async function - self._is_async = asyncio.iscoroutinefunction(func) + self._is_async = inspect.iscoroutinefunction(func) # Initialize parent WITHOUT calling _discover_handlers yet # We'll manually set up the attributes first diff --git a/python/packages/core/agent_framework/_workflows/_runner_context.py b/python/packages/core/agent_framework/_workflows/_runner_context.py index d52e135e91..e3711ea96f 100644 --- a/python/packages/core/agent_framework/_workflows/_runner_context.py +++ b/python/packages/core/agent_framework/_workflows/_runner_context.py @@ -99,11 +99,11 @@ class RunnerContext(Protocol): If checkpoint storage is not configured, checkpoint methods may raise. """ - async def send_message(self, WorkflowMessage: WorkflowMessage) -> None: + async def send_message(self, message: WorkflowMessage) -> None: """Send a WorkflowMessage from the executor to the context. Args: - WorkflowMessage: The WorkflowMessage to be sent. + message: The WorkflowMessage to be sent. """ ... @@ -288,9 +288,9 @@ class InProcRunnerContext: self._streaming: bool = False # region Messaging and Events - async def send_message(self, WorkflowMessage: WorkflowMessage) -> None: - self._messages.setdefault(WorkflowMessage.source_id, []) - self._messages[WorkflowMessage.source_id].append(WorkflowMessage) + async def send_message(self, message: WorkflowMessage) -> None: + self._messages.setdefault(message.source_id, []) + self._messages[message.source_id].append(message) async def drain_messages(self) -> dict[str, list[WorkflowMessage]]: messages = copy(self._messages) diff --git a/python/packages/core/agent_framework/_workflows/_typing_utils.py b/python/packages/core/agent_framework/_workflows/_typing_utils.py index 41ed071f0a..07b6d15bca 100644 --- a/python/packages/core/agent_framework/_workflows/_typing_utils.py +++ b/python/packages/core/agent_framework/_workflows/_typing_utils.py @@ -193,36 +193,40 @@ def try_coerce_to_type(data: Any, target_type: type | UnionType | Any) -> Any: Returns: The coerced value, or the original value if coercion fails. """ + original_data = data + # If already the right type, return as-is if is_instance_of(data, target_type): return data # Can't coerce to non-concrete targets (Union, generic, etc.) if not isinstance(target_type, type): - return data + return original_data + + target_cls: type[Any] = target_type # int -> float (JSON integers for float fields) - if isinstance(data, int) and target_type is float: + if isinstance(data, int) and target_cls is float: return float(data) - # dict -> dataclass + # dict -> dataclass or pydantic model if isinstance(data, dict): from dataclasses import is_dataclass - if is_dataclass(target_type): + if is_dataclass(target_cls): try: - return target_type(**data) + return target_cls(**data) except (TypeError, ValueError): - return data + return original_data - # dict -> Pydantic model - if hasattr(target_type, "model_validate"): + model_validate = getattr(target_cls, "model_validate", None) + if callable(model_validate): try: - return target_type.model_validate(data) + return model_validate(data) except Exception: - return data + return original_data - return data + return original_data def serialize_type(t: type) -> str: diff --git a/python/packages/core/agent_framework/azure/_assistants_client.py b/python/packages/core/agent_framework/azure/_assistants_client.py index 015a1dcc82..aae89d562d 100644 --- a/python/packages/core/agent_framework/azure/_assistants_client.py +++ b/python/packages/core/agent_framework/azure/_assistants_client.py @@ -12,7 +12,7 @@ from .._settings import load_settings from ..openai import OpenAIAssistantsClient from ..openai._assistants_client import OpenAIAssistantsOptions from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider -from ._shared import AzureOpenAISettings, _apply_azure_defaults +from ._shared import AzureOpenAISettings, _apply_azure_defaults # pyright: ignore[reportPrivateUsage] if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -145,43 +145,46 @@ class AzureOpenAIAssistantsClient( ) _apply_azure_defaults(azure_openai_settings, default_api_version=self.DEFAULT_AZURE_API_VERSION) - if not azure_openai_settings["chat_deployment_name"]: + chat_deployment_name = azure_openai_settings.get("chat_deployment_name") + if not chat_deployment_name: raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." ) + api_key_secret = azure_openai_settings.get("api_key") + token_scope = azure_openai_settings.get("token_endpoint") + # Resolve credential to token provider ad_token_provider = None - if not async_client and not azure_openai_settings["api_key"] and credential: - ad_token_provider = resolve_credential_to_token_provider( - credential, azure_openai_settings["token_endpoint"] - ) + if not async_client and not api_key_secret and credential: + ad_token_provider = resolve_credential_to_token_provider(credential, token_scope) - if not async_client and not azure_openai_settings["api_key"] and not ad_token_provider: + if not async_client and not api_key_secret and not ad_token_provider: raise ValueError("Please provide either api_key, credential, or a client.") # Create Azure client if not provided if not async_client: client_params: dict[str, Any] = { - "api_version": azure_openai_settings["api_version"], "default_headers": default_headers, } + if resolved_api_version := azure_openai_settings.get("api_version"): + client_params["api_version"] = resolved_api_version - if azure_openai_settings["api_key"]: - client_params["api_key"] = azure_openai_settings["api_key"].get_secret_value() + if api_key_secret: + client_params["api_key"] = api_key_secret.get_secret_value() elif ad_token_provider: client_params["azure_ad_token_provider"] = ad_token_provider - if azure_openai_settings["base_url"]: - client_params["base_url"] = str(azure_openai_settings["base_url"]) - elif azure_openai_settings["endpoint"]: - client_params["azure_endpoint"] = str(azure_openai_settings["endpoint"]) + if resolved_base_url := azure_openai_settings.get("base_url"): + client_params["base_url"] = str(resolved_base_url) + elif resolved_endpoint := azure_openai_settings.get("endpoint"): + client_params["azure_endpoint"] = str(resolved_endpoint) async_client = AsyncAzureOpenAI(**client_params) super().__init__( - model_id=azure_openai_settings["chat_deployment_name"], + model_id=chat_deployment_name, assistant_id=assistant_id, assistant_name=assistant_name, assistant_description=assistant_description, diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index b4bd3659ed..b57abd6faf 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -6,7 +6,7 @@ import json import logging import sys from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Generic +from typing import TYPE_CHECKING, Any, Generic, cast from openai.lib.azure import AsyncAzureOpenAI from openai.types.chat.chat_completion import Choice @@ -31,7 +31,7 @@ from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider from ._shared import ( AzureOpenAIConfigMixin, AzureOpenAISettings, - _apply_azure_defaults, + _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] ) if sys.version_info >= (3, 13): @@ -260,19 +260,26 @@ class AzureOpenAIChatClient( # type: ignore[misc] ) _apply_azure_defaults(azure_openai_settings) - if not azure_openai_settings["chat_deployment_name"]: + chat_deployment_name = azure_openai_settings.get("chat_deployment_name") + if not chat_deployment_name: raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable." ) + endpoint_value = azure_openai_settings.get("endpoint") + base_url_value = azure_openai_settings.get("base_url") + api_version_value = cast(str, azure_openai_settings.get("api_version")) + api_key_value = azure_openai_settings.get("api_key") + token_endpoint_value = azure_openai_settings.get("token_endpoint") + super().__init__( - deployment_name=azure_openai_settings["chat_deployment_name"], - endpoint=azure_openai_settings["endpoint"], - base_url=azure_openai_settings["base_url"], - api_version=azure_openai_settings["api_version"], # type: ignore - api_key=azure_openai_settings["api_key"].get_secret_value() if azure_openai_settings["api_key"] else None, - token_endpoint=azure_openai_settings["token_endpoint"], + deployment_name=chat_deployment_name, + endpoint=endpoint_value, + base_url=base_url_value, + api_version=api_version_value, + api_key=api_key_value.get_secret_value() if api_key_value else None, + token_endpoint=token_endpoint_value, credential=credential, default_headers=default_headers, client=async_client, @@ -302,24 +309,29 @@ class AzureOpenAIChatClient( # type: ignore[misc] if not message.model_extra or "context" not in message.model_extra: return text_content - context: dict[str, Any] | str = message.context # type: ignore[assignment, union-attr] - if isinstance(context, str): + context_raw: object = cast(object, message.context) # type: ignore[union-attr] + if isinstance(context_raw, str): try: - context = json.loads(context) + context_raw = json.loads(context_raw) except json.JSONDecodeError: logger.warning("Context is not a valid JSON string, ignoring context.") return text_content - if not isinstance(context, dict): + if not isinstance(context_raw, dict): logger.warning("Context is not a valid dictionary, ignoring context.") return text_content + context = cast(dict[str, Any], context_raw) # `all_retrieved_documents` is currently not used, but can be retrieved # through the raw_representation in the text content. if intent := context.get("intent"): text_content.additional_properties = {"intent": intent} - if citations := context.get("citations"): - text_content.annotations = [] - for citation in citations: - text_content.annotations.append( + citations = context.get("citations") + if isinstance(citations, list) and citations: + annotations: list[Annotation] = [] + for citation_raw in cast(list[object], citations): + if not isinstance(citation_raw, dict): + continue + citation = cast(dict[str, Any], citation_raw) + annotations.append( Annotation( type="citation", title=citation.get("title", ""), @@ -331,4 +343,5 @@ class AzureOpenAIChatClient( # type: ignore[misc] raw_representation=citation, ) ) + text_content.annotations = annotations return text_content diff --git a/python/packages/core/agent_framework/azure/_embedding_client.py b/python/packages/core/agent_framework/azure/_embedding_client.py index 13455e78a4..7003a4611f 100644 --- a/python/packages/core/agent_framework/azure/_embedding_client.py +++ b/python/packages/core/agent_framework/azure/_embedding_client.py @@ -17,7 +17,7 @@ from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider from ._shared import ( AzureOpenAIConfigMixin, AzureOpenAISettings, - _apply_azure_defaults, + _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] ) if sys.version_info >= (3, 13): @@ -118,19 +118,22 @@ class AzureOpenAIEmbeddingClient( ) _apply_azure_defaults(azure_openai_settings) - if not azure_openai_settings.get("embedding_deployment_name"): + embedding_deployment_name = azure_openai_settings.get("embedding_deployment_name") + if not embedding_deployment_name: raise ValueError( "Azure OpenAI embedding deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' environment variable." ) + api_key_secret = azure_openai_settings.get("api_key") + super().__init__( - deployment_name=azure_openai_settings["embedding_deployment_name"], # type: ignore[arg-type] - endpoint=azure_openai_settings["endpoint"], - base_url=azure_openai_settings["base_url"], - api_version=azure_openai_settings["api_version"], # type: ignore - api_key=azure_openai_settings["api_key"].get_secret_value() if azure_openai_settings["api_key"] else None, - token_endpoint=azure_openai_settings["token_endpoint"], + deployment_name=embedding_deployment_name, + endpoint=azure_openai_settings.get("endpoint"), + base_url=azure_openai_settings.get("base_url"), + api_version=azure_openai_settings.get("api_version") or "", + api_key=api_key_secret.get_secret_value() if api_key_secret else None, + token_endpoint=azure_openai_settings.get("token_endpoint"), credential=credential, default_headers=default_headers, client=async_client, diff --git a/python/packages/core/agent_framework/azure/_responses_client.py b/python/packages/core/agent_framework/azure/_responses_client.py index 2debbd7b21..a420108ce0 100644 --- a/python/packages/core/agent_framework/azure/_responses_client.py +++ b/python/packages/core/agent_framework/azure/_responses_client.py @@ -20,7 +20,7 @@ from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider from ._shared import ( AzureOpenAIConfigMixin, AzureOpenAISettings, - _apply_azure_defaults, + _apply_azure_defaults, # pyright: ignore[reportPrivateUsage] ) if sys.version_info >= (3, 13): @@ -207,27 +207,31 @@ class AzureOpenAIResponsesClient( # type: ignore[misc] # TODO(peterychang): This is a temporary hack to ensure that the base_url is set correctly # while this feature is in preview. # But we should only do this if we're on azure. Private deployments may not need this. + endpoint_value = azure_openai_settings.get("endpoint") if ( not azure_openai_settings.get("base_url") - and azure_openai_settings.get("endpoint") - and (hostname := urlparse(str(azure_openai_settings["endpoint"])).hostname) + and endpoint_value + and (hostname := urlparse(str(endpoint_value)).hostname) and hostname.endswith(".openai.azure.com") ): - azure_openai_settings["base_url"] = urljoin(str(azure_openai_settings["endpoint"]), "/openai/v1/") + azure_openai_settings["base_url"] = urljoin(str(endpoint_value), "/openai/v1/") - if not azure_openai_settings["responses_deployment_name"]: + responses_deployment_name = azure_openai_settings.get("responses_deployment_name") + if not responses_deployment_name: raise ValueError( "Azure OpenAI deployment name is required. Set via 'deployment_name' parameter " "or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable." ) + api_key_secret = azure_openai_settings.get("api_key") + super().__init__( - deployment_name=azure_openai_settings["responses_deployment_name"], - endpoint=azure_openai_settings["endpoint"], - base_url=azure_openai_settings["base_url"], - api_version=azure_openai_settings["api_version"], # type: ignore - api_key=azure_openai_settings["api_key"].get_secret_value() if azure_openai_settings["api_key"] else None, - token_endpoint=azure_openai_settings["token_endpoint"], + deployment_name=responses_deployment_name, + endpoint=azure_openai_settings.get("endpoint"), + base_url=azure_openai_settings.get("base_url"), + api_version=azure_openai_settings.get("api_version") or "", + api_key=api_key_secret.get_secret_value() if api_key_secret else None, + token_endpoint=azure_openai_settings.get("token_endpoint"), credential=credential, default_headers=default_headers, client=async_client, diff --git a/python/packages/core/agent_framework/azure/_shared.py b/python/packages/core/agent_framework/azure/_shared.py index dce116a242..5e06fbbe74 100644 --- a/python/packages/core/agent_framework/azure/_shared.py +++ b/python/packages/core/agent_framework/azure/_shared.py @@ -123,6 +123,9 @@ def _apply_azure_defaults( settings["token_endpoint"] = default_token_endpoint +_AZURE_DEFAULTS_APPLIER = _apply_azure_defaults + + class AzureOpenAIConfigMixin(OpenAIBase): """Internal class for configuring a connection to an Azure OpenAI service.""" diff --git a/python/packages/core/agent_framework/declarative/__init__.pyi b/python/packages/core/agent_framework/declarative/__init__.pyi index 214bb132ab..92da0da682 100644 --- a/python/packages/core/agent_framework/declarative/__init__.pyi +++ b/python/packages/core/agent_framework/declarative/__init__.pyi @@ -4,7 +4,6 @@ from agent_framework_declarative import ( AgentExternalInputRequest, AgentExternalInputResponse, AgentFactory, - AgentInvocationError, DeclarativeLoaderError, DeclarativeWorkflowError, ExternalInputRequest, @@ -19,7 +18,6 @@ __all__ = [ "AgentExternalInputRequest", "AgentExternalInputResponse", "AgentFactory", - "AgentInvocationError", "DeclarativeLoaderError", "DeclarativeWorkflowError", "ExternalInputRequest", diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 9a60053068..a595582b33 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -22,7 +22,7 @@ import weakref from collections.abc import Awaitable, Callable, Generator, Mapping, Sequence from enum import Enum from time import perf_counter, time_ns -from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, TypedDict, overload +from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, TypedDict, cast, overload from dotenv import load_dotenv from opentelemetry import metrics, trace @@ -199,6 +199,7 @@ class OtelAttr(str, Enum): T_TYPE_INPUT = "input" T_TYPE_OUTPUT = "output" DURATION_UNIT = "s" + # Agent attributes AGENT_NAME = "gen_ai.agent.name" AGENT_DESCRIPTION = "gen_ai.agent.description" @@ -894,7 +895,6 @@ def get_meter( return metrics.get_meter(name=name, version=version, schema_url=schema_url) -global OBSERVABILITY_SETTINGS OBSERVABILITY_SETTINGS: ObservabilitySettings = ObservabilitySettings() @@ -1053,7 +1053,15 @@ def configure_otel_providers( if vs_code_extension_port is not None: settings_kwargs["vs_code_extension_port"] = vs_code_extension_port - OBSERVABILITY_SETTINGS = ObservabilitySettings(**settings_kwargs) + updated_settings = ObservabilitySettings(**settings_kwargs) + OBSERVABILITY_SETTINGS.enable_instrumentation = updated_settings.enable_instrumentation + OBSERVABILITY_SETTINGS.enable_sensitive_data = updated_settings.enable_sensitive_data + OBSERVABILITY_SETTINGS.enable_console_exporters = updated_settings.enable_console_exporters + OBSERVABILITY_SETTINGS.vs_code_extension_port = updated_settings.vs_code_extension_port + OBSERVABILITY_SETTINGS.env_file_path = updated_settings.env_file_path + OBSERVABILITY_SETTINGS.env_file_encoding = updated_settings.env_file_encoding + OBSERVABILITY_SETTINGS._resource = updated_settings._resource # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS._executed_setup = False # type: ignore[reportPrivateUsage] else: # Update the observability settings with the provided values OBSERVABILITY_SETTINGS.enable_instrumentation = True @@ -1146,6 +1154,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): **kwargs: Any, ) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: """Trace chat responses with OpenTelemetry spans and metrics.""" + from ._types import ChatResponse, ChatResponseUpdate, ResponseStream # type: ignore[reportUnusedImport] + global OBSERVABILITY_SETTINGS super_get_response = super().get_response # type: ignore[misc] @@ -1153,7 +1163,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): return super_get_response(messages=messages, stream=stream, options=options, **kwargs) # type: ignore[no-any-return] opts: dict[str, Any] = options or {} # type: ignore[assignment] - provider_name = str(self.otel_provider_name) + provider_name = str(getattr(self, "otel_provider_name", "unknown")) model_id = kwargs.get("model_id") or opts.get("model_id") or getattr(self, "model_id", None) or "unknown" service_url_func = getattr(self, "service_url", None) service_url = str(service_url_func() if callable(service_url_func) else "unknown") @@ -1166,15 +1176,10 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): ) if stream: - from ._types import ResponseStream - - stream_result = super_get_response(messages=messages, stream=True, options=opts, **kwargs) - if isinstance(stream_result, ResponseStream): - result_stream = stream_result - elif isinstance(stream_result, Awaitable): - result_stream = ResponseStream.from_awaitable(stream_result) - else: - raise RuntimeError("Streaming telemetry requires a ResponseStream result.") + result_stream = cast( + ResponseStream[ChatResponseUpdate, ChatResponse[Any]], + super_get_response(messages=messages, stream=True, options=opts, **kwargs), + ) # Create span directly without trace.use_span() context attachment. # Streaming spans are closed asynchronously in cleanup hooks, which run @@ -1209,14 +1214,14 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): from ._types import ChatResponse try: - response = await result_stream.get_final_response() + response: ChatResponse[Any] = await result_stream.get_final_response() duration = duration_state.get("duration") response_attributes = _get_response_attributes(attributes, response) _capture_response( span=span, attributes=response_attributes, - token_usage_histogram=self.token_usage_histogram, - operation_duration_histogram=self.duration_histogram, + token_usage_histogram=getattr(self, "token_usage_histogram", None), + operation_duration_histogram=getattr(self, "duration_histogram", None), duration=duration, ) if ( @@ -1238,7 +1243,9 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): # Register a weak reference callback to close the span if stream is garbage collected # without being consumed. This ensures spans don't leak if users don't consume streams. - wrapped_stream = result_stream.with_cleanup_hook(_record_duration).with_cleanup_hook(_finalize_stream) + wrapped_stream: ResponseStream[ChatResponseUpdate, ChatResponse[Any]] = result_stream.with_cleanup_hook( + _record_duration + ).with_cleanup_hook(_finalize_stream) weakref.finalize(wrapped_stream, _close_span) return wrapped_stream @@ -1253,7 +1260,15 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): ) start_time_stamp = perf_counter() try: - response = await super_get_response(messages=messages, stream=False, options=opts, **kwargs) + response = cast( + ChatResponse[Any], + await super_get_response( + messages=messages, + stream=False, + options=opts, + **kwargs, + ), + ) except Exception as exception: capture_exception(span=span, exception=exception, timestamp=time_ns()) raise @@ -1262,16 +1277,20 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): _capture_response( span=span, attributes=response_attributes, - token_usage_histogram=self.token_usage_histogram, - operation_duration_histogram=self.duration_histogram, + token_usage_histogram=getattr(self, "token_usage_histogram", None), + operation_duration_histogram=getattr(self, "duration_histogram", None), duration=duration, ) if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: + finish_reason = cast( + "FinishReason | None", + response.finish_reason if response.finish_reason in FINISH_REASON_MAP else None, + ) _capture_messages( span=span, provider_name=provider_name, messages=response.messages, - finish_reason=response.finish_reason, + finish_reason=finish_reason, output=True, ) return response # type: ignore[return-value,no-any-return] @@ -1302,8 +1321,10 @@ class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOpti values: Sequence[EmbeddingInputT], *, options: EmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[EmbeddingT]: + ) -> GeneratedEmbeddings[EmbeddingT, EmbeddingOptionsT]: """Trace embedding generation with OpenTelemetry spans and metrics.""" + from ._types import GeneratedEmbeddings # type: ignore[reportUnusedImport] + global OBSERVABILITY_SETTINGS super_get_embeddings = super().get_embeddings # type: ignore[misc] @@ -1311,7 +1332,7 @@ class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOpti return await super_get_embeddings(values, options=options) # type: ignore[no-any-return] opts: dict[str, Any] = options or {} # type: ignore[assignment] - provider_name = str(self.otel_provider_name) + provider_name = str(getattr(self, "otel_provider_name", "unknown")) model_id = opts.get("model_id") or getattr(self, "model_id", None) or "unknown" service_url_func = getattr(self, "service_url", None) service_url = str(service_url_func() if callable(service_url_func) else "unknown") @@ -1325,14 +1346,18 @@ class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOpti with _get_span(attributes=attributes, span_name_attribute=OtelAttr.REQUEST_MODEL) as span: start_time_stamp = perf_counter() try: - result = await super_get_embeddings(values, options=options) + result = cast( + GeneratedEmbeddings[EmbeddingT, EmbeddingOptionsT], + await super_get_embeddings(values, options=options), + ) except Exception as exception: capture_exception(span=span, exception=exception, timestamp=time_ns()) raise duration = perf_counter() - start_time_stamp response_attributes: dict[str, Any] = {**attributes} - if result.usage and "prompt_tokens" in result.usage: - response_attributes[OtelAttr.INPUT_TOKENS] = result.usage["prompt_tokens"] + usage = result.usage or {} + if (input_tokens := usage.get("input_token_count")) is not None: + response_attributes[OtelAttr.INPUT_TOKENS] = input_tokens _capture_response( span=span, attributes=response_attributes, @@ -1391,7 +1416,12 @@ class AgentTelemetryLayer: ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """Trace agent runs with OpenTelemetry spans and metrics.""" global OBSERVABILITY_SETTINGS - super_run = super().run # type: ignore[misc] + from ._types import ResponseStream, merge_chat_options + + super_run = cast( + "Callable[..., Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]]", + super().run, # type: ignore[misc] + ) provider_name = str(self.otel_provider_name) capture_usage = bool(getattr(self, "_otel_capture_usage", True)) @@ -1403,8 +1433,6 @@ class AgentTelemetryLayer: **kwargs, ) - from ._types import ResponseStream, merge_chat_options - default_options = getattr(self, "default_options", {}) options = kwargs.get("options") merged_options: dict[str, Any] = merge_chat_options(default_options, options or {}) @@ -1420,16 +1448,16 @@ class AgentTelemetryLayer: ) if stream: - run_result = super_run( + run_result: object = super_run( messages=messages, stream=True, session=session, **kwargs, ) if isinstance(run_result, ResponseStream): - result_stream = run_result + result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType] elif isinstance(run_result, Awaitable): - result_stream = ResponseStream.from_awaitable(run_result) + result_stream = ResponseStream.from_awaitable(run_result) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] else: raise RuntimeError("Streaming telemetry requires a ResponseStream result.") @@ -1466,7 +1494,7 @@ class AgentTelemetryLayer: from ._types import AgentResponse try: - response = await result_stream.get_final_response() + response: AgentResponse[Any] = await result_stream.get_final_response() duration = duration_state.get("duration") response_attributes = _get_response_attributes( attributes, @@ -1492,7 +1520,9 @@ class AgentTelemetryLayer: # Register a weak reference callback to close the span if stream is garbage collected # without being consumed. This ensures spans don't leak if users don't consume streams. - wrapped_stream = result_stream.with_cleanup_hook(_record_duration).with_cleanup_hook(_finalize_stream) + wrapped_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = result_stream.with_cleanup_hook( + _record_duration + ).with_cleanup_hook(_finalize_stream) weakref.finalize(wrapped_stream, _close_span) return wrapped_stream @@ -1507,7 +1537,7 @@ class AgentTelemetryLayer: ) start_time_stamp = perf_counter() try: - response = await super_run( + response: AgentResponse[Any] = await super_run( messages=messages, stream=False, session=session, @@ -1598,12 +1628,17 @@ def _get_span( yield current_span -def _get_instructions_from_options(options: Any) -> str | None: +def _get_instructions_from_options(options: Any) -> str | list[str] | None: """Extract instructions from options dict.""" if options is None: return None - if isinstance(options, dict): - return options.get("instructions") + if isinstance(options, Mapping): + instructions = cast(Mapping[str, Any], options).get("instructions") + if isinstance(instructions, str): + return instructions + if isinstance(instructions, list) and all(isinstance(item, str) for item in instructions): # type: ignore[reportUnknownVariableType] + return instructions # type: ignore[reportUnknownVariableType] + return None return None @@ -1662,8 +1697,7 @@ def _get_span_attributes(**kwargs: Any) -> dict[str, Any]: """Get the span attributes from a kwargs dictionary.""" attributes: dict[str, Any] = {} options = kwargs.get("all_options", kwargs.get("options")) - if options is not None and not isinstance(options, dict): - options = None + options_mapping = cast(Mapping[str, Any], options) if isinstance(options, Mapping) else None for source_keys, (otel_key, transform_func, check_options, default_value) in OTEL_ATTR_MAP.items(): # Normalize to tuple of keys @@ -1671,8 +1705,8 @@ def _get_span_attributes(**kwargs: Any) -> dict[str, Any]: value = None for key in keys: - if check_options and options is not None: - value = options.get(key) + if check_options and options_mapping is not None: + value = options_mapping.get(key) if value is None: value = kwargs.get(key) if value is not None: @@ -1743,7 +1777,7 @@ def _to_otel_message(message: Message) -> dict[str, Any]: def _to_otel_part(content: Content) -> dict[str, Any] | None: """Create a otel representation of a Content.""" - from ._types import _get_data_bytes_as_str + from ._types import _get_data_bytes_as_str # pyright: ignore[reportPrivateUsage] match content.type: case "text": @@ -1798,10 +1832,12 @@ def _get_response_attributes( if model_id := getattr(response, "model_id", None): attributes[OtelAttr.RESPONSE_MODEL] = model_id if capture_usage and (usage := response.usage_details): - if usage.get("input_token_count"): - attributes[OtelAttr.INPUT_TOKENS] = usage["input_token_count"] - if usage.get("output_token_count"): - attributes[OtelAttr.OUTPUT_TOKENS] = usage["output_token_count"] + input_tokens = usage.get("input_token_count") + if input_tokens: + attributes[OtelAttr.INPUT_TOKENS] = input_tokens + output_tokens = usage.get("output_token_count") + if output_tokens: + attributes[OtelAttr.OUTPUT_TOKENS] = output_tokens return attributes diff --git a/python/packages/core/agent_framework/openai/_assistant_provider.py b/python/packages/core/agent_framework/openai/_assistant_provider.py index ecf27db316..9746725128 100644 --- a/python/packages/core/agent_framework/openai/_assistant_provider.py +++ b/python/packages/core/agent_framework/openai/_assistant_provider.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from collections.abc import Awaitable, Callable, MutableMapping, Sequence +from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, Generic, cast from openai import AsyncOpenAI @@ -149,24 +149,25 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): env_file_encoding=env_file_encoding, ) - if not settings["api_key"]: + api_key_setting = settings.get("api_key") + if not api_key_setting: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) # Get API key value - api_key_value: str | Callable[[], str | Awaitable[str]] | None - if isinstance(settings["api_key"], SecretString): - api_key_value = settings["api_key"].get_secret_value() + api_key_value: str | Callable[[], str | Awaitable[str]] + if isinstance(api_key_setting, SecretString): + api_key_value = api_key_setting.get_secret_value() else: - api_key_value = settings["api_key"] + api_key_value = api_key_setting # Create client client_args: dict[str, Any] = {"api_key": api_key_value} - if settings["org_id"]: - client_args["organization"] = settings["org_id"] - if settings["base_url"]: - client_args["base_url"] = settings["base_url"] + if org_id_value := settings.get("org_id"): + client_args["organization"] = org_id_value + if base_url_value := settings.get("base_url"): + client_args["base_url"] = base_url_value self._client = AsyncOpenAI(**client_args) @@ -250,7 +251,9 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): """ # Normalize tools normalized_tools = normalize_tools(tools) - assistant_tools = [tool for tool in normalized_tools if isinstance(tool, (FunctionTool, MutableMapping))] + assistant_tools: list[FunctionTool | MutableMapping[str, Any]] = [ + tool for tool in normalized_tools if isinstance(tool, (FunctionTool, MutableMapping)) + ] api_tools = to_assistant_tools(assistant_tools) if assistant_tools else [] # Extract response_format from default_options if present @@ -287,7 +290,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): if not self._client: raise RuntimeError("OpenAI client is not initialized.") - assistant = await self._client.beta.assistants.create(**create_params) + assistant = await self._client.beta.assistants.create(**create_params) # type: ignore[reportDeprecated] # Create Agent - pass default_options which contains response_format return self._create_chat_agent_from_assistant( @@ -353,7 +356,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): if not self._client: raise RuntimeError("OpenAI client is not initialized.") - assistant = await self._client.beta.assistants.retrieve(assistant_id) + assistant = await self._client.beta.assistants.retrieve(assistant_id) # type: ignore[reportDeprecated] # Use as_agent to wrap it return self.as_agent( @@ -466,12 +469,14 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]): for tool in normalized: if isinstance(tool, FunctionTool): provided_functions.add(tool.name) - elif isinstance(tool, MutableMapping) and "function" in tool: - func_spec = tool.get("function", {}) - if isinstance(func_spec, dict): - func_dict = cast(dict[str, Any], func_spec) - if "name" in func_dict: - provided_functions.add(str(func_dict["name"])) + elif isinstance(tool, Mapping): + typed_tool = cast(Mapping[str, Any], tool) + raw_func_spec = typed_tool.get("function") + if isinstance(raw_func_spec, Mapping): + typed_func_spec = cast(Mapping[str, Any], raw_func_spec) + raw_name = typed_func_spec.get("name") + if isinstance(raw_name, str) and raw_name: + provided_functions.add(raw_name) # Check for missing functions missing = required_functions - provided_functions diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 17b801a36a..b1d5e8795c 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -360,23 +360,26 @@ class OpenAIAssistantsClient( # type: ignore[misc] env_file_encoding=env_file_encoding, ) - if not async_client and not openai_settings["api_key"]: + api_key_value = openai_settings.get("api_key") + if not async_client and not api_key_value: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - if not openai_settings["chat_model_id"]: + + chat_model_id = openai_settings.get("chat_model_id") + if not chat_model_id: raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." ) super().__init__( - model_id=openai_settings["chat_model_id"], - api_key=self._get_api_key(openai_settings["api_key"]), - org_id=openai_settings["org_id"], + model_id=chat_model_id, + api_key=self._get_api_key(api_key_value), + org_id=openai_settings.get("org_id"), default_headers=default_headers, client=async_client, - base_url=openai_settings["base_url"], + base_url=openai_settings.get("base_url"), middleware=middleware, function_invocation_configuration=function_invocation_configuration, ) @@ -403,7 +406,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] """Clean up any assistants we created.""" if self._should_delete_assistant and self.assistant_id is not None: client = await self._ensure_client() - await client.beta.assistants.delete(self.assistant_id) + await client.beta.assistants.delete(self.assistant_id) # type: ignore[reportDeprecated] object.__setattr__(self, "assistant_id", None) object.__setattr__(self, "_should_delete_assistant", False) @@ -466,7 +469,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] raise ValueError("Parameter 'model_id' is required for assistant creation.") client = await self._ensure_client() - created_assistant = await client.beta.assistants.create( + created_assistant = await client.beta.assistants.create( # type: ignore[reportDeprecated] model=self.model_id, description=self.assistant_description, name=self.assistant_name, @@ -568,7 +571,8 @@ class OpenAIAssistantsClient( # type: ignore[misc] if isinstance(delta_block, TextDeltaBlock) and delta_block.text and delta_block.text.value: text_content = Content.from_text(delta_block.text.value) if delta_block.text.annotations: - text_content.annotations = [] + annotations: list[Annotation] = [] + text_content.annotations = annotations for annotation in delta_block.text.annotations: if isinstance(annotation, FileCitationDeltaAnnotation): ann: Annotation = Annotation( @@ -589,7 +593,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] end_index=annotation.end_index, ) ] - text_content.annotations.append(ann) + annotations.append(ann) elif isinstance(annotation, FilePathDeltaAnnotation): ann = Annotation( type="citation", @@ -609,7 +613,7 @@ class OpenAIAssistantsClient( # type: ignore[misc] end_index=annotation.end_index, ) ] - text_content.annotations.append(ann) + annotations.append(ann) yield ChatResponseUpdate( role=role, # type: ignore[arg-type] contents=[text_content], @@ -628,7 +632,8 @@ class OpenAIAssistantsClient( # type: ignore[misc] continue text_content = Content.from_text(block.text.value) if block.text.annotations: - text_content.annotations = [] + completed_annotations: list[Annotation] = [] + text_content.annotations = completed_annotations for completed_annotation in block.text.annotations: if isinstance(completed_annotation, FileCitationAnnotation): props: dict[str, Any] = { @@ -644,17 +649,13 @@ class OpenAIAssistantsClient( # type: ignore[misc] and completed_annotation.file_citation.file_id ): ann["file_id"] = completed_annotation.file_citation.file_id - if ( - completed_annotation.start_index is not None - and completed_annotation.end_index is not None - ): - ann["annotated_regions"] = [ - TextSpanRegion( - type="text_span", - start_index=completed_annotation.start_index, - end_index=completed_annotation.end_index, - ) - ] + ann["annotated_regions"] = [ + TextSpanRegion( + type="text_span", + start_index=completed_annotation.start_index, + end_index=completed_annotation.end_index, + ) + ] text_content.annotations.append(ann) elif isinstance(completed_annotation, FilePathAnnotation): ann = Annotation( @@ -666,17 +667,13 @@ class OpenAIAssistantsClient( # type: ignore[misc] ) if completed_annotation.file_path and completed_annotation.file_path.file_id: ann["file_id"] = completed_annotation.file_path.file_id - if ( - completed_annotation.start_index is not None - and completed_annotation.end_index is not None - ): - ann["annotated_regions"] = [ - TextSpanRegion( - type="text_span", - start_index=completed_annotation.start_index, - end_index=completed_annotation.end_index, - ) - ] + ann["annotated_regions"] = [ + TextSpanRegion( + type="text_span", + start_index=completed_annotation.start_index, + end_index=completed_annotation.end_index, + ) + ] text_content.annotations.append(ann) else: logger.debug("Unparsed annotation type: %s", completed_annotation.type) @@ -823,15 +820,16 @@ class OpenAIAssistantsClient( # type: ignore[misc] tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType] elif isinstance(tool, MutableMapping): # Pass through dict-based tools directly (from static factory methods) - tool_definitions.append(tool) + tool_definitions.append(cast(MutableMapping[str, Any], tool)) if len(tool_definitions) > 0: run_options["tools"] = tool_definitions if tool_mode is not None: - if (mode := tool_mode["mode"]) == "required" and ( - func_name := tool_mode.get("required_function_name") - ) is not None: + mode = tool_mode.get("mode") + if mode is None: + raise ValueError("tool_choice mode is required") + if mode == "required" and (func_name := tool_mode.get("required_function_name")) is not None: run_options["tool_choice"] = { "type": "function", "function": {"name": func_name}, diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 0c3d346129..0214c8df20 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -15,7 +15,7 @@ from collections.abc import ( ) from datetime import datetime, timezone from itertools import chain -from typing import Any, Generic, Literal +from typing import Any, Generic, Literal, cast from openai import AsyncOpenAI, BadRequestError from openai.lib._parsing._completions import type_to_response_format_param @@ -301,11 +301,16 @@ class RawOpenAIChatClient( # type: ignore[misc] for tool in normalize_tools(tools): if isinstance(tool, FunctionTool): chat_tools.append(tool.to_json_schema_spec()) - elif isinstance(tool, MutableMapping) and tool.get("type") == "web_search": - # Web search is handled via web_search_options, not tools array - web_search_options = {k: v for k, v in tool.items() if k != "type"} + elif isinstance(tool, MutableMapping): + typed_tool = cast(MutableMapping[str, Any], tool) + if typed_tool.get("type") == "web_search": + # Web search is handled via web_search_options, not tools array + web_search_options = {k: v for k, v in typed_tool.items() if k != "type"} + else: + # Pass through all other dict-based tools unchanged + chat_tools.append(typed_tool) else: - # Pass through all other tools (dicts, SDK types) unchanged + # Pass through all other tools (SDK types) unchanged chat_tools.append(tool) result: dict[str, Any] = {} if chat_tools: @@ -608,10 +613,21 @@ class RawOpenAIChatClient( # type: ignore[misc] # See https://github.com/microsoft/agent-framework/issues/4084 for msg in all_messages: msg_content: Any = msg.get("content") - if isinstance(msg_content, list) and all( - isinstance(c, dict) and c.get("type") == "text" for c in msg_content - ): - msg["content"] = "\n".join(c.get("text", "") for c in msg_content) + if isinstance(msg_content, list): + typed_msg_content = cast(list[object], msg_content) + text_items: list[Mapping[str, Any]] = [] + for item in typed_msg_content: + if not isinstance(item, Mapping): + break + text_item = cast(Mapping[str, Any], item) + if text_item.get("type") != "text": + break + text_items.append(text_item) + else: + msg["content"] = "\n".join( + text_item.get("text", "") if isinstance(text_item.get("text", ""), str) else "" + for text_item in text_items + ) return all_messages @@ -775,21 +791,26 @@ class OpenAIChatClient( # type: ignore[misc] env_file_encoding=env_file_encoding, ) - if not async_client and not openai_settings["api_key"]: + api_key_value = openai_settings.get("api_key") + if not async_client and not api_key_value: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - if not openai_settings["chat_model_id"]: + + chat_model_id = openai_settings.get("chat_model_id") + if not chat_model_id: raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_CHAT_MODEL_ID' environment variable." ) + base_url_value = openai_settings.get("base_url") + super().__init__( - model_id=openai_settings["chat_model_id"], - api_key=self._get_api_key(openai_settings["api_key"]), - base_url=openai_settings["base_url"] if openai_settings["base_url"] else None, - org_id=openai_settings["org_id"], + model_id=chat_model_id, + api_key=self._get_api_key(api_key_value), + base_url=base_url_value if base_url_value else None, + org_id=openai_settings.get("org_id"), default_headers=default_headers, client=async_client, instruction_role=instruction_role, diff --git a/python/packages/core/agent_framework/openai/_embedding_client.py b/python/packages/core/agent_framework/openai/_embedding_client.py index fb479c181c..b940e47c7c 100644 --- a/python/packages/core/agent_framework/openai/_embedding_client.py +++ b/python/packages/core/agent_framework/openai/_embedding_client.py @@ -67,7 +67,7 @@ class RawOpenAIEmbeddingClient( values: Sequence[str], *, options: OpenAIEmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[list[float]]: + ) -> GeneratedEmbeddings[list[float], OpenAIEmbeddingOptionsT]: """Call the OpenAI embeddings API. Args: @@ -81,9 +81,9 @@ class RawOpenAIEmbeddingClient( ValueError: If model_id is not provided or values is empty. """ if not values: - return GeneratedEmbeddings([], options=options) + return GeneratedEmbeddings([], options=options) # type: ignore - opts: dict[str, Any] = dict(options) if options else {} + opts: dict[str, Any] = options or {} # type: ignore model = opts.get("model_id") or self.model_id if not model: raise ValueError("model_id is required") @@ -193,21 +193,26 @@ class OpenAIEmbeddingClient( env_file_encoding=env_file_encoding, ) - if not async_client and not openai_settings["api_key"]: + api_key_value = openai_settings.get("api_key") + if not async_client and not api_key_value: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - if not openai_settings["embedding_model_id"]: + + embedding_model_id = openai_settings.get("embedding_model_id") + if not embedding_model_id: raise ValueError( "OpenAI embedding model ID is required. " "Set via 'model_id' parameter or 'OPENAI_EMBEDDING_MODEL_ID' environment variable." ) + base_url_value = openai_settings.get("base_url") + super().__init__( - model_id=openai_settings["embedding_model_id"], - api_key=self._get_api_key(openai_settings["api_key"]), - base_url=openai_settings["base_url"] if openai_settings["base_url"] else None, - org_id=openai_settings["org_id"], + model_id=embedding_model_id, + api_key=self._get_api_key(api_key_value), + base_url=base_url_value if base_url_value else None, + org_id=openai_settings.get("org_id"), default_headers=default_headers, client=async_client, otel_provider_name=otel_provider_name, diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index f11b60b767..726616adbb 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -460,14 +460,13 @@ class RawOpenAIResponsesClient( # type: ignore[misc] for tool_item in tools_list: if isinstance(tool_item, FunctionTool) and tool_item.kind == SHELL_TOOL_KIND_VALUE: shell_env = (tool_item.additional_properties or {}).get(OPENAI_SHELL_ENVIRONMENT_KEY) - if isinstance(shell_env, Mapping): - response_tools.append( - FunctionShellTool( - type="shell", - environment=dict(shell_env), - ) + response_tools.append( + FunctionShellTool( + type="shell", + environment=shell_env, # type: ignore[typeddict-item] ) - continue + ) + continue if isinstance(tool_item, FunctionTool): params = tool_item.parameters() params["additionalProperties"] = False @@ -496,7 +495,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] if tool_item.kind != SHELL_TOOL_KIND_VALUE: continue shell_env = (tool_item.additional_properties or {}).get(OPENAI_SHELL_ENVIRONMENT_KEY) - if isinstance(shell_env, Mapping) and shell_env.get("type") == "local": + if isinstance(shell_env, Mapping) and shell_env.get("type") == "local": # type: ignore[typeddict-item] return tool_item.name return None @@ -714,7 +713,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] ) if env_config.get("type") == "local": raise ValueError("Local shell requires func. Provide func for local execution.") - return FunctionShellTool(type="shell", environment=env_config) + return FunctionShellTool(type="shell", environment=env_config) # type: ignore[typeddict-item] if isinstance(environment, dict): raise ValueError("When func is provided, environment config is not supported.") @@ -1226,7 +1225,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] """Convert function tool output to the local shell JSON payload format.""" payload: dict[str, Any] if isinstance(content.result, Mapping): - payload = dict(content.result) + payload = dict(content.result) # type: ignore[assignment] else: payload = { "stdout": "" if content.result is None else str(content.result), @@ -1242,7 +1241,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc] """Convert function tool output to shell_call_output payload format.""" payload: dict[str, Any] if isinstance(content.result, Mapping): - payload = dict(content.result) + payload = dict(content.result) # type: ignore[assignment] else: payload = { "stdout": "" if content.result is None else str(content.result), @@ -1252,8 +1251,8 @@ class RawOpenAIResponsesClient( # type: ignore[misc] # Pass through native payload shape when tool already returns shell output entries. direct_output = payload.get("output") - if isinstance(direct_output, list) and all(isinstance(item, Mapping) for item in direct_output): - return [dict(item) for item in direct_output] + if isinstance(direct_output, list) and all(isinstance(item, Mapping) for item in direct_output): # type: ignore[reportUnknownMemberType] + return [dict(item) for item in direct_output] # type: ignore[reportUnknownMemberType] stdout = str(payload.get("stdout", "")) stderr = str(payload.get("stderr", "")) @@ -2293,24 +2292,26 @@ class OpenAIResponsesClient( # type: ignore[misc] env_file_encoding=env_file_encoding, ) - if not async_client and not openai_settings["api_key"]: + api_key_setting = openai_settings.get("api_key") + if not async_client and not api_key_setting: raise ValueError( "OpenAI API key is required. Set via 'api_key' parameter or 'OPENAI_API_KEY' environment variable." ) - if not openai_settings["responses_model_id"]: + responses_model_id = openai_settings.get("responses_model_id") + if not responses_model_id: raise ValueError( "OpenAI model ID is required. " "Set via 'model_id' parameter or 'OPENAI_RESPONSES_MODEL_ID' environment variable." ) super().__init__( - model_id=openai_settings["responses_model_id"], - api_key=self._get_api_key(openai_settings["api_key"]), - org_id=openai_settings["org_id"], + model_id=responses_model_id, + api_key=self._get_api_key(api_key_setting), + org_id=openai_settings.get("org_id"), default_headers=default_headers, client=async_client, instruction_role=instruction_role, - base_url=openai_settings["base_url"], + base_url=openai_settings.get("base_url"), middleware=middleware, function_invocation_configuration=function_invocation_configuration, **kwargs, diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/core/agent_framework/openai/_shared.py index 67f0e91818..9817b7fb11 100644 --- a/python/packages/core/agent_framework/openai/_shared.py +++ b/python/packages/core/agent_framework/openai/_shared.py @@ -6,7 +6,7 @@ import logging import sys from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence from copy import copy -from typing import Any, ClassVar, Union +from typing import Any, ClassVar, Union, cast import openai from openai import ( @@ -332,8 +332,10 @@ def from_assistant_tools( for tool in assistant_tools: if hasattr(tool, "type"): tool_type = tool.type - elif isinstance(tool, dict): - tool_type = tool.get("type") + elif isinstance(tool, Mapping): + typed_tool = cast(Mapping[str, Any], tool) + tool_type_value: Any = typed_tool.get("type") + tool_type = tool_type_value if isinstance(tool_type_value, str) else None else: tool_type = None diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index 5a0b3d8c2d..9d002453df 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -104,11 +104,12 @@ extend = "../../pyproject.toml" [tool.pyright] extends = "../../pyproject.toml" -include = ["tests/workflow"] +include = ["agent_framework", "tests/workflow"] [tool.mypy] plugins = ['pydantic.mypy'] strict = true +incremental = false python_version = "3.10" ignore_missing_imports = true disallow_untyped_defs = true @@ -130,7 +131,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework" -test = "pytest --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" +test = "pytest -m \"not integration\" --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" [tool.flit.module] name = "agent_framework" diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index c572f4727b..e64691e655 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -10,7 +10,7 @@ from unittest.mock import AsyncMock import pytest -from agent_framework import Skill, SkillResource, SkillsProvider, SessionContext +from agent_framework import SessionContext, Skill, SkillResource, SkillsProvider from agent_framework._skills import ( DEFAULT_RESOURCE_EXTENSIONS, _create_instructions, @@ -1348,9 +1348,7 @@ class TestReadAndParseSkillFile: def test_valid_file(self, tmp_path: Path) -> None: skill_dir = tmp_path / "my-skill" skill_dir.mkdir() - (skill_dir / "SKILL.md").write_text( - "---\nname: my-skill\ndescription: A skill.\n---\nBody.", encoding="utf-8" - ) + (skill_dir / "SKILL.md").write_text("---\nname: my-skill\ndescription: A skill.\n---\nBody.", encoding="utf-8") result = _read_and_parse_skill_file(str(skill_dir)) assert result is not None name, desc, content = result @@ -1393,7 +1391,7 @@ class TestCreateResourceElement: def test_xml_escapes_name(self) -> None: r = SkillResource(name='ref"special', content="data") elem = _create_resource_element(r) - assert '"' in elem + assert """ in elem def test_xml_escapes_description(self) -> None: r = SkillResource(name="ref", description='Uses & "quotes"', content="data") diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 8d74dc181d..f7674edc9b 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -5,7 +5,7 @@ from unittest.mock import Mock import pytest from opentelemetry import trace from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from agent_framework import ( Content, @@ -13,7 +13,6 @@ from agent_framework import ( tool, ) from agent_framework._tools import ( - _build_pydantic_model_from_json_schema, _parse_annotation, _parse_inputs, ) @@ -1001,467 +1000,4 @@ def test_parse_annotation_with_annotated_and_literal(): assert get_args(literal_type) == ("A", "B", "C") -def test_build_pydantic_model_from_json_schema_array_of_objects_issue(): - """Test for Tools with complex input schema (array of objects). - - This test verifies that JSON schemas with array properties containing nested objects - are properly parsed, ensuring that the nested object schema is preserved - and not reduced to a bare dict. - - Example from issue: - ``` - const SalesOrderItemSchema = z.object({ - customerMaterialNumber: z.string().optional(), - quantity: z.number(), - unitOfMeasure: z.string() - }); - - const CreateSalesOrderInputSchema = z.object({ - contract: z.string(), - items: z.array(SalesOrderItemSchema) - }); - ``` - - The issue was that agents only saw: - ``` - {"contract": "str", "items": "list[dict]"} - ``` - - Instead of the proper nested schema with all fields. - """ - # Schema matching the issue description - schema = { - "type": "object", - "properties": { - "contract": {"type": "string", "description": "Reference contract number"}, - "items": { - "type": "array", - "description": "Sales order line items", - "items": { - "type": "object", - "properties": { - "customerMaterialNumber": { - "type": "string", - "description": "Customer's material number", - }, - "quantity": {"type": "number", "description": "Order quantity"}, - "unitOfMeasure": { - "type": "string", - "description": "Unit of measure (e.g., 'ST', 'KG', 'TO')", - }, - }, - "required": ["quantity", "unitOfMeasure"], - }, - }, - }, - "required": ["contract", "items"], - } - - model = _build_pydantic_model_from_json_schema("create_sales_order", schema) - - # Test valid data - valid_data = { - "contract": "CONTRACT-123", - "items": [ - { - "customerMaterialNumber": "MAT-001", - "quantity": 10, - "unitOfMeasure": "ST", - }, - {"quantity": 5.5, "unitOfMeasure": "KG"}, - ], - } - - instance = model(**valid_data) - - # Verify the data was parsed correctly - assert instance.contract == "CONTRACT-123" - assert len(instance.items) == 2 - - # Verify first item - assert instance.items[0].customerMaterialNumber == "MAT-001" - assert instance.items[0].quantity == 10 - assert instance.items[0].unitOfMeasure == "ST" - - # Verify second item (optional field not provided) - assert instance.items[1].quantity == 5.5 - assert instance.items[1].unitOfMeasure == "KG" - - # Verify that items are proper BaseModel instances, not bare dicts - assert isinstance(instance.items[0], BaseModel) - assert isinstance(instance.items[1], BaseModel) - - # Verify that the nested object has the expected fields - assert hasattr(instance.items[0], "customerMaterialNumber") - assert hasattr(instance.items[0], "quantity") - assert hasattr(instance.items[0], "unitOfMeasure") - - # CRITICAL: Validate using the same methods that actual chat clients use - # This is what would actually be sent to the LLM - - # Create a FunctionTool wrapper to access the client-facing APIs - def dummy_func(**kwargs): - return kwargs - - test_func = FunctionTool( - func=dummy_func, - name="create_sales_order", - description="Create a sales order", - input_model=model, - ) - - # Test 1: Anthropic client uses tool.parameters() directly - anthropic_schema = test_func.parameters() - - # Verify contract property - assert "contract" in anthropic_schema["properties"] - assert anthropic_schema["properties"]["contract"]["type"] == "string" - - # Verify items array property exists - assert "items" in anthropic_schema["properties"] - items_prop = anthropic_schema["properties"]["items"] - assert items_prop["type"] == "array" - - # THE KEY TEST for Anthropic: array items must have proper object schema - assert "items" in items_prop, "Array should have 'items' schema definition" - array_items_schema = items_prop["items"] - - # Resolve schema if using $ref - if "$ref" in array_items_schema: - ref_path = array_items_schema["$ref"] - assert ref_path.startswith("#/$defs/") or ref_path.startswith("#/definitions/") - ref_name = ref_path.split("/")[-1] - defs = anthropic_schema.get("$defs", anthropic_schema.get("definitions", {})) - assert ref_name in defs, f"Referenced schema '{ref_name}' should exist" - item_schema = defs[ref_name] - else: - item_schema = array_items_schema - - # Verify the nested object has all properties defined - assert "properties" in item_schema, "Array items should have properties (not bare dict)" - item_properties = item_schema["properties"] - - # All three fields must be present in schema sent to LLM - assert "customerMaterialNumber" in item_properties, "customerMaterialNumber missing from LLM schema" - assert "quantity" in item_properties, "quantity missing from LLM schema" - assert "unitOfMeasure" in item_properties, "unitOfMeasure missing from LLM schema" - - # Verify types are correct - assert item_properties["customerMaterialNumber"]["type"] == "string" - assert item_properties["quantity"]["type"] in ["number", "integer"] - assert item_properties["unitOfMeasure"]["type"] == "string" - - # Test 2: OpenAI client uses tool.to_json_schema_spec() - openai_spec = test_func.to_json_schema_spec() - - assert openai_spec["type"] == "function" - assert "function" in openai_spec - openai_schema = openai_spec["function"]["parameters"] - - # Verify the same structure is present in OpenAI format - assert "items" in openai_schema["properties"] - openai_items_prop = openai_schema["properties"]["items"] - assert openai_items_prop["type"] == "array" - assert "items" in openai_items_prop - - openai_array_items = openai_items_prop["items"] - if "$ref" in openai_array_items: - ref_path = openai_array_items["$ref"] - ref_name = ref_path.split("/")[-1] - defs = openai_schema.get("$defs", openai_schema.get("definitions", {})) - openai_item_schema = defs[ref_name] - else: - openai_item_schema = openai_array_items - - assert "properties" in openai_item_schema - openai_props = openai_item_schema["properties"] - assert "customerMaterialNumber" in openai_props - assert "quantity" in openai_props - assert "unitOfMeasure" in openai_props - - # Test validation - missing required quantity - with pytest.raises(ValidationError): - model( - contract="CONTRACT-456", - items=[ - { - "customerMaterialNumber": "MAT-002", - "unitOfMeasure": "TO", - # Missing required 'quantity' - } - ], - ) - - # Test validation - missing required unitOfMeasure - with pytest.raises(ValidationError): - model( - contract="CONTRACT-789", - items=[ - { - "quantity": 20 - # Missing required 'unitOfMeasure' - } - ], - ) - - -def test_one_of_discriminator_polymorphism(): - """Test that oneOf with discriminator creates proper polymorphic union types. - - Tests that oneOf + discriminator patterns are properly converted to Pydantic discriminated unions. - """ - schema = { - "$defs": { - "CreateProject": { - "description": "Action: Create an Azure DevOps project.", - "properties": { - "name": { - "const": "create_project", - "default": "create_project", - "type": "string", - }, - "params": {"$ref": "#/$defs/CreateProjectParams"}, - }, - "required": ["params"], - "type": "object", - }, - "CreateProjectParams": { - "description": "Parameters for the create_project action.", - "properties": { - "orgUrl": {"minLength": 1, "type": "string"}, - "projectName": {"minLength": 1, "type": "string"}, - "description": {"default": "", "type": "string"}, - "template": {"default": "Agile", "type": "string"}, - "sourceControl": { - "default": "Git", - "enum": ["Git", "Tfvc"], - "type": "string", - }, - "visibility": {"default": "private", "type": "string"}, - }, - "required": ["orgUrl", "projectName"], - "type": "object", - }, - "DeployRequest": { - "description": "Request to deploy Azure DevOps resources.", - "properties": { - "projectName": {"minLength": 1, "type": "string"}, - "organization": {"minLength": 1, "type": "string"}, - "actions": { - "items": { - "discriminator": { - "mapping": { - "create_project": "#/$defs/CreateProject", - "hello_world": "#/$defs/HelloWorld", - }, - "propertyName": "name", - }, - "oneOf": [ - {"$ref": "#/$defs/HelloWorld"}, - {"$ref": "#/$defs/CreateProject"}, - ], - }, - "type": "array", - }, - }, - "required": ["projectName", "organization"], - "type": "object", - }, - "HelloWorld": { - "description": "Action: Prints a greeting message.", - "properties": { - "name": { - "const": "hello_world", - "default": "hello_world", - "type": "string", - }, - "params": {"$ref": "#/$defs/HelloWorldParams"}, - }, - "required": ["params"], - "type": "object", - }, - "HelloWorldParams": { - "description": "Parameters for the hello_world action.", - "properties": { - "name": { - "description": "Name to greet", - "minLength": 1, - "type": "string", - } - }, - "required": ["name"], - "type": "object", - }, - }, - "properties": {"params": {"$ref": "#/$defs/DeployRequest"}}, - "required": ["params"], - "type": "object", - } - - # Build the model - model = _build_pydantic_model_from_json_schema("deploy_tool", schema) - - # Verify the model structure - assert model is not None - assert issubclass(model, BaseModel) - - # Test with HelloWorld action - hello_world_data = { - "params": { - "projectName": "MyProject", - "organization": "MyOrg", - "actions": [ - { - "name": "hello_world", - "params": {"name": "Alice"}, - } - ], - } - } - - instance = model(**hello_world_data) - assert instance.params.projectName == "MyProject" - assert instance.params.organization == "MyOrg" - assert len(instance.params.actions) == 1 - assert instance.params.actions[0].name == "hello_world" - assert instance.params.actions[0].params.name == "Alice" - - # Test with CreateProject action - create_project_data = { - "params": { - "projectName": "MyProject", - "organization": "MyOrg", - "actions": [ - { - "name": "create_project", - "params": { - "orgUrl": "https://dev.azure.com/myorg", - "projectName": "NewProject", - "sourceControl": "Git", - }, - } - ], - } - } - - instance2 = model(**create_project_data) - assert instance2.params.actions[0].name == "create_project" - assert instance2.params.actions[0].params.projectName == "NewProject" - assert instance2.params.actions[0].params.sourceControl == "Git" - - # Test with mixed actions - mixed_data = { - "params": { - "projectName": "MyProject", - "organization": "MyOrg", - "actions": [ - {"name": "hello_world", "params": {"name": "Bob"}}, - { - "name": "create_project", - "params": { - "orgUrl": "https://dev.azure.com/myorg", - "projectName": "AnotherProject", - }, - }, - ], - } - } - - instance3 = model(**mixed_data) - assert len(instance3.params.actions) == 2 - assert instance3.params.actions[0].name == "hello_world" - assert instance3.params.actions[1].name == "create_project" - - -def test_const_creates_literal(): - """Test that const in JSON Schema creates Literal type.""" - schema = { - "properties": { - "action": { - "const": "create", - "type": "string", - "description": "Action type", - }, - "value": {"type": "integer"}, - }, - "required": ["action", "value"], - } - - model = _build_pydantic_model_from_json_schema("test_const", schema) - - # Verify valid const value works - instance = model(action="create", value=42) - assert instance.action == "create" - assert instance.value == 42 - - # Verify incorrect const value fails - with pytest.raises(ValidationError): - model(action="delete", value=42) - - -def test_enum_creates_literal(): - """Test that enum in JSON Schema creates Literal type.""" - schema = { - "properties": { - "status": { - "enum": ["pending", "approved", "rejected"], - "type": "string", - "description": "Status", - }, - "priority": {"enum": [1, 2, 3], "type": "integer"}, - }, - "required": ["status"], - } - - model = _build_pydantic_model_from_json_schema("test_enum", schema) - - # Verify valid enum values work - instance = model(status="approved", priority=2) - assert instance.status == "approved" - assert instance.priority == 2 - - # Verify invalid enum value fails - with pytest.raises(ValidationError): - model(status="unknown") - - with pytest.raises(ValidationError): - model(status="pending", priority=5) - - -def test_nested_object_with_const_and_enum(): - """Test that const and enum work in nested objects.""" - schema = { - "properties": { - "config": { - "type": "object", - "properties": { - "type": { - "const": "production", - "default": "production", - "type": "string", - }, - "level": {"enum": ["low", "medium", "high"], "type": "string"}, - }, - "required": ["level"], - } - }, - "required": ["config"], - } - - model = _build_pydantic_model_from_json_schema("test_nested", schema) - - # Valid data - instance = model(config={"type": "production", "level": "high"}) - assert instance.config.type == "production" - assert instance.config.level == "high" - - # Invalid const in nested object - with pytest.raises(ValidationError): - model(config={"type": "development", "level": "low"}) - - # Invalid enum in nested object - with pytest.raises(ValidationError): - model(config={"type": "production", "level": "critical"}) - - # endregion diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index bcf3a6891b..0d314c1aa5 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -550,7 +550,6 @@ def test_usage_details(): assert usage["input_token_count"] == 5 assert usage["output_token_count"] == 10 assert usage["total_token_count"] == 15 - assert usage.get("additional_counts", {}) == {} def test_usage_details_addition(): @@ -581,8 +580,8 @@ def test_usage_details_addition(): def test_usage_details_fail(): # TypedDict doesn't validate types at runtime, so this test no longer applies # Creating UsageDetails with wrong types won't raise ValueError - usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, wrong_type="42.923") # type: ignore[typeddict-item] - assert usage["wrong_type"] == "42.923" # type: ignore[typeddict-item] + usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, wrong_type="42.923") + assert usage["wrong_type"] == "42.923" def test_usage_details_additional_counts(): @@ -601,6 +600,15 @@ def test_usage_details_add_with_none_and_type_errors(): # TypedDict doesn't support + operator, use add_usage_details +def test_usage_details_add_skips_non_int(): + u1 = UsageDetails(input_token_count=10, other="test") + u2 = UsageDetails(input_token_count=10, another="test") + u3 = add_usage_details(u1, u2) + assert len(u3.keys()) == 1 + assert "input_token_count" in u3 + assert u3["input_token_count"] == 20 + + # region UserInputRequest and Response @@ -1705,7 +1713,7 @@ def test_chat_response_complex_serialization(): {"role": "user", "contents": [{"type": "text", "text": "Hello"}]}, {"role": "assistant", "contents": [{"type": "text", "text": "Hi there"}]}, ], - "finish_reason": {"value": "stop"}, + "finish_reason": "stop", "usage_details": { "type": "usage_details", "input_token_count": 5, @@ -1831,7 +1839,7 @@ def test_agent_run_response_update_all_content_types(): }, {"type": "text_reasoning", "text": "reasoning"}, ], - "role": {"value": "assistant"}, # Test role as dict + "role": "assistant", # Test role as dict } update = AgentResponseUpdate.from_dict(update_data) @@ -2394,7 +2402,7 @@ def test_content_add_usage_content_non_integer_values(): result = usage1 + usage2 # Non-integer "model" should take first non-None value - assert result.usage_details["model"] == "gpt-4" + assert "model" not in result.usage_details # Integer "count" should be summed assert result.usage_details["count"] == 30 diff --git a/python/packages/core/tests/openai/test_openai_embedding_client.py b/python/packages/core/tests/openai/test_openai_embedding_client.py index c606b67e31..3ddb7538a6 100644 --- a/python/packages/core/tests/openai/test_openai_embedding_client.py +++ b/python/packages/core/tests/openai/test_openai_embedding_client.py @@ -212,7 +212,8 @@ def test_azure_construction_with_existing_client() -> None: assert client.client is mock_client -def test_azure_construction_missing_deployment_name_raises() -> None: +def test_azure_construction_missing_deployment_name_raises(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME", raising=False) with pytest.raises(ValueError, match="deployment name is required"): AzureOpenAIEmbeddingClient( api_key="test-key", @@ -272,6 +273,7 @@ skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif( @skip_if_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_openai_get_embeddings() -> None: """End-to-end test of OpenAI embedding generation.""" client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") @@ -289,6 +291,7 @@ async def test_integration_openai_get_embeddings() -> None: @skip_if_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_openai_get_embeddings_multiple() -> None: """Test embedding generation for multiple inputs.""" client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") @@ -302,6 +305,7 @@ async def test_integration_openai_get_embeddings_multiple() -> None: @skip_if_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_openai_get_embeddings_with_dimensions() -> None: """Test embedding generation with custom dimensions.""" client = OpenAIEmbeddingClient(model_id="text-embedding-3-small") @@ -315,6 +319,7 @@ async def test_integration_openai_get_embeddings_with_dimensions() -> None: @skip_if_azure_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_azure_openai_get_embeddings() -> None: """End-to-end test of Azure OpenAI embedding generation.""" client = AzureOpenAIEmbeddingClient() @@ -332,6 +337,7 @@ async def test_integration_azure_openai_get_embeddings() -> None: @skip_if_azure_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_azure_openai_get_embeddings_multiple() -> None: """Test Azure OpenAI embedding generation for multiple inputs.""" client = AzureOpenAIEmbeddingClient() @@ -345,6 +351,7 @@ async def test_integration_azure_openai_get_embeddings_multiple() -> None: @skip_if_azure_openai_integration_tests_disabled @pytest.mark.flaky +@pytest.mark.integration async def test_integration_azure_openai_get_embeddings_with_dimensions() -> None: """Test Azure OpenAI embedding generation with custom dimensions.""" client = AzureOpenAIEmbeddingClient() diff --git a/python/packages/core/tests/workflow/test_agent_executor.py b/python/packages/core/tests/workflow/test_agent_executor.py index 788e96e61e..599e62d635 100644 --- a/python/packages/core/tests/workflow/test_agent_executor.py +++ b/python/packages/core/tests/workflow/test_agent_executor.py @@ -5,6 +5,7 @@ from collections.abc import AsyncIterable, Awaitable from typing import TYPE_CHECKING, Any, Literal, overload import pytest + from agent_framework import ( AgentExecutor, AgentResponse, @@ -59,30 +60,19 @@ class _CountingAgent(BaseAgent): stream: bool = False, session: AgentSession | None = None, **kwargs: Any, - ) -> ( - Awaitable[AgentResponse[Any]] - | ResponseStream[AgentResponseUpdate, AgentResponse[Any]] - ): + ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: self.call_count += 1 if stream: async def _stream() -> AsyncIterable[AgentResponseUpdate]: yield AgentResponseUpdate( - contents=[ - Content.from_text( - text=f"Response #{self.call_count}: {self.name}" - ) - ] + contents=[Content.from_text(text=f"Response #{self.call_count}: {self.name}")] ) return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) async def _run() -> AgentResponse: - return AgentResponse( - messages=[ - Message("assistant", [f"Response #{self.call_count}: {self.name}"]) - ] - ) + return AgentResponse(messages=[Message("assistant", [f"Response #{self.call_count}: {self.name}"])]) return _run() @@ -120,10 +110,7 @@ class _StreamingHookAgent(BaseAgent): stream: bool = False, session: AgentSession | None = None, **kwargs: Any, - ) -> ( - Awaitable[AgentResponse[Any]] - | ResponseStream[AgentResponseUpdate, AgentResponse[Any]] - ): + ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: if stream: async def _stream() -> AsyncIterable[AgentResponseUpdate]: @@ -138,9 +125,9 @@ class _StreamingHookAgent(BaseAgent): self.result_hook_called = True return response - return ResponseStream( - _stream(), finalizer=AgentResponse.from_updates - ).with_result_hook(_mark_result_hook_called) + return ResponseStream(_stream(), finalizer=AgentResponse.from_updates).with_result_hook( + _mark_result_hook_called + ) async def _run() -> AgentResponse: return AgentResponse(messages=[Message("assistant", ["hook test"])]) @@ -148,9 +135,7 @@ class _StreamingHookAgent(BaseAgent): return _run() -async def test_agent_executor_streaming_finalizes_stream_and_runs_result_hooks() -> ( - None -): +async def test_agent_executor_streaming_finalizes_stream_and_runs_result_hooks() -> None: """AgentExecutor should call get_final_response() so stream result hooks execute.""" agent = _StreamingHookAgent(id="hook_agent", name="HookAgent") executor = AgentExecutor(agent, id="hook_exec") @@ -217,9 +202,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None: executor_state = executor_states[executor.id] # type: ignore[index] assert "cache" in executor_state, "Checkpoint should store executor cache state" - assert "agent_session" in executor_state, ( - "Checkpoint should store executor session state" - ) + assert "agent_session" in executor_state, "Checkpoint should store executor session state" # Verify session state structure session_state = executor_state["agent_session"] # type: ignore[index] @@ -240,15 +223,11 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None: assert restored_agent.call_count == 0 # Build new workflow with the restored executor - wf_resume = SequentialBuilder( - participants=[restored_executor], checkpoint_storage=storage - ).build() + wf_resume = SequentialBuilder(participants=[restored_executor], checkpoint_storage=storage).build() # Resume from checkpoint resumed_output: AgentExecutorResponse | None = None - async for ev in wf_resume.run( - checkpoint_id=restore_checkpoint.checkpoint_id, stream=True - ): + async for ev in wf_resume.run(checkpoint_id=restore_checkpoint.checkpoint_id, stream=True): if ev.type == "output": resumed_output = ev.data # type: ignore[assignment] if ev.type == "status" and ev.state in ( @@ -391,11 +370,7 @@ async def test_prepare_agent_run_args_strips_all_reserved_kwargs_at_once( assert options is not None assert options["additional_function_arguments"]["custom"] == 1 - warned_keys = { - r.message.split("'")[1] - for r in caplog.records - if "reserved" in r.message.lower() - } + warned_keys = {r.message.split("'")[1] for r in caplog.records if "reserved" in r.message.lower()} assert warned_keys == {"session", "stream", "messages"} diff --git a/python/packages/core/tests/workflow/test_agent_utils.py b/python/packages/core/tests/workflow/test_agent_utils.py index 07d1e64c08..633ba1072c 100644 --- a/python/packages/core/tests/workflow/test_agent_utils.py +++ b/python/packages/core/tests/workflow/test_agent_utils.py @@ -16,10 +16,31 @@ class MockAgent: self.description: str | None = None @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... - def run(self, messages: AgentRunInputs | None = None, *, stream: bool = False, session: AgentSession | None = None, **kwargs: Any) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = None, + *, + stream: bool = False, + session: AgentSession | None = None, + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def create_session(self, **kwargs: Any) -> AgentSession: """Creates a new conversation session for the agent.""" diff --git a/python/packages/core/tests/workflow/test_edge.py b/python/packages/core/tests/workflow/test_edge.py index ecaa341726..422d530631 100644 --- a/python/packages/core/tests/workflow/test_edge.py +++ b/python/packages/core/tests/workflow/test_edge.py @@ -4,9 +4,8 @@ from dataclasses import dataclass from typing import Any from unittest.mock import patch -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter - import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from agent_framework import ( Executor, diff --git a/python/packages/core/tests/workflow/test_executor.py b/python/packages/core/tests/workflow/test_executor.py index 77827c0634..77777e198b 100644 --- a/python/packages/core/tests/workflow/test_executor.py +++ b/python/packages/core/tests/workflow/test_executor.py @@ -3,6 +3,8 @@ from dataclasses import dataclass import pytest +from typing_extensions import Never + from agent_framework import ( Executor, Message, @@ -14,7 +16,6 @@ from agent_framework import ( handler, response_handler, ) -from typing_extensions import Never # Module-level types for string forward reference tests @@ -155,11 +156,7 @@ async def test_executor_invoked_event_contains_input_data(): workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, collector).build() events = await workflow.run("hello world") - invoked_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_invoked" - ] + invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"] assert len(invoked_events) == 2 @@ -193,16 +190,10 @@ async def test_executor_completed_event_contains_sent_messages(): sender = MultiSenderExecutor(id="sender") collector = CollectorExecutor(id="collector") - workflow = ( - WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build() - ) + workflow = WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build() events = await workflow.run("hello") - completed_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_completed" - ] + completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"] # Sender should have completed with the sent messages sender_completed = next(e for e in completed_events if e.executor_id == "sender") @@ -210,9 +201,7 @@ async def test_executor_completed_event_contains_sent_messages(): assert sender_completed.data == ["hello-first", "hello-second"] # Collector should have completed with no sent messages (None) - collector_completed_events = [ - e for e in completed_events if e.executor_id == "collector" - ] + collector_completed_events = [e for e in completed_events if e.executor_id == "collector"] # Collector is called twice (once per message from sender) assert len(collector_completed_events) == 2 for collector_completed in collector_completed_events: @@ -231,11 +220,7 @@ async def test_executor_completed_event_includes_yielded_outputs(): workflow = WorkflowBuilder(start_executor=executor).build() events = await workflow.run("test") - completed_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_completed" - ] + completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"] assert len(completed_events) == 1 assert completed_events[0].executor_id == "yielder" @@ -263,9 +248,7 @@ async def test_executor_events_with_complex_message_types(): class ProcessorExecutor(Executor): @handler - async def handle( - self, request: Request, ctx: WorkflowContext[Response] - ) -> None: + async def handle(self, request: Request, ctx: WorkflowContext[Response]) -> None: response = Response(results=[request.query.upper()] * request.limit) await ctx.send_message(response) @@ -277,23 +260,13 @@ async def test_executor_events_with_complex_message_types(): processor = ProcessorExecutor(id="processor") collector = CollectorExecutor(id="collector") - workflow = ( - WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build() - ) + workflow = WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build() input_request = Request(query="hello", limit=3) events = await workflow.run(input_request) - invoked_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_invoked" - ] - completed_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_completed" - ] + invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"] + completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"] # Check processor invoked event has the Request object processor_invoked = next(e for e in invoked_events if e.executor_id == "processor") @@ -302,9 +275,7 @@ async def test_executor_events_with_complex_message_types(): assert processor_invoked.data.limit == 3 # Check processor completed event has the Response object - processor_completed = next( - e for e in completed_events if e.executor_id == "processor" - ) + processor_completed = next(e for e in completed_events if e.executor_id == "processor") assert processor_completed.data is not None assert len(processor_completed.data) == 1 assert isinstance(processor_completed.data[0], Response) @@ -390,9 +361,7 @@ def test_executor_workflow_output_types_property(): # Test executor with union workflow output types class UnionWorkflowOutputExecutor(Executor): @handler - async def handle( - self, text: str, ctx: WorkflowContext[int, str | bool] - ) -> None: + async def handle(self, text: str, ctx: WorkflowContext[int, str | bool]) -> None: pass executor = UnionWorkflowOutputExecutor(id="union_workflow_output") @@ -403,15 +372,11 @@ def test_executor_workflow_output_types_property(): # Test executor with multiple handlers having different workflow output types class MultiHandlerWorkflowExecutor(Executor): @handler - async def handle_string( - self, text: str, ctx: WorkflowContext[int, str] - ) -> None: + async def handle_string(self, text: str, ctx: WorkflowContext[int, str]) -> None: pass @handler - async def handle_number( - self, num: int, ctx: WorkflowContext[bool, float] - ) -> None: + async def handle_number(self, num: int, ctx: WorkflowContext[bool, float]) -> None: pass executor = MultiHandlerWorkflowExecutor(id="multi_workflow") @@ -465,9 +430,7 @@ def test_executor_output_types_includes_response_handlers(): pass @response_handler - async def handle_response( - self, original_request: str, response: bool, ctx: WorkflowContext[float] - ) -> None: + async def handle_response(self, original_request: str, response: bool, ctx: WorkflowContext[float]) -> None: pass executor = RequestResponseExecutor(id="request_response") @@ -574,9 +537,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): """Test that executor_invoked event (type='executor_invoked').data captures original input, not mutated input.""" @executor(id="Mutator") - async def mutator( - messages: list[Message], ctx: WorkflowContext[list[Message]] - ) -> None: + async def mutator(messages: list[Message], ctx: WorkflowContext[list[Message]]) -> None: # The handler mutates the input list by appending new messages original_len = len(messages) messages.append(Message(role="assistant", text="Added by executor")) @@ -591,11 +552,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler(): events = await workflow.run(input_messages) # Find the invoked event for the Mutator executor - invoked_events = [ - e - for e in events - if isinstance(e, WorkflowEvent) and e.type == "executor_invoked" - ] + invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"] assert len(invoked_events) == 1 mutator_invoked = invoked_events[0] @@ -672,12 +629,8 @@ class TestHandlerExplicitTypes: assert handler_func._handler_spec["output_types"] == [list] # pyright: ignore[reportFunctionMemberAccess] # Verify can_handle - assert exec_instance.can_handle( - WorkflowMessage(data={"key": "value"}, source_id="mock") - ) - assert not exec_instance.can_handle( - WorkflowMessage(data="string", source_id="mock") - ) + assert exec_instance.can_handle(WorkflowMessage(data={"key": "value"}, source_id="mock")) + assert not exec_instance.can_handle(WorkflowMessage(data="string", source_id="mock")) def test_handler_with_explicit_union_input_type(self): """Test that explicit union input_type is handled correctly.""" @@ -698,9 +651,7 @@ class TestHandlerExplicitTypes: assert exec_instance.can_handle(WorkflowMessage(data="hello", source_id="mock")) assert exec_instance.can_handle(WorkflowMessage(data=42, source_id="mock")) # Cannot handle float - assert not exec_instance.can_handle( - WorkflowMessage(data=3.14, source_id="mock") - ) + assert not exec_instance.can_handle(WorkflowMessage(data=3.14, source_id="mock")) def test_handler_with_explicit_union_output_type(self): """Test that explicit union output is normalized to a list.""" @@ -776,9 +727,7 @@ class TestHandlerExplicitTypes: class OnlyWorkflowOutputExecutor(Executor): # pyright: ignore[reportUnusedClass] @handler(workflow_output=bool) - async def handle( - self, message: str, ctx: WorkflowContext[int, str] - ) -> None: + async def handle(self, message: str, ctx: WorkflowContext[int, str]) -> None: pass def test_handler_explicit_input_type_allows_no_message_annotation(self): @@ -803,9 +752,7 @@ class TestHandlerExplicitTypes: pass @handler - async def handle_introspected( - self, message: float, ctx: WorkflowContext[bool] - ) -> None: + async def handle_introspected(self, message: float, ctx: WorkflowContext[bool]) -> None: pass exec_instance = MixedExecutor(id="mixed") @@ -831,9 +778,7 @@ class TestHandlerExplicitTypes: # Should resolve the string to the actual type assert ForwardRefMessage in exec_instance._handlers # pyright: ignore[reportPrivateUsage] - assert exec_instance.can_handle( - WorkflowMessage(data=ForwardRefMessage("hello"), source_id="mock") - ) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefMessage("hello"), source_id="mock")) def test_handler_with_string_forward_reference_union(self): """Test that string forward references work with union types.""" @@ -846,12 +791,8 @@ class TestHandlerExplicitTypes: exec_instance = StringUnionExecutor(id="string_union") # Should handle both types - assert exec_instance.can_handle( - WorkflowMessage(data=ForwardRefTypeA("hello"), source_id="mock") - ) - assert exec_instance.can_handle( - WorkflowMessage(data=ForwardRefTypeB(42), source_id="mock") - ) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeA("hello"), source_id="mock")) + assert exec_instance.can_handle(WorkflowMessage(data=ForwardRefTypeB(42), source_id="mock")) def test_handler_with_string_forward_reference_output_type(self): """Test that string forward references work for output_type.""" @@ -890,9 +831,7 @@ class TestHandlerExplicitTypes: class PrecedenceExecutor(Executor): @handler(input=int, output=float, workflow_output=str) - async def handle( - self, message: int, ctx: WorkflowContext[int, bool] - ) -> None: + async def handle(self, message: int, ctx: WorkflowContext[int, bool]) -> None: pass exec_instance = PrecedenceExecutor(id="precedence") @@ -958,9 +897,7 @@ class TestHandlerExplicitTypes: async def handle(self, message, ctx: WorkflowContext) -> None: # type: ignore[no-untyped-def] pass - exec_instance = StringUnionWorkflowOutputExecutor( - id="string_union_workflow_output" - ) + exec_instance = StringUnionWorkflowOutputExecutor(id="string_union_workflow_output") # Should resolve both types from string union assert ForwardRefTypeA in exec_instance.workflow_output_types @@ -971,14 +908,10 @@ class TestHandlerExplicitTypes: class IntrospectedWorkflowOutputExecutor(Executor): @handler - async def handle( - self, message: str, ctx: WorkflowContext[int, bool] - ) -> None: + async def handle(self, message: str, ctx: WorkflowContext[int, bool]) -> None: pass - exec_instance = IntrospectedWorkflowOutputExecutor( - id="introspected_workflow_output" - ) + exec_instance = IntrospectedWorkflowOutputExecutor(id="introspected_workflow_output") # Should use introspected types from WorkflowContext[int, bool] assert int in exec_instance.output_types diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index b5a8bb9902..eacf70c6db 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -717,9 +717,23 @@ class TestWorkflowAgent: return AgentSession() @overload - def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, @@ -813,9 +827,23 @@ class TestWorkflowAgent: return AgentSession() @overload - def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: str | Content | Message | Sequence[str | Content | Message] | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, diff --git a/python/packages/core/tests/workflow/test_workflow_kwargs.py b/python/packages/core/tests/workflow/test_workflow_kwargs.py index 0850c6b060..d315f75f85 100644 --- a/python/packages/core/tests/workflow/test_workflow_kwargs.py +++ b/python/packages/core/tests/workflow/test_workflow_kwargs.py @@ -52,9 +52,23 @@ class _KwargsCapturingAgent(BaseAgent): self.captured_kwargs = [] @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, @@ -90,9 +104,23 @@ class _OptionsAwareAgent(BaseAgent): self.captured_kwargs = [] @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, @@ -475,9 +503,23 @@ async def test_kwargs_preserved_on_response_continuation() -> None: self._asked = False @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, @@ -538,9 +580,23 @@ async def test_kwargs_overridden_on_response_continuation() -> None: self._asked = False @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, @@ -605,9 +661,23 @@ async def test_kwargs_empty_value_passed_on_continuation() -> None: self._asked = False @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[False] = ..., session: AgentSession | None = ..., **kwargs: Any) -> Awaitable[AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... @overload - def run(self, messages: AgentRunInputs | None = ..., *, stream: Literal[True], session: AgentSession | None = ..., **kwargs: Any) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... def run( self, diff --git a/python/packages/core/tests/workflow/test_workflow_states.py b/python/packages/core/tests/workflow/test_workflow_states.py index 34c7e8c93f..bf2e277d10 100644 --- a/python/packages/core/tests/workflow/test_workflow_states.py +++ b/python/packages/core/tests/workflow/test_workflow_states.py @@ -38,7 +38,9 @@ async def test_executor_failed_and_workflow_failed_events_streaming(): events.append(ev) # executor_failed event (type='executor_failed') should be emitted before workflow failed event - executor_failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"] + executor_failed_events: list[WorkflowEvent[Any]] = [ + e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed" + ] assert executor_failed_events, "executor_failed event should be emitted when start executor fails" assert executor_failed_events[0].executor_id == "f" assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK @@ -96,7 +98,9 @@ async def test_executor_failed_event_from_second_executor_in_chain(): events.append(ev) # executor_failed event should be emitted for the failing executor - executor_failed_events: list[WorkflowEvent[Any]] = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed"] + executor_failed_events: list[WorkflowEvent[Any]] = [ + e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_failed" + ] assert executor_failed_events, "executor_failed event should be emitted when second executor fails" assert executor_failed_events[0].executor_id == "failing" assert executor_failed_events[0].origin is WorkflowEventSource.FRAMEWORK diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 79bedb657d..625189a2f4 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -15,7 +15,6 @@ from agent_framework import ( from agent_framework import ( FunctionTool as AFFunctionTool, ) -from agent_framework._tools import _create_model_from_json_schema # type: ignore from agent_framework.exceptions import AgentException from dotenv import load_dotenv @@ -34,7 +33,7 @@ from ._models import ( RemoteConnection, Tool, WebSearchTool, - _safe_mode_context, + _safe_mode_context, # type: ignore[reportPrivateUsage] agent_schema_dispatch, ) @@ -445,7 +444,7 @@ class AgentFactory: if tools := self._parse_tools(prompt_agent.tools): chat_options["tools"] = tools if output_schema := prompt_agent.outputSchema: - chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema()) + chat_options["response_format"] = output_schema.to_json_schema() # Step 3: Create the agent instance return Agent( client=client, @@ -563,7 +562,7 @@ class AgentFactory: if tools := self._parse_tools(prompt_agent.tools): chat_options["tools"] = tools if output_schema := prompt_agent.outputSchema: - chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema()) + chat_options["response_format"] = output_schema.to_json_schema() return Agent( client=client, name=prompt_agent.name, @@ -598,6 +597,9 @@ class AgentFactory: case ApiKeyConnection(): if prompt_agent.model.connection.endpoint: provider_kwargs["project_endpoint"] = prompt_agent.model.connection.endpoint + case ReferenceConnection(): + # Reference connections are resolved by concrete providers when supported. + pass # Create the provider and use it to create the agent provider = provider_class(**provider_kwargs) @@ -608,8 +610,7 @@ class AgentFactory: # Parse response format into default_options default_options: dict[str, Any] | None = None if prompt_agent.outputSchema: - response_format = _create_model_from_json_schema("agent", prompt_agent.outputSchema.to_json_schema()) - default_options = {"response_format": response_format} + default_options = {"response_format": prompt_agent.outputSchema.to_json_schema()} # Create the agent using the provider # The provider's create_agent returns a Agent directly diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py index 01a68e6a8e..e7af9fde9a 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_base.py @@ -25,6 +25,7 @@ See: dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/ from __future__ import annotations +import locale import logging import sys import uuid @@ -103,6 +104,8 @@ DECLARATIVE_STATE_KEY = "_declarative_workflow_state" # Types that PowerFx can serialize directly # Note: Decimal is included because PowerFx returns Decimal for numeric values _POWERFX_SAFE_TYPES = (str, int, float, bool, type(None), _Decimal) +_POWERFX_EVAL_LOCALE = "en-US" +_POWERFX_NUMERIC_LOCALE_CANDIDATES = ("en_US.UTF-8", "en_US", "C") def _make_powerfx_safe(value: Any) -> Any: @@ -121,10 +124,12 @@ def _make_powerfx_safe(value: Any) -> Any: return value if isinstance(value, dict): - return {k: _make_powerfx_safe(v) for k, v in value.items()} + value_dict = cast(Mapping[Any, Any], value) + return {str(k): _make_powerfx_safe(v) for k, v in value_dict.items()} if isinstance(value, list): - return [_make_powerfx_safe(item) for item in value] + value_list = cast(list[Any], value) # type: ignore[redundant-cast] + return [_make_powerfx_safe(item) for item in value_list] # Try to convert objects with __dict__ or dataclass-style attributes if hasattr(value, "__dict__"): @@ -382,21 +387,33 @@ class DeclarativeWorkflowState: f"Install dotnet and the powerfx package for full PowerFx support." ) - engine = Engine() symbols = self._to_powerfx_symbols() + # Use setlocale(category) query form so we can restore the exact prior value. + # getlocale() returns a normalized tuple and is not always a lossless + # round-trip for setlocale across platforms/locales. + original_numeric_locale = locale.setlocale(locale.LC_NUMERIC) try: - from System.Globalization import CultureInfo + for locale_candidate in _POWERFX_NUMERIC_LOCALE_CANDIDATES: + try: + locale.setlocale(locale.LC_NUMERIC, locale_candidate) + break + except locale.Error: + continue - original_culture = CultureInfo.CurrentCulture - original_ui_culture = CultureInfo.CurrentUICulture - en_us_culture = CultureInfo("en-US") - CultureInfo.CurrentCulture = en_us_culture - CultureInfo.CurrentUICulture = en_us_culture + engine = Engine() try: - return engine.eval(formula, symbols=symbols) + from System.Globalization import ( # pyright: ignore[reportMissingImports] + CultureInfo, # pyright: ignore[reportUnknownVariableType] + ) + except ImportError: + return engine.eval(formula, symbols=symbols, locale=_POWERFX_EVAL_LOCALE) + + original_culture = cast(Any, CultureInfo.CurrentCulture) # pyright: ignore[reportUnknownMemberType] + try: + CultureInfo.CurrentCulture = CultureInfo(_POWERFX_EVAL_LOCALE) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + return engine.eval(formula, symbols=symbols, locale=_POWERFX_EVAL_LOCALE) finally: - CultureInfo.CurrentCulture = original_culture - CultureInfo.CurrentUICulture = original_ui_culture + CultureInfo.CurrentCulture = original_culture # pyright: ignore[reportUnknownMemberType] except ValueError as e: error_msg = str(e) # Handle undefined variable errors gracefully by returning None @@ -405,6 +422,8 @@ class DeclarativeWorkflowState: logger.debug(f"PowerFx: undefined variable in expression '{formula}', returning None") return None raise + finally: + locale.setlocale(locale.LC_NUMERIC, original_numeric_locale) def _eval_custom_function(self, formula: str) -> Any | None: """Handle custom functions not supported by the Python PowerFx library. @@ -424,7 +443,7 @@ class DeclarativeWorkflowState: args_str = match.group(1) # Parse comma-separated arguments (handling nested parentheses) args = self._parse_function_args(args_str) - evaluated_args = [] + evaluated_args: list[str] = [] for arg in args: arg = arg.strip() if arg.startswith('"') and arg.endswith('"'): @@ -576,37 +595,44 @@ class DeclarativeWorkflowState: """ messages: Any = self.eval(f"={inner_expr}") if isinstance(messages, list) and messages: - last_msg: Any = messages[-1] + message_list = cast(list[Any], messages) # type: ignore[redundant-cast] + last_msg: Any = message_list[-1] if isinstance(last_msg, dict): + last_msg_dict = cast(dict[str, Any], last_msg) # Try "text" key first (simple dict format) - if "text" in last_msg: - return str(last_msg["text"]) + if "text" in last_msg_dict: + return str(last_msg_dict["text"]) # Try extracting from "contents" (Message dict format) # Message.text concatenates text from all TextContent items - contents = last_msg.get("contents", []) - if isinstance(contents, list): - text_parts = [] + contents_obj = last_msg_dict.get("contents", []) + if isinstance(contents_obj, list): + contents = cast(list[Any], contents_obj) # type: ignore[redundant-cast] + text_parts: list[str] = [] for content in contents: if isinstance(content, dict): + content_dict = cast(dict[str, Any], content) # TextContent has a "text" key - if content.get("type") == "text" or "text" in content: - text_parts.append(str(content.get("text", ""))) - elif hasattr(content, "text"): - text_parts.append(str(getattr(content, "text", ""))) + if content_dict.get("type") == "text" or "text" in content_dict: + text_parts.append(str(content_dict.get("text", ""))) + else: + content_obj: object = content + if hasattr(content_obj, "text"): + text_parts.append(str(getattr(content_obj, "text", ""))) if text_parts: return " ".join(text_parts) return "" - if hasattr(last_msg, "text"): - return str(getattr(last_msg, "text", "")) + last_msg_obj: object = last_msg + if hasattr(last_msg_obj, "text"): + return str(getattr(last_msg_obj, "text", "")) return "" def _parse_function_args(self, args_str: str) -> list[str]: """Parse comma-separated function arguments, handling nested parentheses and strings.""" - args = [] - current = [] + args: list[str] = [] + current: list[str] = [] depth = 0 in_string = False - string_char = None + string_char: str | None = None for char in args_str: if char in ('"', "'") and not in_string: diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py index 65e129d921..6843c5bd92 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_declarative_builder.py @@ -14,7 +14,7 @@ action definitions and creates a proper workflow graph with: from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from agent_framework import ( Workflow, @@ -983,8 +983,9 @@ class DeclarativeWorkflowBuilder: last_executor = chain[-1] # Skip terminators — they handle their own control flow - action_def = getattr(last_executor, "_action_def", {}) - if isinstance(action_def, dict) and action_def.get("kind", "") in TERMINATOR_ACTIONS: + action_def_obj = getattr(last_executor, "_action_def", {}) + action_def = cast(dict[str, Any], action_def_obj) if isinstance(action_def_obj, dict) else {} + if action_def.get("kind", "") in TERMINATOR_ACTIONS: return None # Check if last executor is a structure with branch_exits diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py index c2fded5fb8..02cc6dab11 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_agents.py @@ -188,9 +188,9 @@ def _validate_conversation_history(messages: list[Message], agent_name: str) -> tool_result_ids: set[str] = set() for i, msg in enumerate(messages): - if not hasattr(msg, "contents") or msg.contents is None: + if not (contents := getattr(msg, "contents", None)): continue - for content in msg.contents: + for content in contents: if content.type == "function_call" and content.call_id: tool_call_ids.add(content.call_id) logger.debug( diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py index 4643cfd34b..677fd1aac8 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_basic.py @@ -7,7 +7,8 @@ Each action becomes a node in the workflow graph. """ import uuid -from typing import Any +from collections.abc import Mapping +from typing import Any, cast from agent_framework import ( WorkflowContext, @@ -28,9 +29,12 @@ def _get_variable_path(action_def: dict[str, Any], key: str = "variable") -> str variable = action_def.get(key) if isinstance(variable, str): return variable - if isinstance(variable, dict): - return variable.get("path") - return action_def.get("path") + if isinstance(variable, Mapping): + path = variable.get("path") # type: ignore[reportUnknownVariableType] + return path if isinstance(path, str) else None + + fallback_path = action_def.get("path") + return fallback_path if isinstance(fallback_path, str) else None class SetValueExecutor(DeclarativeActionExecutor): @@ -150,16 +154,23 @@ class SetMultipleVariablesExecutor(DeclarativeActionExecutor): """Handle the SetMultipleVariables action.""" state = await self._ensure_state_initialized(ctx, trigger) - assignments = self._action_def.get("assignments", []) + assignments = cast( + list[Mapping[str, Any]], + self._action_def.get("assignments") if isinstance(self._action_def.get("assignments"), list) else [], + ) for assignment in assignments: + if not isinstance(assignment, Mapping): + continue variable = assignment.get("variable") path: str | None if isinstance(variable, str): path = variable - elif isinstance(variable, dict): - path = variable.get("path") + elif isinstance(variable, Mapping): + path_value = variable.get("path") # type: ignore[reportUnknownMemberType] + path = path_value if isinstance(path_value, str) else None else: - path = assignment.get("path") + fallback_path = assignment.get("path") + path = fallback_path if isinstance(fallback_path, str) else None value = assignment.get("value") if path: evaluated_value = state.eval_if_expression(value) @@ -249,7 +260,10 @@ class SendActivityExecutor(DeclarativeActionExecutor): activity = self._action_def.get("activity", "") # Activity can be a string directly or a dict with a "text" field - text = activity.get("text", "") if isinstance(activity, dict) else activity + if isinstance(activity, Mapping): + text: Any = activity.get("text", "") # type: ignore[reportUnknownMemberType] + else: + text = activity if isinstance(text, str): # First evaluate any =expression syntax @@ -260,7 +274,7 @@ class SendActivityExecutor(DeclarativeActionExecutor): # Yield the text as workflow output if text: - await ctx.yield_output(str(text)) + await ctx.yield_output(str(text)) # type: ignore[reportUnknownArgumentType] await ctx.send_message(ActionComplete()) @@ -336,11 +350,14 @@ class EditTableExecutor(DeclarativeActionExecutor): if table_path: # Get current table value - current_table = state.get(table_path) - if current_table is None: + current_table_value = state.get(table_path) + current_table: list[Any] + if current_table_value is None: current_table = [] - elif not isinstance(current_table, list): - current_table = [current_table] + elif isinstance(current_table_value, list): + current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType] + else: + current_table = [current_table_value] if operation == "add" or operation == "insert": evaluated_value = state.eval_if_expression(value) @@ -413,11 +430,14 @@ class EditTableV2Executor(DeclarativeActionExecutor): if table_path: # Get current table value - current_table = state.get(table_path) - if current_table is None: + current_table_value = state.get(table_path) + current_table: list[Any] + if current_table_value is None: current_table = [] - elif not isinstance(current_table, list): - current_table = [current_table] + elif isinstance(current_table_value, list): + current_table = list(current_table_value) # type: ignore[reportUnknownArgumentType] + else: + current_table = [current_table_value] if operation == "add": evaluated_item = state.eval_if_expression(item) @@ -433,9 +453,12 @@ class EditTableV2Executor(DeclarativeActionExecutor): evaluated_item = state.eval_if_expression(item) if key_field and isinstance(evaluated_item, dict): # Remove by key match - key_value = evaluated_item.get(key_field) + evaluated_item_dict = cast(dict[str, Any], evaluated_item) + key_value = evaluated_item_dict.get(key_field) current_table = [ - r for r in current_table if not (isinstance(r, dict) and r.get(key_field) == key_value) + r + for r in current_table + if not (isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value) ] elif evaluated_item in current_table: current_table.remove(evaluated_item) @@ -451,11 +474,11 @@ class EditTableV2Executor(DeclarativeActionExecutor): elif operation == "addorupdate": evaluated_item = state.eval_if_expression(item) if key_field and isinstance(evaluated_item, dict): - key_value = evaluated_item.get(key_field) + key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType] # Find existing item with same key found_idx = -1 for i, r in enumerate(current_table): - if isinstance(r, dict) and r.get(key_field) == key_value: + if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value: found_idx = i break if found_idx >= 0: @@ -476,9 +499,9 @@ class EditTableV2Executor(DeclarativeActionExecutor): if 0 <= idx < len(current_table): current_table[idx] = evaluated_item elif key_field and isinstance(evaluated_item, dict): - key_value = evaluated_item.get(key_field) + key_value = evaluated_item.get(key_field) # type: ignore[reportUnknownArgumentType] for i, r in enumerate(current_table): - if isinstance(r, dict) and r.get(key_field) == key_value: + if isinstance(r, dict) and cast(dict[str, Any], r).get(key_field) == key_value: current_table[i] = evaluated_item break @@ -568,11 +591,13 @@ class ParseValueExecutor(DeclarativeActionExecutor): if value is None: return {} if isinstance(value, dict): - return value + return cast(dict[str, Any], value) if isinstance(value, str): try: parsed = json.loads(value) - return parsed if isinstance(parsed, dict) else {"value": parsed} + if isinstance(parsed, dict): + return cast(dict[str, Any], parsed) + return {"value": parsed} except json.JSONDecodeError: return {"value": value} return {"value": value} @@ -581,11 +606,13 @@ class ParseValueExecutor(DeclarativeActionExecutor): if value is None: return [] if isinstance(value, list): - return value + return cast(list[Any], value) # type: ignore[redundant-cast] if isinstance(value, str): try: parsed = json.loads(value) - return parsed if isinstance(parsed, list) else [parsed] + if isinstance(parsed, list): + return cast(list[Any], parsed) # type: ignore[redundant-cast] + return [parsed] except json.JSONDecodeError: return [value] return [value] diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_tools.py b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_tools.py index 829d48103f..85aa4f6a5a 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_executors_tools.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_executors_tools.py @@ -15,9 +15,11 @@ import json import logging import uuid from abc import abstractmethod +from collections.abc import Mapping from dataclasses import dataclass, field from inspect import isawaitable -from typing import Any +from typing import Any, cast +from collections.abc import Callable from agent_framework import ( Content, @@ -127,7 +129,7 @@ class ToolInvocationResult: success: bool result: Any = None error: str | None = None - messages: list[Message] = field(default_factory=list) + messages: list[Message] = field(default_factory=cast(Callable[..., list[Message]], list)) rejected: bool = False rejection_reason: str | None = None @@ -267,15 +269,14 @@ class BaseToolExecutor(DeclarativeActionExecutor): Returns: Tuple of (messages_var, result_var, auto_send) """ - output_config = self._action_def.get("output", {}) + output_config: dict[str, str | bool] = self._action_def.get("output", {}) - if not isinstance(output_config, dict): + if not isinstance(output_config, Mapping): return None, None, True messages_var = output_config.get("messages") result_var = output_config.get("result") auto_send = bool(output_config.get("autoSend", True)) - return ( str(messages_var) if messages_var else None, str(result_var) if result_var else None, @@ -494,7 +495,7 @@ class BaseToolExecutor(DeclarativeActionExecutor): type(arguments_def).__name__, ) elif isinstance(arguments_def, dict): - for key, value in arguments_def.items(): + for key, value in arguments_def.items(): # type: ignore[reportUnknownVariableType] arguments[key] = state.eval_if_expression(value) # Check if approval is required diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_powerfx_functions.py b/python/packages/declarative/agent_framework_declarative/_workflows/_powerfx_functions.py index df66ef59fd..f61120a469 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_powerfx_functions.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_powerfx_functions.py @@ -44,14 +44,16 @@ def message_text(messages: Any) -> str: content: Any = messages_dict.get("content", "") if isinstance(content, str): return content - if hasattr(content, "text"): - return str(content.text) + text_attr = getattr(content, "text", None) + if text_attr is not None: + return str(text_attr) return str(content) if content else "" if isinstance(messages, list): # List of messages - concatenate all text texts: list[str] = [] - for msg in messages: + message_list = cast(list[Any], messages) # type: ignore[redundant-cast] + for msg in message_list: if isinstance(msg, str): texts.append(msg) elif isinstance(msg, dict): @@ -61,14 +63,16 @@ def message_text(messages: Any) -> str: texts.append(msg_content) elif msg_content: texts.append(str(msg_content)) - elif hasattr(msg, "content"): - msg_obj_content: Any = msg.content - if isinstance(msg_obj_content, str): - texts.append(msg_obj_content) - elif hasattr(msg_obj_content, "text"): - texts.append(str(msg_obj_content.text)) - elif msg_obj_content: - texts.append(str(msg_obj_content)) + else: + msg_obj: object = msg + if hasattr(msg_obj, "content"): + msg_obj_content: Any = getattr(msg_obj, "content", None) + if isinstance(msg_obj_content, str): + texts.append(msg_obj_content) + elif (msg_obj_text := getattr(msg_obj_content, "text", None)) is not None: + texts.append(str(msg_obj_text)) + elif msg_obj_content: + texts.append(str(msg_obj_content)) return " ".join(texts) # Try to get text attribute @@ -191,10 +195,8 @@ def is_blank(value: Any) -> bool: return True if isinstance(value, str) and not value.strip(): return True - if isinstance(value, list): - return len(value) == 0 - if isinstance(value, dict): - return len(value) == 0 + if isinstance(value, (list, dict)): + return len(value) == 0 # type: ignore[reportUnknownArgumentType] return False diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_state.py b/python/packages/declarative/agent_framework_declarative/_workflows/_state.py index 7417fa26fe..76530f50dd 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_state.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_state.py @@ -284,8 +284,9 @@ class WorkflowState: if existing is None: self.set(path, [value]) elif isinstance(existing, list): - existing.append(value) - self.set(path, existing) + existing_list = cast(list[Any], existing) # type: ignore[redundant-cast] + existing_list.append(value) + self.set(path, existing_list) else: raise ValueError(f"Cannot append to non-list at path '{path}'") @@ -614,9 +615,9 @@ class WorkflowState: if isinstance(value, str): return self.eval(value) if isinstance(value, dict): - return {str(k): self.eval_if_expression(v) for k, v in value.items()} + return {str(k): self.eval_if_expression(v) for k, v in value.items()} # type: ignore[reportUnknownVariableType] if isinstance(value, list): - return [self.eval_if_expression(item) for item in value] + return [self.eval_if_expression(item) for item in value] # type: ignore[reportUnknownVariableType] return value def reset_local(self) -> None: diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index d2462353e7..2534339ad7 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -94,7 +94,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative" -test = "pytest --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/declarative/tests/test_declarative_loader.py b/python/packages/declarative/tests/test_declarative_loader.py index aee0d762d9..2ca87bfa65 100644 --- a/python/packages/declarative/tests/test_declarative_loader.py +++ b/python/packages/declarative/tests/test_declarative_loader.py @@ -560,8 +560,6 @@ instructions: You are a helpful assistant. """Test that outputSchema is passed as response_format in Agent.default_options.""" from unittest.mock import MagicMock - from pydantic import BaseModel - from agent_framework_declarative import AgentFactory agent_def = { @@ -580,8 +578,10 @@ instructions: You are a helpful assistant. agent = factory.create_agent_from_dict(agent_def) assert "response_format" in agent.default_options - assert isinstance(agent.default_options["response_format"], type) - assert issubclass(agent.default_options["response_format"], BaseModel) + response_format = agent.default_options["response_format"] + assert isinstance(response_format, dict) + assert response_format["type"] == "object" + assert response_format["properties"]["answer"]["type"] == "string" def test_create_agent_from_dict_chat_options_in_default_options(self): """Test that chat options (temperature, top_p) are in Agent.default_options.""" diff --git a/python/packages/declarative/tests/test_powerfx_yaml_compatibility.py b/python/packages/declarative/tests/test_powerfx_yaml_compatibility.py index 8ea3c3af57..308982c632 100644 --- a/python/packages/declarative/tests/test_powerfx_yaml_compatibility.py +++ b/python/packages/declarative/tests/test_powerfx_yaml_compatibility.py @@ -16,6 +16,7 @@ Coverage includes: - String interpolation: {Variable.Path} """ +import locale from unittest.mock import MagicMock import pytest @@ -494,29 +495,38 @@ class TestPowerFxUndefinedVariables: assert result is None async def test_undefined_variable_returns_none_with_non_english_ui_culture(self, mock_state): - """Test that undefined variables return None even when CurrentUICulture is non-English. + """Test that undefined variables return None even when locale is non-English. - Regression test for #4321: on non-English systems, CurrentUICulture causes + Regression test for #4321: on non-English systems, locale settings can cause PowerFx to emit localized error messages that don't match the English string guards ("isn't recognized", "Name isn't valid"), crashing the workflow. - The fix sets CurrentUICulture to en-US alongside CurrentCulture before eval. + The fix evaluates with locale='en-US' and restores the ambient LC_NUMERIC. """ - from System.Globalization import CultureInfo - state = DeclarativeWorkflowState(mock_state) state.initialize() - # Simulate a non-English UI culture (e.g. Italian) - original_ui_culture = CultureInfo.CurrentUICulture - CultureInfo.CurrentUICulture = CultureInfo("it-IT") + # Simulate a non-English locale (e.g. Italian) + original_numeric_locale = locale.setlocale(locale.LC_NUMERIC) + test_numeric_locale: str | None = None try: + for locale_candidate in ("it_IT.UTF-8", "it_IT", "fr_FR.UTF-8", "fr_FR", "de_DE.UTF-8", "de_DE"): + try: + locale.setlocale(locale.LC_NUMERIC, locale_candidate) + test_numeric_locale = locale.setlocale(locale.LC_NUMERIC) + break + except locale.Error: + continue + + if test_numeric_locale is None: + pytest.skip("No non-English LC_NUMERIC locale available on this system") + # Should return None, not raise ValueError with Italian error text result = state.eval("=Local.StatusConversationId") assert result is None - # Verify the production code restored CurrentUICulture after eval - assert str(CultureInfo.CurrentUICulture) == str(CultureInfo("it-IT")) + # Verify the production code restored LC_NUMERIC after eval + assert locale.setlocale(locale.LC_NUMERIC) == test_numeric_locale finally: - CultureInfo.CurrentUICulture = original_ui_culture + locale.setlocale(locale.LC_NUMERIC, original_numeric_locale) class TestStringInterpolation: diff --git a/python/packages/devui/agent_framework_devui/__init__.py b/python/packages/devui/agent_framework_devui/__init__.py index f703e85a63..6af274743a 100644 --- a/python/packages/devui/agent_framework_devui/__init__.py +++ b/python/packages/devui/agent_framework_devui/__init__.py @@ -73,7 +73,7 @@ def register_cleanup(entity: Any, *hooks: Callable[[], Any]) -> None: ) -def _get_registered_cleanup_hooks(entity: Any) -> list[Callable[[], Any]]: +def _get_registered_cleanup_hooks(entity: Any) -> list[Callable[[], Any]]: # type: ignore[reportUnusedFunction] """Get cleanup hooks registered for an entity (internal use). Args: @@ -193,7 +193,7 @@ def serve( if entities: logger.info(f"Registering {len(entities)} in-memory entities") # Store entities for later registration during server startup - server._pending_entities = entities + server.set_pending_entities(entities) app = server.get_app() diff --git a/python/packages/devui/agent_framework_devui/_conversations.py b/python/packages/devui/agent_framework_devui/_conversations.py index f0e91e0d87..8130835002 100644 --- a/python/packages/devui/agent_framework_devui/_conversations.py +++ b/python/packages/devui/agent_framework_devui/_conversations.py @@ -11,12 +11,14 @@ from __future__ import annotations import time import uuid from abc import ABC, abstractmethod +from collections.abc import MutableSequence from typing import Any, Literal, cast from agent_framework import AgentSession, Message from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage, WorkflowCheckpoint from openai.types.conversations import Conversation, ConversationDeletedResource from openai.types.conversations.conversation_item import ConversationItem +from openai.types.conversations.message import Content as OpenAIContent from openai.types.conversations.message import Message as OpenAIMessage from openai.types.conversations.text_content import TextContent from openai.types.responses import ( @@ -300,12 +302,17 @@ class InMemoryConversationStore(ConversationStore): stored_messages: list[Message] = conv_data["messages"] # Convert items to Messages and add to storage - chat_messages = [] + chat_messages: list[Message] = [] for item in items: # Simple conversion - assume text content for now role = item.get("role", "user") content = item.get("content", []) - text = content[0].get("text", "") if content else "" + first_content = cast( + dict[str, Any], + content[0] if content and isinstance(content, list) and isinstance(content[0], dict) else {}, + ) + text_obj = first_content.get("text", "") + text = text_obj if isinstance(text_obj, str) else str(text_obj) chat_msg = Message(role=role, text=text) # type: ignore[arg-type] chat_messages.append(chat_msg) @@ -318,23 +325,18 @@ class InMemoryConversationStore(ConversationStore): for msg in chat_messages: item_id = f"item_{uuid.uuid4().hex}" - # Extract role - handle both string and enum - role_str = msg.role if hasattr(msg.role, "value") else str(msg.role) - role = cast(MessageRole, role_str) # Safe: Agent Framework roles match OpenAI roles - # Convert Message contents to OpenAI TextContent format - message_content = [] + message_content: MutableSequence[OpenAIContent] = [] for content_item in msg.contents: if content_item.type == "text": # Extract text from TextContent object - text_value = getattr(content_item, "text", "") - message_content.append(TextContent(type="text", text=text_value)) + message_content.append(TextContent(type="text", text=content_item.text or "")) # Create Message object (concrete type from ConversationItem union) message = OpenAIMessage( id=item_id, type="message", # Required discriminator for union - role=role, + role=cast(MessageRole, msg.role), # Safe: Agent Framework roles match OpenAI roles, content=message_content, status="completed", # Required field ) @@ -383,8 +385,8 @@ class InMemoryConversationStore(ConversationStore): # A single Message may produce multiple ConversationItems # (e.g., a message with both text and a function call) message_contents: list[TextContent | ResponseInputImage | ResponseInputFile] = [] - function_calls = [] - function_results = [] + function_calls: list[ResponseFunctionToolCallItem] = [] + function_results: list[ResponseFunctionToolCallOutputItem] = [] for content in msg.contents: content_type = getattr(content, "type", None) @@ -628,7 +630,7 @@ class InMemoryConversationStore(ConversationStore): async def list_conversations_by_metadata(self, metadata_filter: dict[str, str]) -> list[Conversation]: """Filter conversations by metadata (e.g., agent_id).""" - results = [] + results: list[Conversation] = [] for conv_data in self._conversations.values(): conv_meta = conv_data.get("metadata", {}).copy() # Copy to avoid mutating original @@ -704,7 +706,8 @@ class CheckpointConversationManager: ValueError: If conversation not found """ # Access internal conversations dict (we know it's InMemoryConversationStore) - conv_data = self._store._conversations.get(conversation_id) + conversations_dict = cast(dict[str, dict[str, Any]], getattr(self._store, "_conversations", {})) + conv_data = conversations_dict.get(conversation_id) if not conv_data: raise ValueError(f"Conversation {conversation_id} not found") diff --git a/python/packages/devui/agent_framework_devui/_deployment.py b/python/packages/devui/agent_framework_devui/_deployment.py index db2de27ecf..34147db1f9 100644 --- a/python/packages/devui/agent_framework_devui/_deployment.py +++ b/python/packages/devui/agent_framework_devui/_deployment.py @@ -10,6 +10,7 @@ import uuid from collections.abc import AsyncGenerator from datetime import datetime, timezone from pathlib import Path +from typing import cast from urllib.parse import urlparse from .models._discovery_models import Deployment, DeploymentConfig, DeploymentEvent @@ -175,7 +176,7 @@ class DeploymentManager: # Check required resource providers are registered required_providers = ["Microsoft.App", "Microsoft.ContainerRegistry", "Microsoft.OperationalInsights"] - unregistered_providers = [] + unregistered_providers: list[str] = [] # Get list of registered providers provider_check = await asyncio.create_subprocess_exec( @@ -195,7 +196,12 @@ class DeploymentManager: import json try: - registered = json.loads(stdout.decode()) + registered_raw = json.loads(stdout.decode()) + registered: list[str] = [] + if isinstance(registered_raw, list): + for item_obj in cast(list[object], registered_raw): + if isinstance(item_obj, str): + registered.append(item_obj) for provider in required_providers: if provider not in registered: unregistered_providers.append(provider) @@ -385,7 +391,7 @@ CMD ["devui", "/app/entity", "--mode", "{config.ui_mode}", "--host", "0.0.0.0", ) # Stream output line by line - output_lines = [] + output_lines: list[str] = [] try: if not process.stdout: raise ValueError("Failed to capture process output") @@ -473,8 +479,11 @@ CMD ["devui", "/app/entity", "--mode", "{config.ui_mode}", "--host", "0.0.0.0", for url in urls: # Strip common trailing punctuation to ensure clean URL parsing url_clean = url.rstrip(".,;:!?'\")}]") - host = urlparse(url_clean).hostname - if host and (host == "azurecontainerapps.io" or host.endswith(".azurecontainerapps.io")): + parsed_url = urlparse(str(url_clean)) + host = parsed_url.hostname + if isinstance(host, str) and ( + host == "azurecontainerapps.io" or host.endswith(".azurecontainerapps.io") + ): await event_queue.put( DeploymentEvent(type="deploy.progress", message="Deployment URL generated!") ) diff --git a/python/packages/devui/agent_framework_devui/_discovery.py b/python/packages/devui/agent_framework_devui/_discovery.py index a5fada1ba9..372e870c15 100644 --- a/python/packages/devui/agent_framework_devui/_discovery.py +++ b/python/packages/devui/agent_framework_devui/_discovery.py @@ -11,7 +11,7 @@ import logging import sys import uuid from pathlib import Path -from typing import Any +from typing import Any, cast from dotenv import load_dotenv @@ -141,7 +141,7 @@ class EntityDiscovery: self._loaded_objects[entity_id] = entity_obj # Check module-level registry for cleanup hooks - from . import _get_registered_cleanup_hooks + from . import _get_registered_cleanup_hooks # type: ignore[reportPrivateUsage] registered_hooks = _get_registered_cleanup_hooks(entity_obj) if registered_hooks: @@ -299,7 +299,7 @@ class EntityDiscovery: self._loaded_objects[entity_id] = entity_object # Check module-level registry for cleanup hooks - from . import _get_registered_cleanup_hooks + from . import _get_registered_cleanup_hooks # type: ignore[reportPrivateUsage] registered_hooks = _get_registered_cleanup_hooks(entity_object) if registered_hooks: @@ -379,6 +379,8 @@ class EntityDiscovery: deployment_supported = True deployment_reason = "Ready for deployment (pending path verification)" + class_name = type(entity_object).__name__ + # Create EntityInfo with Agent Framework specifics return EntityInfo( id=entity_id, @@ -400,9 +402,7 @@ class EntityDiscovery: deployment_reason=deployment_reason, metadata={ "source": "agent_framework_object", - "class_name": entity_object.__class__.__name__ - if hasattr(entity_object, "__class__") - else str(type(entity_object)), + "class_name": class_name, }, ) @@ -854,7 +854,7 @@ class EntityDiscovery: "module_path": module_path, "entity_type": obj_type, "source": source, - "class_name": obj.__class__.__name__ if hasattr(obj, "__class__") else str(type(obj)), + "class_name": type(obj).__name__, }, ) @@ -874,47 +874,63 @@ class EntityDiscovery: Returns: List of tool/executor names """ - tools = [] + tools: list[str] = [] try: if obj_type == "agent": - # For agents, check default_options.get("tools") chat_options = getattr(obj, "default_options", None) - chat_options_tools = None - if chat_options: - chat_options_tools = chat_options.get("tools") + chat_options_tools: object | None = None + if isinstance(chat_options, dict): + chat_options_dict = cast(dict[str, Any], chat_options) + chat_options_tools = chat_options_dict.get("tools") - if chat_options_tools: - for tool in chat_options_tools: - if hasattr(tool, "__name__"): - tools.append(tool.__name__) - elif hasattr(tool, "name"): - tools.append(tool.name) + if chat_options_tools is not None: + tool_iterable: list[object] = ( + cast(list[object], chat_options_tools) + if isinstance(chat_options_tools, list) + else [chat_options_tools] + ) + for tool_obj in tool_iterable: + tool_name = getattr(tool_obj, "__name__", None) + if isinstance(tool_name, str): + tools.append(tool_name) + continue + + named_tool = getattr(tool_obj, "name", None) + if isinstance(named_tool, str): + tools.append(named_tool) else: - tools.append(str(tool)) + tools.append(str(tool_obj)) else: - # Fallback to direct tools attribute agent_tools = getattr(obj, "tools", None) - if agent_tools: - for tool in agent_tools: - if hasattr(tool, "__name__"): - tools.append(tool.__name__) - elif hasattr(tool, "name"): - tools.append(tool.name) + if isinstance(agent_tools, list): + for tool_obj in cast(list[object], agent_tools): + tool_name = getattr(tool_obj, "__name__", None) + if isinstance(tool_name, str): + tools.append(tool_name) + continue + + named_tool = getattr(tool_obj, "name", None) + if isinstance(named_tool, str): + tools.append(named_tool) else: - tools.append(str(tool)) + tools.append(str(tool_obj)) elif obj_type == "workflow": - # For workflows, extract executor names if hasattr(obj, "get_executors_list"): executor_objects = obj.get_executors_list() - tools = [getattr(ex, "id", str(ex)) for ex in executor_objects] + if isinstance(executor_objects, list): + for executor_obj in cast(list[object], executor_objects): + tools.append(str(getattr(executor_obj, "id", executor_obj))) elif hasattr(obj, "executors"): executors = obj.executors if isinstance(executors, list): - tools = [getattr(ex, "id", str(ex)) for ex in executors] + for executor_obj in cast(list[object], executors): + tools.append(str(getattr(executor_obj, "id", executor_obj))) elif isinstance(executors, dict): - tools = list(executors.keys()) + executors_dict = cast(dict[str, Any], executors) + for key_obj in executors_dict: + tools.append(str(key_obj)) except Exception as e: logger.debug(f"Error extracting tools from {obj_type} {type(obj)}: {e}") diff --git a/python/packages/devui/agent_framework_devui/_executor.py b/python/packages/devui/agent_framework_devui/_executor.py index 1b1b77162a..3f732dd80c 100644 --- a/python/packages/devui/agent_framework_devui/_executor.py +++ b/python/packages/devui/agent_framework_devui/_executor.py @@ -7,7 +7,7 @@ from __future__ import annotations import json import logging from collections.abc import AsyncGenerator -from typing import Any +from typing import Any, cast from agent_framework import Content, SupportsAgentRun, Workflow @@ -24,7 +24,8 @@ logger = logging.getLogger(__name__) def _get_event_type(event: Any) -> str | None: """Safely get the type of an event, handling both objects and dicts.""" if isinstance(event, dict): - return event.get("type") + event_type = cast(dict[str, Any], event).get("type") + return event_type if isinstance(event_type, str) else None return getattr(event, "type", None) @@ -71,7 +72,8 @@ class AgentFrameworkExecutor: from opentelemetry.sdk.trace import TracerProvider # Only set up if no provider exists yet - if not hasattr(trace, "_TRACER_PROVIDER") or trace._TRACER_PROVIDER is None: + current_provider = trace.get_tracer_provider() + if current_provider.__class__.__name__ == "ProxyTracerProvider": resource = Resource.create({ "service.name": "agent-framework-server", "service.version": "1.0.0", @@ -94,21 +96,29 @@ class AgentFrameworkExecutor: # Configure if instrumentation is enabled (via enable_instrumentation() or env var) if OBSERVABILITY_SETTINGS.ENABLED: - # Only configure providers if not already executed - if not OBSERVABILITY_SETTINGS._executed_setup: - # Call configure_otel_providers to set up exporters. - # If OTEL_EXPORTER_OTLP_ENDPOINT is set, exporters will be created automatically. - # If not set, no exporters are created (no console spam), but DevUI's - # TracerProvider from _setup_instrumentation_provider() remains active for local capture. - configure_otel_providers(enable_sensitive_data=OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED) - logger.info("Enabled Agent Framework observability") - else: - logger.debug("Agent Framework observability already configured") + # Call configure_otel_providers to set up exporters. + # If OTEL_EXPORTER_OTLP_ENDPOINT is set, exporters will be created automatically. + # If not set, no exporters are created (no console spam), but DevUI's + # TracerProvider from _setup_instrumentation_provider() remains active for local capture. + configure_otel_providers(enable_sensitive_data=OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED) + logger.info("Enabled Agent Framework observability") else: logger.debug("Instrumentation not enabled, skipping observability setup") except Exception as e: logger.warning(f"Failed to enable Agent Framework observability: {e}") + def _get_request_conversation_id(self, request: AgentFrameworkRequest) -> str | None: + """Read conversation id using public request fields.""" + if isinstance(request.conversation, str): + return request.conversation + + if isinstance(request.conversation, dict): + conversation_id = request.conversation.get("id") + if isinstance(conversation_id, str): + return conversation_id + + return None + async def _ensure_mcp_connections(self, agent: Any) -> None: """Ensure MCP tool connections are healthy before agent execution. @@ -317,7 +327,7 @@ class AgentFrameworkExecutor: # Get session from conversation parameter (OpenAI standard!) session = None - conversation_id = request._get_conversation_id() + conversation_id = self._get_request_conversation_id(request) if conversation_id: session = self.conversation_store.get_session(conversation_id) if session: @@ -344,7 +354,7 @@ class AgentFrameworkExecutor: if session: run_kwargs["session"] = session - stream = agent.run(user_message, **run_kwargs) + stream = cast(Any, agent.run(user_message, **run_kwargs)) async for update in stream: for trace_event in trace_collector.get_pending_events(): yield trace_event @@ -388,7 +398,7 @@ class AgentFrameworkExecutor: entity_id = request.get_entity_id() or "unknown" # Get or create session conversation for checkpoint storage - conversation_id = request._get_conversation_id() + conversation_id = self._get_request_conversation_id(request) if not conversation_id: # Create default session if not provided import time @@ -463,11 +473,14 @@ class AgentFrameworkExecutor: logger.info(f"Resuming workflow with HIL responses for {len(hil_responses)} request(s)") # Unwrap primitive responses if they're wrapped in {response: value} format - unwrapped_responses = {} + unwrapped_responses: dict[str, Any] = {} for request_id, response_value in hil_responses.items(): - if isinstance(response_value, dict) and "response" in response_value: - response_value = response_value["response"] - unwrapped_responses[request_id] = response_value + normalized_response: Any = response_value + if isinstance(response_value, dict): + response_dict = cast(dict[str, Any], response_value) + if "response" in response_dict: + normalized_response = response_dict["response"] + unwrapped_responses[request_id] = normalized_response hil_responses = unwrapped_responses @@ -568,7 +581,8 @@ class AgentFrameworkExecutor: # Handle OpenAI ResponseInputParam (List[ResponseInputItemParam]) if isinstance(input_data, list): - return self._convert_openai_input_to_chat_message(input_data, Message, Role) + input_items: Any = cast(Any, input_data) + return self._convert_openai_input_to_chat_message(input_items, Message, Role) # Fallback for other formats return self._extract_user_message_fallback(input_data) @@ -593,27 +607,31 @@ class AgentFrameworkExecutor: for item in input_items: # Handle dict format (from JSON) if isinstance(item, dict): - item_type = item.get("type") + item_dict = cast(dict[str, Any], item) + item_type = item_dict.get("type") if item_type == "message": # Extract content from OpenAI message - message_content = item.get("content", []) + message_content = item_dict.get("content", []) # Handle both string content and list content if isinstance(message_content, str): contents.append(Content.from_text(text=message_content)) elif isinstance(message_content, list): - for content_item in message_content: + message_content_items: Any = cast(Any, message_content) + for content_item in message_content_items: # Handle dict content items if isinstance(content_item, dict): - content_type = content_item.get("type") + content_dict = cast(dict[str, Any], content_item) + content_type = content_dict.get("type") if content_type == "input_text": - text = content_item.get("text", "") - contents.append(Content.from_text(text=text)) + text = content_dict.get("text", "") + if isinstance(text, str): + contents.append(Content.from_text(text=text)) elif content_type == "input_image": - image_url = content_item.get("image_url", "") - if image_url: + image_url = content_dict.get("image_url", "") + if isinstance(image_url, str) and image_url: # Extract media type from data URI if possible # Parse media type from data URL, fallback to image/png if image_url.startswith("data:"): @@ -631,9 +649,12 @@ class AgentFrameworkExecutor: elif content_type == "input_file": # Handle file input - file_data = content_item.get("file_data") - file_url = content_item.get("file_url") - filename = content_item.get("filename", "") + file_data = content_dict.get("file_data") + file_url = content_dict.get("file_url") + filename = content_dict.get("filename", "") + + if not isinstance(filename, str): + filename = "" # Determine media type from filename media_type = "application/octet-stream" # default @@ -656,8 +677,10 @@ class AgentFrameworkExecutor: # Use file_data or file_url # Include filename in additional_properties for OpenAI/Azure file handling - additional_props = {"filename": filename} if filename else None - if file_data: + additional_props: dict[str, Any] | None = ( + {"filename": filename} if filename else None + ) + if isinstance(file_data, str) and file_data: # Assume file_data is base64, create data URI data_uri = f"data:{media_type};base64,{file_data}" contents.append( @@ -667,7 +690,7 @@ class AgentFrameworkExecutor: additional_properties=additional_props, ) ) - elif file_url: + elif isinstance(file_url, str) and file_url: contents.append( Content.from_uri( uri=file_url, @@ -679,15 +702,35 @@ class AgentFrameworkExecutor: elif content_type == "function_approval_response": # Handle function approval response (DevUI extension) try: - request_id = content_item.get("request_id", "") - approved = content_item.get("approved", False) - function_call_data = content_item.get("function_call", {}) + request_id = content_dict.get("request_id", "") + approved = content_dict.get("approved", False) + function_call_data = content_dict.get("function_call", {}) + + if not isinstance(request_id, str): + request_id = "" + if not isinstance(approved, bool): + approved = False + if not isinstance(function_call_data, dict): + function_call_data = {} + + function_call_data_dict = cast(dict[str, Any], function_call_data) + + function_call_id = function_call_data_dict.get("id", "") + function_call_name = function_call_data_dict.get("name", "") + function_call_args = function_call_data_dict.get("arguments", {}) + + if not isinstance(function_call_id, str): + function_call_id = "" + if not isinstance(function_call_name, str): + function_call_name = "" + if not isinstance(function_call_args, dict): + function_call_args = {} # Create FunctionCallContent from the function_call data function_call = Content.from_function_call( - call_id=function_call_data.get("id", ""), - name=function_call_data.get("name", ""), - arguments=function_call_data.get("arguments", {}), + call_id=function_call_id, + name=function_call_name, + arguments=cast(dict[str, Any], function_call_args), ) # Create FunctionApprovalResponseContent with correct signature @@ -739,12 +782,14 @@ class AgentFrameworkExecutor: if isinstance(input_data, str): return input_data if isinstance(input_data, dict): + typed_input_data = cast(dict[str, Any], input_data) # Try common field names for field in ["message", "text", "input", "content", "query"]: - if field in input_data: - return str(input_data[field]) + if field in typed_input_data: + value = typed_input_data[field] + return value if isinstance(value, str) else str(value) # Fallback to JSON string - return json.dumps(input_data) + return json.dumps(typed_input_data) return str(input_data) def _is_openai_multimodal_format(self, input_data: Any) -> bool: @@ -758,8 +803,12 @@ class AgentFrameworkExecutor: """ if not isinstance(input_data, list) or not input_data: return False - first_item = input_data[0] - return isinstance(first_item, dict) and first_item.get("type") == "message" + input_data_items: Any = cast(Any, input_data) + first_item = input_data_items[0] + if not isinstance(first_item, dict): + return False + first_type = cast(dict[str, Any], first_item).get("type") + return isinstance(first_type, str) and first_type == "message" async def _parse_workflow_input(self, workflow: Any, raw_input: Any) -> Any: """Parse input based on workflow's expected input type. @@ -775,7 +824,7 @@ class AgentFrameworkExecutor: # Handle JSON string input (from frontend api.ts JSON.stringify) if isinstance(raw_input, str): try: - parsed = json.loads(raw_input) + parsed: Any = json.loads(raw_input) raw_input = parsed except (json.JSONDecodeError, TypeError): # Plain text string, continue with string handling @@ -789,14 +838,14 @@ class AgentFrameworkExecutor: # Handle structured input (dict) if isinstance(raw_input, dict): - return self._parse_structured_workflow_input(workflow, raw_input) + return self._parse_structured_workflow_input(workflow, cast(dict[str, Any], raw_input)) # Handle string input return self._parse_raw_workflow_input(workflow, str(raw_input)) except Exception as e: logger.warning(f"Error parsing workflow input: {e}") - return raw_input + return cast(Any, raw_input) def _get_start_executor_message_types(self, workflow: Any) -> tuple[Any | None, list[Any]]: """Return start executor and its declared input types.""" @@ -823,7 +872,8 @@ class AgentFrameworkExecutor: try: handlers = start_executor._handlers if isinstance(handlers, dict): - message_types = list(handlers.keys()) + handlers_dict: Any = cast(Any, handlers) + message_types = list(handlers_dict.keys()) except Exception as exc: # pragma: no cover - defensive logging path logger.debug(f"Failed to read executor handlers: {exc}") @@ -847,7 +897,8 @@ class AgentFrameworkExecutor: parsed = json.loads(input_data) # Only use parsed value if it's a list (ResponseInputParam format expected for HIL) if isinstance(parsed, list): - input_data = parsed + parsed_list: Any = cast(Any, parsed) + input_data = parsed_list else: # Parsed to dict, string, or primitive - not HIL response format return None @@ -864,19 +915,32 @@ class AgentFrameworkExecutor: if not isinstance(input_data, list): return None - for item in input_data: - if isinstance(item, dict) and item.get("type") == "message": - message_content = item.get("content", []) + input_items: Any = cast(Any, input_data) + for item in input_items: + if isinstance(item, dict): + item_dict = cast(dict[str, Any], item) + if item_dict.get("type") != "message": + continue + message_content = item_dict.get("content", []) if isinstance(message_content, list): - for content_item in message_content: + message_content_items: Any = cast(Any, message_content) + for content_item in message_content_items: if isinstance(content_item, dict): - content_type = content_item.get("type") + content_dict = cast(dict[str, Any], content_item) + content_type = content_dict.get("type") if content_type == "workflow_hil_response": # Extract responses dict - # dict.get() returns Any, so we explicitly type it - responses: dict[str, Any] = content_item.get("responses", {}) # type: ignore[assignment] + responses_raw = content_dict.get("responses", {}) + if not isinstance(responses_raw, dict): + continue + + responses_dict: Any = cast(Any, responses_raw) + responses = { + str(response_key): response_value + for response_key, response_value in responses_dict.items() + } logger.info(f"Found workflow HIL responses: {list(responses.keys())}") return responses @@ -1000,11 +1064,12 @@ class AgentFrameworkExecutor: return # Find the source executor in the workflow - if not hasattr(workflow, "executors") or not isinstance(workflow.executors, dict): + executors = getattr(workflow, "executors", None) + if not isinstance(executors, dict): logger.debug("Workflow doesn't have executors dict") return - source_executor = workflow.executors.get(source_executor_id) + source_executor = cast(dict[str, Any], executors).get(source_executor_id) if not source_executor: logger.debug(f"Could not find executor '{source_executor_id}' in workflow") return diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index bcb99634cb..9e79b308c5 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -11,7 +11,7 @@ import uuid from collections import OrderedDict from collections.abc import Sequence from datetime import datetime -from typing import Any, Union +from typing import Any, Union, cast from uuid import uuid4 from agent_framework import Content, Message @@ -61,6 +61,17 @@ EventType = Union[ ] +def _to_str_dict(value: Any) -> dict[str, Any] | None: + """Cast arbitrary dict-like payload to a string-keyed dictionary.""" + if not isinstance(value, dict): + return None + return cast(dict[str, Any], value) + + +def _stringify_name(value: Any) -> str: + return value if isinstance(value, str) else str(value) + + def _serialize_content_recursive(value: Any) -> Any: """Recursively serialize Agent Framework Content objects to JSON-compatible values. @@ -88,16 +99,21 @@ def _serialize_content_recursive(value: Any) -> Any: # Handle dictionaries - recursively process values if isinstance(value, dict): - return {key: _serialize_content_recursive(val) for key, val in value.items()} + value_dict = cast(dict[str, Any], value) + return {str(key): _serialize_content_recursive(val) for key, val in value_dict.items()} # Handle lists and tuples - recursively process elements if isinstance(value, (list, tuple)): - serialized = [_serialize_content_recursive(item) for item in value] + sequence_items: Any = cast(Any, value) + serialized: list[Any] = [_serialize_content_recursive(item) for item in sequence_items] # For single-item lists containing text Content, extract just the text # This handles the MCP case where result = [Content.from_text(text="Hello")] # and we want output = "Hello" not output = '[{"type": "text", "text": "Hello"}]' - if len(serialized) == 1 and isinstance(serialized[0], dict) and serialized[0].get("type") == "text": - return serialized[0].get("text", "") + if len(serialized) == 1: + first_item = _to_str_dict(serialized[0]) + if first_item and first_item.get("type") == "text": + text_value = first_item.get("text", "") + return text_value if isinstance(text_value, str) else str(text_value) return serialized # For other objects with model_dump(), try that @@ -156,8 +172,10 @@ class MessageMapper: context = self._get_or_create_context(request) # Handle error events - if isinstance(raw_event, dict) and raw_event.get("type") == "error": - return [await self._create_error_event(raw_event.get("message", "Unknown error"), context)] + raw_event_dict = _to_str_dict(raw_event) + if raw_event_dict and raw_event_dict.get("type") == "error": + message = raw_event_dict.get("message", "Unknown error") + return [await self._create_error_event(_stringify_name(message), context)] # Handle ResponseTraceEvent objects from our trace collector from .models import ResponseTraceEvent @@ -185,15 +203,12 @@ class MessageMapper: # Handle WorkflowEvent with type='output' or 'data' wrapping AgentResponseUpdate # This must be checked BEFORE generic WorkflowEvent check # Note: AgentExecutor uses type='output' for streaming updates - if ( - isinstance(raw_event, WorkflowEvent) - and raw_event.type in ("output", "data") - and raw_event.data - and isinstance(raw_event.data, AgentResponseUpdate) - ): - # Preserve executor_id in context for proper output routing - context["current_executor_id"] = raw_event.executor_id - return await self._convert_agent_update(raw_event.data, context) + if isinstance(raw_event, WorkflowEvent) and raw_event.type in ("output", "data"): + event_data = getattr(cast(Any, raw_event), "data", None) + if isinstance(event_data, AgentResponseUpdate): + # Preserve executor_id in context for proper output routing + context["current_executor_id"] = getattr(cast(Any, raw_event), "executor_id", None) + return await self._convert_agent_update(event_data, context) # Handle complete agent response (AgentResponse) - for non-streaming agent execution if isinstance(raw_event, AgentResponse): @@ -210,10 +225,11 @@ class MessageMapper: except ImportError as e: logger.warning(f"Could not import Agent Framework types: {e}") # Fallback to attribute-based detection - if hasattr(raw_event, "contents"): - return await self._convert_agent_update(raw_event, context) - if hasattr(raw_event, "__class__") and "Event" in raw_event.__class__.__name__: - return await self._convert_workflow_event(raw_event, context) + candidate_event = cast(Any, raw_event) + if hasattr(candidate_event, "contents"): + return await self._convert_agent_update(candidate_event, context) + if "Event" in type(candidate_event).__name__: + return await self._convert_workflow_event(candidate_event, context) # Unknown event type return [await self._create_unknown_event(raw_event, context)] @@ -256,32 +272,36 @@ class MessageMapper: item = getattr(event, "item", None) if item: # Handle both object and dict formats - item_type = item.get("type") if isinstance(item, dict) else getattr(item, "type", None) + item_dict = _to_str_dict(item) + item_type = item_dict.get("type") if item_dict is not None else getattr(item, "type", None) # Track function calls to accumulate their arguments if item_type == "function_call": # Handle both object and dict formats - if isinstance(item, dict): - call_id = item.get("call_id") or item.get("id") - if call_id: + item_dict = _to_str_dict(item) + if item_dict is not None: + call_id_value = item_dict.get("call_id") or item_dict.get("id") + if call_id_value: + call_id = str(call_id_value) function_calls[call_id] = { - "id": item.get("id", call_id), + "id": str(item_dict.get("id", call_id)), "call_id": call_id, - "name": item.get("name", ""), - "arguments": item.get("arguments", ""), + "name": _stringify_name(item_dict.get("name", "")), + "arguments": _stringify_name(item_dict.get("arguments", "")), "type": "function_call", - "status": item.get("status", "completed"), + "status": _stringify_name(item_dict.get("status", "completed")), } else: - call_id = getattr(item, "call_id", None) or getattr(item, "id", None) - if call_id: + call_id_value = getattr(item, "call_id", None) or getattr(item, "id", None) + if call_id_value: + call_id = str(call_id_value) function_calls[call_id] = { - "id": getattr(item, "id", call_id), + "id": str(getattr(item, "id", call_id)), "call_id": call_id, - "name": getattr(item, "name", ""), - "arguments": getattr(item, "arguments", ""), + "name": _stringify_name(getattr(item, "name", "")), + "arguments": _stringify_name(getattr(item, "arguments", "")), "type": "function_call", - "status": getattr(item, "status", "completed"), + "status": _stringify_name(getattr(item, "status", "completed")), } # Other output items (message, etc.) - track for later @@ -299,8 +319,9 @@ class MessageMapper: # Handle function result complete events elif event_type == "response.function_result.complete": - call_id = getattr(event, "call_id", None) - if call_id: + call_id_value = getattr(event, "call_id", None) + if call_id_value: + call_id = str(call_id_value) function_results[call_id] = { "type": "function_call_output", "call_id": call_id, @@ -322,7 +343,7 @@ class MessageMapper: # Build final text message from accumulated deltas # Combine all text parts (usually there's just one message) - all_text_parts = [] + all_text_parts: list[str] = [] for _item_id, parts in text_parts_by_message.items(): all_text_parts.extend(parts) @@ -493,14 +514,14 @@ class MessageMapper: return value.value # Handle lists/tuples/sets - recursively serialize elements - if isinstance(value, (list, tuple)): - return [self._serialize_value(item) for item in value] - if isinstance(value, set): - return [self._serialize_value(item) for item in value] + if isinstance(value, (list, tuple, set)): + value_items: Any = cast(Any, value) + return [self._serialize_value(item) for item in value_items] # Handle dicts - recursively serialize values if isinstance(value, dict): - return {k: self._serialize_value(v) for k, v in value.items()} + value_dict = cast(dict[str, Any], value) + return {str(k): self._serialize_value(v) for k, v in value_dict.items()} # Handle SerializationMixin (like Message) - call to_dict() if hasattr(value, "to_dict") and callable(getattr(value, "to_dict", None)): @@ -551,14 +572,15 @@ class MessageMapper: # Handle dict first (most common) if isinstance(request_data, dict): - return {k: self._serialize_value(v) for k, v in request_data.items()} + request_dict = cast(dict[str, Any], request_data) + return {str(k): self._serialize_value(v) for k, v in request_dict.items()} # Handle dataclasses with nested SerializationMixin objects # We can't use asdict() directly because it doesn't handle Message if is_dataclass(request_data) and not isinstance(request_data, type): try: # Manually serialize each field to handle nested SerializationMixin - result = {} + result: dict[str, Any] = {} for field in fields(request_data): field_value = getattr(request_data, field.name) result[field.name] = self._serialize_value(field_value) @@ -900,8 +922,9 @@ class MessageMapper: text = str(output_data) elif isinstance(output_data, list): # Handle list of Message objects (from Magentic yield_output([final_answer])) - text_parts = [] - for item in output_data: + text_parts: list[str] = [] + output_items_list: Any = cast(Any, output_data) + for item in output_items_list: if isinstance(item, Message): item_text = getattr(item, "text", None) if item_text: @@ -912,17 +935,17 @@ class MessageMapper: text_parts.append(item) else: try: - text_parts.append(json.dumps(item, indent=2)) + text_parts.append(json.dumps(self._serialize_value(item), indent=2)) except (TypeError, ValueError): text_parts.append(str(item)) - text = "\n".join(text_parts) if text_parts else str(output_data) + text = "\n".join(text_parts) if text_parts else str(cast(Any, output_data)) elif isinstance(output_data, str): # String output text = output_data else: # Object/dict → JSON string try: - text = json.dumps(output_data, indent=2) + text = json.dumps(self._serialize_value(output_data), indent=2) except (TypeError, ValueError): # Fallback to string representation if not JSON serializable text = str(output_data) @@ -1420,10 +1443,10 @@ class MessageMapper: None - no event emitted (usage goes in final Response.usage) """ # Extract usage from UsageContent.usage_details (UsageDetails object) - details = content.usage_details or {} - total_tokens = details.get("total_token_count", 0) - prompt_tokens = details.get("input_token_count", 0) - completion_tokens = details.get("output_token_count", 0) + details = _to_str_dict(getattr(content, "usage_details", None)) or {} + total_tokens = int(details.get("total_token_count", 0) or 0) + prompt_tokens = int(details.get("input_token_count", 0) or 0) + completion_tokens = int(details.get("output_token_count", 0) or 0) # Accumulate for final Response.usage request_id = context.get("request_id", "default") diff --git a/python/packages/devui/agent_framework_devui/_openai/_executor.py b/python/packages/devui/agent_framework_devui/_openai/_executor.py index 986d2d3a84..ac0e641e60 100644 --- a/python/packages/devui/agent_framework_devui/_openai/_executor.py +++ b/python/packages/devui/agent_framework_devui/_openai/_executor.py @@ -22,6 +22,26 @@ from ..models import AgentFrameworkRequest, OpenAIResponse logger = logging.getLogger(__name__) +def _extract_error_details(body: Any) -> tuple[str | None, str | None, str | None]: + """Extract typed OpenAI error fields from error body payload.""" + if not isinstance(body, dict): + return None, None, None + + error_dict: dict[str, Any] = body.get("error") # type: ignore[assignment, reportUnknownVariableType] + if not isinstance(error_dict, dict): + return None, None, None + + message = error_dict.get("message") + error_type = error_dict.get("type") + code = error_dict.get("code") + + return ( + message if isinstance(message, str) else None, + error_type if isinstance(error_type, str) else None, + code if isinstance(code, str) else None, + ) + + class OpenAIExecutor: """Executor for OpenAI Responses API - mirrors AgentFrameworkExecutor interface. @@ -138,68 +158,64 @@ class OpenAIExecutor: except AuthenticationError as e: # 401 - Invalid API key or authentication issue logger.error(f"OpenAI authentication error: {e}", exc_info=True) - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) yield { "type": "response.failed", "response": { "id": f"resp_{os.urandom(16).hex()}", "status": "failed", "error": { - "message": error_data.get("message", str(e)), - "type": error_data.get("type", "authentication_error"), - "code": error_data.get("code", "invalid_api_key"), + "message": message or str(e), + "type": error_type or "authentication_error", + "code": code or "invalid_api_key", }, }, } except PermissionDeniedError as e: # 403 - Permission denied logger.error(f"OpenAI permission denied: {e}", exc_info=True) - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) yield { "type": "response.failed", "response": { "id": f"resp_{os.urandom(16).hex()}", "status": "failed", "error": { - "message": error_data.get("message", str(e)), - "type": error_data.get("type", "permission_denied"), - "code": error_data.get("code", "insufficient_permissions"), + "message": message or str(e), + "type": error_type or "permission_denied", + "code": code or "insufficient_permissions", }, }, } except RateLimitError as e: # 429 - Rate limit exceeded logger.error(f"OpenAI rate limit exceeded: {e}", exc_info=True) - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) yield { "type": "response.failed", "response": { "id": f"resp_{os.urandom(16).hex()}", "status": "failed", "error": { - "message": error_data.get("message", str(e)), - "type": error_data.get("type", "rate_limit_error"), - "code": error_data.get("code", "rate_limit_exceeded"), + "message": message or str(e), + "type": error_type or "rate_limit_error", + "code": code or "rate_limit_exceeded", }, }, } except APIStatusError as e: # Other OpenAI API errors logger.error(f"OpenAI API error: {e}", exc_info=True) - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) yield { "type": "response.failed", "response": { "id": f"resp_{os.urandom(16).hex()}", "status": "failed", "error": { - "message": error_data.get("message", str(e)), - "type": error_data.get("type", "api_error"), - "code": error_data.get("code", "unknown_error"), + "message": message or str(e), + "type": error_type or "api_error", + "code": code or "unknown_error", }, }, } diff --git a/python/packages/devui/agent_framework_devui/_server.py b/python/packages/devui/agent_framework_devui/_server.py index e7994d3d3b..ff26937843 100644 --- a/python/packages/devui/agent_framework_devui/_server.py +++ b/python/packages/devui/agent_framework_devui/_server.py @@ -31,6 +31,29 @@ from .models._discovery_models import Deployment, DeploymentConfig, DiscoveryRes logger = logging.getLogger(__name__) + +def _extract_error_details(body: object) -> tuple[str | None, str | None, str | None]: + """Extract typed OpenAI-style error payload fields.""" + if not isinstance(body, dict): + return None, None, None + + body_dict = cast(dict[str, object], body) + error_obj = body_dict.get("error") + if not isinstance(error_obj, dict): + return None, None, None + + error_dict = cast(dict[str, object], error_obj) + message = error_dict.get("message") + error_type = error_dict.get("type") + code = error_dict.get("code") + + return ( + message if isinstance(message, str) else None, + error_type if isinstance(error_type, str) else None, + code if isinstance(code, str) else None, + ) + + # Get package version try: __version__ = importlib.metadata.version("agent-framework-devui") @@ -83,6 +106,10 @@ class DevServer: self._pending_entities: list[Any] | None = None self._running_tasks: dict[str, asyncio.Task[Any]] = {} # Track running response tasks for cancellation + def set_pending_entities(self, entities: list[Any]) -> None: + """Set in-memory entities to register on startup.""" + self._pending_entities = entities + def _is_dev_mode(self) -> bool: """Check if running in developer mode. @@ -378,6 +405,8 @@ class DevServer: # Token valid, proceed return await call_next(request) + _ = auth_middleware + self._register_routes(app) self._mount_ui(app) @@ -452,7 +481,7 @@ class DevServer: if entity_info.type == "workflow" and entity_obj: # Entity object already loaded by load_entity() above # Get workflow structure - workflow_dump = None + workflow_dump: dict[str, Any] | str | None = None if hasattr(entity_obj, "to_dict") and callable(getattr(entity_obj, "to_dict", None)): try: workflow_dump = entity_obj.to_dict() # type: ignore[attr-defined] @@ -475,7 +504,11 @@ class DevServer: except Exception: workflow_dump = raw_dump else: - workflow_dump = parsed_dump if isinstance(parsed_dump, dict) else raw_dump + if isinstance(parsed_dump, dict): + parsed_dump_dict = cast(dict[str, Any], parsed_dump) + workflow_dump = {str(k): v for k, v in parsed_dump_dict.items()} + else: + workflow_dump = raw_dump else: workflow_dump = raw_dump elif hasattr(entity_obj, "__dict__"): @@ -838,34 +871,31 @@ class DevServer: except AuthenticationError as e: # 401 - Invalid API key or authentication issue logger.error(f"OpenAI authentication error creating conversation: {e}") - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) error = OpenAIError.create( - message=error_data.get("message", str(e)), - type=error_data.get("type", "authentication_error"), - code=error_data.get("code", "invalid_api_key"), + message=message or str(e), + type=error_type or "authentication_error", + code=code or "invalid_api_key", ) return JSONResponse(status_code=401, content=error.to_dict()) except PermissionDeniedError as e: # 403 - Permission denied logger.error(f"OpenAI permission denied creating conversation: {e}") - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) error = OpenAIError.create( - message=error_data.get("message", str(e)), - type=error_data.get("type", "permission_denied"), - code=error_data.get("code", "insufficient_permissions"), + message=message or str(e), + type=error_type or "permission_denied", + code=code or "insufficient_permissions", ) return JSONResponse(status_code=403, content=error.to_dict()) except APIStatusError as e: # Other OpenAI API errors (rate limit, etc.) logger.error(f"OpenAI API error creating conversation: {e}") - error_body = e.body if hasattr(e, "body") else {} - error_data = error_body.get("error", {}) if isinstance(error_body, dict) else {} + message, error_type, code = _extract_error_details(e.body if hasattr(e, "body") else None) error = OpenAIError.create( - message=error_data.get("message", str(e)), - type=error_data.get("type", "api_error"), - code=error_data.get("code", "unknown_error"), + message=message or str(e), + type=error_type or "api_error", + code=code or "unknown_error", ) return JSONResponse( status_code=e.status_code if hasattr(e, "status_code") else 500, content=error.to_dict() @@ -902,7 +932,7 @@ class DevServer: executor = await self._ensure_executor() # Build filter criteria - filters = {} + filters: dict[str, str] = {} if agent_id: filters["agent_id"] = agent_id if entity_id: @@ -997,15 +1027,16 @@ class DevServer: conversation_id, limit=limit, after=after, order=order ) # Handle both Pydantic models and dicts (some stores return raw dicts) - serialized_items = [] + serialized_items: list[dict[str, Any]] = [] for item in items: if hasattr(item, "model_dump"): serialized_items.append(item.model_dump()) elif isinstance(item, dict): - serialized_items.append(item) + item_dict = cast(dict[str, Any], item) + serialized_items.append({str(k): v for k, v in item_dict.items()}) else: logger.warning(f"Unexpected item type: {type(item)}, converting to dict") - serialized_items.append(dict(item)) + serialized_items.append({str(k): v for k, v in dict(item).items()}) # Get stored traces for context inspection (DevUI extension) traces = executor.conversation_store.get_traces(conversation_id) @@ -1038,9 +1069,14 @@ class DevServer: if not item: raise HTTPException(status_code=404, detail="Item not found") # Handle both Pydantic models and dicts - result: dict[str, Any] = ( - item.model_dump() if hasattr(item, "model_dump") else cast(dict[str, Any], item) - ) + result: dict[str, Any] + if hasattr(item, "model_dump"): + result = item.model_dump() + elif isinstance(item, dict): + item_dict = cast(dict[str, Any], item) + result = {str(k): v for k, v in item_dict.items()} + else: + result = {"value": item} return result except HTTPException: raise @@ -1085,16 +1121,42 @@ class DevServer: # Checkpoints are exposed as conversation items with type="checkpoint" # ============================================================================ + registered_route_handlers = ( + health_check, + get_meta, + discover_entities, + get_entity_info, + reload_entity, + create_deployment, + list_deployments, + get_deployment, + delete_deployment, + deploy_entity, + create_response, + cancel_response, + create_conversation, + list_conversations, + retrieve_conversation, + update_conversation, + delete_conversation, + create_conversation_items, + list_conversation_items, + retrieve_conversation_item, + delete_conversation_item, + ) + _ = registered_route_handlers + async def _stream_execution( self, executor: AgentFrameworkExecutor, request: AgentFrameworkRequest ) -> AsyncGenerator[str]: """Stream execution directly through executor.""" try: # Collect events for final response.completed event - events = [] + events: list[Any] = [] # Get conversation_id for trace storage - conversation_id = request._get_conversation_id() + conversation_getter = getattr(request, "_get_conversation_id", None) + conversation_id = conversation_getter() if callable(conversation_getter) else None # Stream all events async for event in executor.execute_streaming(request): @@ -1104,7 +1166,7 @@ class DevServer: if conversation_id and hasattr(event, "type") and event.type == "response.trace.completed": try: trace_data = event.data if hasattr(event, "data") else None - if trace_data: + if trace_data and isinstance(conversation_id, str): executor.conversation_store.add_trace(conversation_id, trace_data) except Exception as e: logger.debug(f"Failed to store trace event: {e}") @@ -1136,8 +1198,9 @@ class DevServer: # We need to increment from that last_seq = 0 for event in reversed(events): - if hasattr(event, "sequence_number") and event.sequence_number is not None: - last_seq = event.sequence_number + sequence_number = getattr(event, "sequence_number", None) + if isinstance(sequence_number, int): + last_seq = sequence_number break completed_event = ResponseCompletedEvent( diff --git a/python/packages/devui/agent_framework_devui/_session.py b/python/packages/devui/agent_framework_devui/_session.py index 5cabeee072..93ac9b31e4 100644 --- a/python/packages/devui/agent_framework_devui/_session.py +++ b/python/packages/devui/agent_framework_devui/_session.py @@ -5,13 +5,37 @@ import logging import uuid from datetime import datetime -from typing import Any +from typing import Any, TypedDict, cast + +from typing_extensions import NotRequired logger = logging.getLogger(__name__) -# Type aliases for better readability -SessionData = dict[str, Any] -RequestRecord = dict[str, Any] + +class RequestRecord(TypedDict): + """Tracked execution request data.""" + + id: str + timestamp: datetime + entity_id: str + executor: str + input: Any + model_id: str + stream: bool + execution_time: NotRequired[float] + status: NotRequired[str] + + +class SessionData(TypedDict): + """Stored session state.""" + + id: str + created_at: datetime + requests: list[RequestRecord] + context: dict[str, Any] + active: bool + + SessionSummary = dict[str, Any] @@ -95,7 +119,7 @@ class SessionManager: "stream": True, } session["requests"].append(request_record) - return str(request_record["id"]) + return request_record["id"] def update_request_record(self, session_id: str, request_id: str, updates: dict[str, Any]) -> None: """Update a request record in a session. @@ -111,7 +135,8 @@ class SessionManager: for request in session["requests"]: if request["id"] == request_id: - request.update(updates) + request_data = cast(dict[str, Any], request) + request_data.update(updates) break def get_session_history(self, session_id: str) -> SessionSummary | None: @@ -138,7 +163,7 @@ class SessionManager: "timestamp": req["timestamp"].isoformat(), "entity_id": req["entity_id"], "executor": req["executor"], - "model": req["model"], + "model": req["model_id"], "input_length": len(str(req["input"])) if req["input"] else 0, "execution_time": req.get("execution_time"), "status": req.get("status", "unknown"), @@ -153,7 +178,7 @@ class SessionManager: Returns: List of active session summaries """ - active_sessions = [] + active_sessions: list[SessionSummary] = [] for session_id, session in self.sessions.items(): if session["active"]: @@ -178,7 +203,7 @@ class SessionManager: """ cutoff_time = datetime.now().timestamp() - (max_age_hours * 3600) - sessions_to_remove = [] + sessions_to_remove: list[str] = [] for session_id, session in self.sessions.items(): if session["created_at"].timestamp() < cutoff_time: sessions_to_remove.append(session_id) diff --git a/python/packages/devui/agent_framework_devui/_utils.py b/python/packages/devui/agent_framework_devui/_utils.py index 66886b8ea7..889a690c87 100644 --- a/python/packages/devui/agent_framework_devui/_utils.py +++ b/python/packages/devui/agent_framework_devui/_utils.py @@ -7,12 +7,20 @@ import json import logging from dataclasses import fields, is_dataclass from types import UnionType -from typing import Any, Union, get_args, get_origin, get_type_hints +from typing import Any, Union, cast, get_args, get_origin, get_type_hints from agent_framework import Message logger = logging.getLogger(__name__) + +def _string_key_dict(value: object) -> dict[str, Any] | None: + """Cast value to a dict.""" + if not isinstance(value, dict): + return None + return cast(dict[str, Any], value) + + # ============================================================================ # Agent Metadata Extraction # ============================================================================ @@ -39,18 +47,21 @@ def extract_agent_metadata(entity_object: Any) -> dict[str, Any]: # Try to get instructions if hasattr(entity_object, "default_options"): chat_opts = entity_object.default_options - if isinstance(chat_opts, dict): - if "instructions" in chat_opts: - metadata["instructions"] = chat_opts.get("instructions") + chat_opts_dict = _string_key_dict(chat_opts) + if chat_opts_dict is not None: + if "instructions" in chat_opts_dict: + metadata["instructions"] = chat_opts_dict.get("instructions") elif hasattr(chat_opts, "instructions"): metadata["instructions"] = chat_opts.instructions # Try to get model - check both default_options and client if hasattr(entity_object, "default_options"): chat_opts = entity_object.default_options - if isinstance(chat_opts, dict): - if chat_opts.get("model_id"): - metadata["model"] = chat_opts.get("model_id") + chat_opts_dict = _string_key_dict(chat_opts) + if chat_opts_dict is not None: + model_id = chat_opts_dict.get("model_id") + if model_id: + metadata["model"] = model_id elif hasattr(chat_opts, "model_id") and chat_opts.model_id: metadata["model"] = chat_opts.model_id if metadata["model"] is None and hasattr(entity_object, "client") and hasattr(entity_object.client, "model_id"): @@ -112,7 +123,7 @@ def extract_executor_message_types(executor: Any) -> list[Any]: try: handlers = executor._handlers if isinstance(handlers, dict): - message_types = list(handlers.keys()) + message_types = list(handlers.keys()) # type: ignore[arg-type] # pyright: ignore[reportUnknownArgumentType] except Exception as exc: # pragma: no cover - defensive logging path logger.debug(f"Failed to read executor handlers: {exc}") @@ -366,11 +377,10 @@ def extract_response_type_from_executor(executor: Any, request_type: type) -> ty _, second_param_type = param_items[1] if len(param_items) > 1 else (None, None) # Check if first param matches request_type - first_matches_request = first_param_type == request_type or ( - hasattr(first_param_type, "__name__") - and hasattr(request_type, "__name__") - and first_param_type.__name__ == request_type.__name__ - ) + first_matches_request = first_param_type == request_type + if not first_matches_request and isinstance(first_param_type, type): + request_type_name = request_type.__name__ + first_matches_request = first_param_type.__name__ == request_type_name # Verify we have a matching request type and valid response type (must be a type class) if first_matches_request and second_param_type is not None and isinstance(second_param_type, type): @@ -432,7 +442,7 @@ def generate_input_schema(input_type: type) -> dict[str, Any]: return generate_schema_from_dataclass(input_type) # 5. Fallback to string - type_name = getattr(input_type, "__name__", str(input_type)) + type_name = input_type.__name__ if isinstance(input_type, type) else str(cast(Any, input_type)) return {"type": "string", "description": f"Input type: {type_name}"} @@ -466,8 +476,9 @@ def parse_input_for_type(input_data: Any, target_type: type) -> Any: return _parse_string_input(input_data, target_type) # Handle dict input - if isinstance(input_data, dict): - return _parse_dict_input(input_data, target_type) + parsed_dict = _string_key_dict(input_data) + if parsed_dict is not None: + return _parse_dict_input(parsed_dict, target_type) # Fallback: return original return input_data diff --git a/python/packages/devui/agent_framework_devui/models/_discovery_models.py b/python/packages/devui/agent_framework_devui/models/_discovery_models.py index ff217a48d2..47e6d1bdcc 100644 --- a/python/packages/devui/agent_framework_devui/models/_discovery_models.py +++ b/python/packages/devui/agent_framework_devui/models/_discovery_models.py @@ -2,8 +2,11 @@ """Discovery API models for entity information.""" +from __future__ import annotations + import re -from typing import Any +from typing import Any, cast +from collections.abc import Callable from pydantic import BaseModel, Field, field_validator @@ -57,7 +60,7 @@ class EntityInfo(BaseModel): class DiscoveryResponse(BaseModel): """Response model for entity discovery.""" - entities: list[EntityInfo] = Field(default_factory=list) + entities: list[EntityInfo] = Field(default_factory=cast(Callable[..., list[EntityInfo]], list)) # ============================================================================ diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 6f41307dde..a56cf1ab4f 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -94,7 +94,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui" -test = "pytest --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/durabletask/agent_framework_durabletask/_entities.py b/python/packages/durabletask/agent_framework_durabletask/_entities.py index 650e1b8013..460b6b0429 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_entities.py +++ b/python/packages/durabletask/agent_framework_durabletask/_entities.py @@ -206,9 +206,7 @@ class AgentEntity: request_message=request_message, ) - run_callable = getattr(self.agent, "run", None) - if run_callable is None or not callable(run_callable): - raise AttributeError("Agent does not implement run() method") + run_callable = self.agent.run # Try streaming first with run(stream=True) try: diff --git a/python/packages/durabletask/agent_framework_durabletask/_response_utils.py b/python/packages/durabletask/agent_framework_durabletask/_response_utils.py index fe371b592f..2d0ee84d3e 100644 --- a/python/packages/durabletask/agent_framework_durabletask/_response_utils.py +++ b/python/packages/durabletask/agent_framework_durabletask/_response_utils.py @@ -58,8 +58,8 @@ def ensure_response_format( """ if response_format is not None: # Set the response format on the response so .value knows how to parse - response._response_format = response_format - response._value_parsed = False # Reset to allow re-parsing with new format + response._response_format = response_format # pyright: ignore[reportPrivateUsage] + response._value_parsed = False # pyright: ignore[reportPrivateUsage] # Reset to allow re-parsing with new format # Access response.value to trigger parsing (may raise ValidationError) # Validate that parsing succeeded diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 95a00929a2..56493f3126 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -73,6 +73,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_durabletask"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -98,8 +99,8 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask" -test = "pytest --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] -build-backend = "flit_core.buildapi" \ No newline at end of file +build-backend = "flit_core.buildapi" diff --git a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py index 9bccc60309..16451ae85a 100644 --- a/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py +++ b/python/packages/foundry_local/agent_framework_foundry_local/_foundry_local_client.py @@ -248,18 +248,19 @@ class FoundryLocalClient( env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) + model_id_setting: str = settings["model_id"] # type: ignore[assignment] # pyright: ignore[reportTypedDictNotRequiredAccess] + manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout) model_info = manager.get_model_info( - alias_or_model_id=settings["model_id"], + alias_or_model_id=model_id_setting, device=device, ) if model_info is None: message = ( - f"Model with ID or alias '{settings['model_id']}:{device.value}' not found in Foundry Local." + f"Model with ID or alias '{model_id_setting}:{device.value}' not found in Foundry Local." if device else ( - f"Model with ID or alias '{settings['model_id']}' for your current device " - "not found in Foundry Local." + f"Model with ID or alias '{model_id_setting}' for your current device not found in Foundry Local." ) ) raise ValueError(message) diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index dd2af572f2..97dd99f1ca 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -59,6 +59,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_foundry_local"] exclude = ['tests'] [tool.mypy] @@ -85,7 +86,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local" -test = "pytest --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 053e0d3de0..1c30af36dc 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -7,7 +7,7 @@ import contextlib import logging import sys from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence -from typing import Any, ClassVar, Generic, Literal, TypedDict, overload +from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload from agent_framework import ( AgentMiddlewareTypes, @@ -30,6 +30,7 @@ from copilot.generated.session_events import SessionEvent, SessionEventType from copilot.types import ( CopilotClientOptions, MCPServerConfig, + MessageOptions, PermissionRequest, PermissionRequestResult, ResumeSessionConfig, @@ -266,10 +267,13 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): if self._client is None: client_options: CopilotClientOptions = {} - if self._settings["cli_path"]: - client_options["cli_path"] = self._settings["cli_path"] - if self._settings["log_level"]: - client_options["log_level"] = self._settings["log_level"] # type: ignore[typeddict-item] + cli_path = self._settings.get("cli_path") + if cli_path: + client_options["cli_path"] = cli_path + + log_level = self._settings.get("log_level") + if log_level: + client_options["log_level"] = log_level # type: ignore[typeddict-item] self._client = CopilotClient(client_options if client_options else None) @@ -372,14 +376,15 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): session = self.create_session() opts: dict[str, Any] = dict(options) if options else {} - timeout = opts.pop("timeout", None) or self._settings["timeout"] or DEFAULT_TIMEOUT_SECONDS + timeout = opts.pop("timeout", None) or self._settings.get("timeout") or DEFAULT_TIMEOUT_SECONDS copilot_session = await self._get_or_create_session(session, streaming=False, runtime_options=opts) input_messages = normalize_messages(messages) prompt = "\n".join([message.text for message in input_messages]) + message_options = cast(MessageOptions, {"prompt": prompt}) try: - response_event = await copilot_session.send_and_wait({"prompt": prompt}, timeout=timeout) + response_event = await copilot_session.send_and_wait(message_options, timeout=timeout) except Exception as ex: raise AgentException(f"GitHub Copilot request failed: {ex}") from ex @@ -439,6 +444,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): copilot_session = await self._get_or_create_session(session, streaming=True, runtime_options=opts) input_messages = normalize_messages(messages) prompt = "\n".join([message.text for message in input_messages]) + message_options = cast(MessageOptions, {"prompt": prompt}) queue: asyncio.Queue[AgentResponseUpdate | Exception | None] = asyncio.Queue() @@ -462,7 +468,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): unsubscribe = copilot_session.on(event_handler) try: - await copilot_session.send({"prompt": prompt}) + await copilot_session.send(message_options) while (item := await queue.get()) is not None: if isinstance(item, Exception): @@ -597,7 +603,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]): opts = runtime_options or {} config: SessionConfig = {"streaming": streaming} - model = opts.get("model") or self._settings["model"] + model = opts.get("model") or self._settings.get("model") if model: config["model"] = model # type: ignore[typeddict-item] diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 1a60ff4298..47069e34fa 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_github_copilot"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_github_copilot" -test = "pytest --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/lab/gaia/agent_framework_lab_gaia/gaia.py b/python/packages/lab/gaia/agent_framework_lab_gaia/gaia.py index 08619b84bc..cba407ded3 100644 --- a/python/packages/lab/gaia/agent_framework_lab_gaia/gaia.py +++ b/python/packages/lab/gaia/agent_framework_lab_gaia/gaia.py @@ -13,7 +13,7 @@ import time from collections.abc import Iterable from datetime import datetime from pathlib import Path -from typing import Any +from typing import Any, cast from opentelemetry.trace import NoOpTracer, SpanKind, get_tracer from tqdm import tqdm @@ -163,7 +163,7 @@ def _normalize_str(s: str, remove_punct: bool = True) -> str: return no_spaces.lower() -def gaia_scorer(model_answer: str, ground_truth: str) -> bool: +def gaia_scorer(model_answer: str | None, ground_truth: str) -> bool: """Official GAIA scoring function. Args: @@ -193,7 +193,7 @@ def gaia_scorer(model_answer: str, ground_truth: str) -> bool: ma_elems = _split_string(model_answer) if len(gt_elems) != len(ma_elems): return False - comparisons = [] + comparisons: list[bool] = [] for ma, gt in zip(ma_elems, gt_elems, strict=False): if is_float(gt): comparisons.append(abs(_normalize_number_str(ma) - float(gt)) < 1e-6) @@ -204,18 +204,39 @@ def gaia_scorer(model_answer: str, ground_truth: str) -> bool: return _normalize_str(model_answer) == _normalize_str(ground_truth) +def _coerce_record(raw: object) -> dict[str, Any] | None: + if isinstance(raw, dict): + raw_dict = cast(dict[object, Any], raw) + if all(isinstance(key, str) for key in raw_dict): + return cast(dict[str, Any], raw_dict) + return None + + +def _parse_level(level: object) -> int | None: + if isinstance(level, int): + return level + if isinstance(level, str) and level.isdigit(): + return int(level) + return None + + def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]: """Read JSONL file and yield parsed records.""" with path.open("rb") as f: for line in f: if not line.strip(): continue + parsed: object try: import orjson - yield orjson.loads(line) + parsed = orjson.loads(line) except Exception: - yield json.loads(line) + parsed = json.loads(line) + + record = _coerce_record(parsed) + if record is not None: + yield record def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max_n: int | None = None) -> list[Task]: @@ -232,41 +253,43 @@ def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max try: import pyarrow.parquet as pq - table = pq.read_table(p) - for row in table.to_pylist(): + pq_any = cast(Any, pq) + table: Any = pq_any.read_table(p) + rows = cast(list[object], table.to_pylist()) + for row in rows: + record = _coerce_record(row) + if record is None: + continue + # Robustly extract fields used across variants - q = row.get("Question") or row.get("question") or row.get("query") or row.get("prompt") - ans = row.get("Final answer") or row.get("answer") or row.get("final_answer") + q_obj = record.get("Question") or record.get("question") or record.get("query") or record.get("prompt") + ans = record.get("Final answer") or record.get("answer") or record.get("final_answer") + if not isinstance(q_obj, str): + continue + q = q_obj + qid = str( - row.get("task_id") - or row.get("question_id") - or row.get("id") - or row.get("uuid") + record.get("task_id") + or record.get("question_id") + or record.get("id") + or record.get("uuid") or f"{p.stem}:{len(tasks)}" ) - lvl = row.get("Level") or row.get("level") - - # Convert level to int if it's a string - def _parse_level(lvl: Any) -> int | None: - """Parse level value to integer if possible.""" - if isinstance(lvl, int): - return lvl - if isinstance(lvl, str) and lvl.isdigit(): - return int(lvl) - return None - - lvl = _parse_level(lvl) - fname = row.get("file_name") or row.get("filename") or None + lvl = _parse_level(record.get("Level") or record.get("level")) + fname_obj = record.get("file_name") or record.get("filename") + fname = fname_obj if isinstance(fname_obj, str) else None # Only evaluate examples with public answers (dev/validation split) # Skip if no question, no answer, or answer is placeholder like "?" - if not q or ans is None or str(ans).strip() in ["?", ""]: + if ans is None or str(ans).strip() in ["?", ""]: continue if wanted_levels and (lvl not in wanted_levels): continue - tasks.append(Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=row)) + tasks.append( + Task(task_id=qid, question=q, answer=str(ans), level=lvl, file_name=fname, metadata=record) + ) except ImportError: print("Warning: pyarrow not installed. Install with: pip install pyarrow") continue @@ -279,8 +302,12 @@ def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max for p in repo_dir.rglob("metadata.jsonl"): for rec in _read_jsonl(p): # Robustly extract fields used across variants - q = rec.get("Question") or rec.get("question") or rec.get("query") or rec.get("prompt") + q_obj = rec.get("Question") or rec.get("question") or rec.get("query") or rec.get("prompt") ans = rec.get("Final answer") or rec.get("answer") or rec.get("final_answer") + if not isinstance(q_obj, str): + continue + q = q_obj + qid = str( rec.get("task_id") or rec.get("question_id") @@ -288,15 +315,13 @@ def _load_gaia_local(repo_dir: Path, wanted_levels: list[int] | None = None, max or rec.get("uuid") or f"{p.stem}:{len(tasks)}" ) - lvl = rec.get("Level") or rec.get("level") - # Convert level to int if it's a string - if isinstance(lvl, str) and lvl.isdigit(): - lvl = int(lvl) - fname = rec.get("file_name") or rec.get("filename") or None + lvl = _parse_level(rec.get("Level") or rec.get("level")) + fname_obj = rec.get("file_name") or rec.get("filename") + fname = fname_obj if isinstance(fname_obj, str) else None # Only evaluate examples with public answers (dev/validation split) # Skip if no question, no answer, or answer is placeholder like "?" - if not q or ans is None or str(ans).strip() in ["?", ""]: + if ans is None or str(ans).strip() in ["?", ""]: continue if wanted_levels and (lvl not in wanted_levels): @@ -366,9 +391,10 @@ class GAIA: "with access to gaia-benchmark/GAIA." ) - from huggingface_hub import snapshot_download + import huggingface_hub - local_dir = snapshot_download( # type: ignore + hf_hub = cast(Any, huggingface_hub) + local_dir = hf_hub.snapshot_download( repo_id="gaia-benchmark/GAIA", repo_type="dataset", revision="682dd723ee1e1697e00360edccf2366dc8418dd9", @@ -376,6 +402,8 @@ class GAIA: local_dir=str(self.data_dir), force_download=False, ) + if not isinstance(local_dir, str): + raise TypeError("snapshot_download returned unexpected non-string path") return Path(local_dir) async def _run_single_task( @@ -522,7 +550,7 @@ class GAIA: # Run tasks semaphore = asyncio.Semaphore(parallel) - results = [] + results: list[TaskResult] = [] tasks_coroutines = [self._run_single_task(task, task_runner, semaphore, timeout) for task in tasks] @@ -561,7 +589,7 @@ class GAIA: with open(output_path, "w", encoding="utf-8") as f: for result in results: # Convert messages to serializable format - serializable_messages = [] + serializable_messages: list[dict[str, Any] | str] = [] if result.prediction.messages: for msg in result.prediction.messages: if hasattr(msg, "model_dump"): @@ -569,7 +597,7 @@ class GAIA: serializable_messages.append(msg.model_dump()) elif hasattr(msg, "__dict__"): # Regular object with attributes - serializable_messages.append(vars(msg)) + serializable_messages.append(cast(dict[str, Any], getattr(msg, "__dict__", {}))) else: # Fallback to string representation serializable_messages.append(str(msg)) @@ -614,16 +642,20 @@ def viewer_main() -> None: args = parser.parse_args() # Load results - results = [] + results: list[dict[str, Any]] = [] with open(args.results_file, encoding="utf-8") as f: for line in f: if line.strip(): try: import orjson - results.append(orjson.loads(line)) + parsed: object = orjson.loads(line) except ImportError: - results.append(json.loads(line)) + parsed = json.loads(line) + + record = _coerce_record(parsed) + if record is not None: + results.append(record) # Apply filters if args.level is not None: diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index 03d2ed9e55..17650293ac 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -122,6 +122,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["gaia/agent_framework_lab_gaia", "lightning/agent_framework_lab_lightning", "tau2/agent_framework_lab_tau2"] exclude = ['gaia/tests', 'lightning/tests', 'tau2/tests', 'namespace', '**/samples'] [tool.mypy] @@ -151,10 +152,10 @@ mypy-gaia = "mypy --config-file $POE_ROOT/pyproject.toml gaia/agent_framework_la mypy-lightning = "mypy --config-file $POE_ROOT/pyproject.toml lightning/agent_framework_lab_lightning" mypy-tau2 = "mypy --config-file $POE_ROOT/pyproject.toml tau2/agent_framework_lab_tau2" mypy = ["mypy-gaia", "mypy-lightning", "mypy-tau2"] -test = "pytest --cov-report=term-missing:skip-covered --junitxml=test-results.xml" -test-gaia = "pytest gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered" -test-lightning = "pytest lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered" -test-tau2 = "pytest tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered" +test = "pytest -m \"not integration\" --cov-report=term-missing:skip-covered --junitxml=test-results.xml" +test-gaia = "pytest -m \"not integration\" gaia/tests --cov=agent_framework_lab_gaia --cov-report=term-missing:skip-covered" +test-lightning = "pytest -m \"not integration\" lightning/tests --cov=agent_framework_lab_lightning --cov-report=term-missing:skip-covered" +test-tau2 = "pytest -m \"not integration\" tau2/tests --cov=agent_framework_lab_tau2 --cov-report=term-missing:skip-covered" build = "echo 'Skipping build'" publish = "echo 'Skipping publish'" diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py index bd8d521e28..bb617e3ad9 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_message_utils.py @@ -23,7 +23,7 @@ def flip_messages(messages: list[Message]) -> list[Message]: """Remove function call content from message contents.""" return [content for content in messages if content.type != "function_call"] - flipped_messages = [] + flipped_messages: list[Message] = [] for msg in messages: role_value = _get_role_value(msg.role) if role_value == "assistant": diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py b/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py index 75c0676cb6..5b1390c3dc 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/_tau2_utils.py @@ -3,7 +3,7 @@ import json from collections.abc import Mapping from copy import deepcopy -from typing import Any +from typing import Any, TypeGuard, cast import numpy as np from agent_framework._tools import FunctionTool @@ -27,6 +27,26 @@ from tau2.environment.tool import Tool # type: ignore[import-untyped] _original_set_state = Environment.set_state +def _to_str(value: object, default: str = "") -> str: + if isinstance(value, str): + return value + if value is None: + return default + return str(value) + + +def _is_any_list(value: Any) -> TypeGuard[list[Any]]: + return isinstance(value, list) + + +def _is_any_mapping(value: Any) -> TypeGuard[Mapping[Any, Any]]: + return isinstance(value, Mapping) + + +def _is_any_sequence(value: Any) -> TypeGuard[list[Any] | tuple[Any, ...] | set[Any]]: + return isinstance(value, (list, tuple, set)) + + def convert_tau2_tool_to_function_tool(tau2_tool: Tool) -> FunctionTool: """Convert a tau2 Tool to a FunctionTool for agent framework compatibility. @@ -41,7 +61,7 @@ def convert_tau2_tool_to_function_tool(tau2_tool: Tool) -> FunctionTool: return FunctionTool( name=tau2_tool.name, - description=tau2_tool._get_description(), + description=tau2_tool._get_description(), # pyright: ignore[reportPrivateUsage] func=wrapped_func, input_model=tau2_tool.params, ) @@ -53,27 +73,26 @@ def convert_agent_framework_messages_to_tau2_messages(messages: list[Message]) - Handles role mapping, text extraction, function calls, and function results. Function results are converted to separate ToolMessage instances. """ - tau2_messages = [] + tau2_messages: list[Tau2Message] = [] for msg in messages: role_str = str(msg.role) # Extract text content from all text-type contents - text_content = None text_contents = [c for c in msg.contents if hasattr(c, "text") and hasattr(c, "type") and c.type == "text"] - if text_contents: - text_content = " ".join(c.text for c in text_contents) # type: ignore[misc] + content_parts: list[str] = [_to_str(getattr(c, "text", "")) for c in text_contents] + content_value = " ".join(content_parts) # Extract function calls and convert to ToolCall objects function_calls = [c for c in msg.contents if hasattr(c, "type") and c.type == "function_call"] - tool_calls = None + tool_calls: list[ToolCall] | None = None if function_calls: tool_calls = [] for fc in function_calls: arguments = fc.parse_arguments() or {} tool_call = ToolCall( - id=fc.call_id, - name=fc.name, + id=_to_str(fc.call_id), + name=_to_str(fc.name), arguments=arguments, requestor="assistant" if role_str == "assistant" else "user", ) @@ -84,11 +103,11 @@ def convert_agent_framework_messages_to_tau2_messages(messages: list[Message]) - # Create main message based on role if role_str == "system": - tau2_messages.append(SystemMessage(role="system", content=text_content)) + tau2_messages.append(SystemMessage(role="system", content=content_value)) elif role_str == "user": - tau2_messages.append(UserMessage(role="user", content=text_content, tool_calls=tool_calls)) + tau2_messages.append(UserMessage(role="user", content=content_value, tool_calls=tool_calls)) elif role_str == "assistant": - tau2_messages.append(AssistantMessage(role="assistant", content=text_content, tool_calls=tool_calls)) + tau2_messages.append(AssistantMessage(role="assistant", content=content_value, tool_calls=tool_calls)) elif role_str == "tool": # Tool messages are handled as function results below pass @@ -98,7 +117,7 @@ def convert_agent_framework_messages_to_tau2_messages(messages: list[Message]) - dumpable_content = _dump_function_result(fr.result) content = dumpable_content if isinstance(dumpable_content, str) else json.dumps(dumpable_content) tool_msg = ToolMessage( - id=fr.call_id, + id=_to_str(fr.call_id), role="tool", content=content, requestor="assistant", # Most tool calls originate from assistant @@ -126,12 +145,10 @@ def patch_env_set_state() -> None: if self.solo_mode and any(isinstance(message, UserMessage) for message in message_history): raise ValueError("User messages are not allowed in solo mode") - def get_actions_from_messages( - messages: list[Tau2Message], - ) -> list[tuple[ToolCall, ToolMessage]]: + def get_actions_from_messages(messages: list[Tau2Message]) -> list[tuple[ToolCall, ToolMessage]]: """Get the actions from the messages.""" messages = deepcopy(messages)[::-1] - actions = [] + actions: list[tuple[ToolCall, ToolMessage]] = [] while messages: message = messages.pop() if isinstance(message, ToolMessage): @@ -153,10 +170,13 @@ def patch_env_set_state() -> None: return actions if initialization_data is not None: - if initialization_data.agent_data is not None: - self.tools.update_db(initialization_data.agent_data) - if initialization_data.user_data is not None: - self.user_tools.update_db(initialization_data.user_data) + agent_data = cast(object, getattr(initialization_data, "agent_data", None)) + if isinstance(agent_data, dict): + self.tools.update_db(cast(dict[str, Any], agent_data)) + + user_data = cast(object, getattr(initialization_data, "user_data", None)) + if isinstance(user_data, dict): + self.user_tools.update_db(cast(dict[str, Any], user_data)) if initialization_actions is not None: for action in initialization_actions: @@ -188,10 +208,11 @@ def unpatch_env_set_state() -> None: def _dump_function_result(result: Any) -> Any: if isinstance(result, BaseModel): return result.model_dump_json() - if isinstance(result, list): + if _is_any_list(result): return [_dump_function_result(item) for item in result] if isinstance(result, dict): - return {k: _dump_function_result(v) for k, v in result.items()} + result_dict = cast(dict[str, Any], result) + return {k: _dump_function_result(v) for k, v in result_dict.items()} if result is None: return None return result @@ -208,11 +229,11 @@ def _to_native(obj: Any) -> Any: return _to_native(obj.item()) # 3) Dict-like -> dict - if isinstance(obj, Mapping): + if _is_any_mapping(obj): return {_to_native(k): _to_native(v) for k, v in obj.items()} # 4) Lists/Tuples/Sets -> list - if isinstance(obj, (list, tuple, set)): + if _is_any_sequence(obj): return [_to_native(x) for x in obj] # 5) Anything else: leave as-is @@ -227,9 +248,10 @@ def _recursive_json_deserialize(obj: Any) -> Any: return _recursive_json_deserialize(deserialized) except (json.JSONDecodeError, TypeError): return obj - elif isinstance(obj, list): + elif _is_any_list(obj): return [_recursive_json_deserialize(item) for item in obj] elif isinstance(obj, dict): - return {k: _recursive_json_deserialize(v) for k, v in obj.items()} + typed_obj = cast(dict[str, Any], obj) + return {k: _recursive_json_deserialize(v) for k, v in typed_obj.items()} else: return obj diff --git a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py index 78a9496444..8d4aee310f 100644 --- a/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py +++ b/python/packages/lab/tau2/agent_framework_lab_tau2/runner.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from typing import Any +from typing import Any, cast from agent_framework import ( Agent, @@ -38,6 +38,16 @@ from ._tau2_utils import convert_agent_framework_messages_to_tau2_messages, conv __all__ = ["ASSISTANT_AGENT_ID", "ORCHESTRATOR_ID", "USER_SIMULATOR_ID", "TaskRunner"] + +def _get_openai_schema(tool: Any) -> dict[str, Any]: + schema = getattr(tool, "openai_schema", None) + if isinstance(schema, dict): + schema_dict = cast(dict[object, Any], schema) + if all(isinstance(key, str) for key in schema_dict): + return cast(dict[str, Any], schema_dict) + raise TypeError(f"Tool {tool} does not expose a dict openai_schema") + + # Agent instructions matching tau2's LLMAgent ASSISTANT_AGENT_INSTRUCTION = """ You are a customer service agent that helps the user according to the provided below. @@ -205,7 +215,7 @@ class TaskRunner: context_providers=[ SlidingWindowHistoryProvider( system_message=assistant_system_prompt, - tool_definitions=[tool.openai_schema for tool in tools], + tool_definitions=[_get_openai_schema(tool) for tool in tools], max_tokens=self.assistant_window_size, ) ], diff --git a/python/packages/mem0/agent_framework_mem0/_context_provider.py b/python/packages/mem0/agent_framework_mem0/_context_provider.py index 26ebca2d11..36b878e411 100644 --- a/python/packages/mem0/agent_framework_mem0/_context_provider.py +++ b/python/packages/mem0/agent_framework_mem0/_context_provider.py @@ -88,7 +88,7 @@ class Mem0ContextProvider(BaseContextProvider): async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: """Async context manager exit.""" if self._should_close_client and self.mem0_client and isinstance(self.mem0_client, AbstractAsyncContextManager): - await self.mem0_client.__aexit__(exc_type, exc_val, exc_tb) + await self.mem0_client.__aexit__(exc_type, exc_val, exc_tb) # pyright: ignore[reportUnknownMemberType] # -- Hooks pattern --------------------------------------------------------- diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index dc20e77fb6..506c4d75b1 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -61,6 +61,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_mem0"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -86,7 +87,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0" -test = "pytest --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index cc7fc0c9a7..e31c1971da 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -329,11 +329,11 @@ class OllamaChatClient( env_file_path=env_file_path, ) - self.model_id = ollama_settings["model_id"] + self.model_id = ollama_settings["model_id"] # type: ignore[assignment, reportTypedDictNotRequiredAccess] # we can just pass in None for the host, the default is set by the Ollama package. self.client = client or AsyncClient(host=ollama_settings.get("host")) # Save Host URL for serialization with to_dict() - self.host = str(self.client._client.base_url) # pyright: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType] + self.host = str(self.client._client.base_url) # type: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType] super().__init__( middleware=middleware, diff --git a/python/packages/ollama/agent_framework_ollama/_embedding_client.py b/python/packages/ollama/agent_framework_ollama/_embedding_client.py index 4fcf75b465..5cd35fc9f3 100644 --- a/python/packages/ollama/agent_framework_ollama/_embedding_client.py +++ b/python/packages/ollama/agent_framework_ollama/_embedding_client.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging import sys from collections.abc import Sequence -from typing import Any, ClassVar, Generic, TypedDict +from typing import Any, ClassVar, Generic, TypedDict, cast from agent_framework import ( BaseEmbeddingClient, @@ -107,9 +107,9 @@ class RawOllamaEmbeddingClient( env_file_encoding=env_file_encoding, ) - self.model_id = ollama_settings["embedding_model_id"] + self.model_id = ollama_settings["embedding_model_id"] # type: ignore[assignment,reportTypedDictNotRequiredAccess] self.client = client or AsyncClient(host=ollama_settings.get("host")) - self.host = str(self.client._client.base_url) # pyright: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType] + self.host = str(self.client._client.base_url) # type: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType] super().__init__(**kwargs) def service_url(self) -> str: @@ -120,8 +120,8 @@ class RawOllamaEmbeddingClient( self, values: Sequence[str], *, - options: OllamaEmbeddingOptionsT | None = None, - ) -> GeneratedEmbeddings[list[float]]: + options: OllamaEmbeddingOptionsT | None = None, # type: ignore + ) -> GeneratedEmbeddings[list[float], OllamaEmbeddingOptionsT]: """Call the Ollama embed API. Args: @@ -137,7 +137,7 @@ class RawOllamaEmbeddingClient( if not values: return GeneratedEmbeddings([], options=options) - opts: dict[str, Any] = dict(options) if options else {} + opts: dict[str, Any] = options or {} # type: ignore model = opts.get("model_id") or self.model_id if not model: raise ValueError("model_id is required") @@ -156,7 +156,7 @@ class RawOllamaEmbeddingClient( Embedding( vector=list(emb), dimensions=len(emb), - model_id=response.get("model") or model, + model_id=response.get("model") or model, # type: ignore[assignment] ) for emb in response.get("embeddings", []) ] @@ -166,7 +166,7 @@ class RawOllamaEmbeddingClient( if prompt_eval_count is not None: usage_dict = {"input_token_count": prompt_eval_count} - return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict) + return GeneratedEmbeddings(embeddings, options=cast(OllamaEmbeddingOptionsT, opts), usage=usage_dict) class OllamaEmbeddingClient( diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index c8bd9052ad..dd9ecaf46b 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -62,6 +62,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_ollama"] exclude = ['tests'] [tool.mypy] @@ -89,7 +90,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama" -test = "pytest --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests" [tool.uv.build-backend] module-name = "agent_framework_ollama" diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index 5d6e84ef05..4352a8af47 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -33,24 +33,25 @@ import inspect import json import logging import sys -from collections.abc import Awaitable, Callable, Sequence +from collections.abc import Awaitable, Callable, Mapping, Sequence +from copy import deepcopy from dataclasses import dataclass -from typing import Any, cast +from typing import Any from agent_framework import Agent, SupportsAgentRun from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware from agent_framework._sessions import AgentSession from agent_framework._tools import FunctionTool, tool -from agent_framework._types import AgentResponse, AgentResponseUpdate, Content, Message -from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse +from agent_framework._types import AgentResponse, Content, Message +from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest from agent_framework._workflows._agent_utils import resolve_agent_id from agent_framework._workflows._checkpoint import CheckpointStorage from agent_framework._workflows._events import WorkflowEvent from agent_framework._workflows._request_info_mixin import response_handler +from agent_framework._workflows._typing_utils import is_chat_agent from agent_framework._workflows._workflow import Workflow from agent_framework._workflows._workflow_builder import WorkflowBuilder from agent_framework._workflows._workflow_context import WorkflowContext -from typing_extensions import Never from ._base_group_chat_orchestrator import TerminationCondition from ._orchestrator_helpers import clean_conversation_for_handoff @@ -252,9 +253,8 @@ class HandoffAgentExecutor(AgentExecutor): Returns: A cloned ``Agent`` instance with handoff tools added """ - # Clone the agent to avoid mutating the original - cloned_agent = self._clone_chat_agent(agent) # type: ignore + cloned_agent = self._clone_chat_agent(agent) # Add handoff tools to the cloned agent self._apply_auto_tools(cloned_agent, handoffs) # Add middleware to handle handoff tool invocations @@ -347,46 +347,26 @@ class HandoffAgentExecutor(AgentExecutor): ) ) - def _clone_chat_agent(self, agent: Agent) -> Agent: + def _clone_chat_agent(self, agent: Agent[Any]) -> Agent[Any]: """Produce a deep copy of the Agent while preserving runtime configuration.""" options = agent.default_options - middleware = list(agent.middleware or []) # Reconstruct the original tools list by combining regular tools with MCP tools. # Agent.__init__ separates MCP tools during initialization, # so we need to recombine them here to pass the complete tools list to the constructor. # This makes sure MCP tools are preserved when cloning agents for handoff workflows. - tools_from_options = options.get("tools") - all_tools = list(tools_from_options) if tools_from_options else [] - if agent.mcp_tools: - all_tools.extend(agent.mcp_tools) - - logit_bias = options.get("logit_bias") - metadata = options.get("metadata") + tools_from_options = options.pop("tools", []) + new_tools = [*tools_from_options, *(agent.mcp_tools if agent.mcp_tools else [])] + # this ensures all options (including custom ones) are kept + cloned_options = deepcopy(options) # Disable parallel tool calls to prevent the agent from invoking multiple handoff tools at once. - cloned_options: dict[str, Any] = { - "allow_multiple_tool_calls": False, - # Handoff workflows already manage full conversation context explicitly - # across executors. Keep provider-side conversation storage disabled to - # avoid stale tool-call state (Responses API previous_response chains). - "store": False, - "frequency_penalty": options.get("frequency_penalty"), - "instructions": options.get("instructions"), - "logit_bias": dict(logit_bias) if logit_bias else None, - "max_tokens": options.get("max_tokens"), - "metadata": dict(metadata) if metadata else None, - "model_id": options.get("model_id"), - "presence_penalty": options.get("presence_penalty"), - "response_format": options.get("response_format"), - "seed": options.get("seed"), - "stop": options.get("stop"), - "temperature": options.get("temperature"), - "tool_choice": options.get("tool_choice"), - "tools": all_tools if all_tools else None, - "top_p": options.get("top_p"), - "user": options.get("user"), - } + cloned_options["allow_multiple_tool_calls"] = False + cloned_options["store"] = False + cloned_options["tools"] = new_tools + + # restore the original tools, in case they are shared between agents + options["tools"] = tools_from_options return Agent( client=agent.client, @@ -394,8 +374,8 @@ class HandoffAgentExecutor(AgentExecutor): name=agent.name, description=agent.description, context_providers=agent.context_providers, - middleware=middleware, - default_options=cloned_options, # type: ignore[arg-type] + middleware=agent.agent_middleware, + default_options=cloned_options, # type: ignore[assignment] ) def _apply_auto_tools(self, agent: Agent, targets: Sequence[HandoffConfiguration]) -> None: @@ -445,9 +425,7 @@ class HandoffAgentExecutor(AgentExecutor): return _handoff_tool @override - async def _run_agent_and_emit( - self, ctx: WorkflowContext[AgentExecutorResponse, AgentResponse | AgentResponseUpdate] - ) -> None: + async def _run_agent_and_emit(self, ctx: WorkflowContext[Any, Any]) -> None: """Override to support handoff.""" incoming_messages = list(self._cache) cleaned_incoming_messages = clean_conversation_for_handoff(incoming_messages) @@ -469,7 +447,7 @@ class HandoffAgentExecutor(AgentExecutor): # Broadcast the initial cache to all other agents. Subsequent runs won't # need this since responses are broadcast after each agent run and user input. if self._is_start_agent and not self._full_conversation: - await self._broadcast_messages(cleaned_incoming_messages, cast(WorkflowContext[AgentExecutorRequest], ctx)) + await self._broadcast_messages(cleaned_incoming_messages, ctx) # Persist only cleaned chat history between turns to avoid replaying stale tool calls. self._full_conversation.extend(cleaned_incoming_messages) @@ -483,29 +461,30 @@ class HandoffAgentExecutor(AgentExecutor): # If an existing session still has a service conversation id, clear it to avoid # replaying stale unresolved tool calls across resumed turns. if ( - cast(Agent, self._agent).default_options.get("store") is False + is_chat_agent(self._agent) + and self._agent.default_options.get("store") is False and self._session.service_session_id is not None ): self._session.service_session_id = None # Check termination condition before running the agent - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): + if await self._check_terminate_and_yield(ctx): return # Run the agent if ctx.is_streaming(): # Streaming mode: emit incremental updates - response = await self._run_agent_streaming(cast(WorkflowContext[Never, AgentResponseUpdate], ctx)) + response = await self._run_agent_streaming(ctx) else: # Non-streaming mode: use run() and emit single event - response = await self._run_agent(cast(WorkflowContext[Never, AgentResponse], ctx)) + response = await self._run_agent(ctx) # Clear the cache after running the agent self._cache.clear() # A function approval request is issued by the base AgentExecutor if response is None: - if cast(Agent, self._agent).default_options.get("store") is False: + if is_chat_agent(self._agent) and self._agent.default_options.get("store") is False: self._persist_pending_approval_function_calls() # Agent did not complete (e.g., waiting for user input); do not emit response logger.debug("AgentExecutor %s: Agent did not complete, awaiting user input", self.id) @@ -525,7 +504,7 @@ class HandoffAgentExecutor(AgentExecutor): ) # Broadcast only the cleaned response to other agents (without function_calls/results) - await self._broadcast_messages(cleaned_response, cast(WorkflowContext[AgentExecutorRequest], ctx)) + await self._broadcast_messages(cleaned_response, ctx) # Check if a handoff was requested if handoff_target := self._is_handoff_requested(response): @@ -535,7 +514,7 @@ class HandoffAgentExecutor(AgentExecutor): f"target '{handoff_target}'. Valid targets are: {', '.join(self._handoff_targets)}" ) - await cast(WorkflowContext[AgentExecutorRequest], ctx).send_message( + await ctx.send_message( AgentExecutorRequest(messages=[], should_respond=True), target_id=handoff_target, ) @@ -548,7 +527,7 @@ class HandoffAgentExecutor(AgentExecutor): # Re-evaluate termination after appending and broadcasting this response. # Without this check, workflows that become terminal due to the latest assistant # message would still emit request_info and require an unnecessary extra resume. - if await self._check_terminate_and_yield(cast(WorkflowContext[Never, list[Message]], ctx)): + if await self._check_terminate_and_yield(ctx): return # Handle case where no handoff was requested @@ -570,7 +549,7 @@ class HandoffAgentExecutor(AgentExecutor): self, original_request: HandoffAgentUserRequest, response: list[Message], - ctx: WorkflowContext[AgentExecutorResponse, AgentResponse], + ctx: WorkflowContext[Any, Any], ) -> None: """Handle user response for a request that is issued after agent runs. @@ -588,22 +567,20 @@ class HandoffAgentExecutor(AgentExecutor): If the response is empty, it indicates termination of the handoff workflow. """ if not response: - await cast(WorkflowContext[Never, list[Message]], ctx).yield_output(self._full_conversation) + await ctx.yield_output(self._full_conversation) return # Broadcast the user response to all other agents - await self._broadcast_messages(response, cast(WorkflowContext[AgentExecutorRequest], ctx)) + await self._broadcast_messages(response, ctx) # Append the user response messages to the cache self._cache.extend(response) - await self._run_agent_and_emit( - cast(WorkflowContext[AgentExecutorResponse, AgentResponse | AgentResponseUpdate], ctx) - ) + await self._run_agent_and_emit(ctx) async def _broadcast_messages( self, messages: list[Message], - ctx: WorkflowContext[AgentExecutorRequest], + ctx: WorkflowContext[Any, Any], ) -> None: """Broadcast the workflow cache to the agent before running.""" agent_executor_request = AgentExecutorRequest( @@ -628,15 +605,15 @@ class HandoffAgentExecutor(AgentExecutor): if content.type == "function_result": payload = content.result parsed_payload: dict[str, Any] | None = None - if isinstance(payload, dict): - parsed_payload = payload + if isinstance(payload, Mapping): + parsed_payload = {key: value for key, value in payload.items() if isinstance(key, str)} # pyright: ignore[reportUnknownVariableType] elif isinstance(payload, str): try: maybe_payload = json.loads(payload) except json.JSONDecodeError: maybe_payload = None - if isinstance(maybe_payload, dict): - parsed_payload = maybe_payload + if isinstance(maybe_payload, Mapping): + parsed_payload = {key: value for key, value in maybe_payload.items() if isinstance(key, str)} # pyright: ignore[reportUnknownVariableType] if parsed_payload: handoff_target = parsed_payload.get(HANDOFF_FUNCTION_RESULT_KEY) @@ -647,7 +624,7 @@ class HandoffAgentExecutor(AgentExecutor): return None - async def _check_terminate_and_yield(self, ctx: WorkflowContext[Never, list[Message]]) -> bool: + async def _check_terminate_and_yield(self, ctx: WorkflowContext[Any, Any]) -> bool: """Check termination conditions and yield completion if met. Args: diff --git a/python/packages/orchestrations/pyproject.toml b/python/packages/orchestrations/pyproject.toml index c670842715..e15e02f3e3 100644 --- a/python/packages/orchestrations/pyproject.toml +++ b/python/packages/orchestrations/pyproject.toml @@ -58,6 +58,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_orchestrations"] exclude = ['tests'] [tool.mypy] @@ -84,7 +85,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations" -test = "pytest --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" +test = "pytest -m \"not integration\" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/packages/purview/agent_framework_purview/_client.py b/python/packages/purview/agent_framework_purview/_client.py index a1f404849b..e592f34da5 100644 --- a/python/packages/purview/agent_framework_purview/_client.py +++ b/python/packages/purview/agent_framework_purview/_client.py @@ -6,7 +6,7 @@ import base64 import inspect import json import logging -from typing import Any, cast +from typing import Any, Literal, TypeVar, overload from uuid import uuid4 import httpx @@ -36,6 +36,8 @@ from ._settings import PurviewSettings, get_purview_scopes logger = logging.getLogger("agent_framework.purview") +ResponseT = TypeVar("ResponseT") + class PurviewClient: """Async client for calling Graph Purview endpoints. @@ -98,7 +100,7 @@ class PurviewClient: with get_tracer().start_as_current_span("purview.process_content"): token = await self._get_token(tenant_id=request.tenant_id) url = f"{self._graph_uri}/users/{request.user_id}/dataSecurityAndGovernance/processContent" - headers = {} + headers: dict[str, str] = {} # Add If-None-Match header if scope_identifier is present if hasattr(request, "scope_identifier") and request.scope_identifier: headers["If-None-Match"] = request.scope_identifier @@ -106,21 +108,23 @@ class PurviewClient: if hasattr(request, "process_inline") and request.process_inline: headers["Prefer"] = "evaluateInline" - response = await self._post( + response: ProcessContentResponse | tuple[ProcessContentResponse, httpx.Headers] = await self._post( url, request, ProcessContentResponse, token, headers=headers, return_response=True ) if isinstance(response, tuple) and len(response) == 2: response_obj, _ = response - return cast(ProcessContentResponse, response_obj) + return response_obj - return cast(ProcessContentResponse, response) + return response async def get_protection_scopes(self, request: ProtectionScopesRequest) -> ProtectionScopesResponse: with get_tracer().start_as_current_span("purview.get_protection_scopes"): token = await self._get_token() url = f"{self._graph_uri}/users/{request.user_id}/dataSecurityAndGovernance/protectionScopes/compute" - response = await self._post(url, request, ProtectionScopesResponse, token, return_response=True) + response: ProtectionScopesResponse | tuple[ProtectionScopesResponse, httpx.Headers] = await self._post( + url, request, ProtectionScopesResponse, token, return_response=True + ) # Extract etag from response headers if isinstance(response, tuple) and len(response) == 2: @@ -128,25 +132,47 @@ class PurviewClient: if "etag" in headers: etag_value = headers["etag"].strip('"') response_obj.scope_identifier = etag_value - return cast(ProtectionScopesResponse, response_obj) + return response_obj - return cast(ProtectionScopesResponse, response) + return response async def send_content_activities(self, request: ContentActivitiesRequest) -> ContentActivitiesResponse: with get_tracer().start_as_current_span("purview.send_content_activities"): token = await self._get_token() url = f"{self._graph_uri}/users/{request.user_id}/dataSecurityAndGovernance/activities/contentActivities" - return cast(ContentActivitiesResponse, await self._post(url, request, ContentActivitiesResponse, token)) + return await self._post(url, request, ContentActivitiesResponse, token) + + @overload + async def _post( + self, + url: str, + model: Any, + response_type: type[ResponseT], + token: str, + headers: dict[str, str] | None = None, + return_response: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def _post( + self, + url: str, + model: Any, + response_type: type[ResponseT], + token: str, + headers: dict[str, str] | None = None, + return_response: Literal[True] = True, + ) -> tuple[ResponseT, httpx.Headers]: ... async def _post( self, url: str, model: Any, - response_type: type[Any], + response_type: type[ResponseT], token: str, headers: dict[str, str] | None = None, return_response: bool = False, - ) -> Any: + ) -> ResponseT | tuple[ResponseT, httpx.Headers]: if hasattr(model, "correlation_id") and not model.correlation_id: model.correlation_id = str(uuid4()) @@ -174,7 +200,7 @@ class PurviewClient: raise PurviewAuthenticationError(f"Auth failure {resp.status_code}: {resp.text}") if resp.status_code == 402: if self._settings.get("ignore_payment_required", False): - return response_type() # type: ignore[call-arg, no-any-return] + return response_type() # type: ignore[call-arg] raise PurviewPaymentRequiredError(f"Payment required {resp.status_code}: {resp.text}") if resp.status_code == 429: raise PurviewRateLimitError(f"Rate limited {resp.status_code}: {resp.text}") @@ -187,18 +213,18 @@ class PurviewClient: try: # Prefer pydantic-style model_validate if present, else fall back to constructor. - if hasattr(response_type, "model_validate"): - response_obj = response_type.model_validate(data) # type: ignore[no-any-return] - else: - response_obj = response_type(**data) # type: ignore[call-arg, no-any-return] + model_validate = getattr(response_type, "model_validate", None) + response_obj = model_validate(data) if callable(model_validate) else response_type(**data) # type: ignore[call-arg] # Extract correlation_id from response headers if response object supports it if "client-request-id" in resp.headers and hasattr(response_obj, "correlation_id"): - response_obj.correlation_id = resp.headers["client-request-id"] - logger.info(f"Purview response from {url} with correlation_id: {response_obj.correlation_id}") + response_correlation_id = resp.headers["client-request-id"] + response_obj.correlation_id = response_correlation_id # pyright: ignore[reportAttributeAccessIssue] + logger.info(f"Purview response from {url} with correlation_id: {response_correlation_id}") + typed_response_obj = response_obj if isinstance(response_obj, response_type) else response_type(**data) if return_response: - return (response_obj, resp.headers) - return response_obj + return (typed_response_obj, resp.headers) + return typed_response_obj except Exception as ex: raise PurviewServiceError(f"Failed to deserialize Purview response: {ex}") from ex diff --git a/python/packages/purview/agent_framework_purview/_middleware.py b/python/packages/purview/agent_framework_purview/_middleware.py index 55619d0a39..c0e89a04a5 100644 --- a/python/packages/purview/agent_framework_purview/_middleware.py +++ b/python/packages/purview/agent_framework_purview/_middleware.py @@ -67,6 +67,7 @@ class PurviewPolicyMiddleware(AgentMiddleware): call_next: Callable[[], Awaitable[None]], ) -> None: # type: ignore[override] resolved_user_id: str | None = None + session_id: str | None = None try: # Pre (prompt) check session_id = self._get_agent_session_id(context) @@ -107,7 +108,7 @@ class PurviewPolicyMiddleware(AgentMiddleware): should_block_response, _ = await self._processor.process_messages( context.result.messages, # type: ignore[union-attr] Activity.DOWNLOAD_TEXT, - session_id=session_id, + session_id=session_id_response, user_id=resolved_user_id, ) if should_block_response: @@ -173,6 +174,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware): call_next: Callable[[], Awaitable[None]], ) -> None: # type: ignore[override] resolved_user_id: str | None = None + session_id: str | None = None try: session_id = context.options.get("conversation_id") if context.options else None should_block_prompt, resolved_user_id = await self._processor.process_messages( diff --git a/python/packages/purview/agent_framework_purview/_models.py b/python/packages/purview/agent_framework_purview/_models.py index ad6cc5b331..503871deef 100644 --- a/python/packages/purview/agent_framework_purview/_models.py +++ b/python/packages/purview/agent_framework_purview/_models.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from collections.abc import Mapping, MutableMapping, Sequence +from collections.abc import Iterable, Mapping, MutableMapping, Sequence from datetime import datetime from enum import Enum, Flag, auto from typing import Any, ClassVar, TypeVar, cast @@ -60,6 +60,23 @@ _PROTECTION_SCOPE_ACTIVITIES_SERIALIZE_ORDER: list[tuple[str, ProtectionScopeAct ] +def _as_object_list(value: object) -> list[object] | None: + if not isinstance(value, (list, tuple, set)): + return None + return list(cast(Iterable[object], value)) + + +def _as_str_dict(value: object) -> dict[str, str]: + if not isinstance(value, dict): + return {} + + aliases: dict[str, str] = {} + for raw_key, raw_value in cast(dict[object, object], value).items(): + if isinstance(raw_key, str) and isinstance(raw_value, str): + aliases[raw_key] = raw_value + return aliases + + def deserialize_flag( value: object, mapping: Mapping[str, FlagT], enum_cls: type[FlagT] ) -> FlagT | None: # pragma: no cover @@ -82,8 +99,11 @@ def deserialize_flag( if not raw: return enum_cls(0) parts.extend([p.strip() for p in raw.split(",") if p.strip()]) - elif isinstance(value, (list, tuple, set)): - for item in value: + else: + iterable_items = _as_object_list(value) + if iterable_items is None: + return None + for item in iterable_items: if isinstance(item, str): parts.extend([p.strip() for p in item.split(",") if p.strip()]) elif isinstance(item, enum_cls): @@ -93,8 +113,6 @@ def deserialize_flag( flag_value |= enum_cls(item) except Exception: logger.warning(f"Failed to convert int {item} to {enum_cls.__name__}") - else: - return None for part in parts: member = mapping.get(part) @@ -196,10 +214,10 @@ class _AliasSerializable(SerializationMixin): # Collect all aliases from parent classes too all_aliases: dict[str, str] = {} for cls in type(self).__mro__: - if hasattr(cls, "_ALIASES") and isinstance(cls._ALIASES, dict): - for internal, external in cls._ALIASES.items(): - if external not in all_aliases: - all_aliases[external] = internal + aliases_obj = _as_str_dict(getattr(cls, "_ALIASES", None)) + for internal, external in aliases_obj.items(): + if external not in all_aliases: + all_aliases[external] = internal # Normalize all aliased keys in kwargs for external, internal in all_aliases.items(): @@ -248,11 +266,11 @@ class _AliasSerializable(SerializationMixin): # Collect all aliases from class hierarchy all_aliases: dict[str, str] = {} for cls in type(self).__mro__: - if hasattr(cls, "_ALIASES") and isinstance(cls._ALIASES, dict): - # Parent aliases first (will be overridden by child if same key) - for internal, external in cls._ALIASES.items(): - if internal not in all_aliases: - all_aliases[internal] = external + aliases_obj = _as_str_dict(getattr(cls, "_ALIASES", None)) + # Parent aliases first (will be overridden by child if same key) + for internal, external in aliases_obj.items(): + if internal not in all_aliases: + all_aliases[internal] = external if not all_aliases: return base @@ -836,17 +854,15 @@ class ProcessContentResponse(_AliasSerializable): # Convert to objects converted_policy_actions: list[DlpActionInfo] | None = None if policy_actions is not None: - converted_policy_actions = cast( - list[DlpActionInfo], - [p if isinstance(p, DlpActionInfo) else DlpActionInfo(**p) for p in policy_actions], - ) + converted_policy_actions = [ + p if isinstance(p, DlpActionInfo) else DlpActionInfo(**p) for p in policy_actions + ] converted_processing_errors: list[ProcessingError] | None = None if processing_errors is not None: - converted_processing_errors = cast( - list[ProcessingError], - [pe if isinstance(pe, ProcessingError) else ProcessingError(**pe) for pe in processing_errors], - ) + converted_processing_errors = [ + pe if isinstance(pe, ProcessingError) else ProcessingError(**pe) for pe in processing_errors + ] super().__init__(**kwargs) self.id = id @@ -885,17 +901,15 @@ class PolicyScope(_AliasSerializable): # Convert nested objects converted_locations: list[PolicyLocation] | None = None if locations is not None: - converted_locations = cast( - list[PolicyLocation], - [loc if isinstance(loc, PolicyLocation) else PolicyLocation(**loc) for loc in locations], - ) + converted_locations = [ + loc if isinstance(loc, PolicyLocation) else PolicyLocation(**loc) for loc in locations + ] converted_policy_actions: list[DlpActionInfo] | None = None if policy_actions is not None: - converted_policy_actions = cast( - list[DlpActionInfo], - [p if isinstance(p, DlpActionInfo) else DlpActionInfo(**p) for p in policy_actions], - ) + converted_policy_actions = [ + p if isinstance(p, DlpActionInfo) else DlpActionInfo(**p) for p in policy_actions + ] # Call parent without explicit params with aliases super().__init__(**kwargs) @@ -947,9 +961,7 @@ class ProtectionScopesResponse(_AliasSerializable): converted_scopes: list[PolicyScope] | None = None if scopes is not None: - converted_scopes = cast( - list[PolicyScope], [s if isinstance(s, PolicyScope) else PolicyScope(**s) for s in scopes] - ) + converted_scopes = [s if isinstance(s, PolicyScope) else PolicyScope(**s) for s in scopes] # Don't pass parameters that have aliases - let parent normalize them super().__init__(**kwargs) diff --git a/python/packages/purview/agent_framework_purview/_processor.py b/python/packages/purview/agent_framework_purview/_processor.py index e911fae7a5..241de80d61 100644 --- a/python/packages/purview/agent_framework_purview/_processor.py +++ b/python/packages/purview/agent_framework_purview/_processor.py @@ -177,14 +177,13 @@ class ScopedContentProcessor: else: raise ValueError("App location not provided or inferable") + app_name = self._settings.get("app_name") or "Unknown" protected_app = ProtectedAppMetadata( - name=self._settings["app_name"], + name=app_name, version=self._settings.get("app_version", "Unknown"), application_location=policy_location, ) - integrated_app = IntegratedAppMetadata( - name=self._settings["app_name"], version=self._settings.get("app_version", "Unknown") - ) + integrated_app = IntegratedAppMetadata(name=app_name, version=self._settings.get("app_version", "Unknown")) device_meta = DeviceMetadata( operating_system_specifications=OperatingSystemSpecifications( operating_system_platform="Unknown", operating_system_version="Unknown" @@ -234,9 +233,9 @@ class ScopedContentProcessor: if cached_ps_resp is not None and isinstance(cached_ps_resp, ProtectionScopesResponse): ps_resp = cached_ps_resp else: + ttl = self._settings.get("cache_ttl_seconds") + ttl_seconds = ttl if ttl is not None else 14400 try: - ttl = self._settings.get("cache_ttl_seconds") - ttl_seconds = ttl if ttl is not None else 14400 ps_resp = await self._client.get_protection_scopes(ps_req) await self._cache.set(cache_key, ps_resp, ttl_seconds=ttl_seconds) except PurviewPaymentRequiredError as ex: diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index aed447580a..f30b749435 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -60,6 +60,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_purview"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -85,7 +86,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview" -test = "pytest --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.9,<4.0"] diff --git a/python/packages/redis/agent_framework_redis/_context_provider.py b/python/packages/redis/agent_framework_redis/_context_provider.py index 75886d25c3..32b6a6cc5d 100644 --- a/python/packages/redis/agent_framework_redis/_context_provider.py +++ b/python/packages/redis/agent_framework_redis/_context_provider.py @@ -12,7 +12,7 @@ import json import sys from functools import reduce from operator import and_ -from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast +from typing import TYPE_CHECKING, Any, ClassVar, Literal import numpy as np from agent_framework import Message @@ -107,9 +107,10 @@ class RedisContextProvider(BaseContextProvider): self._token_escaper: TokenEscaper = TokenEscaper() self._index_initialized: bool = False self._schema_dict: dict[str, Any] | None = None - self.redis_index = redis_index or AsyncSearchIndex.from_dict( + index = redis_index or AsyncSearchIndex.from_dict( # pyright: ignore[reportUnknownMemberType] self.schema_dict, redis_url=self.redis_url, validate_on_load=True ) + self.redis_index: Any = index # -- Hooks pattern --------------------------------------------------------- @@ -189,7 +190,7 @@ class RedisContextProvider(BaseContextProvider): def _build_filter_from_dict(self, filters: dict[str, str | None]) -> Any | None: """Builds a combined filter expression from simple equality tags.""" - parts = [Tag(k) == v for k, v in filters.items() if v] + parts: list[FilterExpression] = [Tag(k) == v for k, v in filters.items() if v] return reduce(and_, parts) if parts else None def _build_schema_dict( @@ -278,7 +279,9 @@ class RedisContextProvider(BaseContextProvider): sig["fields"][name] = {"type": ftype} return sig - existing_index = await AsyncSearchIndex.from_existing(self.index_name, redis_url=self.redis_url) + existing_index: Any = await AsyncSearchIndex.from_existing( # pyright: ignore[reportUnknownMemberType] + self.index_name, redis_url=self.redis_url + ) existing_schema = existing_index.schema.to_dict() current_schema = self.schema_dict existing_sig = _schema_signature(existing_schema) @@ -319,7 +322,9 @@ class RedisContextProvider(BaseContextProvider): if self.redis_vectorizer and self.vector_field_name: text_list = [d["content"] for d in prepared] - embeddings = await self.redis_vectorizer.aembed_many(text_list, batch_size=len(text_list)) + embeddings = await self.redis_vectorizer.aembed_many( # pyright: ignore[reportUnknownMemberType] + text_list, batch_size=len(text_list) + ) for i, d in enumerate(prepared): vec = np.asarray(embeddings[i], dtype=np.float32).tobytes() field_name: str = self.vector_field_name @@ -365,7 +370,7 @@ class RedisContextProvider(BaseContextProvider): try: if self.redis_vectorizer and self.vector_field_name: - vector = await self.redis_vectorizer.aembed(q) + vector = await self.redis_vectorizer.aembed(q) # pyright: ignore[reportUnknownMemberType] query = HybridQuery( text=q, text_field_name="content", @@ -374,13 +379,12 @@ class RedisContextProvider(BaseContextProvider): text_scorer=text_scorer, filter_expression=combined_filter, linear_alpha=linear_alpha, - dtype=self.redis_vectorizer.dtype, + dtype=self.redis_vectorizer.dtype, # pyright: ignore[reportUnknownMemberType] num_results=num_results, return_fields=return_fields, stopwords=None, ) - hybrid_results = await self.redis_index.query(query) - return cast(list[dict[str, Any]], hybrid_results) + return await self.redis_index.query(query) # type: ignore[no-any-return] query = TextQuery( text=q, text_field_name="content", @@ -390,8 +394,7 @@ class RedisContextProvider(BaseContextProvider): return_fields=return_fields, stopwords=None, ) - text_results = await self.redis_index.query(query) - return cast(list[dict[str, Any]], text_results) + return await self.redis_index.query(query) # type: ignore[no-any-return] except Exception as exc: # pragma: no cover raise IntegrationInvalidRequestException(f"Redis text search failed: {exc}") from exc diff --git a/python/packages/redis/agent_framework_redis/_history_provider.py b/python/packages/redis/agent_framework_redis/_history_provider.py index 7f246c885b..e1a20b6218 100644 --- a/python/packages/redis/agent_framework_redis/_history_provider.py +++ b/python/packages/redis/agent_framework_redis/_history_provider.py @@ -118,11 +118,11 @@ class RedisHistoryProvider(BaseHistoryProvider): List of stored Message objects in chronological order. """ key = self._redis_key(session_id) - redis_messages = await self._redis_client.lrange(key, 0, -1) # type: ignore[misc] + redis_messages: list[str] = await self._redis_client.lrange(key, 0, -1) # type: ignore[misc] messages: list[Message] = [] if redis_messages: - for serialized in redis_messages: - messages.append(Message.from_dict(self._deserialize_json(serialized))) + for serialized in redis_messages: # type: ignore[union-attr] + messages.append(Message.from_dict(self._deserialize_json(serialized))) # type: ignore[union-attr] return messages async def save_messages(self, session_id: str | None, messages: Sequence[Message], **kwargs: Any) -> None: diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index 76b84ad600..21aaf47865 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -63,6 +63,7 @@ omit = [ [tool.pyright] extends = "../../pyproject.toml" +include = ["agent_framework_redis"] [tool.mypy] plugins = ['pydantic.mypy'] @@ -88,7 +89,7 @@ include = "../../shared_tasks.toml" [tool.poe.tasks] mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis" -test = "pytest --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests" +test = "pytest -m \"not integration\" --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests" [build-system] requires = ["flit-core >= 3.11,<4.0"] diff --git a/python/pyproject.toml b/python/pyproject.toml index b8588b7b9d..9f4ca3c08c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -183,10 +183,11 @@ omit = [ ] [tool.pyright] -include = ["agent_framework*"] +exclude = ["**/tests/**", "**/.venv/**", "packages/devui/frontend/**"] typeCheckingMode = "strict" reportUnnecessaryIsInstance = false reportMissingTypeStubs = false +reportUnnecessaryCast = "error" [tool.mypy] plugins = ['pydantic.mypy'] diff --git a/python/uv.lock b/python/uv.lock index 28877c91d2..7233077c30 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -525,7 +525,7 @@ source = { editable = "packages/github_copilot" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "github-copilot-sdk", version = "0.1.25", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "github-copilot-sdk", version = "0.1.29", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "github-copilot-sdk", version = "0.1.30", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] [package.metadata] @@ -1377,19 +1377,19 @@ wheels = [ [[package]] name = "claude-agent-sdk" -version = "0.1.44" +version = "0.1.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/40/5661e10daf69ee5c864f82a1888cc33c9378b2d7f7d11db3c2360aef3a30/claude_agent_sdk-0.1.44.tar.gz", hash = "sha256:8629436e7af367a1cbc81aa2a58a93aa68b8b2e4e14b0c5be5ac3627bd462c1b", size = 62439, upload-time = "2026-02-26T01:17:28.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/e2/c5d5c4743ece496492a930bb75b878c830a9a9878ae3327b2d292647a8fa/claude_agent_sdk-0.1.45.tar.gz", hash = "sha256:97c1e981431b5af1e08c34731906ab8d4a58fe0774a04df0ea9587dcabc85151", size = 62436, upload-time = "2026-03-03T17:21:08.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/1a/dcde83a6477bfdf8c5510fd84006cca763296e6bc5576e90cd89b97ec034/claude_agent_sdk-0.1.44-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1dd976ad3efb673aefd5037dc75ee7926fb5033c4b9ab7382897ab647fed74e6", size = 55828889, upload-time = "2026-02-26T01:17:15.474Z" }, - { url = "https://files.pythonhosted.org/packages/4b/33/3b161256956968e18c81e2b2650fed7d2a1144d51042ed6317848643e5d7/claude_agent_sdk-0.1.44-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:d35b38ca40fa28f50fa88705599a298ab30c121c56b53655025eeceb463ac399", size = 70795212, upload-time = "2026-02-26T01:17:18.873Z" }, - { url = "https://files.pythonhosted.org/packages/17/cb/67af9796dad77a94dfe851138f5ffc9e2e0a14407ba55fea07462c1cc8e5/claude_agent_sdk-0.1.44-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:853c15501f71a913a6cc6b40dc0b24b9505166cad164206b8eab229889e670b8", size = 71424685, upload-time = "2026-02-26T01:17:22.345Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/2d3806c791250a76de2c1be863fc01d420729ad61496253e3d3033464c72/claude_agent_sdk-0.1.44-py3-none-win_amd64.whl", hash = "sha256:597e2fcad372086f93e4f6a380d3088ec4dd9b9efce309c5281b52a256fd5d25", size = 73493771, upload-time = "2026-02-26T01:17:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/20/29/a28b6dfac54dfceddaa47e16c2b9cb61cc2ace4b4a1de064ab6d76debcbd/claude_agent_sdk-0.1.45-py3-none-macosx_11_0_arm64.whl", hash = "sha256:26a5cc60c3a394f5b814f6b2f67650819cbcd38c405bbdc11582b3e097b3a770", size = 57761380, upload-time = "2026-03-03T17:20:55.066Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/a803cc6e40de8b13cc822c66fd96c96d88f994983c2622d80cb8b708bb30/claude_agent_sdk-0.1.45-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:decc741b53e0b2c10a64fd84c15acca1102077d9f99941c54905172cd95160c9", size = 73402101, upload-time = "2026-03-03T17:20:58.604Z" }, + { url = "https://files.pythonhosted.org/packages/32/51/bdb9832728189673c60c605854c2153e17dce384a64a6dc88cdbb254ce86/claude_agent_sdk-0.1.45-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:7d48dcf4178c704e4ccbf3f1f4ebf20b3de3f03d0592086c1f3abd16b8ca441e", size = 74091498, upload-time = "2026-03-03T17:21:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/13/37/02e60d7f93aedc8f63f9404cbf2a48bf5d47c27ccb9c0a0f03c803882fa5/claude_agent_sdk-0.1.45-py3-none-win_amd64.whl", hash = "sha256:d1cf34995109c513d8daabcae7208edc260b553b53462a9ac06a7c40e240a288", size = 75784070, upload-time = "2026-03-03T17:21:05.573Z" }, ] [[package]] @@ -2301,7 +2301,7 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "0.1.29" +version = "0.1.30" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -2322,12 +2322,12 @@ dependencies = [ { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8e/2155e40594a60084266d33cefd2333fe3ce44e7189773e6eff9943e25d81/github_copilot_sdk-0.1.29-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:0215045cf6ec2cebfc6dbb0e257e2116d4aa05751f80cc48d5f3c8c658933094", size = 58182462, upload-time = "2026-02-27T22:09:59.687Z" }, - { url = "https://files.pythonhosted.org/packages/55/6a/9fa577564702eb1eb143c16afcdadf7d6305da53fbbd05a0925035808d9e/github_copilot_sdk-0.1.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:441c917aad8501da5264026b0da5c0e834571256e812617437654ab16bdad77f", size = 54934772, upload-time = "2026-02-27T22:10:02.911Z" }, - { url = "https://files.pythonhosted.org/packages/69/77/0e0fd6f6a0177d93f5f3e5d0e9ed5044fc53c54e58e65bbc6b08eb789350/github_copilot_sdk-0.1.29-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:88230b779dee1695fc44043060006224138c5b5d6724890f7ecdc378ff0d8f73", size = 61071028, upload-time = "2026-02-27T22:10:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/94/f5/9a73bd6e34db4d0ce546b04725cfad1c9fa58426265876b640376381b623/github_copilot_sdk-0.1.29-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2019bbbaea39d8db54250d11431d89952dd0ad0a16b58159b6b018ea625c78c9", size = 59251702, upload-time = "2026-02-27T22:10:09.466Z" }, - { url = "https://files.pythonhosted.org/packages/ea/32/60713b1ae3ed80b62113f993bd2f4552d2b03753cfea37f90086ac8e6d6e/github_copilot_sdk-0.1.29-py3-none-win_amd64.whl", hash = "sha256:a326fe5ab6ecd7cef5de39d5a5fe18e09e629eb29b401be23a709e83fc578578", size = 53690857, upload-time = "2026-02-27T22:10:12.778Z" }, - { url = "https://files.pythonhosted.org/packages/58/31/d082f4ac13cf3e4ba3a7846b8468521d6d38967de3788a61b6001707fbb5/github_copilot_sdk-0.1.29-py3-none-win_arm64.whl", hash = "sha256:1ace40f23ab8d8c97f8d61d31d01946ade9c83ea7982671864ec5aef0cd7dd01", size = 51699152, upload-time = "2026-02-27T22:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/92b8037c0673999ac1c49e9d079cf6d36283e6ee3453d66b54878da81bc8/github_copilot_sdk-0.1.30-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:47e95246a63beeebf192db6013662c5f39778ccfa6b1b718b79cbec6b6a88bf8", size = 58182964, upload-time = "2026-03-03T17:21:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/08/79/9d0628fa819df73e92ebbd4af949cdd82850cc4bde79b3e78040fcd8ed80/github_copilot_sdk-0.1.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:601cbe1c5a576906b73cbf8591429451c91148bff5a564e56e1e83ff99b2dc10", size = 54935274, upload-time = "2026-03-03T17:21:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/10/5d/f407e9c9155f912780b4587ab74abf3b94fae91af0463bad317cc8aacdfe/github_copilot_sdk-0.1.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:735fb90683bea27a418a0d45df430492db2a395e5ae88d575ac138be49d6cf07", size = 61071530, upload-time = "2026-03-03T17:22:01.601Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/5c2ab2baf5f185150058c774da2b5e4c613b4532c48b499ce127419da461/github_copilot_sdk-0.1.30-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:21ade06dfe5ca111663c42fff000ab3ec6595e51b1cf4ab56ff550cdd7a2992f", size = 59252204, upload-time = "2026-03-03T17:22:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/4e72ccdc8868250ba8c5d48a1fef5a8244361c2a586820de9b77df0c79ed/github_copilot_sdk-0.1.30-py3-none-win_amd64.whl", hash = "sha256:f1be9e49da2af370a914d4425bfecbc2daecf8e5de0074beaa1e22735bdd5da6", size = 53691358, upload-time = "2026-03-03T17:22:09.474Z" }, + { url = "https://files.pythonhosted.org/packages/53/4f/25ff085d0d5d50d1197fd6ae9a53adc4cc8298940212f5a69f7ced68c33e/github_copilot_sdk-0.1.30-py3-none-win_arm64.whl", hash = "sha256:3e0691eb3030c385f629d63d74ded938e0577fcd98f452259efd5d7fb2283576", size = 51699653, upload-time = "2026-03-03T17:22:13.215Z" }, ] [[package]]