autogen/dotnet/samples/dev-team/DevTeam.Backend/Services/GithubWebHookProcessor.cs

152 lines
6.6 KiB
C#

// Copyright (c) Microsoft Corporation. All rights reserved.
// GithubWebHookProcessor.cs
using System.Globalization;
using Google.Protobuf;
using Microsoft.AutoGen.Contracts;
using Microsoft.AutoGen.Core;
using Octokit.Webhooks;
using Octokit.Webhooks.Events;
using Octokit.Webhooks.Events.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
namespace DevTeam.Backend.Services;
public sealed class GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logger, Client client) : WebhookEventProcessor
{
private readonly ILogger<GithubWebHookProcessor> _logger = logger;
private readonly Client _client = 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 ?? 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
.Select(l => l.Name.Split('.'))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1]);
if (labels == null || labels.Count == 0)
{
_logger.LogWarning("No labels found in issue. Skipping processing.");
return;
}
long? parentNumber = labels.TryGetValue("Parent", out var 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, skillName, labels[skillName], suffix, input, org, repo);
}
else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue?.User.Type.Value == UserType.Bot)
{
_logger.LogInformation("Processing HandleClosingIssue");
await HandleClosingIssue(issueNumber, skillName, labels[skillName], suffix);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing issue event");
throw;
}
}
protected override async Task ProcessIssueCommentWebhookAsync(
WebhookHeaders headers,
IssueCommentEvent issueCommentEvent,
IssueCommentAction action)
{
ArgumentNullException.ThrowIfNull(headers);
ArgumentNullException.ThrowIfNull(issueCommentEvent);
ArgumentNullException.ThrowIfNull(action);
try
{
_logger.LogInformation("Processing issue comment event");
var org = issueCommentEvent.Repository!.Owner.Login;
var repo = issueCommentEvent.Repository.Name;
var issueNumber = issueCommentEvent.Issue.Number;
var input = issueCommentEvent.Comment.Body;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issueCommentEvent.Issue.Labels
.Select(l => l.Name.Split('.'))
.Where(parts => parts.Length == 2)
.ToDictionary(parts => parts[0], parts => parts[1]);
var skillName = labels.Keys.First(k => k != "Parent");
long? parentNumber = labels.TryGetValue("Parent", out var value) ? long.Parse(value, CultureInfo.InvariantCulture) : null;
var suffix = $"{org}-{repo}";
// we only respond to non-bot comments
if (issueCommentEvent.Sender!.Type.Value != UserType.Bot)
{
await HandleNewAsk(issueNumber, skillName, labels[skillName], suffix, input, org, repo);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Processing issue comment event");
throw;
}
}
private async Task HandleClosingIssue(long issueNumber, string skillName, string functionName, string suffix)
{
var subject = suffix + issueNumber.ToString();
IMessage evt = (skillName, functionName) switch
{
("PM", "Readme") => new ReadmeChainClosed { },
("DevLead", "Plan") => new DevPlanChainClosed { },
("Developer", "Implement") => new CodeChainClosed { },
_ => new CloudEvent() // TODO: default event
};
await _client.PublishMessageAsync(evt, Consts.TopicName, subject);
}
private async Task HandleNewAsk(long issueNumber, string skillName, string functionName, string suffix, string input, string org, string repo)
{
try
{
_logger.LogInformation("Handling new ask");
var subject = suffix + issueNumber.ToString();
IMessage evt = (skillName, functionName) switch
{
("Do", "It") => new NewAsk { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo },
("PM", "Readme") => new ReadmeRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo },
("DevLead", "Plan") => new DevPlanRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo },
("Developer", "Implement") => new CodeGenerationRequested { Ask = input, IssueNumber = issueNumber, Org = org, Repo = repo },
_ => new CloudEvent()
};
await _client.PublishMessageAsync(evt, Consts.TopicName, subject);
}
catch (Exception ex)
{
_logger.LogError(ex, "Handling new ask");
throw;
}
}
}