Skip to content
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

Release Note Summarization with Azure OpenAI #6

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-dotnettools.csharp"
]
}
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to .NET Functions",
"type": "coreclr",
"request": "attach",
"processId": "${command:azureFunctions.pickProcess}"
}
]
}
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"azureFunctions.deploySubpath": "AzureDevOpsReleaseNotes/bin/Release/net6.0/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish (functions)"
}
81 changes: 81 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "clean (functions)",
"command": "dotnet",
"args": [
"clean",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile",
"options": {
"cwd": "${workspaceFolder}/AzureDevOpsReleaseNotes"
}
},
{
"label": "build (functions)",
"command": "dotnet",
"args": [
"build",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean (functions)",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile",
"options": {
"cwd": "${workspaceFolder}/AzureDevOpsReleaseNotes"
}
},
{
"label": "clean release (functions)",
"command": "dotnet",
"args": [
"clean",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile",
"options": {
"cwd": "${workspaceFolder}/AzureDevOpsReleaseNotes"
}
},
{
"label": "publish (functions)",
"command": "dotnet",
"args": [
"publish",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean release (functions)",
"problemMatcher": "$msCompile",
"options": {
"cwd": "${workspaceFolder}/AzureDevOpsReleaseNotes"
}
},
{
"type": "func",
"dependsOn": "build (functions)",
"options": {
"cwd": "${workspaceFolder}/AzureDevOpsReleaseNotes/bin/Debug/net6.0"
},
"command": "host start",
"isBackground": true,
"problemMatcher": "$func-dotnet-watch"
}
]
}
2 changes: 1 addition & 1 deletion AzureDevOpsReleaseNotes.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2042
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ADOReleaseNotes", "AzureDevOpsReleaseNotes\AzureDevOpsReleaseNotes.csproj", "{E92517EA-AA39-43EC-965C-04E371715592}"
EndProject
Expand Down
22 changes: 12 additions & 10 deletions AzureDevOpsReleaseNotes/AzureDevOpsReleaseNotes.csproj
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<RootNamespace>AzureDevOpsReleaseNotes</RootNamespace>
<ApplicationIcon />
<OutputType>Library</OutputType>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.22" />
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.140.0-preview" />
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="16.140.0-preview" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="16.140.0-preview" />
</ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.9" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.39" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="5.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.2.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.2.0" />
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.205.1" />
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="19.230.0-preview" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.230.0-preview" /> </ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
93 changes: 69 additions & 24 deletions AzureDevOpsReleaseNotes/ReleaseNotesWebHook.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
using Azure;
using Azure.AI.OpenAI;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Specialized;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
Expand All @@ -7,61 +12,74 @@
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.WindowsAzure.Storage;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace AzureDevOpsReleaseNotes
{
public static class ReleaseNotesWebHook
{
[FunctionName("ReleaseNotesWebHook")]
public static async void RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req, ILogger log)
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
//Extract data from request body
string requestBody = new StreamReader(req.Body).ReadToEnd();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string releaseName = data?.resource?.release?.name;
string releaseBody = data?.resource?.release?.description;

VssBasicCredential credentials = new VssBasicCredential(Environment.GetEnvironmentVariable("DevOps.Username"), Environment.GetEnvironmentVariable("DevOps.AccessToken"));
VssConnection connection = new VssConnection(new Uri(Environment.GetEnvironmentVariable("DevOps.OrganizationURL")), credentials);
VssBasicCredential credentials = new(Environment.GetEnvironmentVariable("DevOps.Username"), Environment.GetEnvironmentVariable("DevOps.AccessToken"));
VssConnection connection = new(new Uri(Environment.GetEnvironmentVariable("DevOps.OrganizationURL")), credentials);

//Time span of 14 days from today
var dateSinceLastRelease = DateTime.Today.Subtract(new TimeSpan(14, 0, 0, 0));

//Accumulate closed work items from the past 14 days in text format
var workItems = GetClosedItems(connection, dateSinceLastRelease);
var pulls = GetMergedPRs(connection, dateSinceLastRelease);

//Create a new blob markdown file
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("StorageAccountConnectionString"));
var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("releases");
var blob = container.GetBlockBlobReference(releaseName + ".md");
BlobContainerClient container = new(Environment.GetEnvironmentVariable("StorageAccountConnectionString"), "releases");
container.CreateIfNotExists();

var description = await GenerateReleaseDescriptionAsync(pulls, workItems);

//Format text content of blob
var text = String.Format("# {0} \n {1} \n\n" + "# Work Items Resolved:" + workItems + "\n\n# Changes Merged:" + pulls, releaseName, releaseBody);
var text = string.Format("# {0} \n {1} \n\n" + "# Work Items Resolved:" + workItems + "\n\n# Changes Merged:" + pulls, releaseName, description.Choices[0].Message);

var blob = container.GetAppendBlobClient(releaseName + ".md");
blob.CreateIfNotExists();

var stream = new MemoryStream();
stream.Write(System.Text.Encoding.UTF8.GetBytes(text));

//Append text to blob
stream.Position = 0;
await blob.AppendBlockAsync(stream);

//Add text to blob
await blob.UploadTextAsync(text);
return new OkObjectResult("Release Notes Updated");

}

