-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR adds a new plugin to the Semantic Kernel that can interact with and invoke CrewAI Crews that have been deployed to the CrewAI Enterprise service. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Ben Thomas <[email protected]> Co-authored-by: Dmytro Struk <[email protected]>
- Loading branch information
1 parent
0b2bd01
commit 2537edb
Showing
19 changed files
with
1,152 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
{ | ||
/// <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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
dotnet/src/Plugins/Plugins.AI.UnitTests/CrewAI/CrewAIEnterpriseClientTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
Oops, something went wrong.