-
Notifications
You must be signed in to change notification settings - Fork 4.2k
.Net: CrewAI Plugin #10363
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
Merged
Merged
.Net: CrewAI Plugin #10363
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
935dcfc
Adding CrewAI Plugin with unit tests.
80edb01
Merging from main.
f5ed1a9
Improving input type handling.
c10da78
Fixing spelling and test issues.
9d9db34
More spelling fixes.
d1aab01
Merge branch 'main' into crew-agent
alliscode d440fd9
Working on feedback from PR.
0c045cb
More fixes per PR feedback.
56df37d
Update dotnet/SK-dotnet.sln
alliscode c377fdf
More fixes per feedback.
22b026b
Merge branch 'crew-agent' of https://github.com/alliscode/semantic-ke…
6cc16cf
Verifying changes with CrewAI enterprise.
bd76b65
Fixing test failure.
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.