diff --git a/readme.md b/readme.md new file mode 100644 index 000000000..e925413eb --- /dev/null +++ b/readme.md @@ -0,0 +1,44 @@ +> ⚠️ This project is still an experimentation phase and is not intended to be used in production yet. + +# AI Agents Framework + +An opinionated .NET framework, that is built on top of Semantic Kernel and Orleans, which helps creating and hosting event-driven AI Agents. + +At the moment the library resides in `src/` only, but we plan to publish them as a Nuget Package in the future. + +## Examples + +We have created a few examples to help you get started with the framework and to explore its capabilities. + +- [GitHub Dev Team Sample](samples/gh-flow/README.md): Build an AI Developer Team using event-driven agents, that help you automate the requirements engineering, planning, and coding process on GitHub. + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit . + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Legal Notices + +Microsoft and any contributors grant you a license to the Microsoft documentation and other content +in this repository under the [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/legalcode), +see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the +[LICENSE-CODE](LICENSE-CODE) file. + +Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation +may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. +The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. +Microsoft's general trademark guidelines can be found at . + +Privacy information can be found at + +Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, +or trademarks, whether by implication, estoppel or otherwise. diff --git a/samples/gh-flow/docs/github-flow-getting-started.md b/samples/gh-flow/docs/github-flow-getting-started.md index b287910ff..ebfb16825 100644 --- a/samples/gh-flow/docs/github-flow-getting-started.md +++ b/samples/gh-flow/docs/github-flow-getting-started.md @@ -50,6 +50,25 @@ https://docs.github.com/en/codespaces/developing-in-a-codespace/creating-a-codes In this sample's folder there are two files called appsettings.azure.template.json and appsettings.local.template.json. If you run this demo locally, use the local template and if you want to run it within Azure use the Azure template. Rename the selected file to appsettings.json and fill out the config values within the file. +### GitHubOptions + +For the GitHubOptions section, you'll need to fill in the following values: +- **AppKey (PrivateKey)**: this is a key generated while creating a GitHub App. If you haven't saved it during creation, you'll need to generate a new one. Go to the settings of your GitHub app, scroll down to "Private keys" and click on "Generate a new private key". It will download a .pem file that contains your App Key. Then copy and paste all the **-----BEGIN RSA PRIVATE KEY---- your key -----END RSA PRIVATE KEY-----** content here, in one line. +- **AppId**: This can be found on the same page where you created your app. Go to the settings of your GitHub app and you can see the App ID at the top of the page. +- **InstallationId**: Access to your GitHub app installation and take note of the number (long type) at the end of the URL (which should be in the following format: https://github.com/settings/installations/installation-id). +- **WebhookSecret**: This is a value that you set when you create your app. In the app settings, go to the "Webhooks" section. Here you can find the "Secret" field where you can set your Webhook Secret. + +### AzureOptions + +The following fields are required and need to be filled in: +- **SubscriptionId**: The id of the subscription you want to work on. +- **Location** +- **ContainerInstancesResourceGroup**: The name of the resource group where container instances will be deployed. +- **FilesAccountName**: Azure Storage Account name. +- **FilesShareName**: The name of the File Share. +- **FilesAccountKey**: The File Account key. +- **SandboxImage** + In the Explorer tab in VS Code, find the Solution explorer, right click on the `gh-flow` project and click Debug -> Start new instance ![Alt text](./images/solution-explorer.png) @@ -57,7 +76,7 @@ In the Explorer tab in VS Code, find the Solution explorer, right click on the ` We'll need to expose the running application to the GH App webhooks, for example using [DevTunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview), but any tool like ngrok can also work. The following commands will create a persistent tunnel, so we need to only do this once: ```bash -TUNNEL_NAME=_name_yout_tunnel_here_ +TUNNEL_NAME=_name_your_tunnel_here_ devtunnel user login devtunnel create -a $TUNNEL_NAME devtunnel port create -p 5244 $TUNNEL_NAME @@ -68,7 +87,7 @@ and once we have the tunnel created we can just start forwarding with the follow devtunnel host $TUNNEL_NAME ``` -Copy the local address (it will look something like https://yout_tunnel_name.euw.devtunnels.ms) and append `/api/github/webhooks` at the end. Using this value, update the Github App's webhook URL and you are ready to go! +Copy the local address (it will look something like https://your_tunnel_name.euw.devtunnels.ms) and append `/api/github/webhooks` at the end. Using this value, update the Github App's webhook URL and you are ready to go! Before you go and have the best of times, there is one last thing left to do [load the WAF into the vector DB](#load-the-waf-into-qdrant) @@ -100,4 +119,4 @@ As the last step, we also need to [load the WAF into the vector DB](#load-the-wa ### Load the WAF into Qdrant. If you are running the app locally, we have [Qdrant](https://qdrant.tech/) setup in the Codespace and if you are running in Azure, Qdrant is deployed to ACA. -The loader is a project in the `samples` folder, called `seed-memory`. We need to fill in the `appsettings.json` file in the `config` folder with the OpenAI details and the Qdrant endpoint, then just run the loader with `dotnet run` and you are ready to go. \ No newline at end of file +The loader is a project in the `samples` folder, called `seed-memory`. We need to fill in the `appsettings.json` (after renaming `appsettings.template.json` in `appsettings.json`) file in the `config` folder with the OpenAI details and the Qdrant endpoint, then just run the loader with `dotnet run` and you are ready to go. \ No newline at end of file diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Options/AzureOptions.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Options/AzureOptions.cs index e3780a6a0..4f29ed8b7 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Options/AzureOptions.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Options/AzureOptions.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; namespace Microsoft.AI.DevTeam; + public class AzureOptions { [Required] diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs index 0cb04c08d..03c77b5da 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Program.cs @@ -112,24 +112,24 @@ builder.Host.UseOrleans(siloBuilder => options.ResponseTimeout = TimeSpan.FromMinutes(3); options.SystemResponseTimeout = TimeSpan.FromMinutes(3); }); - siloBuilder.Configure(options => - { - options.ResponseTimeout = TimeSpan.FromMinutes(3); - }); - siloBuilder.UseCosmosClustering( o => + siloBuilder.Configure(options => + { + options.ResponseTimeout = TimeSpan.FromMinutes(3); + }); + siloBuilder.UseCosmosClustering(o => { o.ConfigureCosmosClient(cosmosDbconnectionString); o.ContainerName = "devteam"; o.DatabaseName = "clustering"; o.IsResourceCreationEnabled = true; }); - - siloBuilder.UseCosmosReminderService( o => + + siloBuilder.UseCosmosReminderService(o => { - o.ConfigureCosmosClient(cosmosDbconnectionString); - o.ContainerName = "devteam"; - o.DatabaseName = "reminders"; - o.IsResourceCreationEnabled = true; + o.ConfigureCosmosClient(cosmosDbconnectionString); + o.ContainerName = "devteam"; + o.DatabaseName = "reminders"; + o.IsResourceCreationEnabled = true; }); siloBuilder.AddCosmosGrainStorage( name: "messages", diff --git a/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs b/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs index e661988b1..4cda39249 100644 --- a/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs +++ b/samples/gh-flow/src/Microsoft.AI.DevTeam/Services/GithubWebHookProcessor.cs @@ -12,51 +12,64 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor { private readonly ILogger _logger; private readonly IClusterClient _client; - private readonly IManageGithub _ghService; - private readonly IManageAzure _azService; public GithubWebHookProcessor(ILogger logger, IClusterClient client, IManageGithub ghService, IManageAzure azService) { - _logger = logger; - _client = client; - _ghService = ghService; - _azService = azService; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _client = client ?? throw new ArgumentNullException(nameof(client)); } + protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action) { try { + ArgumentNullException.ThrowIfNull(headers, nameof(headers)); + ArgumentNullException.ThrowIfNull(issuesEvent, nameof(issuesEvent)); + ArgumentNullException.ThrowIfNull(action, nameof(action)); + _logger.LogInformation("Processing issue event"); - var org = issuesEvent.Repository.Owner.Login; - var repo = issuesEvent.Repository.Name; - var issueNumber = issuesEvent.Issue.Number; - var input = issuesEvent.Issue.Body; + var org = issuesEvent.Repository?.Owner.Login ?? throw new InvalidOperationException("Repository owner login is null"); + var repo = issuesEvent.Repository?.Name ?? throw new InvalidOperationException("Repository name is null"); + var issueNumber = issuesEvent.Issue?.Number ?? throw new InvalidOperationException("Issue number is null"); + var input = issuesEvent.Issue?.Body ?? string.Empty; // Assumes the label follows the following convention: Skill.Function example: PM.Readme // Also, we've introduced the Parent label, that ties the sub-issue with the parent issue - var labels = issuesEvent.Issue.Labels + var labels = issuesEvent.Issue?.Labels .Select(l => l.Name.Split('.')) .Where(parts => parts.Length == 2) .ToDictionary(parts => parts[0], parts => parts[1]); - var skillName = labels.Keys.Where(k=>k != "Parent").FirstOrDefault(); - long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null; + if (labels == null || labels.Count == 0) + { + _logger.LogWarning("No labels found in issue. Skipping processing."); + return; + } + + long? parentNumber = labels.TryGetValue("Parent", out string? value) ? long.Parse(value) : null; + var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault(); + + if (skillName == null) + { + _logger.LogWarning("No skill name found in issue. Skipping processing."); + return; + } var suffix = $"{org}-{repo}"; if (issuesEvent.Action == IssuesAction.Opened) { _logger.LogInformation("Processing HandleNewAsk"); - await HandleNewAsk(issueNumber,parentNumber, skillName, labels[skillName], suffix, input, org, repo); + await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], suffix, input, org, repo); } - else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot) + else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue?.User.Type.Value == UserType.Bot) { _logger.LogInformation("Processing HandleClosingIssue"); - await HandleClosingIssue(issueNumber, parentNumber,skillName, labels[skillName], suffix, org, repo); + await HandleClosingIssue(issueNumber, parentNumber, skillName, labels[skillName], suffix, org, repo); } } catch (Exception ex) { - _logger.LogError(ex, "Processing issue event"); - throw; + _logger.LogError(ex, "Processing issue event"); + throw; } } @@ -77,7 +90,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor .Select(l => l.Name.Split('.')) .Where(parts => parts.Length == 2) .ToDictionary(parts => parts[0], parts => parts[1]); - var skillName = labels.Keys.Where(k=>k != "Parent").FirstOrDefault(); + var skillName = labels.Keys.Where(k => k != "Parent").FirstOrDefault(); long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null; var suffix = $"{org}-{repo}"; // we only respond to non-bot comments @@ -91,7 +104,7 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor _logger.LogError(ex, "Processing issue comment event"); throw; } - + } private async Task HandleClosingIssue(long issueNumber, long? parentNumber, string skillName, string functionName, string suffix, string org, string repo) @@ -101,12 +114,12 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor var streamId = StreamId.Create(Consts.MainNamespace, subject); var stream = streamProvider.GetStream(streamId); var eventType = (skillName, functionName) switch - { - ("PM","Readme") => nameof(GithubFlowEventType.ReadmeChainClosed), - ("DevLead","Plan") => nameof(GithubFlowEventType.DevPlanChainClosed), - ("Developer","Implement") => nameof(GithubFlowEventType.CodeChainClosed), - _ => nameof(GithubFlowEventType.NewAsk) - }; + { + ("PM", "Readme") => nameof(GithubFlowEventType.ReadmeChainClosed), + ("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanChainClosed), + ("Developer", "Implement") => nameof(GithubFlowEventType.CodeChainClosed), + _ => nameof(GithubFlowEventType.NewAsk) + }; var data = new Dictionary { { "org", org }, @@ -136,12 +149,12 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor var eventType = (skillName, functionName) switch { ("Do", "It") => nameof(GithubFlowEventType.NewAsk), - ("PM","Readme") => nameof(GithubFlowEventType.ReadmeRequested), - ("DevLead","Plan") => nameof(GithubFlowEventType.DevPlanRequested), - ("Developer","Implement") => nameof(GithubFlowEventType.CodeGenerationRequested), + ("PM", "Readme") => nameof(GithubFlowEventType.ReadmeRequested), + ("DevLead", "Plan") => nameof(GithubFlowEventType.DevPlanRequested), + ("Developer", "Implement") => nameof(GithubFlowEventType.CodeGenerationRequested), _ => nameof(GithubFlowEventType.NewAsk) }; - var data = new Dictionary + var data = new Dictionary { { "org", org }, { "repo", repo }, @@ -159,8 +172,8 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor } catch (Exception ex) { - _logger.LogError(ex, "Handling new ask"); - throw; + _logger.LogError(ex, "Handling new ask"); + throw; } } } diff --git a/src/Microsoft.AI.Agents.Orleans/Agent.cs b/src/Microsoft.AI.Agents.Orleans/Agent.cs index 1f7635ff0..47da5755c 100644 --- a/src/Microsoft.AI.Agents.Orleans/Agent.cs +++ b/src/Microsoft.AI.Agents.Orleans/Agent.cs @@ -8,10 +8,12 @@ public abstract class Agent : Grain, IGrainWithStringKey, IAgent { protected virtual string Namespace { get;set;} public abstract Task HandleEvent(Event item); + private async Task HandleEvent(Event item, StreamSequenceToken? token) { await HandleEvent(item); } + public async Task PublishEvent(string ns, string id, Event item) { var streamProvider = this.GetStreamProvider("StreamProvider"); diff --git a/src/Microsoft.AI.Agents.Orleans/AiAgent.cs b/src/Microsoft.AI.Agents.Orleans/AiAgent.cs index 24cd78512..eaef38a7b 100644 --- a/src/Microsoft.AI.Agents.Orleans/AiAgent.cs +++ b/src/Microsoft.AI.Agents.Orleans/AiAgent.cs @@ -45,8 +45,7 @@ public abstract class AiAgent : Agent, IAiAgent where T : class, new() public virtual async Task CallFunction(string template, KernelArguments arguments, OpenAIPromptExecutionSettings? settings = null) { - var propmptSettings = (settings == null) ? new OpenAIPromptExecutionSettings { MaxTokens = 18000, Temperature = 0.8, TopP = 1 } - : settings; + var propmptSettings = settings ?? new OpenAIPromptExecutionSettings { MaxTokens = 18000, Temperature = 0.8, TopP = 1 }; var function = _kernel.CreateFunctionFromPrompt(template, propmptSettings); var result = (await _kernel.InvokeAsync(function, arguments)).ToString(); AddToHistory(result, ChatUserType.Agent);