GithubWebHookProcessor optimization (#61)

* Created file via AI

* Added some sections to the 'DevTeam' sample's Readme.

* Added null checks to GithubWebHookProcessor.
Removed unused injected services in GithubWebHookProcessor.
Minor optimizations.

* Removed Readme generated from devteam sample run.

---------

Co-authored-by: project-oagent-gh-app[bot] <169608828+project-oagent-gh-app[bot]@users.noreply.github.com>
Co-authored-by: Alessandro Avila <alavil@microsoft.com>
This commit is contained in:
Alessandro Avila
2024-05-16 12:53:27 +02:00
committed by GitHub
parent e9a7a07e13
commit 49758d82cd
7 changed files with 126 additions and 48 deletions

44
readme.md Normal file
View File

@@ -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 <https://cla.opensource.microsoft.com>.
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 <http://go.microsoft.com/fwlink/?LinkID=254653>.
Privacy information can be found at <https://privacy.microsoft.com/en-us/>
Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents,
or trademarks, whether by implication, estoppel or otherwise.

View File

@@ -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.
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.

View File

@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.AI.DevTeam;
public class AzureOptions
{
[Required]

View File

@@ -112,24 +112,24 @@ builder.Host.UseOrleans(siloBuilder =>
options.ResponseTimeout = TimeSpan.FromMinutes(3);
options.SystemResponseTimeout = TimeSpan.FromMinutes(3);
});
siloBuilder.Configure<ClientMessagingOptions>(options =>
{
options.ResponseTimeout = TimeSpan.FromMinutes(3);
});
siloBuilder.UseCosmosClustering( o =>
siloBuilder.Configure<ClientMessagingOptions>(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",

View File

@@ -12,51 +12,64 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
{
private readonly ILogger<GithubWebHookProcessor> _logger;
private readonly IClusterClient _client;
private readonly IManageGithub _ghService;
private readonly IManageAzure _azService;
public GithubWebHookProcessor(ILogger<GithubWebHookProcessor> 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<Event>(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<string, string>
{
{ "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<string, string>
var data = new Dictionary<string, string>
{
{ "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;
}
}
}

View File

@@ -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");

View File

@@ -45,8 +45,7 @@ public abstract class AiAgent<T> : Agent, IAiAgent where T : class, new()
public virtual async Task<string> 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);