Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: CrewAI Plugin #10363

Merged
merged 13 commits into from
Feb 4, 2025
18 changes: 18 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions dotnet/samples/Concepts/Concepts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<ProjectReference Include="..\..\src\Functions\Functions.Prompty\Functions.Prompty.csproj" />
<ProjectReference Include="..\..\src\Planners\Planners.Handlebars\Planners.Handlebars.csproj" />
<ProjectReference Include="..\..\src\Planners\Planners.OpenAI\Planners.OpenAI.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.AI\Plugins.AI.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Core\Plugins.Core.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Memory\Plugins.Memory.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.MsGraph\Plugins.MsGraph.csproj" />
Expand Down
108 changes: 108 additions & 0 deletions dotnet/samples/Concepts/Plugins/CrewAI_Plugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.AI.CrewAI;

namespace Plugins;

/// <summary>
/// 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.
/// </summary>
public class CrewAI_Plugin(ITestOutputHelper output) : BaseTest(output)
alliscode marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Shows how to kickoff an existing CrewAI Enterprise Crew and wait for it to complete.
/// </summary>
[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);
}

/// <summary>
/// Shows how to kickoff an existing CrewAI Enterprise Crew as a plugin.
/// </summary>
[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);
}
}
1 change: 1 addition & 0 deletions dotnet/samples/Concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public static void Initialize(IConfigurationRoot configRoot)
public static VertexAIConfig VertexAI => LoadSection<VertexAIConfig>();
public static AzureCosmosDbMongoDbConfig AzureCosmosDbMongoDb => LoadSection<AzureCosmosDbMongoDbConfig>();

public static CrewAIConfig CrewAI => LoadSection<CrewAIConfig>();

private static T LoadSection<T>([CallerMemberName] string? caller = null)
{
if (s_instance is null)
Expand Down Expand Up @@ -309,4 +311,10 @@ public MsGraphConfiguration(
this.RedirectUri = redirectUri;
}
}

public class CrewAIConfig
{
public string Endpoint { get; set; }
public string AuthToken { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests for the <see cref="CrewAIEnterpriseClient"/> class.
/// </summary>
public sealed partial class CrewAIEnterpriseClientTests
{
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
private readonly CrewAIEnterpriseClient _client;

/// <summary>
/// Initializes a new instance of the <see cref="CrewAIEnterpriseClientTests"/> class.
/// </summary>
public CrewAIEnterpriseClientTests()
{
this._httpMessageHandlerMock = new Mock<HttpMessageHandler>();
using var httpClientFactory = new MockHttpClientFactory(this._httpMessageHandlerMock);
this._client = new CrewAIEnterpriseClient(
endpoint: new Uri("http://example.com"),
authTokenProvider: () => Task.FromResult("token"),
httpClientFactory);
}

/// <summary>
/// Tests that <see cref="CrewAIEnterpriseClient.GetInputsAsync"/> returns the required inputs from the CrewAI API.
/// </summary>
/// <returns></returns>
[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<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.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);
}

/// <summary>
/// Tests that <see cref="CrewAIEnterpriseClient.KickoffAsync"/> returns the kickoff id from the CrewAI API.
/// </summary>
/// <returns></returns>
[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<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(responseMessage);

// Act
var result = await this._client.KickoffAsync(new { key = "value" });

// Assert
Assert.NotNull(result);
Assert.Equal("12345", result.KickoffId);
}

/// <summary>
/// Tests that <see cref="CrewAIEnterpriseClient.GetStatusAsync"/> returns the status of the CrewAI Crew.
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[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<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.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());
}
}
Loading
Loading