mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Add PoC CopilotStudio agent (#84)
* Add project and skeleton files. * Add CopilotStudioAgent implementation * Add CopilotStudio integration tests * Fix typos and PR feedback. * Fix .net framework build errors. * Address PR comments * Remove temp test. * Add rawresponse for streaming updates and more comments. * Add TODO to review streaming updates * Address PR comments and fix some issues with streaming messages. * Map additional properties to agent reponses * Update CopilotStudio integration tests to match new approach. * Update copilot studio namespaces/project names to match new naming * Add todo's for AIContent types. * Remove files from PR. * Fix up sln file. * Update .gitignore. * Remove duplicate package version items.
This commit is contained in:
committed by
GitHub
Unverified
parent
b85c17c935
commit
b708e2507d
+4
-1
@@ -173,4 +173,7 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
""
|
||||
],
|
||||
"SelectedNode": "\\agent-framework-dotnet.slnx",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
Binary file not shown.
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"Version": 1,
|
||||
"WorkspaceRootPath": "C:\\Users\\crickman\\source\\repos\\af1\\",
|
||||
"Documents": [],
|
||||
"DocumentGroupContainers": [
|
||||
{
|
||||
"Orientation": 0,
|
||||
"VerticalTabListWidth": 256,
|
||||
"DocumentGroups": [
|
||||
{
|
||||
"DockedWidth": 200,
|
||||
"SelectedChildIndex": -1,
|
||||
"Children": [
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{e1b7d1f8-9b3c-49b1-8f4f-bfc63a88835d}"
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{d78612c7-9962-4b83-95d9-268046dad23a}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,30 +6,34 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Azure.* -->
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.1.0-beta.2" />
|
||||
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.1.0-beta.3" />
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.14.0" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.0-preview.1.25356.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.0-preview.1.25356.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<!-- Agent SDKs -->
|
||||
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.1.125-beta" />
|
||||
<!-- Identity -->
|
||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.67.2" />
|
||||
<!-- Test -->
|
||||
<PackageVersion Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<Project Path="tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj" Id="e91d4745-7ccd-4de8-9bc0-31081f540038">
|
||||
<BuildType Solution="Publish|*" Project="Release" />
|
||||
</Project>
|
||||
<Project Path="tests/CopilotStudio.IntegrationTests/CopilotStudio.IntegrationTests.csproj">
|
||||
<BuildType Solution="Publish|*" Project="Release" />
|
||||
</Project>
|
||||
<Project Path="tests/OpenAIAssistant.IntegrationTests/OpenAIAssistant.IntegrationTests.csproj" Id="a35b6971-6f27-4904-a168-8e12b229cced">
|
||||
<BuildType Solution="Publish|*" Project="Release" />
|
||||
</Project>
|
||||
@@ -114,6 +117,7 @@
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime.Abstractions/Microsoft.Extensions.AI.Agents.Runtime.Abstractions.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.Runtime.InProcess/Microsoft.Extensions.AI.Agents.Runtime.InProcess.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents/Microsoft.Extensions.AI.Agents.csproj" />
|
||||
<Project Path="src/Microsoft.Extensions.AI.Agents.CopilotStudio/Microsoft.Extensions.AI.Agents.CopilotStudio.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/UnitTests/">
|
||||
<Project Path="tests/Microsoft.Agents.Orchestration.UnitTests/Microsoft.Agents.Orchestration.UnitTests.csproj">
|
||||
|
||||
@@ -50,7 +50,7 @@ public abstract class Agent
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
|
||||
/// </summary>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
@@ -66,7 +66,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
@@ -88,7 +88,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
@@ -107,7 +107,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A <see cref="ChatResponse"/> containing the list of <see cref="ChatMessage"/> items.</returns>
|
||||
@@ -120,7 +120,7 @@ public abstract class Agent
|
||||
/// <summary>
|
||||
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread.
|
||||
/// </summary>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
@@ -136,7 +136,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
@@ -158,7 +158,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
@@ -177,7 +177,7 @@ public abstract class Agent
|
||||
/// Run the agent with the provided message and arguments.
|
||||
/// </summary>
|
||||
/// <param name="messages">The messages to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="options">Optional parameters for agent invocation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>An async list of response items that each contain a <see cref="ChatResponseUpdate"/>.</returns>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.Core.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.CopilotStudio;
|
||||
|
||||
/// <summary>
|
||||
/// Contains code to process <see cref="IActivity"/> responses from the Copilot Studio agent and convert them to <see cref="ChatMessage"/> objects.
|
||||
/// </summary>
|
||||
internal static class ActivityProcessor
|
||||
{
|
||||
public static async IAsyncEnumerable<(ChatMessage message, bool reasoning)> ProcessActivityAsync(IAsyncEnumerable<IActivity> activities, bool streaming, ILogger logger)
|
||||
{
|
||||
await foreach (IActivity activity in activities.ConfigureAwait(false))
|
||||
{
|
||||
switch (activity.Type)
|
||||
{
|
||||
case "message":
|
||||
// For streaming scenarios, we sometimes receive intermediate text via "typing" activities, but not always.
|
||||
// In some cases the response is also returned multiple times via "typing" activities, so the only reliable
|
||||
// way to get the final response is to wait for a "message" activity.
|
||||
|
||||
// TODO: Prototype a custom AIContent type for CardActions, where the user is instructed to
|
||||
// pick from a list of actions.
|
||||
// The activity text doesn't make sense without the actions, as the message
|
||||
// is often instructing the user to pick from the provided list of actions.
|
||||
yield return (CreateChatMessageFromActivity(activity, [new TextContent(activity.Text)]), false);
|
||||
break;
|
||||
case "typing":
|
||||
case "event":
|
||||
// TODO: Revisit usage of TextReasoningContent here, to evaluate whether all are really reasoning
|
||||
// or whether simply an AIContent base type would be more appropriate.
|
||||
yield return (CreateChatMessageFromActivity(activity, [new TextReasoningContent(activity.Text)]), true);
|
||||
break;
|
||||
default:
|
||||
logger.LogWarning("Unknown activity type '{ActivityType}' received.", activity.Type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ChatMessage CreateChatMessageFromActivity(IActivity activity, IEnumerable<AIContent> messageContent)
|
||||
{
|
||||
return new ChatMessage(ChatRole.Assistant, [.. messageContent])
|
||||
{
|
||||
AuthorName = activity.From?.Name,
|
||||
MessageId = activity.Id,
|
||||
RawRepresentation = activity
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.CopilotStudio.Client;
|
||||
using Microsoft.Agents.Core.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.CopilotStudio;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Copilot Studio agent in the cloud.
|
||||
/// </summary>
|
||||
public class CopilotStudioAgent : Agent
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The client used to interact with the Copilot Agent service.
|
||||
/// </summary>
|
||||
public CopilotClient Client { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CopilotStudioAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="client">A client used to interact with the Copilot Agent service.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory to use for logging.</param>
|
||||
public CopilotStudioAgent(CopilotClient client, ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
this.Client = client;
|
||||
|
||||
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger<CopilotStudioAgent>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override AgentThread GetNewThread()
|
||||
{
|
||||
return new CopilotStudioAgentThread();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<ChatResponse> RunAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
|
||||
// Ensure that we have a valid thread to work with.
|
||||
CopilotStudioAgentThread copilotStudioAgentThread = base.ValidateOrCreateThreadType(thread, () => new CopilotStudioAgentThread());
|
||||
if (copilotStudioAgentThread.Id is null)
|
||||
{
|
||||
// If the thread ID is null, we need to start a new conversation and set the thread ID accordingly.
|
||||
copilotStudioAgentThread.Id = await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Invoke the Copilot Studio agent with the provided messages.
|
||||
string question = string.Join("\n", messages.Select(m => m.Text));
|
||||
var responseMessages = ActivityProcessor.ProcessActivityAsync(this.Client.AskQuestionAsync(question, copilotStudioAgentThread.Id, cancellationToken), streaming: false, this._logger);
|
||||
|
||||
// Enumerate the response messages
|
||||
var responseMessagesList = new List<ChatMessage>();
|
||||
await foreach ((ChatMessage message, bool reasoning) in responseMessages.ConfigureAwait(false))
|
||||
{
|
||||
// If the message is a reasoning message, return it as part of the intermediate messages
|
||||
// instead of the final response.
|
||||
if (reasoning)
|
||||
{
|
||||
if (options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages.Invoke([message]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the message to the list
|
||||
responseMessagesList.Add(message);
|
||||
}
|
||||
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
return new ChatResponse(responseMessagesList)
|
||||
{
|
||||
ResponseId = responseMessagesList.LastOrDefault()?.MessageId,
|
||||
ConversationId = copilotStudioAgentThread.Id,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async IAsyncEnumerable<ChatResponseUpdate> RunStreamingAsync(
|
||||
IReadOnlyCollection<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(messages);
|
||||
|
||||
// Ensure that we have a valid thread to work with.
|
||||
CopilotStudioAgentThread copilotStudioAgentThread = base.ValidateOrCreateThreadType(thread, () => new CopilotStudioAgentThread());
|
||||
if (copilotStudioAgentThread.Id is null)
|
||||
{
|
||||
// If the thread ID is null, we need to start a new conversation and set the thread ID accordingly.
|
||||
copilotStudioAgentThread.Id = await this.StartNewConversationAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Invoke the Copilot Studio agent with the provided messages.
|
||||
string question = string.Join("\n", messages.Select(m => m.Text));
|
||||
var responseMessages = ActivityProcessor.ProcessActivityAsync(this.Client.AskQuestionAsync(question, copilotStudioAgentThread.Id, cancellationToken), streaming: true, this._logger);
|
||||
|
||||
// Enumerate the response messages
|
||||
await foreach ((ChatMessage message, bool reasoning) in responseMessages.ConfigureAwait(false))
|
||||
{
|
||||
// If the message is a reasoning message, return it as part of the intermediate messages.
|
||||
if (reasoning && options?.OnIntermediateMessages is not null)
|
||||
{
|
||||
await options.OnIntermediateMessages.Invoke([message]).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// TODO: Review list of ChatResponse properties to ensure we set all availble values.
|
||||
// Setting ResponseId and MessageId end up being particularly important for streaming consumers
|
||||
// so that they can tell things like response boundaries.
|
||||
yield return new ChatResponseUpdate(message.Role, message.Contents)
|
||||
{
|
||||
AdditionalProperties = message.AdditionalProperties,
|
||||
AuthorName = message.AuthorName,
|
||||
RawRepresentation = message.RawRepresentation,
|
||||
ResponseId = message.MessageId,
|
||||
MessageId = message.MessageId,
|
||||
ConversationId = copilotStudioAgentThread.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> StartNewConversationAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string? conversationId = null;
|
||||
await foreach (IActivity activity in this.Client.StartConversationAsync(emitStartConversationEvent: true, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (activity.Conversation is not null)
|
||||
{
|
||||
conversationId = activity.Conversation.Id;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(conversationId))
|
||||
{
|
||||
throw new System.InvalidOperationException("Failed to start a new conversation.");
|
||||
}
|
||||
|
||||
return conversationId!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Extensions.AI.Agents.CopilotStudio;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a thread for interacting with a Copilot Studio agent.
|
||||
/// </summary>
|
||||
public class CopilotStudioAgentThread : AgentThread
|
||||
{
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<VersionSuffix>alpha</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Extensions.AI.Agents.Abstractions\Microsoft.Extensions.AI.Agents.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Agents.CopilotStudio.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- NuGet Package Settings -->
|
||||
<Title>Microsoft Agent Framework: Copilot Studio Agent</Title>
|
||||
<Description>Contains the Microsoft Agent Framework Copilot Studio Agent.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
+4
-4
@@ -17,7 +17,7 @@ public static class ChatClientAgentExtensions
|
||||
/// </summary>
|
||||
/// <param name="agent">Target agent to run.</param>
|
||||
/// <param name="messages">The messages to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
@@ -41,7 +41,7 @@ public static class ChatClientAgentExtensions
|
||||
/// </summary>
|
||||
/// <param name="agent">Target agent to run.</param>
|
||||
/// <param name="prompt">The prompt to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
@@ -65,7 +65,7 @@ public static class ChatClientAgentExtensions
|
||||
/// </summary>
|
||||
/// <param name="agent">Target agent to run.</param>
|
||||
/// <param name="messages">The messages to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
@@ -88,7 +88,7 @@ public static class ChatClientAgentExtensions
|
||||
/// </summary>
|
||||
/// <param name="agent">Target agent to run.</param>
|
||||
/// <param name="prompt">The prompt to pass to the agent.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent reponse.</param>
|
||||
/// <param name="thread">The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response.</param>
|
||||
/// <param name="agentRunOptions">Optional parameters for agent invocation.</param>
|
||||
/// <param name="chatOptions">Optional chat options.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
|
||||
@@ -16,6 +16,18 @@ namespace AgentConformance.IntegrationTests;
|
||||
public abstract class RunStreamingTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var thread = agent.GetNewThread();
|
||||
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
|
||||
|
||||
// Act
|
||||
var chatResponses = await agent.RunStreamingAsync(thread).ToListAsync();
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithStringReturnsExpectedResultAsync()
|
||||
{
|
||||
|
||||
@@ -16,6 +16,21 @@ namespace AgentConformance.IntegrationTests;
|
||||
public abstract class RunTests<TAgentFixture>(Func<TAgentFixture> createAgentFixture) : AgentTests<TAgentFixture>(createAgentFixture)
|
||||
where TAgentFixture : IAgentFixture
|
||||
{
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithNoMessageDoesNotFailAsync()
|
||||
{
|
||||
// Arrange
|
||||
var agent = this.Fixture.Agent;
|
||||
var thread = agent.GetNewThread();
|
||||
await using var cleanup = new ThreadCleanup(thread, this.Fixture);
|
||||
|
||||
// Act
|
||||
var chatResponse = await agent.RunAsync(thread);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(chatResponse);
|
||||
}
|
||||
|
||||
[RetryFact(Constants.RetryCount, Constants.RetryDelay)]
|
||||
public virtual async Task RunWithStringReturnsExpectedResultAsync()
|
||||
{
|
||||
|
||||
+4
@@ -11,4 +11,8 @@
|
||||
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
|
||||
<InjectSharedIntegrationTestCode>True</InjectSharedIntegrationTestCode>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Extensions.AI.Agents.CopilotStudio\Microsoft.Extensions.AI.Agents.CopilotStudio.csproj" />
|
||||
<ProjectReference Include="..\AgentConformance.IntegrationTests\AgentConformance.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
using AgentConformance.IntegrationTests.Support;
|
||||
using CopilotStudio.IntegrationTests.Support;
|
||||
using Microsoft.Agents.CopilotStudio.Client;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.AI.Agents.CopilotStudio;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CopilotStudio.IntegrationTests;
|
||||
|
||||
public class CopilotStudioFixture : IAgentFixture
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
private Agent _agent;
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
|
||||
public Agent Agent => this._agent;
|
||||
|
||||
public Task<List<ChatMessage>> GetChatHistoryAsync(AgentThread thread)
|
||||
{
|
||||
throw new NotSupportedException("CopilotStudio doesn't allow retrieval of chat history.");
|
||||
}
|
||||
|
||||
public Task DeleteThreadAsync(AgentThread thread)
|
||||
{
|
||||
// Chat Completion does not require/support deleting threads, so this is a no-op.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
const string CopilotStudioHttpClientName = nameof(CopilotStudioAgent);
|
||||
|
||||
var config = TestConfiguration.LoadSection<CopilotStudioAgentConfiguration>();
|
||||
var settings = new CopilotStudioConnectionSettings(config.TenantId, config.AppClientId)
|
||||
{
|
||||
DirectConnectUrl = config.DirectConnectUrl,
|
||||
};
|
||||
|
||||
ServiceCollection services = new();
|
||||
|
||||
services
|
||||
.AddSingleton(settings)
|
||||
.AddSingleton<CopilotStudioTokenHandler>()
|
||||
.AddHttpClient(CopilotStudioHttpClientName)
|
||||
.ConfigurePrimaryHttpMessageHandler<CopilotStudioTokenHandler>();
|
||||
|
||||
IHttpClientFactory httpClientFactory =
|
||||
services
|
||||
.BuildServiceProvider()
|
||||
.GetRequiredService<IHttpClientFactory>();
|
||||
|
||||
CopilotClient client = new(settings, httpClientFactory, NullLogger.Instance, CopilotStudioHttpClientName);
|
||||
|
||||
this._agent = new CopilotStudioAgent(client);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace CopilotStudio.IntegrationTests;
|
||||
|
||||
public class CopilotStudioRunStreamingTests() : RunStreamingTests<CopilotStudioFixture>(() => new())
|
||||
{
|
||||
[Fact(Skip = "Copilot Studio does not support thread history retrieval, so this test is not applicable.")]
|
||||
public override Task ThreadMaintainsHistoryAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using AgentConformance.IntegrationTests;
|
||||
|
||||
namespace CopilotStudio.IntegrationTests;
|
||||
|
||||
public class CopilotStudioRunTests() : RunTests<CopilotStudioFixture>(() => new())
|
||||
{
|
||||
[Fact(Skip = "Copilot Studio does not support thread history retrieval, so this test is not applicable.")]
|
||||
public override Task ThreadMaintainsHistoryAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace CopilotStudio.IntegrationTests.Support;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
|
||||
#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
|
||||
|
||||
internal sealed class CopilotStudioAgentConfiguration
|
||||
{
|
||||
public string DirectConnectUrl { get; set; }
|
||||
|
||||
public string TenantId { get; set; }
|
||||
|
||||
public string AppClientId { get; set; }
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.CopilotStudio.Client;
|
||||
using Microsoft.Agents.CopilotStudio.Client.Discovery;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace CopilotStudio.IntegrationTests.Support;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ConnectionSettings"/> with additional properties to specify Application (Client) Id,
|
||||
/// Tenant Id, and optionally the Application Client secret.
|
||||
/// </summary>
|
||||
internal sealed class CopilotStudioConnectionSettings : ConnectionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Application ID for creating the authentication for the connection
|
||||
/// </summary>
|
||||
public string AppClientId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Application secret for creating the authentication for the connection
|
||||
/// </summary>
|
||||
public string? AppClientSecret { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for creating the authentication for the connection
|
||||
/// </summary>
|
||||
public string TenantId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Use interactive or service connection for authentication.
|
||||
/// Defaults to true, meaning interactive authentication will be used.
|
||||
/// </summary>
|
||||
public bool UseInteractiveAuthentication { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a new instance of the <see cref="CopilotStudioConnectionSettings"/> from provided settings.
|
||||
/// </summary>
|
||||
public CopilotStudioConnectionSettings(string tenantId, string appClientId, string? appClientSecret = null)
|
||||
{
|
||||
this.TenantId = tenantId;
|
||||
this.AppClientId = appClientId;
|
||||
this.AppClientSecret = appClientSecret;
|
||||
this.Cloud = PowerPlatformCloud.Prod;
|
||||
this.CopilotAgentType = AgentType.Published;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a new instance of the <see cref="CopilotStudioConnectionSettings"/> from a configuration section.
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public CopilotStudioConnectionSettings(IConfigurationSection config)
|
||||
: base(config)
|
||||
{
|
||||
this.AppClientId = config[nameof(this.AppClientId)] ?? throw new ArgumentException($"{nameof(this.AppClientId)} not found in config");
|
||||
this.TenantId = config[nameof(this.TenantId)] ?? throw new ArgumentException($"{nameof(this.TenantId)} not found in config");
|
||||
this.AppClientSecret = config[nameof(this.AppClientSecret)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.CopilotStudio.Client;
|
||||
using Microsoft.Identity.Client;
|
||||
using Microsoft.Identity.Client.Extensions.Msal;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace CopilotStudio.IntegrationTests.Support;
|
||||
|
||||
#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="HttpClientHandler"/> that adds an authentication token to the request headers for Copilot Studio API calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information on how to setup various authentication flows, see the Microsoft Identity documentation at https://aka.ms/msal.
|
||||
/// </remarks>
|
||||
internal sealed class CopilotStudioTokenHandler : HttpClientHandler
|
||||
{
|
||||
private const string AuthenticationHeader = "Bearer";
|
||||
private const string CacheFolderName = "mcs_client_console";
|
||||
private const string KeyChainServiceName = "copilot_studio_client_app";
|
||||
private const string KeyChainAccountName = "copilot_studio_client";
|
||||
|
||||
private readonly CopilotStudioConnectionSettings _settings;
|
||||
private readonly string[] _scopes;
|
||||
|
||||
private IConfidentialClientApplication? _clientApplication;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CopilotStudioTokenHandler"/> class with the specified connection settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">The connection settings for Copilot Studio.</param>
|
||||
public CopilotStudioTokenHandler(CopilotStudioConnectionSettings settings)
|
||||
{
|
||||
Throw.IfNull(settings);
|
||||
|
||||
this._settings = settings;
|
||||
this._scopes = [CopilotClient.ScopeFromSettings(this._settings)];
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Headers.Authorization is null)
|
||||
{
|
||||
AuthenticationResult authResponse = await this.AuthenticateAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(AuthenticationHeader, authResponse.AccessToken);
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task<AuthenticationResult> AuthenticateAsync(CancellationToken cancellationToken) =>
|
||||
this._settings.UseInteractiveAuthentication ?
|
||||
this.AuthenticateInteractiveAsync(cancellationToken) :
|
||||
this.AuthenticateServiceAsync(cancellationToken);
|
||||
|
||||
private async Task<AuthenticationResult> AuthenticateServiceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this._clientApplication is null)
|
||||
{
|
||||
this._clientApplication = ConfidentialClientApplicationBuilder.Create(this._settings.AppClientId)
|
||||
.WithAuthority(AzureCloudInstance.AzurePublic, this._settings.TenantId)
|
||||
.WithClientSecret(this._settings.AppClientSecret)
|
||||
.Build();
|
||||
|
||||
MsalCacheHelper tokenCacheHelper = await CreateCacheHelperAsync("AppTokenCache").ConfigureAwait(false);
|
||||
tokenCacheHelper.RegisterCache(this._clientApplication.AppTokenCache);
|
||||
}
|
||||
|
||||
AuthenticationResult authResponse;
|
||||
|
||||
authResponse = await this._clientApplication.AcquireTokenForClient(this._scopes).ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return authResponse;
|
||||
}
|
||||
|
||||
private async Task<AuthenticationResult> AuthenticateInteractiveAsync(CancellationToken cancellationToken = default!)
|
||||
{
|
||||
IPublicClientApplication app =
|
||||
PublicClientApplicationBuilder.Create(this._settings.AppClientId)
|
||||
.WithAuthority(AadAuthorityAudience.AzureAdMyOrg)
|
||||
.WithTenantId(this._settings.TenantId)
|
||||
.WithRedirectUri("http://localhost")
|
||||
.Build();
|
||||
|
||||
MsalCacheHelper tokenCacheHelper = await CreateCacheHelperAsync("TokenCache").ConfigureAwait(false);
|
||||
tokenCacheHelper.RegisterCache(app.UserTokenCache);
|
||||
|
||||
IEnumerable<IAccount> accounts = await app.GetAccountsAsync().ConfigureAwait(false);
|
||||
IAccount? account = accounts.FirstOrDefault();
|
||||
|
||||
AuthenticationResult authResponse;
|
||||
|
||||
try
|
||||
{
|
||||
authResponse = await app.AcquireTokenSilent(this._scopes, account).ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (MsalUiRequiredException)
|
||||
{
|
||||
authResponse = await app.AcquireTokenInteractive(this._scopes).ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return authResponse;
|
||||
}
|
||||
|
||||
private static async Task<MsalCacheHelper> CreateCacheHelperAsync(string cacheFileName)
|
||||
{
|
||||
string currentDir = Path.Combine(AppContext.BaseDirectory, CacheFolderName);
|
||||
|
||||
if (!Directory.Exists(currentDir))
|
||||
{
|
||||
Directory.CreateDirectory(currentDir);
|
||||
}
|
||||
|
||||
StorageCreationPropertiesBuilder storageProperties = new(cacheFileName, currentDir);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
storageProperties.WithLinuxUnprotectedFile();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
storageProperties.WithMacKeyChain(KeyChainServiceName, KeyChainAccountName);
|
||||
}
|
||||
|
||||
MsalCacheHelper tokenCacheHelper = await MsalCacheHelper.CreateAsync(storageProperties.Build()).ConfigureAwait(false);
|
||||
|
||||
return tokenCacheHelper;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user