public static string GetClosedItems(VssConnection connection, DateTime releaseSpan)
{
string project = Environment.GetEnvironmentVariable("DevOps.ProjectName");
var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

//Query that grabs all of the Work Items marked "Done" in the last 14 days
Wiql wiql = new Wiql()
Wiql wiql = new()
{
Query = "Select [State], [Title] " +
"From WorkItems Where " +
"[System.TeamProject] = '" + project + "' " +
"And [System.State] = 'Done' " +
"And [System.State] = 'Resolved' " +
"OR [System.State] = 'Closed' " +
"And [Closed Date] >= '" + releaseSpan.ToString() + "' " +
"Order By [State] Asc, [Changed Date] Desc"
};
Expand All @@ -72,21 +90,21 @@ public static string GetClosedItems(VssConnection connection, DateTime releaseSp

if (workItemQueryResult.WorkItems.Count() != 0)
{
List<int> list = new List<int>();
List<int> list = new();
foreach (var item in workItemQueryResult.WorkItems)
{
list.Add(item.Id);
}

//Extraxt desired work item fields
//Extract desired work item fields
string[] fields = { "System.Id", "System.Title" };
var workItems = workItemTrackingHttpClient.GetWorkItemsAsync(list, fields, workItemQueryResult.AsOf).Result;

//Format Work Item info into text
string txtWorkItems = string.Empty;
foreach (var workItem in workItems)
{
txtWorkItems += String.Format("\n 1. #{0}-{1}", workItem.Id, workItem.Fields["System.Title"]);
txtWorkItems += string.Format("\n 1. #{0}-{1}", workItem.Id, workItem.Fields["System.Title"]);
}
return txtWorkItems;
}
Expand All @@ -102,36 +120,63 @@ public static string GetMergedPRs(VssConnection connection, DateTime releaseSpan
using (gitClient)
{
//Get first repo in project
var releaseRepo = gitClient.GetRepositoriesAsync().Result[0];
var releaseRepo = gitClient.GetRepositoriesAsync().Result[0];

//Grabs all completed PRs merged into master branch
List<GitPullRequest> prs = gitClient.GetPullRequestsAsync(
releaseRepo.Id,
new GitPullRequestSearchCriteria()
{
TargetRefName = "refs/heads/master",
Status = PullRequestStatus.Completed
TargetRefName = "refs/heads/main",
Status = PullRequestStatus.Completed

}).Result;

if (prs.Count != 0)
{
//Query that grabs PRs merged since the specified date
var pulls = from p in prs
where p.ClosedDate >= releaseSpan
select p;
where p.ClosedDate >= releaseSpan
select p;

//Format PR info into text
var txtPRs = string.Empty;
foreach (var pull in pulls)
{
txtPRs += String.Format("\n 1. #{0}-{1}", pull.PullRequestId, pull.Title);
txtPRs += string.Format("\n 1. #{0}-{1}", pull.PullRequestId, pull.Title);
}

return txtPRs;
}
return string.Empty;
}
}

public static async Task<ChatCompletions> GenerateReleaseDescriptionAsync(string pulls, string workItems)
{
OpenAIClient client = new OpenAIClient(
new Uri(Environment.GetEnvironmentVariable("OpenAIEndpoint")),
new AzureKeyCredential(Environment.GetEnvironmentVariable("OpenAIKey")));

Response<ChatCompletions> completionsResponse = await client.GetChatCompletionsAsync(
new ChatCompletionsOptions()
{
Messages =
{
new ChatMessage(ChatRole.System, @"You are an AI powered software project manager who is in charge of managing release notes."),
new ChatMessage(ChatRole.User, @"Here's all the resolved work items and merged pull requests since the last release. Summarize them in a few sentences and make a generic description if there aren't any." + "\n\n" + "Work Items Resolved:" + workItems + "\n\n" + "Changes Merged:" + pulls ),
},
Temperature = (float)0.7,
MaxTokens = 800,
NucleusSamplingFactor = (float)0.95,
FrequencyPenalty = 0,
PresencePenalty = 0,
DeploymentName=Environment.GetEnvironmentVariable("ModelDeploymentName")
});

ChatCompletions completions = completionsResponse.Value;

return completions;
}
}
}
12 changes: 0 additions & 12 deletions AzureDevOpsReleaseNotes/dev.settings.json

This file was deleted.

3 changes: 2 additions & 1 deletion AzureDevOpsReleaseNotes/host.json
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{
}
"version": "2.0"
}