diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 97b4f6647f05..ebbc17856430 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,8 +9,8 @@ updates:
- package-ecosystem: "nuget"
directory: "dotnet/"
schedule:
- interval: "weekly"
- day: "monday"
+ interval: "cron"
+ cronjob: "0 8 * * 4,0" # Every Thursday(4) and Sunday(0) at 8:00 UTC
ignore:
# For all System.* and Microsoft.Extensions/Bcl.* packages, ignore all major version updates
- dependency-name: "System.*"
@@ -24,20 +24,6 @@ updates:
- ".NET"
- "dependencies"
- # Maintain dependencies for nuget
- - package-ecosystem: "nuget"
- directory: "samples/dotnet"
- schedule:
- interval: "weekly"
- day: "monday"
-
- # Maintain dependencies for npm
- - package-ecosystem: "npm"
- directory: "samples/apps"
- schedule:
- interval: "weekly"
- day: "monday"
-
# Maintain dependencies for pip
- package-ecosystem: "pip"
directory: "python/"
@@ -55,4 +41,4 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- day: "monday"
+ day: "tuesday"
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index af629d01a3a8..c60c6aedd669 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -5,26 +5,25 @@
true
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
+
@@ -33,7 +32,7 @@
-
+
@@ -48,15 +47,7 @@
-
-
-
-
-
-
-
-
@@ -64,52 +55,51 @@
-
-
-
+
+
-
-
-
+
+
+
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
+
-
+
+
+
+
-
-
-
+
-
+
@@ -134,62 +124,62 @@
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
@@ -199,23 +189,23 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx
index ee828c730ad8..b662baf25562 100644
--- a/dotnet/SK-dotnet.slnx
+++ b/dotnet/SK-dotnet.slnx
@@ -38,6 +38,7 @@
+
@@ -49,6 +50,10 @@
+
+
+
+
@@ -87,6 +92,7 @@
+
diff --git a/dotnet/docs/EXPERIMENTS.md b/dotnet/docs/EXPERIMENTS.md
index 56b5e073a0f0..e51bfd54b04a 100644
--- a/dotnet/docs/EXPERIMENTS.md
+++ b/dotnet/docs/EXPERIMENTS.md
@@ -76,13 +76,6 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
| SKEXP0060 | Handlebars planner |
| SKEXP0060 | OpenAI Stepwise planner |
| | | | | | | |
-| SKEXP0070 | Ollama AI connector | | | | | |
-| SKEXP0070 | Gemini AI connector | | | | | |
-| SKEXP0070 | Mistral AI connector | | | | | |
-| SKEXP0070 | ONNX AI connector | | | | | |
-| SKEXP0070 | Hugging Face AI connector | | | | | |
-| SKEXP0070 | Amazon AI connector | | | | | |
-| | | | | | | |
| SKEXP0080 | Process Framework |
| SKEXP0081 | Process Framework - Foundry Process
| | | | | | | |
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index b28ab2ff0693..f5aac39af5c1 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -1,7 +1,7 @@
- 1.57.0
+ 1.60.0$(VersionPrefix)-$(VersionSuffix)$(VersionPrefix)
@@ -9,7 +9,7 @@
true
- 1.56.0
+ 1.59.0$(NoWarn);CP0003
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index 18c9411c171e..97460d3e7c79 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -8,7 +8,7 @@
falsetrue
- $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001,CA1724,IDE1006,IDE0009,MEVD9000
+ $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101,SKEXP0110,OPENAI001,CA1724,IDE1006,IDE0009,MEVD9000Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-apiplugin.json b/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-apiplugin.json
new file mode 100644
index 000000000000..adcd81020dda
--- /dev/null
+++ b/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-apiplugin.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.1/schema.json",
+ "schema_version": "v2.1",
+ "name_for_human": "OData Service for namespace microsoft.graph",
+ "description_for_human": "This OData service is located at https://graph.microsoft.com/beta",
+ "description_for_model": "This OData service is located at https://graph.microsoft.com/beta",
+ "contact_email": "publisher-email@example.com",
+ "namespace": "Retrieval",
+ "capabilities": {
+ "conversation_starters": [
+ {
+ "text": "Invoke action retrieval"
+ }
+ ]
+ },
+ "functions": [
+ {
+ "name": "copilot_retrieval",
+ "description": "Invoke action retrieval"
+ }
+ ],
+ "runtimes": [
+ {
+ "type": "OpenApi",
+ "auth": {
+ "type": "None"
+ },
+ "spec": {
+ "url": "retrieval-openapi.yml"
+ },
+ "run_for_functions": [
+ "copilot_retrieval"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-openapi.yml b/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-openapi.yml
new file mode 100644
index 000000000000..1ed7e8e7953a
--- /dev/null
+++ b/dotnet/samples/Concepts/Resources/Plugins/CopilotAgentPlugins/RetrievalPlugin/retrieval-openapi.yml
@@ -0,0 +1,162 @@
+openapi: 3.0.4
+info:
+ title: OData Service for namespace microsoft.graph - Subset
+ description: This OData service is located at https://graph.microsoft.com/beta
+ version: beta
+servers:
+ - url: https://graph.microsoft.com/beta
+paths:
+ /copilot/retrieval:
+ post:
+ tags:
+ - copilot.copilotRoot.Actions
+ summary: Invoke action retrieval
+ operationId: copilot_retrieval
+ requestBody:
+ description: Action parameters
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ queryString:
+ type: string
+ dataSource:
+ title: retrievalDataSource
+ enum:
+ - sharePoint
+ - oneDriveBusiness
+ - externalItem
+ - mail
+ - calendar
+ - teams
+ - people
+ - sharePointEmbedded
+ - unknownFutureValue
+ type: string
+ filterExpression:
+ type: string
+ nullable: true
+ resourceMetadata:
+ type: array
+ items:
+ type: string
+ nullable: true
+ maximumNumberOfResults:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: number
+ format: int32
+ nullable: true
+ required: true
+ responses:
+ 2XX:
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/microsoft.graph.retrievalResponse'
+ deprecated: true
+ x-ms-deprecation:
+ removalDate: '2025-12-31T00:00:00.0000000+00:00'
+ date: '2024-02-23T00:00:00.0000000+00:00'
+ version: 2024-12/PrivatePreview:retrievalAPI
+components:
+ schemas:
+ microsoft.graph.retrievalResponse:
+ title: retrievalResponse
+ required:
+ - '@odata.type'
+ type: object
+ properties:
+ retrievalHits:
+ type: array
+ items:
+ $ref: '#/components/schemas/microsoft.graph.retrievalHit'
+ '@odata.type':
+ type: string
+ microsoft.graph.retrievalHit:
+ title: retrievalHit
+ required:
+ - '@odata.type'
+ type: object
+ properties:
+ extracts:
+ type: array
+ items:
+ $ref: '#/components/schemas/microsoft.graph.retrievalExtract'
+ resourceMetadata:
+ $ref: '#/components/schemas/microsoft.graph.searchResourceMetadataDictionary'
+ resourceType:
+ title: retrievalEntityType
+ enum:
+ - site
+ - list
+ - listItem
+ - drive
+ - driveItem
+ - externalItem
+ - unknownFutureValue
+ type: string
+ sensitivityLabel:
+ $ref: '#/components/schemas/microsoft.graph.searchSensitivityLabelInfo'
+ webUrl:
+ type: string
+ nullable: true
+ '@odata.type':
+ type: string
+ microsoft.graph.retrievalExtract:
+ title: retrievalExtract
+ required:
+ - '@odata.type'
+ type: object
+ properties:
+ text:
+ type: string
+ nullable: true
+ '@odata.type':
+ type: string
+ microsoft.graph.searchResourceMetadataDictionary:
+ title: searchResourceMetadataDictionary
+ required:
+ - '@odata.type'
+ type: object
+ properties:
+ '@odata.type':
+ type: string
+ microsoft.graph.searchSensitivityLabelInfo:
+ title: searchSensitivityLabelInfo
+ required:
+ - '@odata.type'
+ type: object
+ properties:
+ color:
+ type: string
+ nullable: true
+ readOnly: true
+ displayName:
+ type: string
+ nullable: true
+ readOnly: true
+ isEncrypted:
+ type: boolean
+ nullable: true
+ readOnly: true
+ priority:
+ maximum: 2147483647
+ minimum: -2147483648
+ type: number
+ format: int32
+ nullable: true
+ readOnly: true
+ sensitivityLabelId:
+ type: string
+ nullable: true
+ readOnly: true
+ tooltip:
+ type: string
+ nullable: true
+ readOnly: true
+ '@odata.type':
+ type: string
+ description: "Represents a sensitivityLabel.\nThis model is shared with the CCS retrieval API and search where it is already unhidden."
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
new file mode 100644
index 000000000000..bc5375269c8a
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/A2AClient.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+ $(NoWarn);CS1591;VSTHRD111;CA2007;SKEXP0110
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
new file mode 100644
index 000000000000..ed5be9a77d5f
--- /dev/null
+++ b/dotnet/samples/Demos/A2AClientServer/A2AClient/HostClientAgent.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using SharpA2A.Core;
+
+namespace A2A;
+
+internal sealed class HostClientAgent
+{
+ internal HostClientAgent(ILogger logger)
+ {
+ this._logger = logger;
+ }
+ internal async Task InitializeAgentAsync(string modelId, string apiKey, string[] agentUrls)
+ {
+ try
+ {
+ this._logger.LogInformation("Initializing Semantic Kernel agent with model: {ModelId}", modelId);
+
+ // Connect to the remote agents via A2A
+ var createAgentTasks = agentUrls.Select(agentUrl => this.CreateAgentAsync(agentUrl));
+ var agents = await Task.WhenAll(createAgentTasks);
+ var agentFunctions = agents.Select(agent => AgentKernelFunctionFactory.CreateFromAgent(agent)).ToList();
+ var agentPlugin = KernelPluginFactory.CreateFromFunctions("AgentPlugin", agentFunctions);
+
+ // Define the Host agent
+ var builder = Kernel.CreateBuilder();
+ builder.AddOpenAIChatCompletion(modelId, apiKey);
+ builder.Plugins.Add(agentPlugin);
+ var kernel = builder.Build();
+ kernel.FunctionInvocationFilters.Add(new ConsoleOutputFunctionInvocationFilter());
+
+ this.Agent = new ChatCompletionAgent()
+ {
+ Kernel = kernel,
+ Name = "HostClient",
+ Instructions =
+ """
+ You specialize in handling queries for users and using your tools to provide answers.
+ """,
+ Arguments = new KernelArguments(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
+ };
+ }
+ catch (Exception ex)
+ {
+ this._logger.LogError(ex, "Failed to initialize HostClientAgent");
+ throw;
+ }
+ }
+
+ ///
+ /// The associated
+ ///
+ public Agent? Agent { get; private set; }
+
+ #region private
+ private readonly ILogger _logger;
+
+ private async Task CreateAgentAsync(string agentUri)
+ {
+ var httpClient = new HttpClient
+ {
+ BaseAddress = new Uri(agentUri),
+ Timeout = TimeSpan.FromSeconds(60)
+ };
+
+ var client = new A2AClient(httpClient);
+ var cardResolver = new A2ACardResolver(httpClient);
+ var agentCard = await cardResolver.GetAgentCardAsync();
+
+ return new A2AAgent(client, agentCard!);
+ }
+ #endregion
+}
+
+internal sealed class ConsoleOutputFunctionInvocationFilter() : IFunctionInvocationFilter
+{
+ private static string IndentMultilineString(string multilineText, int indentLevel = 1, int spacesPerIndent = 4)
+ {
+ // Create the indentation string
+ var indentation = new string(' ', indentLevel * spacesPerIndent);
+
+ // Split the text into lines, add indentation, and rejoin
+ char[] NewLineChars = { '\r', '\n' };
+ string[] lines = multilineText.Split(NewLineChars, StringSplitOptions.None);
+
+ return string.Join(Environment.NewLine, lines.Select(line => indentation + line));
+ }
+ public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+
+ Console.WriteLine($"\nCalling Agent {context.Function.Name} with arguments:");
+ Console.ForegroundColor = ConsoleColor.Gray;
+
+ foreach (var kvp in context.Arguments)
+ {
+ Console.WriteLine(IndentMultilineString($" {kvp.Key}: {kvp.Value}"));
+ }
+
+ await next(context);
+
+ if (context.Result.GetValue
diff --git a/dotnet/samples/Demos/ModelContextProtocolPluginAuth/ModelContextProtocolPluginAuth.csproj b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/ModelContextProtocolPluginAuth.csproj
new file mode 100644
index 000000000000..35e7321db7d2
--- /dev/null
+++ b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/ModelContextProtocolPluginAuth.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+ $(NoWarn);CA2249;CS0612;SKEXP0001;VSTHRD111;CA2007;RCS1263
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Demos/ModelContextProtocolPluginAuth/Program.cs b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/Program.cs
new file mode 100644
index 000000000000..29f6b0c509f8
--- /dev/null
+++ b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/Program.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using System.Net;
+using System.Text;
+using System.Web;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+using ModelContextProtocol.Client;
+
+var config = new ConfigurationBuilder()
+ .AddUserSecrets()
+ .AddEnvironmentVariables()
+ .Build();
+
+if (config["OpenAI:ApiKey"] is not { } apiKey)
+{
+ Console.Error.WriteLine("Please provide a valid OpenAI:ApiKey to run this sample. See the associated README.md for more details.");
+ return;
+}
+
+// We can customize a shared HttpClient with a custom handler if desired
+using var sharedHandler = new SocketsHttpHandler
+{
+ PooledConnectionLifetime = TimeSpan.FromMinutes(2),
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1)
+};
+using var httpClient = new HttpClient(sharedHandler);
+
+var consoleLoggerFactory = LoggerFactory.Create(builder =>
+{
+ builder.AddConsole();
+});
+
+// Create SSE client transport for the MCP server
+var serverUrl = "http://localhost:7071/";
+var transport = new SseClientTransport(new()
+{
+ Endpoint = new Uri(serverUrl),
+ Name = "Secure Weather Client",
+ OAuth = new()
+ {
+ ClientName = "ProtectedMcpClient",
+ RedirectUri = new Uri("http://localhost:1179/callback"),
+ AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
+ }
+}, httpClient, consoleLoggerFactory);
+
+// Create an MCPClient for the protected MCP server
+await using var mcpClient = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
+
+// Retrieve the list of tools available on the GitHub server
+var tools = await mcpClient.ListToolsAsync().ConfigureAwait(false);
+foreach (var tool in tools)
+{
+ Console.WriteLine($"{tool.Name}: {tool.Description}");
+}
+
+// Prepare and build kernel with the MCP tools as Kernel functions
+var builder = Kernel.CreateBuilder();
+builder.Services
+ .AddLogging(c => c.AddDebug().SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace))
+ .AddOpenAIChatCompletion(
+ modelId: config["OpenAI:ChatModelId"] ?? "gpt-4o-mini",
+ apiKey: apiKey);
+Kernel kernel = builder.Build();
+kernel.Plugins.AddFromFunctions("WeatherApi", tools.Select(aiFunction => aiFunction.AsKernelFunction()));
+
+// Enable automatic function calling
+OpenAIPromptExecutionSettings executionSettings = new()
+{
+ Temperature = 0,
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(options: new() { RetainArgumentTypes = true })
+};
+
+// Test using weather tools
+var prompt = "Get current weather alerts for New York?";
+var result = await kernel.InvokePromptAsync(prompt, new(executionSettings)).ConfigureAwait(false);
+Console.WriteLine($"\n\n{prompt}\n{result}");
+
+// Define the agent
+ChatCompletionAgent agent = new()
+{
+ Instructions = "Answer questions about weather alerts for US states.",
+ Name = "WeatherAgent",
+ Kernel = kernel,
+ Arguments = new KernelArguments(executionSettings),
+};
+
+// Respond to user input, invoking functions where appropriate.
+ChatMessageContent response = await agent.InvokeAsync("Get the current weather alerts for Washington?").FirstAsync();
+Console.WriteLine($"\n\nResponse from WeatherAgent:\n{response.Content}");
+
+///
+/// Handles the OAuth authorization URL by starting a local HTTP server and opening a browser.
+/// This implementation demonstrates how SDK consumers can provide their own authorization flow.
+///
+/// The authorization URL to open in the browser.
+/// The redirect URI where the authorization code will be sent.
+/// The cancellation token.
+/// The authorization code extracted from the callback, or null if the operation failed.
+static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken)
+{
+ Console.WriteLine("Starting OAuth authorization flow...");
+ Console.WriteLine($"Opening browser to: {authorizationUrl}");
+
+ var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority);
+ if (!listenerPrefix.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
+ {
+ listenerPrefix += "/";
+ }
+
+ using var listener = new HttpListener();
+ listener.Prefixes.Add(listenerPrefix);
+
+ try
+ {
+ listener.Start();
+ Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}");
+
+ OpenBrowser(authorizationUrl);
+
+ var context = await listener.GetContextAsync();
+ var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty);
+ var code = query["code"];
+ var error = query["error"];
+
+ string responseHtml = "
Authentication complete
You can close this window now.
";
+ byte[] buffer = Encoding.UTF8.GetBytes(responseHtml);
+ context.Response.ContentLength64 = buffer.Length;
+ context.Response.ContentType = "text/html";
+ context.Response.OutputStream.Write(buffer, 0, buffer.Length);
+ context.Response.Close();
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ Console.WriteLine($"Auth error: {error}");
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(code))
+ {
+ Console.WriteLine("No authorization code received");
+ return null;
+ }
+
+ Console.WriteLine("Authorization code received successfully.");
+ return code;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error getting auth code: {ex.Message}");
+ return null;
+ }
+ finally
+ {
+ if (listener.IsListening)
+ {
+ listener.Stop();
+ }
+ }
+}
+
+///
+/// Opens the specified URL in the default browser.
+///
+/// The URL to open.
+static void OpenBrowser(Uri url)
+{
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = url.ToString(),
+ UseShellExecute = true
+ };
+ Process.Start(psi);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error opening browser. {ex.Message}");
+ Console.WriteLine($"Please manually open this URL: {url}");
+ }
+}
diff --git a/dotnet/samples/Demos/ModelContextProtocolPluginAuth/README.md b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/README.md
new file mode 100644
index 000000000000..630e5972b06b
--- /dev/null
+++ b/dotnet/samples/Demos/ModelContextProtocolPluginAuth/README.md
@@ -0,0 +1,144 @@
+# Model Context Protocol Sample
+
+This example demonstrates how to use tools from a protected Model Context Protocol server with Semantic Kernel.
+
+MCP is an open protocol that standardizes how applications provide context to LLMs.
+
+For information on Model Context Protocol (MCP) please refer to the [documentation](https://modelcontextprotocol.io/introduction).
+
+The sample shows:
+
+1. How to connect to a protected MCP Server using OAuth 2.0 authentication
+1. How to implement a custom OAuth authorization flow with browser-based authentication
+1. Retrieve the list of tools the MCP Server makes available
+1. Convert the MCP tools to Semantic Kernel functions so they can be added to a Kernel instance
+1. Invoke the tools from Semantic Kernel using function calling
+
+## Installing Prerequisites
+
+- A self-signed certificate to enable HTTPS use in development, see [dotnet dev-certs](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs)
+- .NET 9.0 or later
+- A running TestOAuthServer (for OAuth authentication), see [Start the Test OAuth Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMCPClient#step-1-start-the-test-oauth-server)
+- A running ProtectedMCPServer (for MCP services), see [Start the Protected MCP Server](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMCPClient#step-2-start-the-protected-mcp-server)
+
+## Configuring Secrets or Environment Variables
+
+The example requires credentials to access OpenAI.
+
+If you have set up those credentials as secrets within Secret Manager or through environment variables for other samples from the solution in which this project is found, they will be re-used.
+
+### To set your secrets with Secret Manager
+
+```text
+cd dotnet/samples/Demos/ModelContextProtocolPluginAuth
+
+dotnet user-secrets init
+
+dotnet user-secrets set "OpenAI:ChatModelId" "..."
+dotnet user-secrets set "OpenAI:ApiKey" "..."
+ "..."
+```
+
+### To set your secrets with environment variables
+
+Use these names:
+
+```text
+# OpenAI
+OpenAI__ChatModelId
+OpenAI__ApiKey
+```
+
+## Setup and Running
+
+### Step 1: Start the Test OAuth Server
+
+First, you need to start the TestOAuthServer which provides OAuth authentication:
+
+```bash
+cd \tests\ModelContextProtocol.TestOAuthServer
+dotnet run --framework net9.0
+```
+
+The OAuth server will start at `https://localhost:7029`
+
+### Step 2: Start the Protected MCP Server
+
+Next, start the ProtectedMCPServer which provides the weather tools:
+
+```bash
+cd \samples\ProtectedMCPServer
+dotnet run
+```
+
+The protected server will start at `http://localhost:7071`
+
+### Step 3: Run the ModelContextProtocolPluginAuth sample
+
+Finally, run this client:
+
+```bash
+dotnet run
+```
+
+## What Happens
+
+1. The client attempts to connect to the protected MCP server at `http://localhost:7071`
+2. The server responds with OAuth metadata indicating authentication is required
+3. The client initiates OAuth 2.0 authorization code flow:
+ - Opens a browser to the authorization URL at the OAuth server
+ - Starts a local HTTP listener on `http://localhost:1179/callback` to receive the authorization code
+ - Exchanges the authorization code for an access token
+4. The client uses the access token to authenticate with the MCP server
+5. The client lists available tools and calls the `GetAlerts` tool for New York state
+
+The following diagram outlines an example OAuth flow:
+
+```mermaid
+sequenceDiagram
+ participant Client as Client
+ participant Server as MCP Server (Resource Server)
+ participant AuthServer as Authorization Server
+
+ Client->>Server: MCP request without access token
+ Server-->>Client: HTTP 401 Unauthorized with WWW-Authenticate header
+ Note over Client: Analyze and delegate tasks
+ Client->>Server: GET /.well-known/oauth-protected-resource
+ Server-->>Client: Resource metadata with authorization server URL
+ Note over Client: Validate RS metadata, build AS metadata URL
+ Client->>AuthServer: GET /.well-known/oauth-authorization-server
+ AuthServer-->>Client: Authorization server metadata
+ Note over Client,AuthServer: OAuth 2.0 authorization flow happens here
+ Client->>AuthServer: Token request
+ AuthServer-->>Client: Access token
+ Client->>Server: MCP request with access token
+ Server-->>Client: MCP response
+ Note over Client,Server: MCP communication continues with valid token
+```
+
+## OAuth Configuration
+
+The client is configured with:
+- **Client ID**: `demo-client`
+- **Client Secret**: `demo-secret`
+- **Redirect URI**: `http://localhost:1179/callback`
+- **OAuth Server**: `https://localhost:7029`
+- **Protected Resource**: `http://localhost:7071`
+
+## Available Tools
+
+Once authenticated, the client can access weather tools including:
+- **GetAlerts**: Get weather alerts for a US state
+- **GetForecast**: Get weather forecast for a location (latitude/longitude)
+
+## Troubleshooting
+
+- Ensure the ASP.NET Core dev certificate is trusted.
+ ```
+ dotnet dev-certs https --clean
+ dotnet dev-certs https --trust
+ ```
+- Ensure all three services are running in the correct order
+- Check that ports 7029, 7071, and 1179 are available
+- If the browser doesn't open automatically, copy the authorization URL from the console and open it manually
+- Make sure to allow the OAuth server's self-signed certificate in your browser
\ No newline at end of file
diff --git a/dotnet/samples/Demos/OllamaFunctionCalling/OllamaFunctionCalling.csproj b/dotnet/samples/Demos/OllamaFunctionCalling/OllamaFunctionCalling.csproj
index bfcc8c0afaf0..e9a03e29dc83 100644
--- a/dotnet/samples/Demos/OllamaFunctionCalling/OllamaFunctionCalling.csproj
+++ b/dotnet/samples/Demos/OllamaFunctionCalling/OllamaFunctionCalling.csproj
@@ -3,7 +3,7 @@
Exenet8.0
- $(NoWarn);CA2007,CA2208,CS1591,CA1024,IDE0009,IDE0055,IDE0073,IDE0211,VSTHRD111,SKEXP0001,SKEXP0070
+ $(NoWarn);CA2007,CA2208,CS1591,CA1024,IDE0009,IDE0055,IDE0073,IDE0211,VSTHRD111,SKEXP0001
diff --git a/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs b/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs
index 3d52b6ea45c1..078f372c96cf 100644
--- a/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs
+++ b/dotnet/samples/Demos/OllamaFunctionCalling/Program.cs
@@ -1,6 +1,4 @@
-#pragma warning disable SKEXP0070 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
-
-using System;
+using System;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.Ollama;
diff --git a/dotnet/samples/Demos/OnnxSimpleRAG/OnnxSimpleRAG.csproj b/dotnet/samples/Demos/OnnxSimpleRAG/OnnxSimpleRAG.csproj
index 7ae69898f692..796a2966697c 100644
--- a/dotnet/samples/Demos/OnnxSimpleRAG/OnnxSimpleRAG.csproj
+++ b/dotnet/samples/Demos/OnnxSimpleRAG/OnnxSimpleRAG.csproj
@@ -3,7 +3,7 @@
Exenet8.0
- $(NoWarn);CA2007;CS0612;VSTHRD111;SKEXP0070;SKEXP0050;SKEXP0001
+ $(NoWarn);CA2007;CS0612;VSTHRD111;SKEXP0050;SKEXP00015ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.AppHost/ProcessFramework.Aspire.AppHost.csproj b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.AppHost/ProcessFramework.Aspire.AppHost.csproj
index 9310b9a042eb..504c120220e9 100644
--- a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.AppHost/ProcessFramework.Aspire.AppHost.csproj
+++ b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.AppHost/ProcessFramework.Aspire.AppHost.csproj
@@ -15,20 +15,15 @@
-
-
+ false
-
-
-
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/ProcessFramework.Aspire.ProcessOrchestrator.csproj b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/ProcessFramework.Aspire.ProcessOrchestrator.csproj
index 7d1d3995191d..7e65e2272553 100644
--- a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/ProcessFramework.Aspire.ProcessOrchestrator.csproj
+++ b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.ProcessOrchestrator/ProcessFramework.Aspire.ProcessOrchestrator.csproj
@@ -6,23 +6,19 @@
enableenable
- $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0101,SKEXP0110,OPENAI001
+ $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0101,SKEXP0110,OPENAI001
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.SummaryAgent/ProcessFramework.Aspire.SummaryAgent.csproj b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.SummaryAgent/ProcessFramework.Aspire.SummaryAgent.csproj
index 187beb78372b..e045b2db37b4 100644
--- a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.SummaryAgent/ProcessFramework.Aspire.SummaryAgent.csproj
+++ b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.SummaryAgent/ProcessFramework.Aspire.SummaryAgent.csproj
@@ -10,16 +10,12 @@
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.TranslatorAgent/ProcessFramework.Aspire.TranslatorAgent.csproj b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.TranslatorAgent/ProcessFramework.Aspire.TranslatorAgent.csproj
index 59be1e8a4d6a..bb4f5bf1e1da 100644
--- a/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.TranslatorAgent/ProcessFramework.Aspire.TranslatorAgent.csproj
+++ b/dotnet/samples/Demos/ProcessFrameworkWithAspire/ProcessFramework.Aspire/ProcessFramework.Aspire.TranslatorAgent/ProcessFramework.Aspire.TranslatorAgent.csproj
@@ -10,17 +10,13 @@
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj
index b2d5022ffa34..c8e87e203414 100644
--- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Grpc/ProcessWithCloudEvents.Grpc.csproj
@@ -5,7 +5,7 @@
enableenable
- $(NoWarn);CA2007,CS1591,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110
+ $(NoWarn);CA2007,CS1591,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj
index 1fafc3012f07..e312860af9a7 100644
--- a/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj
+++ b/dotnet/samples/Demos/ProcessWithCloudEvents/ProcessWithCloudEvents.Processes/ProcessWithCloudEvents.Processes.csproj
@@ -5,7 +5,7 @@
enableenable
- $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110
+ $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110
diff --git a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj b/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj
index d1bd90408672..2912992ce565 100644
--- a/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj
+++ b/dotnet/samples/Demos/ProcessWithDapr/ProcessWithDapr.csproj
@@ -5,7 +5,7 @@
enableenable
- $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110
+ $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0110
diff --git a/dotnet/samples/Demos/StructuredDataPlugin/StructuredDataPlugin.csproj b/dotnet/samples/Demos/StructuredDataPlugin/StructuredDataPlugin.csproj
index de0ceee44233..97dfceb63f85 100644
--- a/dotnet/samples/Demos/StructuredDataPlugin/StructuredDataPlugin.csproj
+++ b/dotnet/samples/Demos/StructuredDataPlugin/StructuredDataPlugin.csproj
@@ -5,7 +5,7 @@
net8.0enableenable
- $(NoWarn),VSTHRD111,CA2007,CA5399,SKEXP0050,SKEXP0070
+ $(NoWarn),VSTHRD111,CA2007,CA5399,SKEXP00505ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj
index ee804dbc02dc..1aac0e84f709 100644
--- a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj
+++ b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj
@@ -7,7 +7,7 @@
disablefalse
- $(NoWarn);CA1024;CA1050;CA1707;CA2007;CS1591;VSTHRD111,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0001
+ $(NoWarn);CA1024;CA1050;CA1707;CA2007;CS1591;VSTHRD111,SKEXP0050,SKEXP0060,SKEXP00015ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj
index d8f4a6ca8316..0314496a6af6 100644
--- a/dotnet/samples/GettingStarted/GettingStarted.csproj
+++ b/dotnet/samples/GettingStarted/GettingStarted.csproj
@@ -7,7 +7,7 @@
truefalse
- $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101
+ $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -46,9 +46,7 @@
-
-
diff --git a/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
new file mode 100644
index 000000000000..a6d7ba601ba0
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/A2A/Step01_A2AAgent.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using SharpA2A.Core;
+
+namespace GettingStarted.A2A;
+
+///
+/// This example demonstrates similarity between using
+/// and other agent types.
+///
+public class Step01_A2AAgent(ITestOutputHelper output) : BaseAgentsTest(output)
+{
+ [Fact]
+ public async Task UseA2AAgent()
+ {
+ // Create an A2A agent instance
+ using var httpClient = CreateHttpClient();
+ var client = new A2AClient(httpClient);
+ var cardResolver = new A2ACardResolver(httpClient);
+ var agentCard = await cardResolver.GetAgentCardAsync();
+ Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
+ var agent = new A2AAgent(client, agentCard);
+
+ // Invoke the A2A agent
+ await foreach (AgentResponseItem response in agent.InvokeAsync("List the latest invoices for Contoso?"))
+ {
+ this.WriteAgentChatMessage(response);
+ }
+ }
+
+ [Fact]
+ public async Task UseA2AAgentStreaming()
+ {
+ // Create an A2A agent instance
+ using var httpClient = CreateHttpClient();
+ var client = new A2AClient(httpClient);
+ var cardResolver = new A2ACardResolver(httpClient);
+ var agentCard = await cardResolver.GetAgentCardAsync();
+ Console.WriteLine(JsonSerializer.Serialize(agentCard, s_jsonSerializerOptions));
+ var agent = new A2AAgent(client, agentCard);
+
+ // Invoke the A2A agent
+ var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?");
+ await WriteAgentStreamMessageAsync(responseItems);
+ }
+
+ #region private
+ private bool EnableLogging { get; set; } = false;
+
+ private HttpClient CreateHttpClient()
+ {
+ if (this.EnableLogging)
+ {
+ var handler = new LoggingHandler(new HttpClientHandler(), this.Output);
+ return new HttpClient(handler)
+ {
+ BaseAddress = TestConfiguration.A2A.AgentUrl
+ };
+ }
+
+ return new HttpClient()
+ {
+ BaseAddress = TestConfiguration.A2A.AgentUrl
+ };
+ }
+
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new() { WriteIndented = true };
+ #endregion
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
index a4bdf74f3505..0abbddeba9de 100644
--- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
+++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
@@ -9,7 +9,7 @@
true
- $(NoWarn);NU1008;CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001
+ $(NoWarn);NU1008;CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101,SKEXP0110,OPENAI001Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -23,11 +23,11 @@
-
+
-
-
+
+
@@ -43,6 +43,7 @@
+
diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step03_OpenAIResponseAgent_ReasoningModel.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step03_OpenAIResponseAgent_ReasoningModel.cs
new file mode 100644
index 000000000000..a88059222efb
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step03_OpenAIResponseAgent_ReasoningModel.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using OpenAI.Responses;
+
+namespace GettingStarted.OpenAIResponseAgents;
+
+///
+/// This example demonstrates using .
+///
+public class Step03_OpenAIResponseAgent_ReasoningModel(ITestOutputHelper output) : BaseResponsesAgentTest(output, "o4-mini")
+{
+ [Fact]
+ public async Task UseOpenAIResponseAgentWithAReasoningModelAsync()
+ {
+ // Define the agent
+ OpenAIResponseAgent agent = new(this.Client)
+ {
+ Name = "ResponseAgent",
+ Instructions = "Answer all queries with a detailed response.",
+ };
+
+ // Invoke the agent and output the response
+ var responseItems = agent.InvokeAsync("Which of the last four Olympic host cities has the highest average temperature?");
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ WriteAgentChatMessage(responseItem);
+ }
+ }
+
+ [Fact]
+ public async Task UseOpenAIResponseAgentWithAReasoningModelAndSummariesAsync()
+ {
+ // Define the agent
+ OpenAIResponseAgent agent = new(this.Client);
+
+ // ResponseCreationOptions allows you to specify tools for the agent.
+ OpenAIResponseAgentInvokeOptions invokeOptions = new()
+ {
+ ResponseCreationOptions = new()
+ {
+ ReasoningOptions = new()
+ {
+ ReasoningEffortLevel = ResponseReasoningEffortLevel.High,
+ // This parameter cannot be used due to a known issue in the OpenAI .NET SDK.
+ // https://github.com/openai/openai-dotnet/issues/457
+ // ReasoningSummaryVerbosity = ResponseReasoningSummaryVerbosity.Detailed,
+ },
+ },
+ };
+
+ // Invoke the agent and output the response
+ var responseItems = agent.InvokeAsync(
+ """
+ Instructions:
+ - Given the React component below, change it so that nonfiction books have red
+ text.
+ - Return only the code in your reply
+ - Do not include any additional formatting, such as markdown code blocks
+ - For formatting, use four space tabs, and do not allow any lines of code to
+ exceed 80 columns
+ const books = [
+ { title: 'Dune', category: 'fiction', id: 1 },
+ { title: 'Frankenstein', category: 'fiction', id: 2 },
+ { title: 'Moneyball', category: 'nonfiction', id: 3 },
+ ];
+ export default function BookList() {
+ const listItems = books.map(book =>
+
+ {book.title}
+
+ );
+ return (
+
{listItems}
+ );
+ }
+ """, options: invokeOptions);
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ WriteAgentChatMessage(responseItem);
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step04_OpenAIResponseAgent_Tools.cs b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step04_OpenAIResponseAgent_Tools.cs
new file mode 100644
index 000000000000..e3d3b6b46f8c
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step04_OpenAIResponseAgent_Tools.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Files;
+using OpenAI.Responses;
+using OpenAI.VectorStores;
+using Plugins;
+using Resources;
+
+namespace GettingStarted.OpenAIResponseAgents;
+
+///
+/// This example demonstrates how to use tools during a model interaction using .
+///
+public class Step04_OpenAIResponseAgent_Tools(ITestOutputHelper output) : BaseResponsesAgentTest(output)
+{
+ [Fact]
+ public async Task InvokeAgentWithFunctionToolsAsync()
+ {
+ // Define the agent
+ OpenAIResponseAgent agent = new(this.Client)
+ {
+ StoreEnabled = false,
+ };
+
+ // Create a plugin that defines the tools to be used by the agent.
+ KernelPlugin plugin = KernelPluginFactory.CreateFromType();
+ var tools = plugin.Select(f => f.ToToolDefinition(plugin.Name));
+ agent.Kernel.Plugins.Add(plugin);
+
+ ICollection messages =
+ [
+ new ChatMessageContent(AuthorRole.User, "What is the special soup and its price?"),
+ new ChatMessageContent(AuthorRole.User, "What is the special drink and its price?"),
+ ];
+ foreach (ChatMessageContent message in messages)
+ {
+ WriteAgentChatMessage(message);
+ }
+
+ // Invoke the agent and output the response
+ var responseItems = agent.InvokeAsync(messages);
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ WriteAgentChatMessage(responseItem);
+ }
+ }
+
+ [Fact]
+ public async Task InvokeAgentWithWebSearchAsync()
+ {
+ // Define the agent
+ OpenAIResponseAgent agent = new(this.Client)
+ {
+ StoreEnabled = false,
+ };
+
+ // ResponseCreationOptions allows you to specify tools for the agent.
+ ResponseCreationOptions creationOptions = new();
+ creationOptions.Tools.Add(ResponseTool.CreateWebSearchTool());
+ OpenAIResponseAgentInvokeOptions invokeOptions = new()
+ {
+ ResponseCreationOptions = creationOptions,
+ };
+
+ // Invoke the agent and output the response
+ var responseItems = agent.InvokeAsync("What was a positive news story from today?", options: invokeOptions);
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ WriteAgentChatMessage(responseItem);
+ }
+ }
+
+ [Fact]
+ public async Task InvokeAgentWithFileSearchAsync()
+ {
+ // Upload a file to the OpenAI File API
+ await using Stream stream = EmbeddedResource.ReadStream("employees.pdf")!;
+ OpenAIFile file = await this.FileClient.UploadFileAsync(stream, filename: "employees.pdf", purpose: FileUploadPurpose.UserData);
+
+ // Create a vector store for the file
+ CreateVectorStoreOperation createStoreOp = await this.VectorStoreClient.CreateVectorStoreAsync(
+ waitUntilCompleted: true,
+ new VectorStoreCreationOptions()
+ {
+ FileIds = { file.Id },
+ });
+
+ // Define the agent
+ OpenAIResponseAgent agent = new(this.Client)
+ {
+ StoreEnabled = false,
+ };
+
+ // ResponseCreationOptions allows you to specify tools for the agent.
+ ResponseCreationOptions creationOptions = new();
+ creationOptions.Tools.Add(ResponseTool.CreateFileSearchTool([createStoreOp.VectorStoreId], null));
+ OpenAIResponseAgentInvokeOptions invokeOptions = new()
+ {
+ ResponseCreationOptions = creationOptions,
+ };
+
+ // Invoke the agent and output the response
+ ICollection messages =
+ [
+ new ChatMessageContent(AuthorRole.User, "Who is the youngest employee?"),
+ new ChatMessageContent(AuthorRole.User, "Who works in sales?"),
+ new ChatMessageContent(AuthorRole.User, "I have a customer request, who can help me?"),
+ ];
+ foreach (ChatMessageContent message in messages)
+ {
+ WriteAgentChatMessage(message);
+ }
+
+ // Invoke the agent and output the response
+ var responseItems = agent.InvokeAsync(messages, options: invokeOptions);
+ await foreach (ChatMessageContent responseItem in responseItems)
+ {
+ WriteAgentChatMessage(responseItem);
+ }
+
+ // Clean up resources
+ RequestOptions noThrowOptions = new() { ErrorOptions = ClientErrorBehaviors.NoThrow };
+ this.FileClient.DeleteFile(file.Id, noThrowOptions);
+ this.VectorStoreClient.DeleteVectorStore(createStoreOp.VectorStoreId, noThrowOptions);
+ }
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs
index 9709d37580db..4bc3987459d0 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01_Concurrent.cs
@@ -21,12 +21,12 @@ public async Task ConcurrentTaskAsync(bool streamedResponse)
{
// Define the agents
ChatCompletionAgent physicist =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an expert in physics. You answer questions from a physics perspective.",
name: "Physicist",
description: "An expert in physics");
ChatCompletionAgent chemist =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.",
name: "Chemist",
description: "An expert in chemistry");
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs
index bcb08bb8a7ff..da8d4a0b5938 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step01a_ConcurrentWithStructuredOutput.cs
@@ -25,15 +25,15 @@ public async Task ConcurrentStructuredOutputAsync()
{
// Define the agents
ChatCompletionAgent agent1 =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an expert in identifying themes in articles. Given an article, identify the main themes.",
description: "An expert in identifying themes in articles");
ChatCompletionAgent agent2 =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an expert in sentiment analysis. Given an article, identify the sentiment.",
description: "An expert in sentiment analysis");
ChatCompletionAgent agent3 =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an expert in entity recognition. Given an article, extract the entities.",
description: "An expert in entity recognition");
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs
index 5bf6f55eeff1..6c8501113708 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02_Sequential.cs
@@ -22,7 +22,7 @@ public async Task SequentialTaskAsync(bool streamedResponse)
{
// Define the agents
ChatCompletionAgent analystAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Analyst",
instructions:
"""
@@ -33,7 +33,7 @@ public async Task SequentialTaskAsync(bool streamedResponse)
""",
description: "A agent that extracts key concepts from a product description.");
ChatCompletionAgent writerAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "copywriter",
instructions:
"""
@@ -43,7 +43,7 @@ Output should be short (around 150 words), output just the copy as a single text
""",
description: "An agent that writes a marketing copy based on the extracted concepts.");
ChatCompletionAgent editorAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "editor",
instructions:
"""
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs
index 0c55ae7e4299..c67be1678877 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step02a_SequentialCancellation.cs
@@ -17,7 +17,7 @@ public async Task SequentialCancelledAsync()
{
// Define the agents
ChatCompletionAgent agent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
"""
If the input message is a number, return the number incremented by one.
""",
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs
index 775f1a73d289..712eab38d175 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03_GroupChat.cs
@@ -27,7 +27,7 @@ public async Task GroupChatAsync(bool streamedResponse)
{
// Define the agents
ChatCompletionAgent writer =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
@@ -40,7 +40,7 @@ Only provide a single proposal per response.
Consider suggestions when refining an idea.
""");
ChatCompletionAgent editor =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Reviewer",
description: "An editor.",
instructions:
@@ -73,7 +73,7 @@ Consider suggestions when refining an idea.
InProcessRuntime runtime = new();
await runtime.StartAsync();
- string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
+ string input = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs
index f7c815e37323..ec40e603219c 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03a_GroupChatWithHumanInTheLoop.cs
@@ -19,7 +19,7 @@ public async Task GroupChatWithHumanAsync()
{
// Define the agents
ChatCompletionAgent writer =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "CopyWriter",
description: "A copy writer",
instructions:
@@ -32,7 +32,7 @@ Only provide a single proposal per response.
Consider suggestions when refining an idea.
""");
ChatCompletionAgent editor =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Reviewer",
description: "An editor.",
instructions:
@@ -73,7 +73,7 @@ Consider suggestions when refining an idea.
await runtime.StartAsync();
// Run the orchestration
- string input = "Create a slogon for a new eletric SUV that is affordable and fun to drive.";
+ string input = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
Console.WriteLine($"\n# INPUT: {input}\n");
OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs
index 6586242fc9c7..950b6f8303f7 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step03b_GroupChatWithAIManager.cs
@@ -23,7 +23,7 @@ public async Task GroupChatWithAIManagerAsync()
{
// Define the agents
ChatCompletionAgent farmer =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Farmer",
description: "A rural farmer from Southeast Asia.",
instructions:
@@ -34,7 +34,7 @@ You value tradition and sustainability.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent developer =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Developer",
description: "An urban software developer from the United States.",
instructions:
@@ -45,7 +45,7 @@ Your life is fast-paced and technology-driven.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent teacher =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Teacher",
description: "A retired history teacher from Eastern Europe",
instructions:
@@ -56,7 +56,7 @@ You bring historical and philosophical perspectives to discussions.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent activist =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Activist",
description: "A young activist from South America.",
instructions:
@@ -66,7 +66,7 @@ You are in a debate. Feel free to challenge the other participants with respect.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent spiritual =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "SpiritualLeader",
description: "A spiritual leader from the Middle East.",
instructions:
@@ -76,7 +76,7 @@ You are in a debate. Feel free to challenge the other participants with respect.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent artist =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Artist",
description: "An artist from Africa.",
instructions:
@@ -86,7 +86,7 @@ You are in a debate. Feel free to challenge the other participants with respect.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent immigrant =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Immigrant",
description: "An immigrant entrepreneur from Asia living in Canada.",
instructions:
@@ -97,7 +97,7 @@ You balance trandition with adaption.
You are in a debate. Feel free to challenge the other participants with respect.
""");
ChatCompletionAgent doctor =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "Doctor",
description: "A doctor from Scandinavia.",
instructions:
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs
index d8ad1791b425..7f2a00fb9abd 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04_Handoff.cs
@@ -23,24 +23,24 @@ public async Task OrderSupportAsync(bool streamedResponse)
{
// Define the agents & tools
ChatCompletionAgent triageAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "A customer support agent that triages issues.",
name: "TriageAgent",
description: "Handle customer requests.");
ChatCompletionAgent statusAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "OrderStatusAgent",
instructions: "Handle order status requests.",
description: "A customer support agent that checks order status.");
statusAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderStatusPlugin()));
ChatCompletionAgent returnAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "OrderReturnAgent",
instructions: "Handle order return requests.",
description: "A customer support agent that handles order returns.");
returnAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderReturnPlugin()));
ChatCompletionAgent refundAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "OrderRefundAgent",
instructions: "Handle order refund requests.",
description: "A customer support agent that handles order refund.");
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs
index 9b7bce047f7c..9c56f68cd583 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step04a_HandoffWithStructuredInput.cs
@@ -23,18 +23,18 @@ public async Task HandoffStructuredInputAsync()
// Define the agents
ChatCompletionAgent triageAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "Given a GitHub issue, triage it.",
name: "TriageAgent",
description: "An agent that triages GitHub issues");
ChatCompletionAgent pythonAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an agent that handles Python related GitHub issues.",
name: "PythonAgent",
description: "An agent that handles Python related issues");
pythonAgent.Kernel.Plugins.Add(plugin);
ChatCompletionAgent dotnetAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
instructions: "You are an agent that handles .NET related GitHub issues.",
name: "DotNetAgent",
description: "An agent that handles .NET related issues");
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Magentic.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Magentic.cs
index 98ab4bf21996..c2520a3d5a94 100644
--- a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Magentic.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step05_Magentic.cs
@@ -36,7 +36,7 @@ public async Task MagenticTaskAsync(bool streamedResponse)
// Define the agents
Kernel researchKernel = CreateKernelWithOpenAIChatCompletion(ResearcherModel);
ChatCompletionAgent researchAgent =
- this.CreateAgent(
+ this.CreateChatCompletionAgent(
name: "ResearchAgent",
description: "A helpful assistant with access to web search. Ask it to perform web searches.",
instructions: "You are a Researcher. You find information without additional computation or quantitative analysis.",
@@ -86,7 +86,7 @@ I am preparing a report on the energy efficiency of different machine learning m
""";
Console.WriteLine($"\n# INPUT:\n{input}\n");
OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
- string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 10));
+ string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 20));
Console.WriteLine($"\n# RESULT: {text}");
await runtime.RunUntilIdleAsync();
diff --git a/dotnet/samples/GettingStartedWithAgents/Orchestration/Step06_DifferentAgentTypes.cs b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step06_DifferentAgentTypes.cs
new file mode 100644
index 000000000000..99671aec5a85
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/Orchestration/Step06_DifferentAgentTypes.cs
@@ -0,0 +1,307 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.Magentic;
+using Microsoft.SemanticKernel.Agents.Orchestration;
+using Microsoft.SemanticKernel.Agents.Orchestration.Concurrent;
+using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat;
+using Microsoft.SemanticKernel.Agents.Orchestration.Handoff;
+using Microsoft.SemanticKernel.Agents.Orchestration.Sequential;
+using Microsoft.SemanticKernel.Agents.Runtime.InProcess;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace GettingStarted.Orchestration;
+
+///
+/// Demonstrates how to use the with two agents:
+/// - A Research agent that can perform web searches
+/// - A Coder agent that can run code using the code interpreter
+///
+public class Step06_DifferentAgentTypes(ITestOutputHelper output) : BaseOrchestrationTest(output)
+{
+ [Fact]
+ public async Task ConcurrentOrchestrationAsync()
+ {
+ // Define the agents
+ Agent physicist =
+ this.CreateChatCompletionAgent(
+ instructions: "You are an expert in physics. You answer questions from a physics perspective.",
+ name: "Physicist",
+ description: "An expert in physics");
+ Agent chemist =
+ await this.CreateAzureAIAgentAsync(
+ instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective.",
+ name: "Chemist",
+ description: "An expert in chemistry");
+
+ // Create a monitor to capturing agent responses (via ResponseCallback)
+ // to display at the end of this sample. (optional)
+ // NOTE: Create your own callback to capture responses in your application or service.
+ OrchestrationMonitor monitor = new();
+
+ // Define the orchestration
+ ConcurrentOrchestration orchestration =
+ new(physicist, chemist)
+ {
+ LoggerFactory = this.LoggerFactory,
+ ResponseCallback = monitor.ResponseCallback,
+ };
+
+ // Start the runtime
+ InProcessRuntime runtime = new();
+ await runtime.StartAsync();
+
+ // Run the orchestration
+ string input = "What is temperature?";
+ Console.WriteLine($"\n# INPUT: {input}\n");
+ OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
+
+ string[] output = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds));
+ Console.WriteLine($"\n# RESULT:\n{string.Join("\n\n", output.Select(text => $"{text}"))}");
+
+ await runtime.RunUntilIdleAsync();
+
+ Console.WriteLine("\n\nORCHESTRATION HISTORY");
+ foreach (ChatMessageContent message in monitor.History)
+ {
+ this.WriteAgentChatMessage(message);
+ }
+ }
+
+ [Fact]
+ public async Task SequentialOrchestrationAsync()
+ {
+ // Define the agents
+ Agent analystAgent =
+ this.CreateChatCompletionAgent(
+ name: "Analyst",
+ instructions:
+ """
+ You are a marketing analyst. Given a product description, identify:
+ - Key features
+ - Target audience
+ - Unique selling points
+ """,
+ description: "A agent that extracts key concepts from a product description.");
+ Agent writerAgent =
+ await this.CreateOpenAIAssistantAgentAsync(
+ name: "copywriter",
+ instructions:
+ """
+ You are a marketing copywriter. Given a block of text describing features, audience, and USPs,
+ compose a compelling marketing copy (like a newsletter section) that highlights these points.
+ Output should be short (around 150 words), output just the copy as a single text block.
+ """,
+ description: "An agent that writes a marketing copy based on the extracted concepts.");
+ Agent editorAgent =
+ await this.CreateAzureAIAgentAsync(
+ name: "editor",
+ instructions:
+ """
+ You are an editor. Given the draft copy, correct grammar, improve clarity, ensure consistent tone,
+ give format and make it polished. Output the final improved copy as a single text block.
+ """,
+ description: "An agent that formats and proofreads the marketing copy.");
+
+ // Create a monitor to capturing agent responses (via ResponseCallback)
+ // to display at the end of this sample. (optional)
+ // NOTE: Create your own callback to capture responses in your application or service.
+ OrchestrationMonitor monitor = new();
+ // Define the orchestration
+ SequentialOrchestration orchestration =
+ new(analystAgent, writerAgent, editorAgent)
+ {
+ LoggerFactory = this.LoggerFactory,
+ ResponseCallback = monitor.ResponseCallback,
+ };
+
+ // Start the runtime
+ InProcessRuntime runtime = new();
+ await runtime.StartAsync();
+
+ // Run the orchestration
+ string input = "An eco-friendly stainless steel water bottle that keeps drinks cold for 24 hours";
+ Console.WriteLine($"\n# INPUT: {input}\n");
+ OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
+ string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 2));
+ Console.WriteLine($"\n# RESULT: {text}");
+
+ await runtime.RunUntilIdleAsync();
+
+ Console.WriteLine("\n\nORCHESTRATION HISTORY");
+ foreach (ChatMessageContent message in monitor.History)
+ {
+ this.WriteAgentChatMessage(message);
+ }
+ }
+
+ [Fact]
+ public async Task GroupChatOrchestrationAsync()
+ {
+ // Define the agents
+ Agent writer =
+ this.CreateChatCompletionAgent(
+ name: "CopyWriter",
+ description: "A copy writer",
+ instructions:
+ """
+ You are a copywriter with ten years of experience and are known for brevity and a dry humor.
+ The goal is to refine and decide on the single best copy as an expert in the field.
+ Only provide a single proposal per response.
+ You're laser focused on the goal at hand.
+ Don't waste time with chit chat.
+ Consider suggestions when refining an idea.
+ """);
+ Agent editor =
+ await this.CreateOpenAIAssistantAgentAsync(
+ name: "Reviewer",
+ description: "An editor.",
+ instructions:
+ """
+ You are an art director who has opinions about copywriting born of a love for David Ogilvy.
+ The goal is to determine if the given copy is acceptable to print.
+ If so, state that it is approved.
+ If not, provide insight on how to refine suggested copy without example.
+ """);
+
+ // Create a monitor to capturing agent responses (via ResponseCallback)
+ // to display at the end of this sample. (optional)
+ // NOTE: Create your own callback to capture responses in your application or service.
+ OrchestrationMonitor monitor = new();
+ // Define the orchestration
+ GroupChatOrchestration orchestration =
+ new(new RoundRobinGroupChatManager()
+ {
+ MaximumInvocationCount = 5
+ },
+ writer,
+ editor)
+ {
+ LoggerFactory = this.LoggerFactory,
+ ResponseCallback = monitor.ResponseCallback,
+ };
+
+ // Start the runtime
+ InProcessRuntime runtime = new();
+ await runtime.StartAsync();
+
+ string input = "Create a slogan for a new electric SUV that is affordable and fun to drive.";
+ Console.WriteLine($"\n# INPUT: {input}\n");
+ OrchestrationResult result = await orchestration.InvokeAsync(input, runtime);
+ string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 3));
+ Console.WriteLine($"\n# RESULT: {text}");
+
+ await runtime.RunUntilIdleAsync();
+
+ Console.WriteLine("\n\nORCHESTRATION HISTORY");
+ foreach (ChatMessageContent message in monitor.History)
+ {
+ this.WriteAgentChatMessage(message);
+ }
+ }
+
+ [Fact]
+ public async Task HandoffOrchestrationAsync()
+ {
+ // Define the agents & tools
+ Agent triageAgent =
+ this.CreateChatCompletionAgent(
+ instructions: "A customer support agent that triages issues.",
+ name: "TriageAgent",
+ description: "Handle customer requests.");
+ Agent statusAgent =
+ this.CreateChatCompletionAgent(
+ name: "OrderStatusAgent",
+ instructions: "Handle order status requests.",
+ description: "A customer support agent that checks order status.");
+ statusAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderStatusPlugin()));
+ Agent returnAgent =
+ this.CreateChatCompletionAgent(
+ name: "OrderReturnAgent",
+ instructions: "Handle order return requests.",
+ description: "A customer support agent that handles order returns.");
+ returnAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderReturnPlugin()));
+ Agent refundAgent =
+ this.CreateChatCompletionAgent(
+ name: "OrderRefundAgent",
+ instructions: "Handle order refund requests.",
+ description: "A customer support agent that handles order refund.");
+ refundAgent.Kernel.Plugins.Add(KernelPluginFactory.CreateFromObject(new OrderRefundPlugin()));
+
+ // Create a monitor to capturing agent responses (via ResponseCallback)
+ // to display at the end of this sample. (optional)
+ // NOTE: Create your own callback to capture responses in your application or service.
+ OrchestrationMonitor monitor = new();
+ // Define user responses for InteractiveCallback (since sample is not interactive)
+ Queue responses = new();
+ string task = "I am a customer that needs help with my orders";
+ responses.Enqueue("I'd like to track the status of my order");
+ responses.Enqueue("My order ID is 123");
+ responses.Enqueue("I want to return another order of mine");
+ responses.Enqueue("Order ID 321");
+ responses.Enqueue("Broken item");
+ responses.Enqueue("No, bye");
+ // Define the orchestration
+ HandoffOrchestration orchestration =
+ new(OrchestrationHandoffs
+ .StartWith(triageAgent)
+ .Add(triageAgent, statusAgent, returnAgent, refundAgent)
+ .Add(statusAgent, triageAgent, "Transfer to this agent if the issue is not status related")
+ .Add(returnAgent, triageAgent, "Transfer to this agent if the issue is not return related")
+ .Add(refundAgent, triageAgent, "Transfer to this agent if the issue is not refund related"),
+ triageAgent,
+ statusAgent,
+ returnAgent,
+ refundAgent)
+ {
+ InteractiveCallback = () =>
+ {
+ string input = responses.Dequeue();
+ Console.WriteLine($"\n# INPUT: {input}\n");
+ return ValueTask.FromResult(new ChatMessageContent(AuthorRole.User, input));
+ },
+ LoggerFactory = this.LoggerFactory,
+ ResponseCallback = monitor.ResponseCallback,
+ };
+
+ // Start the runtime
+ InProcessRuntime runtime = new();
+ await runtime.StartAsync();
+
+ // Run the orchestration
+ Console.WriteLine($"\n# INPUT:\n{task}\n");
+ OrchestrationResult result = await orchestration.InvokeAsync(task, runtime);
+
+ string text = await result.GetValueAsync(TimeSpan.FromSeconds(ResultTimeoutInSeconds * 10));
+ Console.WriteLine($"\n# RESULT: {text}");
+
+ await runtime.RunUntilIdleAsync();
+
+ Console.WriteLine("\n\nORCHESTRATION HISTORY");
+ foreach (ChatMessageContent message in monitor.History)
+ {
+ this.WriteAgentChatMessage(message);
+ }
+ }
+
+ #region private
+ private sealed class OrderStatusPlugin
+ {
+ [KernelFunction]
+ public string CheckOrderStatus(string orderId) => $"Order {orderId} is shipped and will arrive in 2-3 days.";
+ }
+
+ private sealed class OrderReturnPlugin
+ {
+ [KernelFunction]
+ public string ProcessReturn(string orderId, string reason) => $"Return for order {orderId} has been processed successfully.";
+ }
+
+ private sealed class OrderRefundPlugin
+ {
+ [KernelFunction]
+ public string ProcessReturn(string orderId, string reason) => $"Refund for order {orderId} has been processed successfully.";
+ }
+ #endregion
+}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
index e62b869c0af1..80f794e28a3d 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
@@ -27,7 +27,7 @@ public class Step01_Agent(ITestOutputHelper output) : BaseAgentsTest(output)
[InlineData(false)]
public async Task UseSingleChatCompletionAgent(bool useChatClient)
{
- Kernel kernel = this.CreateKernelWithChatCompletion();
+ Kernel kernel = this.CreateKernelWithChatCompletion(useChatClient, out var chatClient);
// Define the agent
ChatCompletionAgent agent =
@@ -35,7 +35,7 @@ public async Task UseSingleChatCompletionAgent(bool useChatClient)
{
Name = ParrotName,
Instructions = ParrotInstructions,
- Kernel = this.CreateKernelWithChatCompletion(useChatClient, out var chatClient),
+ Kernel = kernel
};
// Respond to user input
diff --git a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
index 5e92c4906685..7244d2cb967b 100644
--- a/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
+++ b/dotnet/samples/GettingStartedWithProcesses/GettingStartedWithProcesses.csproj
@@ -10,7 +10,7 @@
- $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001
+ $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0080,SKEXP0081,SKEXP0101,SKEXP0110,OPENAI001
Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_FoundryAgentProcess.cs b/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_FoundryAgentProcess.cs
deleted file mode 100644
index 7465fe5610c9..000000000000
--- a/dotnet/samples/GettingStartedWithProcesses/Step06/Step06_FoundryAgentProcess.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.ClientModel;
-using System.Text;
-using Azure.AI.Agents.Persistent;
-using Azure.Identity;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents.AzureAI;
-using Microsoft.SemanticKernel.Agents.OpenAI;
-using OpenAI;
-
-namespace Step06;
-public class Step06_FoundryAgentProcess : BaseTest
-{
- public Step06_FoundryAgentProcess(ITestOutputHelper output) : base(output, redirectSystemConsoleOutput: true)
- {
- this.Client =
- this.UseOpenAIConfig ?
- OpenAIAssistantAgent.CreateOpenAIClient(new ApiKeyCredential(this.ApiKey ?? throw new ConfigurationNotFoundException("OpenAI:ApiKey"))) :
- !string.IsNullOrWhiteSpace(this.ApiKey) ?
- OpenAIAssistantAgent.CreateAzureOpenAIClient(new ApiKeyCredential(this.ApiKey), new Uri(this.Endpoint!)) :
- OpenAIAssistantAgent.CreateAzureOpenAIClient(new AzureCliCredential(), new Uri(this.Endpoint!));
- }
-
- protected OpenAIClient Client { get; init; }
-
- // Target Open AI Services
- protected override bool ForceOpenAI => true;
-
- ///
- /// This example demonstrates how to create a process with two agents that can chat with each other. A student agent and a teacher agent are created.
- /// The process will keep track of the interaction count between the two agents.
- ///
- [Fact]
- public async Task ProcessWithTwoAgentMathChat()
- {
- var endpoint = TestConfiguration.AzureAI.Endpoint;
- PersistentAgentsClient client = new(endpoint.TrimEnd('/'), new DefaultAzureCredential(), new PersistentAgentsAdministrationClientOptions().WithPolicy(endpoint, "2025-05-15-preview"));
-
- Azure.Response? studentAgent = null;
- Azure.Response? teacherAgent = null;
-
- try
- {
- // Create the single agents
- studentAgent = await client.Administration.CreateAgentAsync(
- model: "gpt-4o",
- name: "Student",
- instructions: "You are a student that answer question from teacher, when teacher gives you question you answer them."
- );
-
- teacherAgent = await client.Administration.CreateAgentAsync(
- model: "gpt-4o",
- name: "Teacher",
- instructions: "You are a teacher that create pre-school math question for student and check answer.\nIf the answer is correct, you stop the conversation by saying [COMPLETE].\nIf the answer is wrong, you ask student to fix it."
- );
-
- // Define the process with a state type
- var processBuilder = new FoundryProcessBuilder("two_agent_math_chat");
-
- // Create a thread for the student
- processBuilder.AddThread("Student", KernelProcessThreadLifetime.Scoped);
- processBuilder.AddThread("Teacher", KernelProcessThreadLifetime.Scoped);
-
- // Add the student
- var student = processBuilder.AddStepFromAgent(studentAgent);
-
- // Add the teacher
- var teacher = processBuilder.AddStepFromAgent(teacherAgent);
-
- /**************************** Orchestrate ***************************/
-
- // When the process starts, activate the student agent
- processBuilder.OnProcessEnter().SendEventTo(
- student,
- thread: "_variables_.Student",
- messagesIn: ["_variables_.TeacherMessages"],
- inputs: new Dictionary { });
-
- // When the student agent exits, update the process state to save the student's messages and update interaction counts
- processBuilder.OnStepExit(student)
- .UpdateProcessState(path: "StudentMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
-
- // When the student agent is finished, send the messages to the teacher agent
- processBuilder.OnEvent(student, "_default_")
- .SendEventTo(teacher, messagesIn: ["_variables_.StudentMessages"], thread: "Teacher");
-
- // When the teacher agent exits with a message containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts and emit the `correct_answer` event
- processBuilder.OnStepExit(teacher, condition: "jmespath(contains(to_string(_agent_.messages_out), '[COMPLETE]'))")
- .EmitEvent(
- eventName: "correct_answer",
- payload: new Dictionary
- {
- { "Question", "_variables_.TeacherMessages" },
- { "Answer", "_variables_.StudentMessages" }
- })
- .UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
-
- // When the teacher agent exits with a message not containing '[COMPLETE]', update the process state to save the teacher's messages and update interaction counts
- processBuilder.OnStepExit(teacher, condition: "_default_")
- .UpdateProcessState(path: "_variables_.TeacherMessages", operation: StateUpdateOperations.Set, value: "_agent_.messages_out");
-
- // When the teacher agent is finished, send the messages to the student agent
- processBuilder.OnEvent(teacher, "_default_", condition: "_default_")
- .SendEventTo(student, messagesIn: ["_variables_.TeacherMessages"], thread: "Student");
-
- // When the teacher agent emits the `correct_answer` event, stop the process
- processBuilder.OnEvent(teacher, "correct_answer")
- .StopProcess();
-
- // Verify that the process can be built and serialized to json
- var processJson = await processBuilder.ToJsonAsync();
- Assert.NotEmpty(processJson);
-
- var content = await RunWorkflowAsync(client, processBuilder, [new(MessageRole.User, "Go")]);
- Assert.NotEmpty(content);
- }
- finally
- {
- // Clean up the agents
- await client.Administration.DeleteAgentAsync(studentAgent?.Value.Id);
- await client.Administration.DeleteAgentAsync(teacherAgent?.Value.Id);
- }
- }
-
- private async Task RunWorkflowAsync(PersistentAgentsClient client, FoundryProcessBuilder processBuilder, List? initialMessages = null) where T : class, new()
- {
- Workflow? workflow = null;
- StringBuilder output = new();
-
- try
- {
- // publish the workflow
- workflow = await client.Administration.Pipeline.PublishWorkflowAsync(processBuilder);
-
- // threadId is used to store the thread ID
- PersistentAgentThread thread = await client.Threads.CreateThreadAsync(messages: initialMessages ?? []);
-
- // create run
- await foreach (var run in client.Runs.CreateRunStreamingAsync(thread.Id, workflow.Id))
- {
- if (run is Azure.AI.Agents.Persistent.MessageContentUpdate contentUpdate)
- {
- output.Append(contentUpdate.Text);
- Console.Write(contentUpdate.Text);
- }
- else if (run is Azure.AI.Agents.Persistent.RunUpdate runUpdate)
- {
- if (runUpdate.UpdateKind == Azure.AI.Agents.Persistent.StreamingUpdateReason.RunInProgress && !runUpdate.Value.Id.StartsWith("wf_run", StringComparison.OrdinalIgnoreCase))
- {
- Console.WriteLine();
- Console.Write($"{runUpdate.Value.Metadata["x-agent-name"]}> ");
- }
- }
- }
-
- // delete thread, so we can start over
- Console.WriteLine($"\nDeleting thread {thread?.Id}...");
- await client.Threads.DeleteThreadAsync(thread?.Id);
- return output.ToString();
- }
- finally
- {
- // // delete workflow
- Console.WriteLine($"Deleting workflow {workflow?.Id}...");
- await client.Administration.Pipeline.DeleteWorkflowAsync(workflow!);
- }
- }
-
- ///
- /// Represents the state of the two-agent math chat process.
- ///
- public class TwoAgentMathState
- {
- public List StudentMessages { get; set; }
-
- public List TeacherMessages { get; set; }
-
- public StudentState StudentState { get; set; } = new();
-
- public int InteractionCount { get; set; }
- }
-
- ///
- /// Represents the state of the student agent.
- ///
- public class StudentState
- {
- public int InteractionCount { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj b/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
index baa7fc9bee3e..17f545356906 100644
--- a/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
+++ b/dotnet/samples/GettingStartedWithTextSearch/GettingStartedWithTextSearch.csproj
@@ -7,7 +7,7 @@
truefalse
- $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101
+ $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -33,9 +33,7 @@
-
-
diff --git a/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj b/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
index b133579aed1a..19c4e8d63cf9 100644
--- a/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
+++ b/dotnet/samples/GettingStartedWithVectorStores/GettingStartedWithVectorStores.csproj
@@ -7,7 +7,7 @@
truefalse
- $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101
+ $(NoWarn);CS8618,IDE0009,IDE1006,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0101Library5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
diff --git a/dotnet/src/Agents/A2A/A2AAgent.cs b/dotnet/src/Agents/A2A/A2AAgent.cs
new file mode 100644
index 000000000000..c831a9a89f79
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AAgent.cs
@@ -0,0 +1,247 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Provides a specialized based on the A2A Protocol.
+///
+public sealed class A2AAgent : Agent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// instance to associate with the agent.
+ /// instance associated ith the agent.
+ public A2AAgent(A2AClient client, AgentCard agentCard)
+ {
+ Verify.NotNull(client);
+ Verify.NotNull(agentCard);
+
+ this.Client = client;
+ this.AgentCard = agentCard;
+ this.Name = agentCard.Name;
+ this.Description = agentCard.Description;
+ }
+
+ ///
+ /// The associated client.
+ ///
+ public A2AClient Client { get; }
+
+ ///
+ /// The associated agent card.
+ ///
+ public AgentCard AgentCard { get; }
+
+ ///
+ public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ var agentThread = await this.EnsureThreadExistsWithMessagesAsync(
+ messages,
+ thread,
+ () => new A2AAgentThread(this.Client),
+ cancellationToken).ConfigureAwait(false);
+
+ // Invoke the agent.
+ var invokeResults = this.InternalInvokeAsync(
+ this.AgentCard.Name,
+ messages,
+ agentThread,
+ options ?? new AgentInvokeOptions(),
+ cancellationToken);
+
+ // Notify the thread of new messages and return them to the caller.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ await this.NotifyThreadOfNewMessage(agentThread, result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, agentThread);
+ }
+ }
+
+ ///
+ public override async IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(messages);
+
+ var agentThread = await this.EnsureThreadExistsWithMessagesAsync(
+ messages,
+ thread,
+ () => new A2AAgentThread(this.Client),
+ cancellationToken).ConfigureAwait(false);
+
+ // Invoke the agent.
+ var chatMessages = new ChatHistory();
+ var invokeResults = this.InternalInvokeStreamingAsync(
+ messages,
+ agentThread,
+ options ?? new AgentInvokeOptions(),
+ chatMessages,
+ cancellationToken);
+
+ // Return the chunks to the caller.
+ await foreach (var result in invokeResults.ConfigureAwait(false))
+ {
+ yield return new(result, agentThread);
+ }
+
+ // Notify the thread of any new messages that were assembled from the streaming response.
+ foreach (var chatMessage in chatMessages)
+ {
+ await this.NotifyThreadOfNewMessage(agentThread, chatMessage, cancellationToken).ConfigureAwait(false);
+
+ if (options?.OnIntermediateMessage is not null)
+ {
+ await options.OnIntermediateMessage(chatMessage).ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ protected override Task CreateChannelAsync(CancellationToken cancellationToken)
+ {
+ throw new NotSupportedException($"{nameof(A2AAgent)} is not for use with {nameof(AgentChat)}.");
+ }
+
+ ///
+ protected override IEnumerable GetChannelKeys()
+ {
+ throw new NotSupportedException($"{nameof(A2AAgent)} is not for use with {nameof(AgentChat)}.");
+ }
+
+ ///
+ protected override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken)
+ {
+ throw new NotSupportedException($"{nameof(A2AAgent)} is not for use with {nameof(AgentChat)}.");
+ }
+
+ #region private
+ private async IAsyncEnumerable> InternalInvokeAsync(string name, ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ Verify.NotNull(messages);
+
+ // Ensure all messages have the correct role.
+ if (!messages.All(m => m.Role == AuthorRole.User))
+ {
+ throw new ArgumentException($"All messages must have the role {AuthorRole.User}.", nameof(messages));
+ }
+
+ // Send all messages to the remote agent in a single request.
+ await foreach (var result in this.InvokeAgentAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ await this.NotifyThreadOfNewMessage(thread, result, cancellationToken).ConfigureAwait(false);
+ yield return new(result, thread);
+ }
+ }
+
+ private async IAsyncEnumerable> InvokeAgentAsync(ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ List parts = [];
+ foreach (var message in messages)
+ {
+ foreach (var item in message.Items)
+ {
+ if (item is TextContent textContent)
+ {
+ parts.Add(new TextPart
+ {
+ Text = textContent.Text ?? string.Empty,
+ });
+ }
+ else
+ {
+ throw new NotSupportedException($"Unsupported content type: {item.GetType().Name}. Only TextContent are supported.");
+ }
+ }
+ }
+
+ var messageSendParams = new MessageSendParams
+ {
+ Message = new Message
+ {
+ MessageId = Guid.NewGuid().ToString(),
+ Role = MessageRole.User,
+ Parts = parts,
+ }
+ };
+
+ A2AResponse response = await this.Client.SendMessageAsync(messageSendParams).ConfigureAwait(false);
+ if (response is AgentTask agentTask)
+ {
+ if (agentTask.Artifacts != null && agentTask.Artifacts.Count > 0)
+ {
+ foreach (var artifact in agentTask.Artifacts)
+ {
+ foreach (var part in artifact.Parts)
+ {
+ if (part is TextPart textPart)
+ {
+ yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, textPart.Text), thread);
+ }
+ }
+ }
+ Console.WriteLine();
+ }
+ }
+ else if (response is Message messageResponse)
+ {
+ foreach (var part in messageResponse.Parts)
+ {
+ if (part is TextPart textPart)
+ {
+ yield return new AgentResponseItem(
+ new ChatMessageContent(
+ AuthorRole.Assistant,
+ textPart.Text),
+ thread);
+ }
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException("Unexpected response type from A2A client.");
+ }
+ }
+
+ private async IAsyncEnumerable> InternalInvokeStreamingAsync(ICollection messages, A2AAgentThread thread, AgentInvokeOptions options, ChatHistory chatMessages, [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ Verify.NotNull(messages);
+
+ // Ensure all messages have the correct role.
+ if (messages.Any(m => m.Role != AuthorRole.User))
+ {
+ throw new ArgumentException($"All messages must have the role {AuthorRole.User}.", nameof(messages));
+ }
+
+ // Send all messages to the remote agent in a single request.
+ await foreach (var result in this.InvokeAgentAsync(messages, thread, options, cancellationToken).ConfigureAwait(false))
+ {
+ await this.NotifyThreadOfNewMessage(thread, result, cancellationToken).ConfigureAwait(false);
+ yield return new(this.ToStreamingAgentResponseItem(result), thread);
+ }
+ }
+
+ private AgentResponseItem ToStreamingAgentResponseItem(AgentResponseItem responseItem)
+ {
+ var messageContent = new StreamingChatMessageContent(
+ responseItem.Message.Role,
+ responseItem.Message.Content,
+ innerContent: responseItem.Message.InnerContent,
+ modelId: responseItem.Message.ModelId,
+ encoding: responseItem.Message.Encoding,
+ metadata: responseItem.Message.Metadata);
+
+ return new AgentResponseItem(messageContent, responseItem.Thread);
+ }
+ #endregion
+}
diff --git a/dotnet/src/Agents/A2A/A2AAgentThread.cs b/dotnet/src/Agents/A2A/A2AAgentThread.cs
new file mode 100644
index 000000000000..3c10a468fcdb
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AAgentThread.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Represents a conversation thread for an A2A agent.
+///
+public sealed class A2AAgentThread : AgentThread
+{
+ ///
+ /// Initializes a new instance of the class that resumes an existing thread.
+ ///
+ /// The agents client to use for interacting with threads.
+ /// The ID of an existing thread to resume.
+ public A2AAgentThread(A2AClient client, string? id = null)
+ {
+ Verify.NotNull(client);
+
+ this._client = client;
+ this.Id = id ?? Guid.NewGuid().ToString("N");
+ }
+
+ ///
+ protected override Task CreateInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.FromResult(Guid.NewGuid().ToString("N"));
+ }
+
+ ///
+ protected override Task DeleteInternalAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ #region private
+ private readonly A2AClient _client;
+ #endregion
+}
diff --git a/dotnet/src/Agents/A2A/A2AHostAgent.cs b/dotnet/src/Agents/A2A/A2AHostAgent.cs
new file mode 100644
index 000000000000..da36fad6e725
--- /dev/null
+++ b/dotnet/src/Agents/A2A/A2AHostAgent.cs
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Host which will attach a to a
+///
+public sealed class A2AHostAgent
+{
+ ///
+ /// Initializes a new instance of the SemanticKernelTravelAgent
+ ///
+ public A2AHostAgent(Agent agent, AgentCard agentCard, TaskManager? taskManager = null)
+ {
+ Verify.NotNull(agent);
+ Verify.NotNull(agentCard);
+
+ this.Agent = agent;
+ this._agentCard = agentCard;
+
+ this.Attach(taskManager ?? new TaskManager());
+ }
+
+ ///
+ /// The associated
+ ///
+ public Agent? Agent { get; private set; }
+
+ ///
+ /// The associated
+ ///
+ public TaskManager? TaskManager => this._taskManager;
+
+ ///
+ /// Attach the to the provided
+ ///
+ ///
+ public void Attach(TaskManager taskManager)
+ {
+ Verify.NotNull(taskManager);
+
+ this._taskManager = taskManager;
+ taskManager.OnTaskCreated = this.ExecuteAgentTaskAsync;
+ taskManager.OnTaskUpdated = this.ExecuteAgentTaskAsync;
+ taskManager.OnAgentCardQuery = this.GetAgentCard;
+ }
+ ///
+ /// Execute the specific
+ ///
+ ///
+ ///
+ ///
+ public async Task ExecuteAgentTaskAsync(AgentTask task)
+ {
+ Verify.NotNull(task);
+ Verify.NotNull(this.Agent);
+
+ if (this._taskManager is null)
+ {
+ throw new InvalidOperationException("TaskManager must be attached before executing an agent task.");
+ }
+
+ await this._taskManager.UpdateStatusAsync(task.Id, TaskState.Working).ConfigureAwait(false);
+
+ // Get message from the user
+ var userMessage = task.History!.Last().Parts.First().AsTextPart().Text;
+
+ // Get the response from the agent
+ var artifact = new Artifact();
+ await foreach (AgentResponseItem response in this.Agent.InvokeAsync(userMessage).ConfigureAwait(false))
+ {
+ var content = response.Message.Content;
+ artifact.Parts.Add(new TextPart() { Text = content! });
+ }
+
+ // Return as artifacts
+ await this._taskManager.ReturnArtifactAsync(task.Id, artifact).ConfigureAwait(false);
+ await this._taskManager.UpdateStatusAsync(task.Id, TaskState.Completed).ConfigureAwait(false);
+ }
+
+ ///
+ /// Return the associated with this hosted agent.
+ ///
+ /// Current URL for the agent
+#pragma warning disable CA1054 // URI-like parameters should not be strings
+ public AgentCard GetAgentCard(string agentUrl)
+ {
+ // Ensure the URL is in the correct format
+ Uri uri = new(agentUrl);
+ agentUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}/";
+
+ this._agentCard.Url = agentUrl;
+ return this._agentCard;
+ }
+#pragma warning restore CA1054 // URI-like parameters should not be strings
+
+ #region private
+ private readonly AgentCard _agentCard;
+ private TaskManager? _taskManager;
+ #endregion
+}
diff --git a/dotnet/src/Agents/A2A/Agents.A2A.csproj b/dotnet/src/Agents/A2A/Agents.A2A.csproj
new file mode 100644
index 000000000000..9defa8d88691
--- /dev/null
+++ b/dotnet/src/Agents/A2A/Agents.A2A.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+ Microsoft.SemanticKernel.Agents.A2A
+ Microsoft.SemanticKernel.Agents.A2A
+ net8.0;netstandard2.0
+ $(NoWarn);SKEXP0110
+ false
+ alpha
+
+
+
+
+
+
+ Semantic Kernel Agents - A2A
+ Defines a concrete Agent based on the A2A Protocol.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs b/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs
new file mode 100644
index 000000000000..211a3cc9494d
--- /dev/null
+++ b/dotnet/src/Agents/A2A/Extensions/AuthorRoleExtensions.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SharpA2A.Core;
+
+namespace Microsoft.SemanticKernel.Agents.A2A;
+
+///
+/// Extensions for converting between amd .
+///
+internal static class AuthorRoleExtensions
+{
+ public static AuthorRole ToAuthorRole(this MessageRole role)
+ {
+ return role switch
+ {
+ MessageRole.User => AuthorRole.User,
+ MessageRole.Agent => AuthorRole.Assistant,
+ _ => throw new ArgumentOutOfRangeException(nameof(role), role, "Invalid message role")
+ };
+ }
+
+ public static MessageRole ToMessageRole(this AuthorRole role)
+ {
+ return role.Label switch
+ {
+ "user" => MessageRole.User,
+ "assistant" => MessageRole.Agent,
+ _ => throw new ArgumentOutOfRangeException(nameof(role), role, "Invalid author role")
+ };
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/Agent.cs b/dotnet/src/Agents/Abstractions/Agent.cs
index 5f21f83bb6c0..e35fbc5738a6 100644
--- a/dotnet/src/Agents/Abstractions/Agent.cs
+++ b/dotnet/src/Agents/Abstractions/Agent.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel.Arguments.Extensions;
@@ -66,6 +67,17 @@ public abstract class Agent
///
public Kernel Kernel { get; init; } = new();
+ ///
+ /// This option forces the agent to clone the original kernel instance during invocation if true. Default is false.
+ ///
+ ///
+ /// implementations that provide instances require the
+ /// kernel to be cloned during agent invocation, but cloning has the side affect of causing modifications to Kernel
+ /// Data by plugins to be lost. Cloning is therefore opt-in.
+ ///
+ [Experimental("SKEXP0130")]
+ public bool UseImmutableKernel { get; set; } = false;
+
///
/// Gets or sets a prompt template based on the agent instructions.
///
diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
index a942eae8ba73..1b252b84eddc 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIAgent.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -136,11 +137,22 @@ public async IAsyncEnumerable> InvokeAsync
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
- Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -228,11 +240,22 @@ public async IAsyncEnumerable> In
() => new AzureAIAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
- var kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await azureAIAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
diff --git a/dotnet/src/Agents/AzureAI/AzureAIChannel.cs b/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
index e1b57d4ad32b..4620cf05f756 100644
--- a/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
+++ b/dotnet/src/Agents/AzureAI/AzureAIChannel.cs
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using Azure;
using Azure.AI.Agents.Persistent;
using Microsoft.SemanticKernel.Agents.AzureAI.Internal;
using Microsoft.SemanticKernel.Agents.Extensions;
@@ -18,9 +20,22 @@ internal sealed class AzureAIChannel(PersistentAgentsClient client, string threa
///
protected override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken)
{
+ const string ErrorMessage = "The message could not be added to the thread due to an error response from the service.";
+
foreach (ChatMessageContent message in history)
{
- await AgentThreadActions.CreateMessageAsync(client, threadId, message, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await AgentThreadActions.CreateMessageAsync(client, threadId, message, cancellationToken).ConfigureAwait(false);
+ }
+ catch (RequestFailedException ex)
+ {
+ throw new AgentThreadOperationException(ErrorMessage, ex);
+ }
+ catch (AggregateException ex)
+ {
+ throw new AgentThreadOperationException(ErrorMessage, ex);
+ }
}
}
diff --git a/dotnet/src/Agents/AzureAI/Extensions/FoundryWorkflowExtensions.cs b/dotnet/src/Agents/AzureAI/Extensions/FoundryWorkflowExtensions.cs
deleted file mode 100644
index 4c02567c5a32..000000000000
--- a/dotnet/src/Agents/AzureAI/Extensions/FoundryWorkflowExtensions.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using Azure.AI.Agents.Persistent;
-using Azure.Core;
-
-namespace Microsoft.SemanticKernel.Agents.AzureAI;
-
-///
-/// Extensions for configuring the PersistentAgentsAdministrationClientOptions with a routing policy for Foundry Workflows.
-///
-public static class FoundryWorkflowExtensions
-{
- ///
- /// Adds a routing policy to the PersistentAgentsAdministrationClientOptions for Foundry Workflows.
- ///
- ///
- ///
- ///
- ///
- ///
- public static PersistentAgentsAdministrationClientOptions WithPolicy(this PersistentAgentsAdministrationClientOptions options, string endpoint, string apiVersion)
- {
- if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var _endpoint))
- {
- throw new ArgumentException("The endpoint must be an absolute URI.", nameof(endpoint));
- }
-
- options.AddPolicy(new HttpPipelineRoutingPolicy(_endpoint, apiVersion), HttpPipelinePosition.PerCall);
-
- return options;
- }
-}
diff --git a/dotnet/src/Agents/AzureAI/Internal/AgentMessageFactory.cs b/dotnet/src/Agents/AzureAI/Internal/AgentMessageFactory.cs
index fd63784909c2..bb916126cc32 100644
--- a/dotnet/src/Agents/AzureAI/Internal/AgentMessageFactory.cs
+++ b/dotnet/src/Agents/AzureAI/Internal/AgentMessageFactory.cs
@@ -62,7 +62,13 @@ public static IEnumerable GetMessageContent(ChatMessag
{
if (content is TextContent textContent)
{
- yield return new MessageInputTextBlock(content.ToString());
+ var text = content.ToString();
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ // Message content must be non-empty.
+ continue;
+ }
+ yield return new MessageInputTextBlock(text);
}
else if (content is ImageContent imageContent)
{
diff --git a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
index 7173ca219923..412642d88204 100644
--- a/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
+++ b/dotnet/src/Agents/AzureAI/Internal/AgentThreadActions.cs
@@ -65,10 +65,16 @@ public static async Task CreateMessageAsync(PersistentAgentsClient client, strin
return;
}
+ var contentBlocks = AgentMessageFactory.GetMessageContent(message);
+ if (!contentBlocks.Any())
+ {
+ return;
+ }
+
await client.Messages.CreateMessageAsync(
threadId,
role: message.Role == AuthorRole.User ? MessageRole.User : MessageRole.Agent,
- contentBlocks: AgentMessageFactory.GetMessageContent(message),
+ contentBlocks: contentBlocks,
attachments: AgentMessageFactory.GetAttachments(message),
metadata: AgentMessageFactory.GetMetadata(message),
cancellationToken).ConfigureAwait(false);
diff --git a/dotnet/src/Agents/Copilot/CopilotStudioAgent.cs b/dotnet/src/Agents/Copilot/CopilotStudioAgent.cs
index 57996b6e95a3..f9424e72a7b9 100644
--- a/dotnet/src/Agents/Copilot/CopilotStudioAgent.cs
+++ b/dotnet/src/Agents/Copilot/CopilotStudioAgent.cs
@@ -66,6 +66,12 @@ public override async IAsyncEnumerable> In
await foreach (ChatMessageContent result in invokeResults.ConfigureAwait(false))
{
await this.NotifyThreadOfNewMessage(agentThread, result, cancellationToken).ConfigureAwait(false);
+
+ if (options?.OnIntermediateMessage is not null)
+ {
+ await options.OnIntermediateMessage(result).ConfigureAwait(false);
+ }
+
yield return new(result, agentThread);
}
}
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 56c8712ab50f..307009fe5099 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -73,11 +73,22 @@ public override async IAsyncEnumerable> In
() => new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);
- Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -168,11 +179,22 @@ public override async IAsyncEnumerable new ChatHistoryAgentThread(),
cancellationToken).ConfigureAwait(false);
- Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await chatHistoryAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -391,6 +413,7 @@ private async IAsyncEnumerable InternalInvokeStream
this.Logger.LogAgentChatServiceInvokedStreamingAgent(nameof(InvokeAsync), this.Id, agentName, serviceType);
+ int messageIndex = messageCount;
AuthorRole? role = null;
StringBuilder builder = new();
await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false))
@@ -401,18 +424,18 @@ private async IAsyncEnumerable InternalInvokeStream
builder.Append(message.ToString());
- yield return message;
- }
+ // Capture mutated messages related function calling / tools
+ for (; messageIndex < chat.Count; messageIndex++)
+ {
+ ChatMessageContent chatMessage = chat[messageIndex];
- // Capture mutated messages related function calling / tools
- for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++)
- {
- ChatMessageContent message = chat[messageIndex];
+ chatMessage.AuthorName = this.Name;
- message.AuthorName = this.Name;
+ await onNewMessage(chatMessage).ConfigureAwait(false);
+ history.Add(chatMessage);
+ }
- await onNewMessage(message).ConfigureAwait(false);
- history.Add(message);
+ yield return message;
}
// Do not duplicate terminated function result to history
diff --git a/dotnet/src/Agents/Magentic/MagenticManagerActor.cs b/dotnet/src/Agents/Magentic/MagenticManagerActor.cs
index bb6cf93b1b20..881dfc365110 100644
--- a/dotnet/src/Agents/Magentic/MagenticManagerActor.cs
+++ b/dotnet/src/Agents/Magentic/MagenticManagerActor.cs
@@ -8,7 +8,6 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Agents.Orchestration;
-using Microsoft.SemanticKernel.Agents.Orchestration.GroupChat;
using Microsoft.SemanticKernel.Agents.Runtime;
using Microsoft.SemanticKernel.Agents.Runtime.Core;
using Microsoft.SemanticKernel.ChatCompletion;
diff --git a/dotnet/src/Agents/Magentic/MagenticMessages.cs b/dotnet/src/Agents/Magentic/MagenticMessages.cs
index 388b9305c69e..8ca25cb6ce5e 100644
--- a/dotnet/src/Agents/Magentic/MagenticMessages.cs
+++ b/dotnet/src/Agents/Magentic/MagenticMessages.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using Microsoft.SemanticKernel.ChatCompletion;
namespace Microsoft.SemanticKernel.Agents.Magentic;
@@ -76,4 +77,9 @@ public sealed class InputTask
/// Extension method to convert a to a message.
///
public static Result AsResultMessage(this ChatMessageContent message) => new() { Message = message };
+
+ ///
+ /// Extension method to convert a to a .
+ ///
+ public static Result AsResultMessage(this string text) => new() { Message = new(AuthorRole.Assistant, text) };
}
diff --git a/dotnet/src/Agents/OpenAI/Extensions/ChatContentMessageExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/ChatContentMessageExtensions.cs
index 3c6db7f5b894..e8f9b8cea96b 100644
--- a/dotnet/src/Agents/OpenAI/Extensions/ChatContentMessageExtensions.cs
+++ b/dotnet/src/Agents/OpenAI/Extensions/ChatContentMessageExtensions.cs
@@ -44,12 +44,12 @@ public static IEnumerable ToThreadInitializationMes
public static ResponseItem ToResponseItem(this ChatMessageContent message)
{
string content = message.Content ?? string.Empty;
- return message.Role.Label switch
+ return message.Role.Label.ToUpperInvariant() switch
{
- "system" => ResponseItem.CreateSystemMessageItem(content),
- "user" => ResponseItem.CreateUserMessageItem(content),
- "developer" => ResponseItem.CreateDeveloperMessageItem(content),
- "assistant" => ResponseItem.CreateAssistantMessageItem(content),
+ "SYSTEM" => ResponseItem.CreateSystemMessageItem(content),
+ "USER" => ResponseItem.CreateUserMessageItem(content),
+ "DEVELOPER" => ResponseItem.CreateDeveloperMessageItem(content),
+ "ASSISTANT" => ResponseItem.CreateAssistantMessageItem(content),
_ => throw new NotSupportedException($"Unsupported role {message.Role.Label}. Only system, user, developer or assistant roles are allowed."),
};
}
diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs
index d923f6bfb023..d65e0f940fff 100644
--- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs
+++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIResponseExtensions.cs
@@ -43,18 +43,25 @@ public static ChatMessageContent ToChatMessageContent(this OpenAIResponse respon
///
/// The response item to convert.
/// A instance.
- public static ChatMessageContent ToChatMessageContent(this ResponseItem item)
+ public static ChatMessageContent? ToChatMessageContent(this ResponseItem item)
{
if (item is MessageResponseItem messageResponseItem)
{
var role = messageResponseItem.Role.ToAuthorRole();
return new ChatMessageContent(role, item.ToChatMessageContentItemCollection(), innerContent: messageResponseItem);
}
+ else if (item is ReasoningResponseItem reasoningResponseItem)
+ {
+ if (reasoningResponseItem.SummaryTextParts is not null && reasoningResponseItem.SummaryTextParts.Count > 0)
+ {
+ return new ChatMessageContent(AuthorRole.Assistant, item.ToChatMessageContentItemCollection(), innerContent: reasoningResponseItem);
+ }
+ }
else if (item is FunctionCallResponseItem functionCallResponseItem)
{
return new ChatMessageContent(AuthorRole.Assistant, item.ToChatMessageContentItemCollection(), innerContent: functionCallResponseItem);
}
- throw new NotSupportedException($"Unsupported response item: {item.GetType()}");
+ return null;
}
///
@@ -68,6 +75,10 @@ public static ChatMessageContentItemCollection ToChatMessageContentItemCollectio
{
return messageResponseItem.Content.ToChatMessageContentItemCollection();
}
+ else if (item is ReasoningResponseItem reasoningResponseItem)
+ {
+ return reasoningResponseItem.SummaryTextParts.ToChatMessageContentItemCollection();
+ }
else if (item is FunctionCallResponseItem functionCallResponseItem)
{
Exception? exception = null;
@@ -92,7 +103,7 @@ public static ChatMessageContentItemCollection ToChatMessageContentItemCollectio
};
return [functionCallContent];
}
- throw new NotImplementedException($"Unsupported response item: {item.GetType()}");
+ return [];
}
///
@@ -183,6 +194,16 @@ private static ChatMessageContentItemCollection ToChatMessageContentItemCollecti
}
return collection;
}
+
+ private static ChatMessageContentItemCollection ToChatMessageContentItemCollection(this IReadOnlyList texts)
+ {
+ var collection = new ChatMessageContentItemCollection();
+ foreach (var text in texts)
+ {
+ collection.Add(new TextContent(text, innerContent: null));
+ }
+ return collection;
+ }
#endregion
}
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs
index fadccba9cd93..008e781fafa8 100644
--- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs
@@ -45,7 +45,13 @@ public static IEnumerable GetMessageContents(ChatMessageContent
{
if (content is TextContent textContent)
{
- yield return MessageContent.FromText(content.ToString());
+ var text = content.ToString();
+ if (string.IsNullOrWhiteSpace(text))
+ {
+ // Message content must be non-empty.
+ continue;
+ }
+ yield return MessageContent.FromText(text);
}
else if (content is ImageContent imageContent)
{
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 1971b8bc9058..11f813fd3267 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -138,11 +139,22 @@ public async IAsyncEnumerable> InvokeAsync
AdditionalInstructions = options?.AdditionalInstructions,
});
- Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await openAIAssistantAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -226,11 +238,22 @@ public async IAsyncEnumerable> In
() => new OpenAIAssistantAgentThread(this.Client),
cancellationToken).ConfigureAwait(false);
- Kernel kernel = (options?.Kernel ?? this.Kernel).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
// Get the context contributions from the AIContextProviders.
-#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AIContext providersContext = await openAIAssistantAgentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
index bdb5de57a121..7f5a5194092d 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
@@ -1,4 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.ClientModel;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -23,9 +25,22 @@ internal sealed class OpenAIAssistantChannel(AssistantClient client, string thre
///
protected override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken)
{
+ const string ErrorMessage = "The message could not be added to the thread due to an error response from the service.";
+
foreach (ChatMessageContent message in history)
{
- await AssistantThreadActions.CreateMessageAsync(this._client, this._threadId, message, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await AssistantThreadActions.CreateMessageAsync(this._client, this._threadId, message, cancellationToken).ConfigureAwait(false);
+ }
+ catch (ClientResultException ex)
+ {
+ throw new AgentThreadOperationException(ErrorMessage, ex);
+ }
+ catch (AggregateException ex)
+ {
+ throw new AgentThreadOperationException(ErrorMessage, ex);
+ }
}
}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs
index 87b0912d01ef..0e1f84d610df 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgent.cs
@@ -37,7 +37,7 @@ public OpenAIResponseAgent(OpenAIResponseClient client)
///
/// Storing of messages is enabled.
///
- public bool StoreEnabled { get; init; } = true;
+ public bool StoreEnabled { get; init; } = false;
///
public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -47,7 +47,7 @@ public override async IAsyncEnumerable> In
AgentThread agentThread = await this.EnsureThreadExistsWithMessagesAsync(messages, thread, cancellationToken).ConfigureAwait(false);
// Get the context contributions from the AIContextProviders.
- OpenAIAssistantAgentInvokeOptions extensionsContextOptions = await this.FinalizeInvokeOptionsAsync(messages, options, agentThread, cancellationToken).ConfigureAwait(false);
+ OpenAIResponseAgentInvokeOptions extensionsContextOptions = await this.FinalizeInvokeOptionsAsync(messages, options, agentThread, cancellationToken).ConfigureAwait(false);
// Invoke responses with the updated chat history.
ChatHistory chatHistory = [.. messages];
@@ -74,7 +74,7 @@ public override async IAsyncEnumerable EnsureThreadExistsWithMessagesAsync(ICollection<
return await this.EnsureThreadExistsWithMessagesAsync(messages, thread, () => new ChatHistoryAgentThread(), cancellationToken).ConfigureAwait(false);
}
- private async Task FinalizeInvokeOptionsAsync(ICollection messages, AgentInvokeOptions? options, AgentThread agentThread, CancellationToken cancellationToken)
+ private async Task FinalizeInvokeOptionsAsync(ICollection messages, AgentInvokeOptions? options, AgentThread agentThread, CancellationToken cancellationToken)
{
- Kernel kernel = this.GetKernel(options).Clone();
+ Kernel kernel = this.GetKernel(options);
+#pragma warning disable SKEXP0110, SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ if (this.UseImmutableKernel)
+ {
+ kernel = kernel.Clone();
+ }
-#pragma warning disable SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ // Get the AIContextProviders contributions to the kernel.
AIContext providersContext = await agentThread.AIContextProviders.ModelInvokingAsync(messages, cancellationToken).ConfigureAwait(false);
+
+ // Check for compatibility AIContextProviders and the UseImmutableKernel setting.
+ if (providersContext.AIFunctions is { Count: > 0 } && !this.UseImmutableKernel)
+ {
+ throw new InvalidOperationException("AIContextProviders with AIFunctions are not supported when Agent UseImmutableKernel setting is false.");
+ }
+
kernel.Plugins.AddFromAIContext(providersContext, "Tools");
#pragma warning restore SKEXP0130 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
string mergedAdditionalInstructions = FormatAdditionalInstructions(providersContext, options);
- OpenAIAssistantAgentInvokeOptions extensionsContextOptions =
+ OpenAIResponseAgentInvokeOptions extensionsContextOptions =
options is null ?
new()
{
diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentInvokeOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentInvokeOptions.cs
index e11f55c41463..ec84f87cc042 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentInvokeOptions.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentInvokeOptions.cs
@@ -24,6 +24,11 @@ public OpenAIResponseAgentInvokeOptions(AgentInvokeOptions options)
: base(options)
{
Verify.NotNull(options);
+
+ if (options is OpenAIResponseAgentInvokeOptions responseAgentInvokeOptions)
+ {
+ this.ResponseCreationOptions = responseAgentInvokeOptions.ResponseCreationOptions;
+ }
}
///
@@ -34,6 +39,8 @@ public OpenAIResponseAgentInvokeOptions(OpenAIResponseAgentInvokeOptions options
: base(options)
{
Verify.NotNull(options);
+
+ this.ResponseCreationOptions = options.ResponseCreationOptions;
}
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs
index b4f424c1001b..3bbaa97d13dd 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentThread.cs
@@ -111,7 +111,11 @@ public async IAsyncEnumerable GetMessagesAsync([EnumeratorCa
var collectionResult = this._client.GetResponseInputItemsAsync(this.ResponseId, default, cancellationToken).ConfigureAwait(false);
await foreach (var responseItem in collectionResult)
{
- yield return responseItem.ToChatMessageContent();
+ var messageContent = responseItem.ToChatMessageContent();
+ if (messageContent is not null)
+ {
+ yield return messageContent;
+ }
}
}
}
diff --git a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs
index aaf084b700c9..cd6a6f73f415 100644
--- a/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs
+++ b/dotnet/src/Agents/Orchestration/GroupChat/GroupChatMessages.cs
@@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Agents.Orchestration.GroupChat;
///
/// Common messages used for agent chat patterns.
///
-public static class GroupChatMessages
+internal static class GroupChatMessages
{
///
/// An empty message instance as a default.
@@ -79,7 +79,7 @@ public sealed class InputTask
public static InputTask AsInputTaskMessage(this IEnumerable messages) => new() { Messages = messages };
///
- /// Extension method to convert a to a .
+ /// Extension method to convert a to a .
///
public static Result AsResultMessage(this string text) => new() { Message = new(AuthorRole.Assistant, text) };
}
diff --git a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs
index c81a80ba1d86..c73070ff6763 100644
--- a/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs
+++ b/dotnet/src/Agents/Runtime/InProcess.Tests/PublishMessageTests.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
@@ -56,10 +55,7 @@ public async Task Test_PublishMessage_MultipleFailures()
Func publishTask = async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" });
// What we are really testing here is that a single exception does not prevent sending to the remaining agents
- (await publishTask.Should().ThrowAsync())
- .Which.Should().Match(
- exception => exception.InnerExceptions.Count == 2 &&
- exception.InnerExceptions.All(exception => exception is TestException));
+ await publishTask.Should().ThrowAsync();
fixture.GetAgentInstances().Values
.Should().HaveCount(2)
@@ -81,11 +77,7 @@ public async Task Test_PublishMessage_MixedSuccessFailure()
Func publicTask = async () => await fixture.RunPublishTestAsync(new TopicId("TestTopic"), new BasicMessage { Content = "1" });
// What we are really testing here is that raising exceptions does not prevent sending to the remaining agents
- (await publicTask.Should().ThrowAsync())
- .Which.Should().Match(
- exception => exception.InnerExceptions.Count == 2 &&
- exception.InnerExceptions.All(
- exception => exception is TestException));
+ await publicTask.Should().ThrowAsync();
fixture.GetAgentInstances().Values
.Should().HaveCount(2, "Two ReceiverAgents should have been created")
diff --git a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs
index 93e3ec89144b..2ab31ab3f9ab 100644
--- a/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs
+++ b/dotnet/src/Agents/Runtime/InProcess/InProcessRuntime.cs
@@ -352,45 +352,41 @@ private async ValueTask PublishMessageServicerAsync(MessageEnvelope envelope, Ca
throw new InvalidOperationException("Message must have a topic to be published.");
}
- List exceptions = [];
+ List? tasks = null;
TopicId topic = envelope.Topic.Value;
foreach (ISubscriptionDefinition subscription in this._subscriptions.Values.Where(subscription => subscription.Matches(topic)))
{
- try
- {
- deliveryToken.ThrowIfCancellationRequested();
+ (tasks ??= []).Add(ProcessSubscriptionAsync(envelope, topic, subscription, deliveryToken));
+ }
- AgentId? sender = envelope.Sender;
+ if (tasks is not null)
+ {
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
- using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken);
- MessageContext messageContext = new(envelope.MessageId, combinedSource.Token)
- {
- Sender = sender,
- Topic = topic,
- IsRpc = false
- };
+ async Task ProcessSubscriptionAsync(MessageEnvelope envelope, TopicId topic, ISubscriptionDefinition subscription, CancellationToken deliveryToken)
+ {
+ deliveryToken.ThrowIfCancellationRequested();
- AgentId agentId = subscription.MapToAgent(topic);
- if (!this.DeliverToSelf && sender.HasValue && sender == agentId)
- {
- continue;
- }
+ AgentId? sender = envelope.Sender;
- IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
+ using CancellationTokenSource combinedSource = CancellationTokenSource.CreateLinkedTokenSource(envelope.Cancellation, deliveryToken);
+ MessageContext messageContext = new(envelope.MessageId, combinedSource.Token)
+ {
+ Sender = sender,
+ Topic = topic,
+ IsRpc = false
+ };
- // TODO: Cancellation propagation!
- await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false);
- }
- catch (Exception ex) when (!ex.IsCriticalException())
+ AgentId agentId = subscription.MapToAgent(topic);
+ if (!this.DeliverToSelf && sender.HasValue && sender == agentId)
{
- exceptions.Add(ex);
+ return;
}
- }
- if (exceptions.Count > 0)
- {
- // TODO: Unwrap TargetInvocationException?
- throw new AggregateException("One or more exceptions occurred while processing the message.", exceptions);
+ IHostableAgent agent = await this.EnsureAgentAsync(agentId).ConfigureAwait(false);
+
+ await agent.OnMessageAsync(envelope.Message, messageContext).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs
new file mode 100644
index 000000000000..3c18d0b594dc
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/A2A/A2AAgentTests.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.A2A;
+
+///
+/// Tests for the class.
+///
+public sealed class A2AAgentTests : BaseA2AClientTest
+{
+ ///
+ /// Tests that the constructor verifies parameters and throws when necessary.
+ ///
+ [Fact]
+ public void ConstructorShouldVerifyParams()
+ {
+ using var httpClient = new HttpClient();
+
+ // Arrange & Act & Assert
+ Assert.Throws(() => new A2AAgent(null!, new()));
+ Assert.Throws(() => new A2AAgent(new(httpClient), null!));
+ }
+
+ [Fact]
+ public void VerifyConstructor()
+ {
+ // Arrange & Act
+ var agent = new A2AAgent(this.Client, this.CreateAgentCard());
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.Equal("InvoiceAgent", agent.Name);
+ Assert.Equal("Handles requests relating to invoices.", agent.Description);
+ }
+
+ [Fact]
+ public async Task VerifyInvokeAsync()
+ {
+ // Arrange
+ this.MessageHandlerStub.ResponsesToReturn.Add(
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") }
+ );
+ var agent = new A2AAgent(this.Client, this.CreateAgentCard());
+
+ // Act
+ var responseItems = agent.InvokeAsync("List the latest invoices for Contoso?");
+
+ // Assert
+ Assert.NotNull(responseItems);
+ var items = await responseItems!.ToListAsync>();
+ Assert.Single(items);
+ Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content);
+ }
+
+ [Fact]
+ public async Task VerifyInvokeStreamingAsync()
+ {
+ // Arrange
+ this.MessageHandlerStub.ResponsesToReturn.Add(
+ new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(InvokeResponse, Encoding.UTF8, "application/json") }
+ );
+ var agent = new A2AAgent(this.Client, this.CreateAgentCard());
+
+ // Act
+ var responseItems = agent.InvokeStreamingAsync("List the latest invoices for Contoso?");
+
+ // Assert
+ Assert.NotNull(responseItems);
+ var items = await responseItems!.ToListAsync>();
+ Assert.Single(items);
+ Assert.StartsWith("Here are the latest invoices for Contoso:", items[0].Message.Content);
+ }
+
+ #region private
+ private const string InvokeResponse =
+ """
+ {"jsonrpc":"2.0","id":"ce7a5ef6-1078-4b6e-ad35-a8bfa6743c5d","result":{"kind":"task","id":"8d328159-ca63-4ce8-b416-4bcf69f9e119","contextId":"496a4a95-392b-4c04-a517-9a043b3f7565","status":{"state":"completed","timestamp":"2025-06-20T09:42:49.4013958Z"},"artifacts":[{"artifactId":"","parts":[{"kind":"text","text":"Here are the latest invoices for Contoso:\n\n1. Invoice ID: INV789, Date: 2025-06-18\n Products: T-Shirts (150 units at $10.00), Hats (200 units at $15.00), Glasses (300 units at $5.00)\n\n2. Invoice ID: INV666, Date: 2025-06-15\n Products: T-Shirts (2500 units at $8.00), Hats (1200 units at $10.00), Glasses (1000 units at $6.00)\n\n3. Invoice ID: INV999, Date: 2025-05-17\n Products: T-Shirts (1400 units at $10.50), Hats (1100 units at $9.00), Glasses (950 units at $12.00)\n\n4. Invoice ID: INV333, Date: 2025-05-13\n Products: T-Shirts (400 units at $11.00), Hats (600 units at $15.00), Glasses (700 units at $5.00)\n\nIf you need more details on any specific invoice, please let me know!"}]}],"history":[{"role":"user","parts":[{"kind":"text","text":"List the latest invoices for Contoso?"}],"messageId":"80a26c0f-2262-4d0f-8e7d-51ac4046173b"}]}}
+ """;
+ #endregion
+}
diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs
new file mode 100644
index 000000000000..b7f838d2764a
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/A2A/A2AHostAgentTests.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.A2A;
+using Microsoft.SemanticKernel.ChatCompletion;
+using SharpA2A.Core;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.A2A;
+
+///
+/// Tests for the class.
+///
+public sealed class A2AHostAgentTests : BaseA2AClientTest
+{
+ ///
+ /// Tests that the constructor verifies parameters and throws when necessary.
+ ///
+ [Fact]
+ public void ConstructorShouldVerifyParams()
+ {
+ // Arrange & Act & Assert
+ Assert.Throws(() => new A2AHostAgent(null!, this.CreateAgentCard()));
+ Assert.Throws(() => new A2AHostAgent(new MockAgent(), null!));
+ }
+
+ [Fact]
+ public async Task VerifyExecuteAgentTaskAsync()
+ {
+ // Arrange
+ var agent = new MockAgent();
+ var taskManager = new TaskManager();
+ var hostAgent = new A2AHostAgent(agent, this.CreateAgentCard(), taskManager);
+
+ // Act
+ var agentTask = await taskManager.CreateTaskAsync();
+ agentTask.History = this.CreateUserMessages(["Hello"]);
+ await hostAgent.ExecuteAgentTaskAsync(agentTask);
+
+ // Assert
+ Assert.NotNull(agentTask);
+ Assert.NotNull(agentTask.Artifacts);
+ Assert.Single(agentTask.Artifacts);
+ Assert.NotNull(agentTask.Artifacts[0].Parts);
+ Assert.Single(agentTask.Artifacts[0].Parts);
+ Assert.Equal("Mock Response", agentTask.Artifacts[0].Parts[0].AsTextPart().Text);
+ }
+
+ #region private
+ private List CreateUserMessages(string[] userMessages)
+ {
+ var messages = new List();
+
+ foreach (var userMessage in userMessages)
+ {
+ messages.Add(new Message()
+ {
+ Role = MessageRole.User,
+ Parts = [new TextPart() { Text = userMessage }],
+ });
+ }
+
+ return messages;
+ }
+ #endregion
+}
+
+internal sealed class MockAgent : Agent
+{
+ public override async IAsyncEnumerable> InvokeAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(100, cancellationToken);
+
+ yield return new AgentResponseItem(new ChatMessageContent(AuthorRole.Assistant, "Mock Response"), thread ?? new MockAgentThread());
+ }
+
+ public override async IAsyncEnumerable> InvokeStreamingAsync(ICollection messages, AgentThread? thread = null, AgentInvokeOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await Task.Delay(100, cancellationToken);
+
+ yield return new AgentResponseItem(new StreamingChatMessageContent(AuthorRole.Assistant, "Mock Streaming Response"), thread ?? new MockAgentThread());
+ }
+
+ protected internal override Task CreateChannelAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected internal override IEnumerable GetChannelKeys()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected internal override Task RestoreChannelAsync(string channelState, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+internal sealed class MockAgentThread : AgentThread
+{
+ protected override Task CreateInternalAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override Task DeleteInternalAsync(CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override Task OnNewMessageInternalAsync(ChatMessageContent newMessage, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs
new file mode 100644
index 000000000000..52fb0620c475
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/A2A/BaseA2AClientTest.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net.Http;
+using SharpA2A.Core;
+
+namespace SemanticKernel.Agents.UnitTests.A2A;
+public class BaseA2AClientTest : IDisposable
+{
+ internal MultipleHttpMessageHandlerStub MessageHandlerStub { get; }
+ internal HttpClient HttpClient { get; }
+ internal A2AClient Client { get; }
+
+ internal BaseA2AClientTest()
+ {
+ this.MessageHandlerStub = new MultipleHttpMessageHandlerStub();
+ this.HttpClient = new HttpClient(this.MessageHandlerStub, disposeHandler: false)
+ {
+ BaseAddress = new Uri("http://127.0.0.1/")
+ };
+ this.Client = new A2AClient(this.HttpClient);
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.MessageHandlerStub.Dispose();
+ this.HttpClient.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
+
+ protected AgentCard CreateAgentCard()
+ {
+ var capabilities = new AgentCapabilities()
+ {
+ Streaming = false,
+ PushNotifications = false,
+ };
+
+ var invoiceQuery = new AgentSkill()
+ {
+ Id = "id_invoice_agent",
+ Name = "InvoiceQuery",
+ Description = "Handles requests relating to invoices.",
+ Tags = ["invoice", "semantic-kernel"],
+ Examples =
+ [
+ "List the latest invoices for Contoso.",
+ ],
+ };
+
+ return new AgentCard()
+ {
+ Name = "InvoiceAgent",
+ Description = "Handles requests relating to invoices.",
+ Url = "http://127.0.0.1/5000",
+ Version = "1.0.0",
+ DefaultInputModes = ["text"],
+ DefaultOutputModes = ["text"],
+ Capabilities = capabilities,
+ Skills = [invoiceQuery],
+ };
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index 8752623526da..e0a5d2938e1e 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -19,11 +19,11 @@
-
+
-
-
+
+
@@ -39,6 +39,7 @@
+
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
index 1f7d2d1e0fb2..def132c60102 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
@@ -176,6 +176,46 @@ public async Task VerifyChatCompletionAgentInvocationAsync()
Times.Once);
}
+ ///
+ /// Verify the invocation and response of .
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentInvocationsCanMutateProvidedKernelAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
+
+ var kernel = CreateKernel(mockService.Object);
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = kernel,
+ Arguments = [],
+ };
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeAsync(Array.Empty() as ICollection).ToArrayAsync();
+
+ // Assert
+ Assert.Single(result);
+
+ mockService.Verify(
+ x =>
+ x.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ kernel, // Use the same kernel instance
+ It.IsAny()),
+ Times.Once);
+ }
+
///
/// Verify the invocation and response of using .
///
@@ -195,7 +235,7 @@ public async Task VerifyChatClientAgentInvocationAsync()
{
Instructions = "test instructions",
Kernel = CreateKernel(mockService.Object),
- Arguments = [],
+ Arguments = new(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
// Act
@@ -208,7 +248,7 @@ public async Task VerifyChatClientAgentInvocationAsync()
x =>
x.GetResponseAsync(
It.IsAny>(),
- It.IsAny(),
+ It.Is(o => GetKernelFromChatOptions(o) == agent.Kernel),
It.IsAny()),
Times.Once);
}
@@ -258,6 +298,52 @@ public async Task VerifyChatCompletionAgentStreamingAsync()
Times.Once);
}
+ ///
+ /// Verify the streaming invocation and response of .
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingCanMutateProvidedKernelAsync()
+ {
+ // Arrange
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).Returns(returnContent.ToAsyncEnumerable());
+
+ var kernel = CreateKernel(mockService.Object);
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = kernel,
+ Arguments = [],
+ };
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeStreamingAsync(Array.Empty() as ICollection).ToArrayAsync();
+
+ // Assert
+ Assert.Equal(2, result.Length);
+
+ mockService.Verify(
+ x =>
+ x.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ kernel, // Use the same kernel instance
+ It.IsAny()),
+ Times.Once);
+ }
+
///
/// Verify the streaming invocation and response of using .
///
@@ -283,7 +369,7 @@ public async Task VerifyChatClientAgentStreamingAsync()
{
Instructions = "test instructions",
Kernel = CreateKernel(mockService.Object),
- Arguments = [],
+ Arguments = new(new PromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }),
};
// Act
@@ -296,7 +382,7 @@ public async Task VerifyChatClientAgentStreamingAsync()
x =>
x.GetStreamingResponseAsync(
It.IsAny>(),
- It.IsAny(),
+ It.Is(o => GetKernelFromChatOptions(o) == agent.Kernel),
It.IsAny()),
Times.Once);
}
@@ -373,6 +459,414 @@ public void VerifyChatCompletionChannelKeys()
Assert.NotEqual(agent3.GetChannelKeys(), agent5.GetChannelKeys());
}
+ ///
+ /// Verify that InvalidOperationException is thrown when UseImmutableKernel is false and AIFunctions exist.
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentThrowsWhenUseImmutableKernelFalseWithAIFunctionsAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ Arguments = [],
+ UseImmutableKernel = false // Explicitly set to false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ async () => await agent.InvokeAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync());
+
+ Assert.NotNull(exception);
+ }
+
+ ///
+ /// Verify that InvalidOperationException is thrown when UseImmutableKernel is default (false) and AIFunctions exist.
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentThrowsWhenUseImmutableKernelDefaultWithAIFunctionsAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ Arguments = []
+ // UseImmutableKernel not set, should default to false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ async () => await agent.InvokeAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync());
+
+ Assert.NotNull(exception);
+ }
+
+ ///
+ /// Verify that kernel remains immutable when UseImmutableKernel is true.
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentKernelImmutabilityWhenUseImmutableKernelTrueAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ Kernel capturedKernel = null!;
+ mockService.Setup(
+ s => s.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback((_, _, kernel, _) => capturedKernel = kernel)
+ .ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
+
+ var originalKernel = CreateKernel(mockService.Object);
+ var originalPluginCount = originalKernel.Plugins.Count;
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = originalKernel,
+ Arguments = [],
+ UseImmutableKernel = true
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync();
+
+ // Assert
+ Assert.Single(result);
+
+ // Verify original kernel was not modified
+ Assert.Equal(originalPluginCount, originalKernel.Plugins.Count);
+
+ // Verify a different kernel instance was used for the service call
+ Assert.NotSame(originalKernel, capturedKernel);
+
+ // Verify the captured kernel has the additional plugin from AIContext
+ Assert.True(capturedKernel.Plugins.Count > originalPluginCount);
+ Assert.Contains(capturedKernel.Plugins, p => p.Name == "Tools");
+ }
+
+ ///
+ /// Verify that mutable kernel behavior works when UseImmutableKernel is false and no AIFunctions exist.
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentMutableKernelWhenUseImmutableKernelFalseNoAIFunctionsAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ Kernel capturedKernel = null!;
+ mockService.Setup(
+ s => s.GetChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback((_, _, kernel, _) => capturedKernel = kernel)
+ .ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
+
+ var originalKernel = CreateKernel(mockService.Object);
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [] // Empty AIFunctions list
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = originalKernel,
+ Arguments = [],
+ UseImmutableKernel = false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync();
+
+ // Assert
+ Assert.Single(result);
+
+ // Verify the same kernel instance was used (mutable behavior)
+ Assert.Same(originalKernel, capturedKernel);
+ }
+
+ ///
+ /// Verify that InvalidOperationException is thrown when UseImmutableKernel is false and AIFunctions exist (streaming).
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingThrowsWhenUseImmutableKernelFalseWithAIFunctionsAsync()
+ {
+ // Arrange
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).Returns(returnContent.ToAsyncEnumerable());
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ Arguments = [],
+ UseImmutableKernel = false // Explicitly set to false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ async () => await agent.InvokeStreamingAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync());
+
+ Assert.NotNull(exception);
+ }
+
+ ///
+ /// Verify that InvalidOperationException is thrown when UseImmutableKernel is default (false) and AIFunctions exist (streaming).
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingThrowsWhenUseImmutableKernelDefaultWithAIFunctionsAsync()
+ {
+ // Arrange
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ Mock mockService = new();
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny())).Returns(returnContent.ToAsyncEnumerable());
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = CreateKernel(mockService.Object),
+ Arguments = []
+ // UseImmutableKernel not set, should default to false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(
+ async () => await agent.InvokeStreamingAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync());
+
+ Assert.NotNull(exception);
+ }
+
+ ///
+ /// Verify that kernel remains immutable when UseImmutableKernel is true (streaming).
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingKernelImmutabilityWhenUseImmutableKernelTrueAsync()
+ {
+ // Arrange
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ Mock mockService = new();
+ Kernel capturedKernel = null!;
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback((_, _, kernel, _) => capturedKernel = kernel)
+ .Returns(returnContent.ToAsyncEnumerable());
+
+ var originalKernel = CreateKernel(mockService.Object);
+ var originalPluginCount = originalKernel.Plugins.Count;
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [new TestAIFunction("TestFunction", "Test function description")]
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = originalKernel,
+ Arguments = [],
+ UseImmutableKernel = true
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeStreamingAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync();
+
+ // Assert
+ Assert.Equal(2, result.Length);
+
+ // Verify original kernel was not modified
+ Assert.Equal(originalPluginCount, originalKernel.Plugins.Count);
+
+ // Verify a different kernel instance was used for the service call
+ Assert.NotSame(originalKernel, capturedKernel);
+
+ // Verify the captured kernel has the additional plugin from AIContext
+ Assert.True(capturedKernel.Plugins.Count > originalPluginCount);
+ Assert.Contains(capturedKernel.Plugins, p => p.Name == "Tools");
+ }
+
+ ///
+ /// Verify that mutable kernel behavior works when UseImmutableKernel is false and no AIFunctions exist (streaming).
+ ///
+ [Fact]
+ public async Task VerifyChatCompletionAgentStreamingMutableKernelWhenUseImmutableKernelFalseNoAIFunctionsAsync()
+ {
+ // Arrange
+ StreamingChatMessageContent[] returnContent =
+ [
+ new(AuthorRole.Assistant, "wh"),
+ new(AuthorRole.Assistant, "at?"),
+ ];
+
+ Mock mockService = new();
+ Kernel capturedKernel = null!;
+ mockService.Setup(
+ s => s.GetStreamingChatMessageContentsAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback((_, _, kernel, _) => capturedKernel = kernel)
+ .Returns(returnContent.ToAsyncEnumerable());
+
+ var originalKernel = CreateKernel(mockService.Object);
+
+ var mockAIContextProvider = new Mock();
+ var aiContext = new AIContext
+ {
+ AIFunctions = [] // Empty AIFunctions list
+ };
+ mockAIContextProvider.Setup(p => p.ModelInvokingAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync(aiContext);
+
+ ChatCompletionAgent agent =
+ new()
+ {
+ Instructions = "test instructions",
+ Kernel = originalKernel,
+ Arguments = [],
+ UseImmutableKernel = false
+ };
+
+ var thread = new ChatHistoryAgentThread();
+ thread.AIContextProviders.Add(mockAIContextProvider.Object);
+
+ // Act
+ AgentResponseItem[] result = await agent.InvokeStreamingAsync(Array.Empty() as ICollection, thread: thread).ToArrayAsync();
+
+ // Assert
+ Assert.Equal(2, result.Length);
+
+ // Verify the same kernel instance was used (mutable behavior)
+ Assert.Same(originalKernel, capturedKernel);
+ }
+
private static Kernel CreateKernel(IChatCompletionService chatCompletionService)
{
var builder = Kernel.CreateBuilder();
@@ -386,4 +880,46 @@ private static Kernel CreateKernel(IChatClient chatClient)
builder.Services.AddSingleton(chatClient);
return builder.Build();
}
+
+ ///
+ /// Gets the Kernel property from ChatOptions using reflection.
+ ///
+ /// The ChatOptions instance to extract Kernel from.
+ /// The Kernel instance if found; otherwise, null.
+ private static Kernel? GetKernelFromChatOptions(ChatOptions options)
+ {
+ // Use reflection to try to get the Kernel property
+ var kernelProperty = options.GetType().GetProperty("Kernel",
+ System.Reflection.BindingFlags.Public |
+ System.Reflection.BindingFlags.NonPublic |
+ System.Reflection.BindingFlags.Instance);
+
+ if (kernelProperty != null)
+ {
+ return kernelProperty.GetValue(options) as Kernel;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Helper class for testing AIFunction behavior.
+ ///
+ private sealed class TestAIFunction : AIFunction
+ {
+ public TestAIFunction(string name, string description = "")
+ {
+ this.Name = name;
+ this.Description = description;
+ }
+
+ public override string Name { get; }
+
+ public override string Description { get; }
+
+ protected override ValueTask