mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-xunit3-mtp-upgrade
This commit is contained in:
@@ -416,6 +416,10 @@
|
||||
<File Path="src/Shared/IntegrationTests/OpenAIConfiguration.cs" />
|
||||
<File Path="src/Shared/IntegrationTests/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/IntegrationTestsAzureCredentials/">
|
||||
<File Path="src/Shared/IntegrationTestsAzureCredentials/README.md" />
|
||||
<File Path="src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs" />
|
||||
</Folder>
|
||||
<Folder Name="/Solution Items/src/Shared/Samples/">
|
||||
<File Path="src/Shared/Samples/BaseSample.cs" />
|
||||
<File Path="src/Shared/Samples/README.md" />
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTests\*.cs" LinkBase="Shared\IntegrationTests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedIntegrationTestAzureCredentialsCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\IntegrationTestsAzureCredentials\*.cs" LinkBase="Shared\IntegrationTestsAzureCredentials" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(InjectSharedBuildTestCode)' == 'true'">
|
||||
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\CodeTests\*.cs" LinkBase="Shared\CodeTests" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<RCNumber>2</RCNumber>
|
||||
<RCNumber>3</RCNumber>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' == 'true'">$(VersionPrefix)-rc$(RCNumber)</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260225.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260225.1</PackageVersion>
|
||||
<GitTag>1.0.0-rc2</GitTag>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260304.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(IsReleaseCandidate)' != 'true' AND '$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260304.1</PackageVersion>
|
||||
<GitTag>1.0.0-rc3</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -346,14 +346,14 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
|
||||
};
|
||||
}
|
||||
|
||||
private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
|
||||
internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage)
|
||||
{
|
||||
TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty)
|
||||
AIContent content = new()
|
||||
{
|
||||
RawRepresentation = assistantMessage
|
||||
};
|
||||
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [textContent])
|
||||
return new AgentResponseUpdate(ChatRole.Assistant, [content])
|
||||
{
|
||||
AgentId = this.Id,
|
||||
ResponseId = assistantMessage.Data?.MessageId,
|
||||
|
||||
@@ -747,9 +747,15 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
{
|
||||
// The agent has a ChatHistoryProvider configured, but the service returned a conversation id,
|
||||
// meaning the service manages chat history server-side. Both cannot be used simultaneously.
|
||||
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true)
|
||||
if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true
|
||||
&& this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
this._logger.LogAgentChatClientHistoryProviderConflict(nameof(ChatClientAgentSession.ConversationId), nameof(this.ChatHistoryProvider), this.Id, this.GetLoggingAgentName());
|
||||
var loggingAgentName = this.GetLoggingAgentName();
|
||||
this._logger.LogAgentChatClientHistoryProviderConflict(
|
||||
nameof(ChatClientAgentSession.ConversationId),
|
||||
nameof(this.ChatHistoryProvider),
|
||||
this.Id,
|
||||
loggingAgentName);
|
||||
}
|
||||
|
||||
if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Integration Tests Azure Credentials
|
||||
|
||||
Adds a helper for loading Azure credentials in integration tests.
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>true</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
```
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides credential instances for integration tests with
|
||||
/// increased timeouts to avoid CI pipeline authentication failures.
|
||||
/// </summary>
|
||||
internal static class TestAzureCliCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The default timeout for Azure CLI credential operations.
|
||||
/// Increased from the default (~13s) to accommodate CI pipeline latency.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="AzureCliCredential"/> with an increased process timeout
|
||||
/// suitable for CI environments.
|
||||
/// </summary>
|
||||
public static AzureCliCredential CreateAzureCliCredential() =>
|
||||
new(new AzureCliCredentialOptions { ProcessTimeout = s_processTimeout });
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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;
|
||||
@@ -170,13 +169,13 @@ public class AIProjectClientFixture : IChatClientAgentFixture
|
||||
|
||||
public virtual async ValueTask 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CS8793</NoWarn>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CS8793</NoWarn>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+1
-2
@@ -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")]
|
||||
|
||||
+1
-2
@@ -7,7 +7,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;
|
||||
@@ -99,7 +98,7 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential());
|
||||
this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
this._agent = await this.CreateChatClientAgentAsync();
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Public packages required by integration tests -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
|
||||
|
||||
@@ -165,7 +165,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);
|
||||
}
|
||||
|
||||
+1
-2
@@ -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";
|
||||
}
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -221,4 +221,26 @@ public sealed class GitHubCopilotAgentTests
|
||||
Assert.Null(result.ConfigDir);
|
||||
Assert.True(result.Streaming);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToAgentResponseUpdate_AssistantMessageEvent_DoesNotEmitTextContent()
|
||||
{
|
||||
var assistantMessage = new AssistantMessageEvent
|
||||
{
|
||||
Data = new AssistantMessageData
|
||||
{
|
||||
MessageId = "msg-456",
|
||||
Content = "Some streamed content that was already delivered via delta events"
|
||||
}
|
||||
};
|
||||
CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false });
|
||||
const string TestId = "agent-id";
|
||||
var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null);
|
||||
AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage);
|
||||
|
||||
// result.Text need to be empty because the content was already delivered via delta events, and we want to avoid emitting duplicate content in the response update.
|
||||
// The content should be delivered through TextContent in the Contents collection instead.
|
||||
Assert.Empty(result.Text);
|
||||
Assert.DoesNotContain(result.Contents, c => c is TextContent);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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(
|
||||
|
||||
+1
-2
@@ -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<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
yield return
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
|
||||
+1
-2
@@ -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<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
yield return
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
|
||||
+1
-2
@@ -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<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
yield return
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
|
||||
+1
-2
@@ -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<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
yield return
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
|
||||
+1
-2
@@ -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<AgentVersion> CreateAgentsAsync(Uri foundryEndpoint)
|
||||
{
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential());
|
||||
AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential());
|
||||
|
||||
yield return
|
||||
await aiProjectClient.CreateAgentAsync(
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
|
||||
|
||||
@@ -14,7 +14,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
|
||||
|
||||
+1
-2
@@ -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;
|
||||
@@ -67,7 +66,7 @@ public abstract class IntegrationTest : IDisposable
|
||||
protected async ValueTask<DeclarativeWorkflowOptions> CreateOptionsAsync(bool externalConversation, IMcpToolHandler? mcpToolProvider, params IEnumerable<AIFunction> functionTools)
|
||||
{
|
||||
AzureAgentProvider agentProvider =
|
||||
new(this.TestEndpoint, new AzureCliCredential())
|
||||
new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential())
|
||||
{
|
||||
Functions = functionTools,
|
||||
};
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests;
|
||||
|
||||
@@ -76,7 +76,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);
|
||||
|
||||
+1
@@ -5,6 +5,7 @@
|
||||
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
|
||||
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedIntegrationTestAzureCredentialsCode>True</InjectSharedIntegrationTestAzureCredentialsCode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -454,6 +454,7 @@ class BaseAgent(SerializationMixin):
|
||||
stream_callback: Callable[[AgentResponseUpdate], None]
|
||||
| Callable[[AgentResponseUpdate], Awaitable[None]]
|
||||
| None = None,
|
||||
propagate_session: bool = False,
|
||||
) -> FunctionTool:
|
||||
"""Create a FunctionTool that wraps this agent.
|
||||
|
||||
@@ -464,6 +465,12 @@ class BaseAgent(SerializationMixin):
|
||||
arg_description: The description for the function argument.
|
||||
If None, defaults to "Task for {tool_name}".
|
||||
stream_callback: Optional callback for streaming responses. If provided, uses run(..., stream=True).
|
||||
propagate_session: If True, the parent agent's ``AgentSession`` is
|
||||
forwarded to this sub-agent's ``run()`` call, so both agents
|
||||
operate within the same logical session (sharing the same
|
||||
``session_id`` and provider-managed state, such as any stored
|
||||
conversation history or metadata). Defaults to False, meaning
|
||||
the sub-agent runs with a new, independent session.
|
||||
|
||||
Returns:
|
||||
A FunctionTool that can be used as a tool by other agents.
|
||||
@@ -480,9 +487,12 @@ class BaseAgent(SerializationMixin):
|
||||
# Create an agent
|
||||
agent = Agent(client=client, name="research-agent", description="Performs research tasks")
|
||||
|
||||
# Convert the agent to a tool
|
||||
# Convert the agent to a tool (independent session)
|
||||
research_tool = agent.as_tool()
|
||||
|
||||
# Convert the agent to a tool (shared session with parent)
|
||||
research_tool = agent.as_tool(propagate_session=True)
|
||||
|
||||
# Use the tool with another agent
|
||||
coordinator = Agent(client=client, name="coordinator", tools=research_tool)
|
||||
"""
|
||||
@@ -509,16 +519,21 @@ class BaseAgent(SerializationMixin):
|
||||
# Extract the input from kwargs using the specified arg_name
|
||||
input_text = kwargs.get(arg_name, "")
|
||||
|
||||
# Forward runtime context kwargs, excluding arg_name and conversation_id.
|
||||
forwarded_kwargs = {k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id", "options")}
|
||||
# Extract parent session when propagate_session is enabled
|
||||
parent_session = kwargs.get("session") if propagate_session else None
|
||||
|
||||
# Forward runtime context kwargs, excluding framework-internal keys.
|
||||
forwarded_kwargs = {
|
||||
k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id", "options", "session")
|
||||
}
|
||||
|
||||
if stream_callback is None:
|
||||
# Use non-streaming mode
|
||||
return (await self.run(input_text, stream=False, **forwarded_kwargs)).text
|
||||
return (await self.run(input_text, stream=False, session=parent_session, **forwarded_kwargs)).text
|
||||
|
||||
# Use streaming mode - accumulate updates and create final response
|
||||
response_updates: list[AgentResponseUpdate] = []
|
||||
async for update in self.run(input_text, stream=True, **forwarded_kwargs):
|
||||
async for update in self.run(input_text, stream=True, session=parent_session, **forwarded_kwargs):
|
||||
response_updates.append(update)
|
||||
if is_async_callback:
|
||||
await stream_callback(update) # type: ignore[misc]
|
||||
@@ -1061,6 +1076,9 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
|
||||
# in function middleware context and tool invocation.
|
||||
existing_additional_args = 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:
|
||||
additional_function_arguments["session"] = active_session
|
||||
|
||||
# Build options dict from run() options merged with provided options
|
||||
run_opts: dict[str, Any] = {
|
||||
|
||||
@@ -707,6 +707,81 @@ async def test_chat_agent_as_tool_name_sanitization(client: SupportsChatGetRespo
|
||||
assert tool.name == expected_tool_name, f"Expected {expected_tool_name}, got {tool.name} for input {agent_name}"
|
||||
|
||||
|
||||
async def test_chat_agent_as_tool_propagate_session_true(client: SupportsChatGetResponse) -> None:
|
||||
"""Test that propagate_session=True forwards the parent's session to the sub-agent."""
|
||||
agent = Agent(client=client, name="SubAgent", description="Sub agent")
|
||||
tool = agent.as_tool(propagate_session=True)
|
||||
|
||||
parent_session = AgentSession(session_id="parent-session-123")
|
||||
parent_session.state["shared_key"] = "shared_value"
|
||||
|
||||
# Spy on the agent's run method to capture the session argument
|
||||
original_run = agent.run
|
||||
captured_session = None
|
||||
|
||||
def capturing_run(*args: Any, **kwargs: Any) -> Any:
|
||||
nonlocal captured_session
|
||||
captured_session = kwargs.get("session")
|
||||
return original_run(*args, **kwargs)
|
||||
|
||||
agent.run = capturing_run # type: ignore[assignment, method-assign]
|
||||
|
||||
await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session)
|
||||
|
||||
assert captured_session is parent_session
|
||||
assert captured_session.session_id == "parent-session-123"
|
||||
assert captured_session.state["shared_key"] == "shared_value"
|
||||
|
||||
|
||||
async def test_chat_agent_as_tool_propagate_session_false_by_default(client: SupportsChatGetResponse) -> None:
|
||||
"""Test that propagate_session defaults to False and does not forward the session."""
|
||||
agent = Agent(client=client, name="SubAgent", description="Sub agent")
|
||||
tool = agent.as_tool() # default: propagate_session=False
|
||||
|
||||
parent_session = AgentSession(session_id="parent-session-456")
|
||||
|
||||
original_run = agent.run
|
||||
captured_session = None
|
||||
|
||||
def capturing_run(*args: Any, **kwargs: Any) -> Any:
|
||||
nonlocal captured_session
|
||||
captured_session = kwargs.get("session")
|
||||
return original_run(*args, **kwargs)
|
||||
|
||||
agent.run = capturing_run # type: ignore[assignment, method-assign]
|
||||
|
||||
await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session)
|
||||
|
||||
assert captured_session is None
|
||||
|
||||
|
||||
async def test_chat_agent_as_tool_propagate_session_shares_state(client: SupportsChatGetResponse) -> None:
|
||||
"""Test that shared session allows the sub-agent to read and write parent's state."""
|
||||
agent = Agent(client=client, name="SubAgent", description="Sub agent")
|
||||
tool = agent.as_tool(propagate_session=True)
|
||||
|
||||
parent_session = AgentSession(session_id="shared-session")
|
||||
parent_session.state["counter"] = 0
|
||||
|
||||
# The sub-agent receives the same session object, so mutations are shared
|
||||
original_run = agent.run
|
||||
captured_session = None
|
||||
|
||||
def capturing_run(*args: Any, **kwargs: Any) -> Any:
|
||||
nonlocal captured_session
|
||||
captured_session = kwargs.get("session")
|
||||
if captured_session:
|
||||
captured_session.state["counter"] += 1
|
||||
return original_run(*args, **kwargs)
|
||||
|
||||
agent.run = capturing_run # type: ignore[assignment, method-assign]
|
||||
|
||||
await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session)
|
||||
|
||||
# The parent's state should reflect the sub-agent's mutation
|
||||
assert parent_session.state["counter"] == 1
|
||||
|
||||
|
||||
async def test_chat_agent_as_mcp_server_basic(client: SupportsChatGetResponse) -> None:
|
||||
"""Test basic as_mcp_server functionality."""
|
||||
agent = Agent(client=client, name="TestAgent", description="Test agent for MCP")
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from agent_framework import AgentContext, AgentSession
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
Agent-as-Tool: Session Propagation Example
|
||||
|
||||
Demonstrates how to share an AgentSession between a coordinator agent and a
|
||||
sub-agent invoked as a tool using ``propagate_session=True``.
|
||||
|
||||
When session propagation is enabled, both agents share the same session object,
|
||||
including session_id and the mutable state dict. This allows correlated
|
||||
conversation tracking and shared state across the agent hierarchy.
|
||||
|
||||
The middleware functions below are purely for observability — they are NOT
|
||||
required for session propagation to work.
|
||||
"""
|
||||
|
||||
|
||||
async def log_session(
|
||||
context: AgentContext,
|
||||
call_next: Callable[[], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Agent middleware that logs the session received by each agent.
|
||||
|
||||
NOT required for session propagation — only used to observe the flow.
|
||||
If propagation is working, both agents will show the same session_id.
|
||||
"""
|
||||
session: AgentSession | None = context.session
|
||||
agent_name = context.agent.name or "unknown"
|
||||
session_id = session.session_id if session else None
|
||||
state = dict(session.state) if session else {}
|
||||
print(f" [{agent_name}] session_id={session_id}, state={state}")
|
||||
await call_next()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Agent-as-Tool: Session Propagation ===\n")
|
||||
|
||||
client = OpenAIResponsesClient()
|
||||
|
||||
# --- Sub-agent: a research specialist ---
|
||||
# The sub-agent has the same log_session middleware to prove it receives the session.
|
||||
research_agent = client.as_agent(
|
||||
name="ResearchAgent",
|
||||
instructions="You are a research assistant. Provide concise answers.",
|
||||
middleware=[log_session],
|
||||
)
|
||||
|
||||
# propagate_session=True: the coordinator's session will be forwarded
|
||||
research_tool = research_agent.as_tool(
|
||||
name="research",
|
||||
description="Research a topic and return findings",
|
||||
arg_name="query",
|
||||
arg_description="The research query",
|
||||
propagate_session=True,
|
||||
)
|
||||
|
||||
# --- Coordinator agent ---
|
||||
coordinator = client.as_agent(
|
||||
name="CoordinatorAgent",
|
||||
instructions="You coordinate research. Use the 'research' tool to look up information.",
|
||||
tools=[research_tool],
|
||||
middleware=[log_session],
|
||||
)
|
||||
|
||||
# Create a shared session and put some state in it
|
||||
session = coordinator.create_session()
|
||||
session.state["request_source"] = "demo"
|
||||
print(f"Session ID: {session.session_id}")
|
||||
print(f"Session state before run: {session.state}\n")
|
||||
|
||||
query = "What are the latest developments in quantum computing?"
|
||||
print(f"User: {query}\n")
|
||||
|
||||
result = await coordinator.run(query, session=session)
|
||||
|
||||
print(f"\nCoordinator: {result}\n")
|
||||
print(f"Session state after run: {session.state}")
|
||||
print(
|
||||
"\nIf both agents show the same session_id above, session propagation is working."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user