diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 76bb693a61b2..46a322eda95f 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -441,6 +441,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kernel-functions-generator"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agents.AzureAI", "src\Agents\AzureAI\Agents.AzureAI.csproj", "{EA35F1B5-9148-4189-BE34-5E00AED56D65}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugins.AI", "src\Plugins\Plugins.AI\Plugins.AI.csproj", "{0C64EC81-8116-4388-87AD-BA14D4B59974}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugins.AI.UnitTests", "src\Plugins\Plugins.AI.UnitTests\Plugins.AI.UnitTests.csproj", "{03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1180,6 +1184,18 @@ Global
{EA35F1B5-9148-4189-BE34-5E00AED56D65}.Publish|Any CPU.Build.0 = Publish|Any CPU
{EA35F1B5-9148-4189-BE34-5E00AED56D65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA35F1B5-9148-4189-BE34-5E00AED56D65}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Publish|Any CPU.Build.0 = Publish|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0C64EC81-8116-4388-87AD-BA14D4B59974}.Release|Any CPU.Build.0 = Release|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1342,6 +1358,8 @@ Global
{2EB6E4C2-606D-B638-2E08-49EA2061C428} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{78785CB1-66CF-4895-D7E5-A440DD84BE86} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{EA35F1B5-9148-4189-BE34-5E00AED56D65} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
+ {0C64EC81-8116-4388-87AD-BA14D4B59974} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
+ {03ACF9DD-00C9-4F2B-80F1-537E2151AF5F} = {D6D598DF-C17C-46F4-B2B9-CDE82E2DE132}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index 82e20bd7d68e..17335b615a19 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -83,6 +83,7 @@
+
diff --git a/dotnet/samples/Concepts/Plugins/CrewAI_Plugin.cs b/dotnet/samples/Concepts/Plugins/CrewAI_Plugin.cs
new file mode 100644
index 000000000000..cf0de1188055
--- /dev/null
+++ b/dotnet/samples/Concepts/Plugins/CrewAI_Plugin.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.AI.CrewAI;
+
+namespace Plugins;
+
+///
+/// This example shows how to interact with an existing CrewAI Enterprise Crew directly or as a plugin.
+/// These examples require a valid CrewAI Enterprise deployment with an endpoint, auth token, and known inputs.
+///
+public class CrewAI_Plugin(ITestOutputHelper output) : BaseTest(output)
+{
+ ///
+ /// Shows how to kickoff an existing CrewAI Enterprise Crew and wait for it to complete.
+ ///
+ [Fact]
+ public async Task UsingCrewAIEnterpriseAsync()
+ {
+ string crewAIEndpoint = TestConfiguration.CrewAI.Endpoint;
+ string crewAIAuthToken = TestConfiguration.CrewAI.AuthToken;
+
+ var crew = new CrewAIEnterprise(
+ endpoint: new Uri(crewAIEndpoint),
+ authTokenProvider: async () => crewAIAuthToken);
+
+ // The required inputs for the Crew must be known in advance. This example is modeled after the
+ // Enterprise Content Marketing Crew Template and requires the following inputs:
+ var inputs = new
+ {
+ company = "CrewAI",
+ topic = "Agentic products for consumers",
+ };
+
+ // Invoke directly with our inputs
+ var kickoffId = await crew.KickoffAsync(inputs);
+ Console.WriteLine($"CrewAI Enterprise Crew kicked off with ID: {kickoffId}");
+
+ // Wait for completion
+ var result = await crew.WaitForCrewCompletionAsync(kickoffId);
+ Console.WriteLine("CrewAI Enterprise Crew completed with the following result:");
+ Console.WriteLine(result);
+ }
+
+ ///
+ /// Shows how to kickoff an existing CrewAI Enterprise Crew as a plugin.
+ ///
+ [Fact]
+ public async Task UsingCrewAIEnterpriseAsPluginAsync()
+ {
+ string crewAIEndpoint = TestConfiguration.CrewAI.Endpoint;
+ string crewAIAuthToken = TestConfiguration.CrewAI.AuthToken;
+ string openAIModelId = TestConfiguration.OpenAI.ChatModelId;
+ string openAIApiKey = TestConfiguration.OpenAI.ApiKey;
+
+ if (openAIModelId is null || openAIApiKey is null)
+ {
+ Console.WriteLine("OpenAI credentials not found. Skipping example.");
+ return;
+ }
+
+ // Setup the Kernel and AI Services
+ Kernel kernel = Kernel.CreateBuilder()
+ .AddOpenAIChatCompletion(
+ modelId: openAIModelId,
+ apiKey: openAIApiKey)
+ .Build();
+
+ var crew = new CrewAIEnterprise(
+ endpoint: new Uri(crewAIEndpoint),
+ authTokenProvider: async () => crewAIAuthToken);
+
+ // The required inputs for the Crew must be known in advance. This example is modeled after the
+ // Enterprise Content Marketing Crew Template and requires string inputs for the company and topic.
+ // We need to describe the type and purpose of each input to allow the LLM to invoke the crew as expected.
+ var crewPluginDefinitions = new[]
+ {
+ new CrewAIInputMetadata(Name: "company", Description: "The name of the company that should be researched", Type: typeof(string)),
+ new CrewAIInputMetadata(Name: "topic", Description: "The topic that should be researched", Type: typeof(string)),
+ };
+
+ // Create the CrewAI Plugin. This builds a plugin that can be added to the Kernel and invoked like any other plugin.
+ // The plugin will contain the following functions:
+ // - Kickoff: Starts the Crew with the specified inputs and returns the Id of the scheduled kickoff.
+ // - KickoffAndWait: Starts the Crew with the specified inputs and waits for the Crew to complete before returning the result.
+ // - WaitForCrewCompletion: Waits for the specified Crew kickoff to complete and returns the result.
+ // - GetCrewKickoffStatus: Gets the status of the specified Crew kickoff.
+ var crewPlugin = crew.CreateKernelPlugin(
+ name: "EnterpriseContentMarketingCrew",
+ description: "Conducts thorough research on the specified company and topic to identify emerging trends, analyze competitor strategies, and gather data-driven insights.",
+ inputMetadata: crewPluginDefinitions);
+
+ // Add the plugin to the Kernel
+ kernel.Plugins.Add(crewPlugin);
+
+ // Invoke the CrewAI Plugin directly as shown below, or use automaic function calling with an LLM.
+ var kickoffAndWaitFunction = crewPlugin["KickoffAndWait"];
+ var result = await kernel.InvokeAsync(
+ function: kickoffAndWaitFunction,
+ arguments: new()
+ {
+ ["company"] = "CrewAI",
+ ["topic"] = "Consumer AI Products"
+ });
+
+ Console.WriteLine(result);
+ }
+}
diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md
index 1fb0d0ffe9d6..4a91b29c1ead 100644
--- a/dotnet/samples/Concepts/README.md
+++ b/dotnet/samples/Concepts/README.md
@@ -166,6 +166,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
- [CreatePluginFromOpenApiSpec_Jira](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_Jira.cs)
- [CreatePluginFromOpenApiSpec_Klarna](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_Klarna.cs)
- [CreatePluginFromOpenApiSpec_RepairService](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs)
+- [CrewAI_Plugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CrewAI_Plugin.cs)
- [OpenApiPlugin_PayloadHandling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs)
- [OpenApiPlugin_CustomHttpContentReader](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_CustomHttpContentReader.cs)
- [OpenApiPlugin_Customization](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs)
diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
index 6ca016248073..567469a8cefe 100644
--- a/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
+++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/TestConfiguration.cs
@@ -49,6 +49,8 @@ public static void Initialize(IConfigurationRoot configRoot)
public static VertexAIConfig VertexAI => LoadSection();
public static AzureCosmosDbMongoDbConfig AzureCosmosDbMongoDb => LoadSection();
+ public static CrewAIConfig CrewAI => LoadSection();
+
private static T LoadSection([CallerMemberName] string? caller = null)
{
if (s_instance is null)
@@ -309,4 +311,10 @@ public MsGraphConfiguration(
this.RedirectUri = redirectUri;
}
}
+
+ public class CrewAIConfig
+ {
+ public string Endpoint { get; set; }
+ public string AuthToken { get; set; }
+ }
}
diff --git a/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseClientTests.cs b/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseClientTests.cs
new file mode 100644
index 000000000000..f49fa4ddce0d
--- /dev/null
+++ b/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseClientTests.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.SemanticKernel.Plugins.AI.CrewAI;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace SemanticKernel.Plugins.AI.UnitTests.CrewAI;
+
+///
+/// Tests for the class.
+///
+public sealed partial class CrewAIEnterpriseClientTests
+{
+ private readonly Mock _httpMessageHandlerMock;
+ private readonly CrewAIEnterpriseClient _client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CrewAIEnterpriseClientTests()
+ {
+ this._httpMessageHandlerMock = new Mock();
+ using var httpClientFactory = new MockHttpClientFactory(this._httpMessageHandlerMock);
+ this._client = new CrewAIEnterpriseClient(
+ endpoint: new Uri("http://example.com"),
+ authTokenProvider: () => Task.FromResult("token"),
+ httpClientFactory);
+ }
+
+ ///
+ /// Tests that returns the required inputs from the CrewAI API.
+ ///
+ ///
+ [Fact]
+ public async Task GetInputsAsyncReturnsCrewAIRequiredInputsAsync()
+ {
+ // Arrange
+ var responseContent = "{\"inputs\": [\"input1\", \"input2\"]}";
+ using var responseMessage = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(responseContent)
+ };
+
+ this._httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(responseMessage);
+
+ // Act
+ var result = await this._client.GetInputsAsync();
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Inputs.Count);
+ Assert.Contains("input1", result.Inputs);
+ Assert.Contains("input2", result.Inputs);
+ }
+
+ ///
+ /// Tests that returns the kickoff id from the CrewAI API.
+ ///
+ ///
+ [Fact]
+ public async Task KickoffAsyncReturnsCrewAIKickoffResponseAsync()
+ {
+ // Arrange
+ var responseContent = "{\"kickoff_id\": \"12345\"}";
+ using var responseMessage = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(responseContent)
+ };
+
+ this._httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(responseMessage);
+
+ // Act
+ var result = await this._client.KickoffAsync(new { key = "value" });
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal("12345", result.KickoffId);
+ }
+
+ ///
+ /// Tests that returns the status of the CrewAI Crew.
+ ///
+ ///
+ ///
+ ///
+ [Theory]
+ [InlineData(CrewAIKickoffState.Pending)]
+ [InlineData(CrewAIKickoffState.Started)]
+ [InlineData(CrewAIKickoffState.Running)]
+ [InlineData(CrewAIKickoffState.Success)]
+ [InlineData(CrewAIKickoffState.Failed)]
+ [InlineData(CrewAIKickoffState.Failure)]
+ [InlineData(CrewAIKickoffState.NotFound)]
+ public async Task GetStatusAsyncReturnsCrewAIStatusResponseAsync(CrewAIKickoffState state)
+ {
+ var crewAIStatusState = state switch
+ {
+ CrewAIKickoffState.Pending => "PENDING",
+ CrewAIKickoffState.Started => "STARTED",
+ CrewAIKickoffState.Running => "RUNNING",
+ CrewAIKickoffState.Success => "SUCCESS",
+ CrewAIKickoffState.Failed => "FAILED",
+ CrewAIKickoffState.Failure => "FAILURE",
+ CrewAIKickoffState.NotFound => "NOT FOUND",
+ _ => throw new ArgumentOutOfRangeException(nameof(state), state, null)
+ };
+
+ // Arrange
+ var responseContent = $"{{\"state\": \"{crewAIStatusState}\", \"result\": \"The Result\", \"last_step\": {{\"step1\": \"value1\"}}}}";
+ using var responseMessage = new HttpResponseMessage
+ {
+ StatusCode = HttpStatusCode.OK,
+ Content = new StringContent(responseContent)
+ };
+
+ this._httpMessageHandlerMock.Protected()
+ .Setup>(
+ "SendAsync",
+ ItExpr.IsAny(),
+ ItExpr.IsAny())
+ .ReturnsAsync(responseMessage);
+
+ // Act
+ var result = await this._client.GetStatusAsync("12345");
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Equal(state, result.State);
+ Assert.Equal("The Result", result.Result);
+ Assert.NotNull(result.LastStep);
+ Assert.Equal("value1", result.LastStep["step1"].ToString());
+ }
+}
diff --git a/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseTests.cs b/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseTests.cs
new file mode 100644
index 000000000000..635e8f63700a
--- /dev/null
+++ b/dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseTests.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.AI.CrewAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Plugins.UnitTests.AI.CrewAI;
+
+///
+/// Unit tests for the class.
+///
+public sealed class CrewAIEnterpriseTests
+{
+ private readonly Mock _mockClient;
+ private readonly CrewAIEnterprise _crewAIEnterprise;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CrewAIEnterpriseTests()
+ {
+ this._mockClient = new Mock(MockBehavior.Strict);
+ this._crewAIEnterprise = new CrewAIEnterprise(this._mockClient.Object, NullLoggerFactory.Instance);
+ }
+
+ ///
+ /// Tests the successful kickoff of a CrewAI task.
+ ///
+ [Fact]
+ public async Task KickoffAsyncSuccessAsync()
+ {
+ // Arrange
+ var response = new CrewAIKickoffResponse { KickoffId = "12345" };
+ this._mockClient.Setup(client => client.KickoffAsync(It.IsAny