Merge branch 'understanding'

This commit is contained in:
Kosta Petan
2024-02-28 21:44:42 +01:00
89 changed files with 948 additions and 3228 deletions

View File

@@ -1,11 +1,5 @@
FROM mcr.microsoft.com/devcontainers/dotnet:0-7.0
FROM mcr.microsoft.com/devcontainers/dotnet:8.0
# Install the xz-utils package
RUN apt-get update && apt-get install -y xz-utils ca-certificates curl gnupg
RUN curl -fsSL https://aka.ms/install-azd.sh | bash
RUN curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | \
sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null && \
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | \
sudo tee /etc/apt/sources.list.d/ngrok.list && \
sudo apt update && sudo apt install ngrok
RUN curl -fsSL https://aka.ms/install-azd.sh | bash

View File

@@ -18,8 +18,7 @@
},
"ghcr.io/azure/azure-dev/azd:0": {
"version": "stable"
},
"ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {}
}
},
"postCreateCommand": "bash .devcontainer/startup.sh",
"hostRequirements": {

View File

@@ -1 +1,78 @@
![](/docs/images/github-sk-dev-team.png)
![](/docs/images/github-sk-dev-team.png)
# How does the event flow look like?
### Hubber agent handles:
```
NewAsk
-> create PM issue
-> create DevLead issue
-> create a branch
```
```
ReadmeGenerated
-> post comment
```
```
DevPlanGenerated
-> post comment
```
```
DevPlanFinished
-> for each step, create Dev issue
```
```
CodeGenerated
-> post comment
```
```
ReadmeFinished
-> commit to branch
```
```
SandboxRunFinished
-> commit to branch
```
### AzureOps agent handles:
```
ReadmeChainClosed
-> store
-> ReadmeStored
```
```
CodeChainClosed
-> store
-> run in sandbox
```
### PM agent handles:
```
ReadmeRequested
-> ReadmeGenerated
```
```
ChainClosed
-> ReadmeFinished
```
### DevLead agent handles:
```
DevPlanRequested
-> DevPlanGenerated
```
```
ChainClosed
-> DevPlanFinished
```
### Dev handles:
```
CodeGenerationRequested
-> CodeGenerated
```
```
ChainClosed
-> CodeFinished
```

View File

@@ -7,12 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{088E1138-FF7
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "apps", "apps", "{4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cli", "src\apps\cli\cli.csproj", "{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gh-flow", "src\apps\gh-flow\gh-flow.csproj", "{37C45587-DD0C-4BFE-817A-21301567A94A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gh-flow-df", "src\apps\gh-flow-df\gh-flow-df.csproj", "{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "seed-memory", "src\apps\seed-memory\seed-memory.csproj", "{110B2C34-0F48-41CE-BC8F-AE3D24E79627}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowsApp", "src\apps\WorkflowsApp\WorkflowsApp.csproj", "{F288CF45-81EF-4F57-ABAC-D7ABE894E7A0}"
@@ -26,24 +22,6 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AI.DevTeam.Skills", "src\libs\Microsoft.AI.DevTeam.Skills\Microsoft.AI.DevTeam.Skills.csproj", "{FF3D28B0-9344-44AF-BEEA-260187BE435E}"
EndProject
Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30612385-E4F4-4FD9-B648-45AF74CB4915}
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4D8667EC-09BE-4CB1-A278-E1CCD83B62B0} = {088E1138-FF7B-4179-AD37-4E3819C56D7E}
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{37C45587-DD0C-4BFE-817A-21301567A94A} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{110B2C34-0F48-41CE-BC8F-AE3D24E79627} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{F288CF45-81EF-4F57-ABAC-D7ABE894E7A0} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{24EB6E3A-C080-4B27-8EA7-F6C91446B840} = {088E1138-FF7B-4179-AD37-4E3819C56D7E}
{1F6EE104-0B3F-49C1-BE3B-B93CAD18BAD0} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
{9FA8DCFB-1726-42FB-B6CD-8AAC027810A1} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
{FF3D28B0-9344-44AF-BEEA-260187BE435E} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
@@ -53,18 +31,6 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|x64.Build.0 = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Debug|x86.Build.0 = Debug|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|Any CPU.Build.0 = Release|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|x64.ActiveCfg = Release|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|x64.Build.0 = Release|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|x86.ActiveCfg = Release|Any CPU
{1FE4A9AE-6403-48D6-8D06-A97CCCE608FA}.Release|x86.Build.0 = Release|Any CPU
{37C45587-DD0C-4BFE-817A-21301567A94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37C45587-DD0C-4BFE-817A-21301567A94A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37C45587-DD0C-4BFE-817A-21301567A94A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -77,18 +43,6 @@ Global
{37C45587-DD0C-4BFE-817A-21301567A94A}.Release|x64.Build.0 = Release|Any CPU
{37C45587-DD0C-4BFE-817A-21301567A94A}.Release|x86.ActiveCfg = Release|Any CPU
{37C45587-DD0C-4BFE-817A-21301567A94A}.Release|x86.Build.0 = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|x64.ActiveCfg = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|x64.Build.0 = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|x86.ActiveCfg = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Debug|x86.Build.0 = Debug|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|Any CPU.Build.0 = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|x64.ActiveCfg = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|x64.Build.0 = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|x86.ActiveCfg = Release|Any CPU
{CF0F7B0D-3CC4-4068-A1A8-64E3151D01E3}.Release|x86.Build.0 = Release|Any CPU
{110B2C34-0F48-41CE-BC8F-AE3D24E79627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{110B2C34-0F48-41CE-BC8F-AE3D24E79627}.Debug|Any CPU.Build.0 = Debug|Any CPU
{110B2C34-0F48-41CE-BC8F-AE3D24E79627}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -150,4 +104,20 @@ Global
{FF3D28B0-9344-44AF-BEEA-260187BE435E}.Release|x86.ActiveCfg = Release|Any CPU
{FF3D28B0-9344-44AF-BEEA-260187BE435E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4D8667EC-09BE-4CB1-A278-E1CCD83B62B0} = {088E1138-FF7B-4179-AD37-4E3819C56D7E}
{37C45587-DD0C-4BFE-817A-21301567A94A} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{110B2C34-0F48-41CE-BC8F-AE3D24E79627} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{F288CF45-81EF-4F57-ABAC-D7ABE894E7A0} = {4D8667EC-09BE-4CB1-A278-E1CCD83B62B0}
{24EB6E3A-C080-4B27-8EA7-F6C91446B840} = {088E1138-FF7B-4179-AD37-4E3819C56D7E}
{1F6EE104-0B3F-49C1-BE3B-B93CAD18BAD0} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
{9FA8DCFB-1726-42FB-B6CD-8AAC027810A1} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
{FF3D28B0-9344-44AF-BEEA-260187BE435E} = {24EB6E3A-C080-4B27-8EA7-F6C91446B840}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30612385-E4F4-4FD9-B648-45AF74CB4915}
EndGlobalSection
EndGlobal

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View File

@@ -1,19 +0,0 @@
using System.Text.Json.Serialization;
public class Subtask
{
public string subtask { get; set; }
public string prompt { get; set; }
}
public class Step
{
public string description { get; set; }
public string step { get; set; }
public List<Subtask> subtasks { get; set; }
}
public class DevLeadPlanResponse
{
public List<Step> steps { get; set; }
}

View File

@@ -1,183 +0,0 @@
using System.CommandLine;
using System.Text.Json;
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Plugins.Memory;
class Program
{
static async Task Main(string[] args)
{
var maxRetryOption = new Option<int>(
name: "--maxRetry",
description: "The number of retires to use if throttled",
getDefaultValue: () => 6);
var fileOption = new Option<FileInfo?>(
name: "--file",
description: "The file used for input to the skill function");
var rootCommand = new RootCommand("CLI tool for the AI Dev team");
rootCommand.AddGlobalOption(fileOption);
rootCommand.Add(maxRetryOption);
var doCommand = new Command("do", "Doers :) ");
var doItCommand = new Command("it", "Do it!");
doItCommand.SetHandler(async (file, maxRetry) => await ChainFunctions(file.FullName, maxRetry), fileOption, maxRetryOption);
doCommand.AddCommand(doItCommand);
var pmCommand = new Command("pm", "Commands for the PM team");
var pmReadmeCommand = new Command("readme", "Produce a Readme for a given input");
pmReadmeCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(PM), PM.Readme, file.FullName, maxRetry), fileOption, maxRetryOption);
var pmBootstrapCommand = new Command("bootstrap", "Bootstrap a project for a given input");
pmBootstrapCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file.FullName, maxRetry), fileOption, maxRetryOption);
pmCommand.AddCommand(pmReadmeCommand);
pmCommand.AddCommand(pmBootstrapCommand);
var devleadCommand = new Command("devlead", "Commands for the Dev Lead team");
var devleadPlanCommand = new Command("plan", "Plan the work for a given input");
devleadPlanCommand.SetHandler(async (file, maxRetry) => await CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, file.FullName, maxRetry), fileOption, maxRetryOption);
devleadCommand.AddCommand(devleadPlanCommand);
var devCommand = new Command("dev", "Commands for the Dev team");
var devPlanCommand = new Command("plan", "Implement the module for a given input");
devPlanCommand.SetHandler(async (file, maxRetry) => await CallWithFile<string>(nameof(Developer), Developer.Implement, file.FullName, maxRetry), fileOption, maxRetryOption);
devCommand.AddCommand(devPlanCommand);
rootCommand.AddCommand(pmCommand);
rootCommand.AddCommand(devleadCommand);
rootCommand.AddCommand(devCommand);
rootCommand.AddCommand(doCommand);
await rootCommand.InvokeAsync(args);
}
public static async Task ChainFunctions(string file, int maxRetry)
{
var sandboxSkill = new SandboxSkill();
var outputPath = Directory.CreateDirectory("output");
Console.WriteLine($"Using output directory: {outputPath}");
var readme = await CallWithFile<string>(nameof(PM), PM.Readme, file, maxRetry);
string readmeFile = Path.Combine(outputPath.FullName, "README.md");
await SaveToFile(readmeFile, readme);
Console.WriteLine($"Saved README to {readmeFile}");
var script = await CallWithFile<string>(nameof(PM), PM.BootstrapProject, file, maxRetry);
await sandboxSkill.RunInDotnetAlpineAsync(script);
await SaveToFile(Path.Combine(outputPath.FullName, "bootstrap.sh"), script);
Console.WriteLine($"Saved bootstrap script to {outputPath.FullName}bootstrap.sh");
var plan = await CallWithFile<DevLeadPlanResponse>(nameof(DevLead), DevLead.Plan, readmeFile, maxRetry);
await SaveToFile(Path.Combine(outputPath.FullName, "plan.json"), JsonSerializer.Serialize(plan));
Console.WriteLine($"Using Plan: \n {plan}");
var implementationTasks = plan.steps.SelectMany(
(step) => step.subtasks.Select(
async (subtask) =>
{
Console.WriteLine($"Implementing {step.step}-{subtask.subtask}");
var implementationResult = string.Empty;
while (true)
{
try
{
implementationResult = await CallFunction<string>(nameof(Developer), Developer.Implement, subtask.prompt, maxRetry);
break;
}
catch (Exception ex)
{
if (ex.Message.Contains("TooMany"))
{
Console.WriteLine("Throttled, retrying...");
continue;
}
throw;
}
}
await sandboxSkill.RunInDotnetAlpineAsync(implementationResult);
await SaveToFile(Path.Combine(outputPath.FullName, $"{step.step}-{subtask.subtask}.sh"), implementationResult);
return implementationResult;
}));
await Task.WhenAll(implementationTasks);
}
public static async Task SaveToFile(string filePath, string content)
{
await File.WriteAllTextAsync(filePath, content);
}
public static async Task<T> CallWithFile<T>(string skillName, string functionName, string filePath, int maxRetry)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"File not found: {filePath}", filePath);
var input = File.ReadAllText(filePath);
return await CallFunction<T>(skillName, functionName, input, maxRetry);
}
public static async Task<T> CallFunction<T>(string skillName, string functionName, string input, int maxRetry)
{
var kernelSettings = KernelSettings.LoadSettings();
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning)
.AddConsole()
.AddDebug();
});
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant:6333", 1536));
var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);
var memoryBuilder = new MemoryBuilder();
var memory = memoryBuilder.WithLoggerFactory(loggerFactory)
.WithQdrantMemoryStore("http://qdrant:6333", 1536)
.WithAzureTextEmbeddingGenerationService(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey)
.Build();
var kernel = new KernelBuilder()
.WithLoggerFactory(loggerFactory)
.WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true)
.Build();
//Console.WriteLine($"Calling skill '{skillName}' function '{functionName}' with input '{input}'");
var interestingMemories = memory.SearchAsync("waf-pages", input, 2);
var wafContext = "Consider the following architectural guidelines:";
await foreach (var m in interestingMemories)
{
wafContext += $"\n {m.Metadata.Text}";
}
var promptTemplate = Skills.ForSkillAndFunction(skillName, functionName);
var function = kernel.CreateSemanticFunction(promptTemplate);
var context = new ContextVariables();
context.Set("input", input);
context.Set("wafContext", wafContext);
var answer = await kernel.RunAsync(context, function).ConfigureAwait(false);
var result = typeof(T) != typeof(string) ? JsonSerializer.Deserialize<T>(answer.ToString()) : (T)(object)answer.ToString();
//Console.WriteLine(answer);
return result;
}
}
public static class PM { public static string Readme = "Readme"; public static string BootstrapProject = "BootstrapProject"; }
public static class DevLead { public static string Plan = "Plan"; }
public static class Developer { public static string Implement = "Implement"; public static string Improve = "Improve"; }

View File

@@ -1,37 +0,0 @@
using DotNet.Testcontainers.Builders;
public class SandboxSkill
{
public async Task<string> RunInAlpineAsync(string input)
{
return await RunInContainer(input, "alpine");
}
public async Task<string> RunInDotnetAlpineAsync(string input)
{
return await RunInContainer(input, "mcr.microsoft.com/dotnet/sdk:7.0");
}
private async Task<string> RunInContainer(string input, string image)
{
var tempScriptFile = $"{Guid.NewGuid().ToString()}.sh";
var tempScriptPath = $"./output/{tempScriptFile}";
await File.WriteAllTextAsync(tempScriptPath, input);
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(),"output", "src"));
var dotnetContainer = new ContainerBuilder()
.WithName(Guid.NewGuid().ToString("D"))
.WithImage(image)
.WithBindMount(Path.Combine(Directory.GetCurrentDirectory(),"output", "src"), "/src")
.WithBindMount(Path.Combine(Directory.GetCurrentDirectory(), tempScriptPath), $"/src/{tempScriptFile}")
.WithWorkingDirectory("/src")
.WithCommand("sh", tempScriptFile)
.Build();
await dotnetContainer.StartAsync()
.ConfigureAwait(false);
// Cleanup
File.Delete(tempScriptPath);
File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "output", "src", tempScriptFile));
return "";
}
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.0.0-beta5" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="1.0.0-beta5" />
<PackageReference Include="Testcontainers" Version="3.2.0" />
<ProjectReference Include="..\..\libs\Microsoft.AI.DevTeam.Skills\Microsoft.AI.DevTeam.Skills.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,93 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
internal class KernelSettings
{
public const string DefaultConfigFile = "config/appsettings.json";
public const string OpenAI = "OPENAI";
public const string AzureOpenAI = "AZUREOPENAI";
[JsonPropertyName("serviceType")]
public string ServiceType { get; set; } = string.Empty;
[JsonPropertyName("serviceId")]
public string ServiceId { get; set; } = string.Empty;
[JsonPropertyName("deploymentOrModelId")]
public string DeploymentOrModelId { get; set; } = string.Empty;
[JsonPropertyName("embeddingDeploymentOrModelId")]
public string EmbeddingDeploymentOrModelId { get; set; } = string.Empty;
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; } = string.Empty;
[JsonPropertyName("apiKey")]
public string ApiKey { get; set; } = string.Empty;
[JsonPropertyName("orgId")]
public string OrgId { get; set; } = string.Empty;
[JsonPropertyName("logLevel")]
public LogLevel? LogLevel { get; set; }
/// <summary>
/// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets.
/// </summary>
internal static KernelSettings LoadSettings()
{
try
{
if (File.Exists(DefaultConfigFile))
{
return FromFile(DefaultConfigFile);
}
Console.WriteLine($"Semantic kernel settings '{DefaultConfigFile}' not found, attempting to load configuration from user secrets.");
return FromUserSecrets();
}
catch (InvalidDataException ide)
{
Console.Error.WriteLine(
"Unable to load semantic kernel settings, please provide configuration settings using instructions in the README.\n" +
"Please refer to: https://github.com/microsoft/semantic-kernel-starters/blob/main/sk-csharp-hello-world/README.md#configuring-the-starter"
);
throw new InvalidOperationException(ide.Message);
}
}
/// <summary>
/// Load the kernel settings from the specified configuration file if it exists.
/// </summary>
internal static KernelSettings FromFile(string configFile = DefaultConfigFile)
{
if (!File.Exists(configFile))
{
throw new FileNotFoundException($"Configuration not found: {configFile}");
}
var configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile(configFile, optional: true, reloadOnChange: true)
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README.");
}
/// <summary>
/// Load the kernel settings from user secrets.
/// </summary>
internal static KernelSettings FromUserSecrets()
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<KernelSettings>()
.AddEnvironmentVariables()
.Build();
return configuration.Get<KernelSettings>()
?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README.");
}
}

View File

@@ -1,7 +0,0 @@
Please write a bash script with the commands that would be required to generate two new .NET applications as follows:
The first application will be a web service called "skdtwebapi" with a two methods: /prompt, which accepts a PUT request with a text body of no more than 2048k, and /skills which accepts a GET request and lists the semantic skills it has.
The second application will be a command line client of the web service and is called "skdt" and will accept one argument, a file name. The command line application will PUT the contents of the text file to the /prompt method of the first application.
You may add comments to the script and the generated output but do not add any other text except the bash script.
You may include commands to build the applications but do not run them.
Use .NET 7.
Configuration parameters required for the webapi applicaton will include AzureOpenAIServiceEndpoint, AIServiceKey, AIModel.

View File

@@ -1,128 +0,0 @@
#!/bin/bash
# Create skdtwebapi web service
dotnet new webapi -n skdtwebapi
cd skdtwebapi
# Add required NuGet packages
dotnet add package Microsoft.AspNetCore.OData
dotnet add package Microsoft.Azure.CognitiveServices.Language
# Add configuration parameters to appsettings.json
echo '{
"AzureOpenAIServiceEndpoint": "",
"AIServiceKey": "",
"AIModel": ""
}' > appsettings.json
# Add /prompt and /skills methods to ValuesController.cs
echo 'using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.CognitiveServices.Language.TextAnalytics;
using Microsoft.Extensions.Configuration;
namespace skdtwebapi.Controllers
{
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly IConfiguration _config;
public ValuesController(IConfiguration config)
{
_config = config;
}
[HttpGet("skills")]
public async Task<ActionResult<IEnumerable<string>>> GetSkills()
{
var credentials = new ApiKeyServiceClientCredentials(_config["AIServiceKey"]);
var client = new TextAnalyticsClient(credentials)
{
Endpoint = _config["AzureOpenAIServiceEndpoint"]
};
var result = await client.EntitiesRecognitionGeneralAsync("en", "I am a software developer");
return result.Entities.Select(e => e.Name).ToList();
}
[HttpPut("prompt")]
public async Task<ActionResult> Prompt([FromBody] string prompt)
{
var credentials = new ApiKeyServiceClientCredentials(_config["AIServiceKey"]);
var client = new TextAnalyticsClient(credentials)
{
Endpoint = _config["AzureOpenAIServiceEndpoint"]
};
var result = await client.EntitiesRecognitionGeneralAsync("en", prompt);
return Ok();
}
}
}' > Controllers/ValuesController.cs
# Create skdt command line client
cd ..
dotnet new console -n skdt
cd skdt
# Add required NuGet packages
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Http
dotnet add package Microsoft.Net.Http.Headers
# Add configuration parameters to appsettings.json
echo '{
"WebApiUrl": "https://localhost:5001",
"AIServiceKey": "",
"AIModel": ""
}' > appsettings.json
# Add code to Program.cs to PUT contents of text file to /prompt method
echo 'using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
namespace skdt
{
class Program
{
static async Task Main(string[] args)
{
var serviceProvider = new ServiceCollection()
.AddHttpClient()
.BuildServiceProvider();
var clientFactory = serviceProvider.GetService<IHttpClientFactory>();
var client = clientFactory.CreateClient();
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var fileContents = await File.ReadAllTextAsync(args[0]);
var response = await client.PutAsync($"{configuration["WebApiUrl"]}/prompt", new StringContent(fileContents));
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Prompt sent successfully.");
}
else
{
Console.WriteLine($"Error sending prompt: {response.StatusCode}");
}
}
}
}' > Program.cs

View File

@@ -1,36 +0,0 @@
Calling skill 'PM' function 'Readme' with file 'util/ToDoListSamplePrompt.txt'
{"variables":[{"key":"input","value":"I\u0027d like to build a typical Todo List Application: a simple productivity tool that allows users to create, manage, and track tasks or to-do items. \nKey features of the Todo List application include the ability to add, edit, and delete tasks, set due dates and reminders, categorize tasks by project or priority, and mark tasks as complete. \nThe Todo List applications also offer collaboration features, such as sharing tasks with others or assigning tasks to team members.\nAdditionally, the Todo List application will offer offer mobile and web-based interfaces, allowing users to access their tasks from anywhere."}]}
# Todo List Application
The Todo List Application is a simple productivity tool designed to help users create, manage, and track tasks or to-do items. With both mobile and web-based interfaces, users can access their tasks from anywhere, making it easy to stay organized and on top of their work.
## Features
- **Add, Edit, and Delete Tasks**: Users can easily create new tasks, modify existing ones, and remove tasks when they are no longer needed.
- **Set Due Dates and Reminders**: Users can set due dates for tasks and receive reminders to ensure they stay on track and complete tasks on time.
- **Categorize Tasks**: Users can organize tasks by project or priority, making it easy to focus on what's most important.
- **Mark Tasks as Complete**: Users can mark tasks as complete, providing a sense of accomplishment and helping to track progress.
- **Collaboration Features**: Users can share tasks with others or assign tasks to team members, making it easy to collaborate and work together on projects.
- **Mobile and Web-Based Interfaces**: Users can access their tasks from anywhere, whether they're on their phone or using a web browser.
## Architecture
The Todo List Application is organized into the following components:
- **Frontend**: The frontend is responsible for displaying the user interface and handling user interactions. It is built using a combination of HTML, CSS, and JavaScript, and communicates with the backend through a RESTful API.
- **Backend**: The backend is responsible for processing user requests, managing the database, and handling business logic. It is built using a server-side programming language (e.g., Node.js, Python, or Ruby) and a database management system (e.g., MySQL, PostgreSQL, or MongoDB).
- **Database**: The database stores all the data related to tasks, projects, and users. It is organized into tables or collections, depending on the chosen database management system.
- **API**: The API provides a set of endpoints for the frontend to interact with the backend. It follows the RESTful architecture, using standard HTTP methods (GET, POST, PUT, DELETE) to perform CRUD operations on tasks, projects, and users.
- **Authentication and Authorization**: The application includes user authentication and authorization features, ensuring that only authorized users can access and modify their tasks and projects.
## Running the Application
To run the Todo List Application, follow these steps:
1. Clone the repository to your local machine.
2. Install the required dependencies using the package manager for your chosen programming language (e.g., `npm install` for Node.js, `pip install -r requirements.txt` for Python, or `bundle install` for Ruby).
3. Set up the database by following the instructions provided in the `database_setup.md` file.
4. Start the backend server by running the appropriate command for your chosen programming language (e.g., `node server.js` for Node.js, `python manage.py runserver` for Python, or `rails server` for Ruby).
5. Open the frontend in your web browser by navigating to the provided URL (e.g., `http://localhost:3000`).
For more detailed instructions, refer to the `installation.md` and `usage.md` files included in the repository.

View File

@@ -1,8 +0,0 @@
I'd like to build a typical Chat Message Applicaton: a simple chat app in React Native that allows the user to manage multiple chat threads.
Each thred is a different conversation. The app can look very similar to Apple iMessage.
Key features of the Chat Message application include the ability to create a new chat thread, view the messages in each thread, and to edite and send a new message.
The frontend for the app will be in typescript and React Native, the backend will be in C# as an Azure Function.
The backend will use a mock function to generate random responses. The backend will store the chats in an Azure CosmosDB.
The application will have a setup for local testing and development that allows testing without deployment to Azure.
Use bicep scripts to create the Azure resources.
The front end should be able to run entirely in the browser.

View File

@@ -1,6 +0,0 @@
I'd like to build a typical Todo List Application: a simple productivity tool that allows users to create, manage, and track tasks or to-do items.
Key features of the Todo List application include the ability to add, edit, and delete tasks, set due dates and reminders, categorize tasks by project or priority, and mark tasks as complete.
The Todo List applications also offer collaboration features, such as sharing tasks with others or assigning tasks to team members.
Additionally, the Todo List application will offer offer mobile and web-based interfaces, allowing users to access their tasks from anywhere.
Use C# as the language.
The app needs to be deployed to Azure, be highly performant, cost effective and secure, following the rules of Well Architected Framework.

View File

@@ -1,201 +0,0 @@
{
"skillName": "FunSkill",
"functionName": "Excuses",
"description": "Turn a scenario into a creative or humorous excuse to send your boss",
"filePath": "skills/FunSkill/Excuses/skprompt.txt",
"results": [
{
"completionConfig": {
"label": "gpt-4",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "gpt-4",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "chat-completion",
"chatSystemMessage": "You are an AI assistant that helps people find information."
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "bob"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:bob",
"durationInMilliseconds": 3171.3077,
"inputTokens": 46,
"outputTokens": 24,
"result": "Excuse: Bob was busy teaching a group of squirrels how to knit sweaters for the upcoming winter fashion show.",
"chatCompletionResult": "Excuse: Bob was busy teaching a group of squirrels how to knit sweaters for the upcoming winter fashion show."
}
},
{
"completionConfig": {
"label": "text-davinci-003",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "text-davinci-003",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "text-completion"
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "bob"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:bob",
"durationInMilliseconds": 2507.7456,
"inputTokens": 55,
"outputTokens": 26,
"result": " didn't show up to work\nExcuse:Bob was abducted by aliens and they needed him to help them fix their spaceship.",
"textCompletionResult": " didn't show up to work\nExcuse:Bob was abducted by aliens and they needed him to help them fix their spaceship."
}
},
{
"completionConfig": {
"label": "text-davinci-003",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "text-davinci-003",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "text-completion"
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "bob"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:bob",
"durationInMilliseconds": 1427.7162,
"inputTokens": 55,
"outputTokens": 26,
"result": " didn't show up to work\nExcuse:Bob was abducted by aliens and they needed him to help them fix their spaceship.",
"textCompletionResult": " didn't show up to work\nExcuse:Bob was abducted by aliens and they needed him to help them fix their spaceship."
}
},
{
"completionConfig": {
"label": "gpt-4",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "gpt-4",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "text-completion"
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "bob"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:bob",
"durationInMilliseconds": 725.1863,
"inputTokens": 46,
"outputTokens": 1,
"result": "bob",
"textCompletionResult": "bob"
}
},
{
"completionConfig": {
"label": "gpt-4",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "gpt-4",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "text-completion"
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "I'm in pain"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:I'm in pain",
"durationInMilliseconds": 725.0451,
"inputTokens": 48,
"outputTokens": 4,
"result": "I'm in pain",
"textCompletionResult": "I'm in pain"
}
},
{
"completionConfig": {
"label": "gpt-4",
"aiService": "AzureOpenAI",
"deploymentOrModelId": "gpt-4",
"endpoint": "https://lightspeed-team-shared-openai-eastus.openai.azure.com/",
"useCustomEndpoint": false,
"endpointType": "text-completion"
},
"requestSettings": {
"temperature": 0,
"topP": 0,
"presencePenalty": 0,
"frequencyPenalty": 0,
"maxTokens": 0,
"stopSequences": []
},
"variables": [
{
"key": "input",
"value": "I'm in pain"
}
],
"output": {
"prompt": "Generate a creative reason or excuse for the given event. Be creative and be funny. Let your imagination run wild.\n\nEvent:I am running late.\nExcuse:I was being held ransom by giraffe gangsters. \n\nEvent:I'm in pain",
"durationInMilliseconds": 1296.1323,
"inputTokens": 48,
"outputTokens": 4,
"result": "I'm in pain",
"textCompletionResult": "I'm in pain"
}
}
]
}

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-dotnettools.csharp",
"ms-semantic-kernel.semantic-kernel"
]
}

View File

@@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to .NET Functions",
"type": "coreclr",
"request": "attach",
"processId": "${command:azureFunctions.pickProcess}"
}
]
}

View File

@@ -1,7 +0,0 @@
{
"azureFunctions.deploySubpath": "bin/Release/net6.0/publish",
"azureFunctions.projectLanguage": "C#",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "publish (functions)"
}

View File

@@ -1,105 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "clean (functions)",
"command": "dotnet",
"args": [
"clean",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile"
},
{
"label": "build (functions)",
"command": "dotnet",
"args": [
"build",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean (functions)",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "clean release (functions)",
"command": "dotnet",
"args": [
"clean",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"problemMatcher": "$msCompile"
},
{
"label": "publish (functions)",
"command": "dotnet",
"args": [
"publish",
"--configuration",
"Release",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"type": "process",
"dependsOn": "clean release (functions)",
"problemMatcher": "$msCompile"
},
{
"type": "func",
"dependsOn": "build (functions)",
"options": {
"cwd": "${workspaceFolder}/bin/Debug/net7.0"
},
"command": "host start",
"isBackground": true,
"problemMatcher": "$func-dotnet-watch"
},
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/sk-csharp-azure-functions.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/sk-csharp-azure-functions.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/sk-csharp-azure-functions.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -1,76 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using Octokit;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class IssuesActivities
{
private readonly GithubService _ghService;
public IssuesActivities(GithubService githubService)
{
_ghService = githubService;
}
[Function(nameof(CreateIssue))]
public async Task<NewIssueResponse> CreateIssue([ActivityTrigger] NewIssueRequest request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var newIssue = new NewIssue($"{request.Function} chain for #{request.IssueRequest.Number}")
{
Body = request.IssueRequest.Input,
};
newIssue.Labels.Add($"{request.Skill}.{request.Function}");
var issue = await ghClient.Issue.Create(request.IssueRequest.Org, request.IssueRequest.Repo, newIssue);
var commentBody = $" - [ ] #{issue.Number} - tracks {request.Skill}.{request.Function}";
var comment = await ghClient.Issue.Comment.Create(request.IssueRequest.Org, request.IssueRequest.Repo, (int)request.IssueRequest.Number, commentBody);
return new NewIssueResponse
{
Number = issue.Number,
CommentId = comment.Id
};
}
[Function("CloseSubOrchestration")]
public async Task Close(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "close")] HttpRequestData req,
[DurableClient] DurableTaskClient client)
{
var request = await req.ReadFromJsonAsync<CloseIssueRequest>();
var ghClient = await _ghService.GetGitHubClient();
var comment = await ghClient.Issue.Comment.Get(request.Org, request.Repo, request.CommentId);
var updatedComment = comment.Body.Replace("[ ]", "[x]");
await ghClient.Issue.Comment.Update(request.Org, request.Repo, request.CommentId, updatedComment);
await client.RaiseEventAsync(request.InstanceId, SubIssueOrchestration.IssueClosed, true);
}
[Function(nameof(GetLastComment))]
public async Task<string> GetLastComment([ActivityTrigger] IssueOrchestrationRequest request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var icOptions = new IssueCommentRequest
{
Direction = SortDirection.Descending
};
var apiOptions = new ApiOptions
{
PageCount = 1,
PageSize = 1,
StartPage = 1
};
var comments = await ghClient.Issue.Comment.GetAllForIssue(request.Org, request.Repo, (int)request.Number, icOptions, apiOptions);
return comments.First().Body;
}
}
}

View File

@@ -1,44 +0,0 @@
using System.Runtime.CompilerServices;
using System.Text.Json;
using Azure.Data.Tables;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class MetadataActivities
{
[Function(nameof(GetMetadata))]
public static async Task<IssueMetadata> GetMetadata(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "metadata/{key}")] HttpRequestData req, string key,
[TableInput("Metadata", Connection = "AzureWebJobsStorage")] TableClient client,
FunctionContext executionContext)
{
var logger = executionContext.GetLogger<SKWebHookEventProcessor>();
logger.LogInformation($"Getting metadata for {key}");
var metadataResponse = await client.GetEntityAsync<IssueMetadata>(key, key);
var metadata = metadataResponse.Value;
logger.LogInformation($"Metadata result {JsonSerializer.Serialize(metadata)}");
return metadata;
}
[Function(nameof(SaveMetadata))]
public static async Task<IssueMetadata> SaveMetadata(
[ActivityTrigger] IssueMetadata metadata,
[TableInput("Metadata", Connection = "AzureWebJobsStorage")] TableClient client,
FunctionContext executionContext)
{
await client.UpsertEntityAsync(metadata);
return metadata;
}
}
}

View File

@@ -1,239 +0,0 @@
using System.Text;
using Azure;
using Azure.Core;
using Azure.Data.Tables;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.ContainerInstance;
using Azure.ResourceManager.ContainerInstance.Models;
using Azure.ResourceManager.Resources;
using Azure.Storage.Files.Shares;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Octokit;
using Octokit.Helpers;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class PullRequestActivities
{
private readonly AzureOptions _azSettings;
private readonly GithubService _ghService;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<PullRequestActivities> logger;
public PullRequestActivities(IOptions<AzureOptions> azOptions, GithubService ghService, IHttpClientFactory httpClientFactory, ILogger<PullRequestActivities> logger)
{
_azSettings = azOptions.Value;
_ghService = ghService;
_httpClientFactory = httpClientFactory;
this.logger = logger;
}
[Function(nameof(SaveOutput))]
public async Task<bool> SaveOutput([ActivityTrigger] SaveOutputRequest request, FunctionContext executionContext)
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var parentDirName = $"{request.Directory}/{request.IssueOrchestrationId}";
var fileName = $"{request.FileName}.{request.Extension}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
await share.CreateIfNotExistsAsync();
await share.GetDirectoryClient($"{request.Directory}").CreateIfNotExistsAsync();
var parentDir = share.GetDirectoryClient(parentDirName);
await parentDir.CreateIfNotExistsAsync();
var directory = parentDir.GetSubdirectoryClient(request.SubOrchestrationId);
await directory.CreateIfNotExistsAsync();
var file = directory.GetFileClient(fileName);
// hack to enable script to save files in the same directory
var cwdHack = "#!/bin/bash\n cd $(dirname $0)";
var output = request.Extension == "sh" ? request.Output.Replace("#!/bin/bash", cwdHack) : request.Output;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(output)))
{
await file.CreateAsync(stream.Length);
await file.UploadRangeAsync(
new HttpRange(0, stream.Length),
stream);
}
return true;
}
[Function(nameof(CreateBranch))]
public async Task<bool> CreateBranch([ActivityTrigger] GHNewBranch request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var repo = await ghClient.Repository.Get(request.Org, request.Repo);
await ghClient.Git.Reference.CreateBranch(request.Org, request.Repo, request.Branch, repo.DefaultBranch);
return true;
}
[Function(nameof(CreatePR))]
public async Task<bool> CreatePR([ActivityTrigger] GHNewBranch request, FunctionContext executionContext)
{
var ghClient = await _ghService.GetGitHubClient();
var repo = await ghClient.Repository.Get(request.Org, request.Repo);
await ghClient.PullRequest.Create(request.Org, request.Repo, new NewPullRequest($"New app #{request.Number}", request.Branch, repo.DefaultBranch));
return true;
}
[Function(nameof(RunInSandbox))]
public async Task<bool> RunInSandbox(
[ActivityTrigger] RunInSandboxRequest request,
[TableInput("ContainersMetadata", Connection = "AzureWebJobsStorage")] TableClient tableClient,
FunctionContext executionContext)
{
var client = new ArmClient(new DefaultAzureCredential());
var containerGroupName = $"sk-sandbox-{request.PrRequest.SubOrchestrationId}";
var containerName = $"sk-sandbox-{request.PrRequest.SubOrchestrationId}";
var image = Environment.GetEnvironmentVariable("SANDBOX_IMAGE", EnvironmentVariableTarget.Process);
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var scriptPath = $"/azfiles/output/{request.PrRequest.IssueOrchestrationId}/{request.PrRequest.SubOrchestrationId}/run.sh";
var collection = resourceGroupResource.GetContainerGroups();
var data = new ContainerGroupData(new AzureLocation(_azSettings.Location), new ContainerInstanceContainer[]
{
new ContainerInstanceContainer(containerName,image,new ContainerResourceRequirements(new ContainerResourceRequestsContent(1.5,1)))
{
Command = { "/bin/bash", $"{scriptPath}" },
VolumeMounts =
{
new ContainerVolumeMount("azfiles","/azfiles/")
{
IsReadOnly = false,
}
},
}}, ContainerInstanceOperatingSystemType.Linux)
{
Volumes =
{
new ContainerVolume("azfiles")
{
AzureFile = new ContainerInstanceAzureFileVolume(_azSettings.FilesShareName,_azSettings.FilesAccountName)
{
StorageAccountKey = _azSettings.FilesAccountKey
},
},
},
RestartPolicy = ContainerGroupRestartPolicy.Never,
Sku = ContainerGroupSku.Standard,
Priority = ContainerGroupPriority.Regular
};
await collection.CreateOrUpdateAsync(WaitUntil.Completed, containerGroupName, data);
var metadata = new ContainerInstanceMetadata
{
PartitionKey = containerGroupName,
RowKey = containerGroupName,
SubOrchestrationId = request.SanboxOrchestrationId,
Processed = false
};
await tableClient.UpsertEntityAsync(metadata);
return true;
}
[Function(nameof(CommitToGithub))]
public async Task<bool> CommitToGithub([ActivityTrigger] GHCommitRequest request, FunctionContext executionContext)
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var ghClient = await _ghService.GetGitHubClient();
var dirName = $"{request.Directory}/{request.IssueOrchestrationId}/{request.SubOrchestrationId}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
var directory = share.GetDirectoryClient(dirName);
var remaining = new Queue<ShareDirectoryClient>();
remaining.Enqueue(directory);
while (remaining.Count > 0)
{
var dir = remaining.Dequeue();
await foreach (var item in dir.GetFilesAndDirectoriesAsync())
{
if (!item.IsDirectory && item.Name != "run.sh") // we don't want the generated script in the PR
{
try
{
var file = dir.GetFileClient(item.Name);
var filePath = file.Path.Replace($"{_azSettings.FilesShareName}/", "")
.Replace($"{dirName}/", "");
var fileStream = await file.OpenReadAsync();
using (var reader = new StreamReader(fileStream, Encoding.UTF8))
{
var value = reader.ReadToEnd();
await ghClient.Repository.Content.CreateFile(
request.Org, request.Repo, filePath,
new CreateFileRequest($"Commit message", value, request.Branch)); // TODO: add more meaningfull commit message
}
}
catch (Exception ex)
{
logger.LogError(ex, $"Error while uploading file {item.Name}");
}
}
else if (item.IsDirectory)
{
remaining.Enqueue(dir.GetSubdirectoryClient(item.Name));
}
}
}
return true;
}
[Function(nameof(Terminated))]
public async Task<ContainerInstanceMetadata> Terminated(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "container/{name}/terminate")] HttpRequestData req, string name,
[TableInput("ContainersMetadata", Connection = "AzureWebJobsStorage")] TableClient tableClient,
[DurableClient] DurableTaskClient client)
{
var metadataResponse = await tableClient.GetEntityAsync<ContainerInstanceMetadata>(name, name);
var metadata = metadataResponse.Value;
if (!metadata.Processed)
{
await client.RaiseEventAsync(metadata.SubOrchestrationId, SubIssueOrchestration.ContainerTerminated, true);
metadata.Processed = true;
await tableClient.UpdateEntityAsync(metadata, metadata.ETag, TableUpdateMode.Replace);
}
return metadata;
}
[Function(nameof(CleanContainers))]
public async Task CleanContainers(
[TimerTrigger("*/30 * * * * *")] TimerInfo myTimer,
FunctionContext executionContext)
{
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
var client = new ArmClient(new DefaultAzureCredential());
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
foreach (var cg in collection.GetAll())
{
var c = await cg.GetAsync();
if (c.Value.Data.ProvisioningState == "Succeeded"
&& c.Value.Data.Containers.First().InstanceView.CurrentState.State == "Terminated")
{
await cg.DeleteAsync(WaitUntil.Started);
await httpClient.PostAsync($"container/{cg.Data.Name}/terminate", default);
}
}
}
}
}

View File

@@ -1,33 +0,0 @@
<Project>
<PropertyGroup>
<!-- Default properties inherited by all projects. Projects can override. -->
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>11</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<NoWarn>CS1591,CA1852,CA1050</NoWarn>
</PropertyGroup>
<PropertyGroup>
<!-- Disable NuGet packaging by default. Projects can override. -->
<IsPackable>disable</IsPackable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup>
<RepoRoot>$([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))</RepoRoot>
</PropertyGroup>
</Project>

View File

@@ -1,25 +0,0 @@
<Project>
<!-- Direct all packages under 'dotnet' to get versions from Directory.Packages.props -->
<!-- using Central Package Management feature -->
<!-- https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
<Sdk Name="Microsoft.Build.CentralPackageVersions" Version="2.1.3" />
<!-- Only run 'dotnet format' on dev machines, Release builds. Skip on GitHub Actions -->
<!-- as this runs in its own Actions job. -->
<Target Name="DotnetFormatOnBuild" BeforeTargets="Build"
Condition=" '$(Configuration)' == 'Release' AND '$(GITHUB_ACTIONS)' == '' ">
<Message Text="Running dotnet format" Importance="high" />
<Exec Command="dotnet format --no-restore -v diag $(ProjectFileName)" />
</Target>
<Target Name="AddInternalsVisibleTo" BeforeTargets="BeforeCompile">
<!-- Handle Add InternalsVisibleTo to any targets that don't support it. -->
<ItemGroup Condition="'@(InternalsVisibleTo->Count())' &gt; 0 AND $([MSBuild]::VersionLessThan($(NETCoreSdkVersion), '5.0.0'))">
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1 Condition="'%(InternalsVisibleTo.PublicKey)' != ''">%(InternalsVisibleTo.Identity), PublicKey="%(InternalsVisibleTo.PublicKey)</_Parameter1>
<_Parameter1 Condition="'%(InternalsVisibleTo.PublicKey)' == ''">%(InternalsVisibleTo.Identity)</_Parameter1>
<_Parameter1_TypeName>System.String</_Parameter1_TypeName>
</AssemblyAttribute>
</ItemGroup>
</Target>
</Project>

View File

@@ -1,10 +0,0 @@
public class AddToPRRequest
{
public string Output { get; set; }
public string IssueOrchestrationId { get; set; }
public string SubOrchestrationId { get; set; }
public string PrSubOrchestrationId { get; set; }
public string Extension { get; set; }
public bool RunInSandbox { get; set; }
public IssueOrchestrationRequest Request { get; set; }
}

View File

@@ -1,7 +0,0 @@
public class CloseIssueRequest
{
public string InstanceId { get; set; }
public int CommentId { get; set; }
public string Org { get; set; }
public string Repo { get; set; }
}

View File

@@ -1,12 +0,0 @@
using Azure;
using Azure.Data.Tables;
public class ContainerInstanceMetadata : ITableEntity
{
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string SubOrchestrationId { get; set; }
public bool Processed { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}

View File

@@ -1,17 +0,0 @@
public class Subtask
{
public string subtask { get; set; }
public string prompt { get; set; }
}
public class Step
{
public string description { get; set; }
public string step { get; set; }
public List<Subtask> subtasks { get; set; }
}
public class DevLeadPlanResponse
{
public List<Step> steps { get; set; }
}

View File

@@ -1,12 +0,0 @@
namespace SK.DevTeam
{
public class GHCommitRequest
{
public object IssueOrchestrationId { get; set; }
public object SubOrchestrationId { get; set; }
public string Org { get; set; }
public string Repo { get; set; }
public object Directory { get; set; }
public string Branch { get; set; }
}
}

View File

@@ -1,10 +0,0 @@
namespace SK.DevTeam
{
public class GHNewBranch
{
public string Org { get; set; }
public string Repo { get; set; }
public string Branch { get; set; }
public object Number { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
using Azure;
using Azure.Data.Tables;
public class IssueMetadata : ITableEntity
{
public long? Number { get; set; }
public int? CommentId { get; set; }
public string? InstanceId { get; set; }
public string? Id { get; set; }
public string? Org { get; set; }
public string? Repo { get; set; }
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public DateTimeOffset? Timestamp { get; set; }
public ETag ETag { get; set; }
}

View File

@@ -1,8 +0,0 @@
public class IssueOrchestrationRequest
{
public string Org { get; set; }
public string Repo { get; set; }
public long Number { get; set; }
public string Input { get; set; }
public string Branch => $"sk-{Number}";
}

View File

@@ -1,6 +0,0 @@
public class NewIssueRequest
{
public IssueOrchestrationRequest IssueRequest { get; set; }
public string Skill { get; set; }
public string Function { get; set; }
}

View File

@@ -1,5 +0,0 @@
public class NewIssueResponse
{
public long Number { get; set; }
public int CommentId { get; set; }
}

View File

@@ -1,6 +0,0 @@
public class RunAndSaveRequest
{
public IssueOrchestrationRequest Request { get; set; }
public string InstanceId { get; set; }
}

View File

@@ -1,8 +0,0 @@
namespace SK.DevTeam
{
public class RunInSandboxRequest
{
public AddToPRRequest PrRequest { get; set; }
public string SanboxOrchestrationId { get; set; }
}
}

View File

@@ -1,12 +0,0 @@
namespace SK.DevTeam
{
public class SaveOutputRequest
{
public string IssueOrchestrationId { get; set; }
public string SubOrchestrationId { get; set; }
public string Output { get; set; }
public string Extension { get; set; }
public string Directory { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
namespace SK.DevTeam
{
public class SkillRequest
{
public IssueOrchestrationRequest IssueRequest { get; set; }
public string Skill { get; set; }
public string Function { get; set; }
}
}

View File

@@ -1,5 +0,0 @@
public class SkillResponse<T>
{
public T Output { get; set; }
public string SuborchestrationId { get; set; }
}

View File

@@ -1,75 +0,0 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.Extensions.Logging;
using static SK.DevTeam.SubIssueOrchestration;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class IssueOrchestration
{
[Function("IssueOrchestrationStart")]
public static async Task<string> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "doit")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
ILogger logger = executionContext.GetLogger("IssueOrchestration_HttpStart");
var request = await req.ReadFromJsonAsync<IssueOrchestrationRequest>();
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
nameof(IssueOrchestration), request);
logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId);
return "";
}
[Function(nameof(IssueOrchestration))]
public static async Task<List<string>> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
var logger = context.CreateReplaySafeLogger(nameof(IssueOrchestration));
var outputs = new List<string>();
var newGHBranchRequest = new GHNewBranch
{
Org = request.Org,
Repo = request.Repo,
Branch = request.Branch,
Number = request.Number
};
var newBranch = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CreateBranch), newGHBranchRequest);
var readmeTask = await context.CallSubOrchestratorAsync<bool>(nameof(ReadmeAndSave), new RunAndSaveRequest
{
Request = request,
InstanceId = context.InstanceId
});
var newPR = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CreatePR), newGHBranchRequest);
var planTask = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(CreatePlan), request);
var plan = JsonSerializer.Deserialize<DevLeadPlanResponse>(planTask.Output);
var implementationTasks = plan.steps.SelectMany(s => s.subtasks.Select(st =>
context.CallSubOrchestratorAsync<bool>(nameof(ImplementAndSave), new RunAndSaveRequest
{
Request = new IssueOrchestrationRequest
{
Number = request.Number,
Org = request.Org,
Repo = request.Repo,
Input = st.prompt,
},
InstanceId = context.InstanceId
})));
await Task.WhenAll(implementationTasks);
return outputs;
}
}
}

View File

@@ -1,154 +0,0 @@
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
namespace SK.DevTeam
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public static class SubIssueOrchestration
{
public static string IssueClosed = "IssueClosed";
public static string ContainerTerminated = "ContainerTerminated";
private static async Task<SkillResponse<string>> CallSkill(TaskOrchestrationContext context, SkillRequest request)
{
var newIssueResponse = await context.CallActivityAsync<NewIssueResponse>(nameof(IssuesActivities.CreateIssue), new NewIssueRequest
{
IssueRequest = request.IssueRequest,
Skill = request.Skill,
Function = request.Function
});
var metadata = await context.CallActivityAsync<IssueMetadata>(nameof(MetadataActivities.SaveMetadata), new IssueMetadata
{
Number = newIssueResponse.Number,
InstanceId = context.InstanceId,
Id = Guid.NewGuid().ToString(),
CommentId = newIssueResponse.CommentId,
Org = request.IssueRequest.Org,
Repo = request.IssueRequest.Repo,
PartitionKey = $"{request.IssueRequest.Org}{request.IssueRequest.Repo}{newIssueResponse.Number}",
RowKey = $"{request.IssueRequest.Org}{request.IssueRequest.Repo}{newIssueResponse.Number}",
Timestamp = DateTimeOffset.UtcNow
});
bool issueClosed = await context.WaitForExternalEvent<bool>(IssueClosed);
var lastComment = await context.CallActivityAsync<string>(nameof(IssuesActivities.GetLastComment), new IssueOrchestrationRequest
{
Org = request.IssueRequest.Org,
Repo = request.IssueRequest.Repo,
Number = newIssueResponse.Number
});
return new SkillResponse<string> { Output = lastComment, SuborchestrationId = context.InstanceId };
}
[Function(nameof(CreateReadme))]
public static async Task<SkillResponse<string>> CreateReadme(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(PM),
Function = nameof(PM.Readme)
});
}
[Function(nameof(CreatePlan))]
public static async Task<SkillResponse<string>> CreatePlan(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(DevLead),
Function = nameof(DevLead.Plan)
});
}
[Function(nameof(Implement))]
public static async Task<SkillResponse<string>> Implement(
[OrchestrationTrigger] TaskOrchestrationContext context, IssueOrchestrationRequest request)
{
return await CallSkill(context, new SkillRequest
{
IssueRequest = request,
Skill = nameof(Developer),
Function = nameof(Developer.Implement)
});
}
[Function(nameof(ImplementAndSave))]
public static async Task<bool> ImplementAndSave(
[OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request)
{
var implementResult = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(Implement), request.Request);
await context.CallSubOrchestratorAsync<string>(nameof(AddToPR), new AddToPRRequest
{
Output = implementResult.Output,
IssueOrchestrationId = request.InstanceId,
SubOrchestrationId = implementResult.SuborchestrationId,
Extension = "sh",
RunInSandbox = true,
Request = request.Request
});
return true;
}
[Function(nameof(ReadmeAndSave))]
public static async Task<bool> ReadmeAndSave(
[OrchestrationTrigger] TaskOrchestrationContext context, RunAndSaveRequest request)
{
var readmeResult = await context.CallSubOrchestratorAsync<SkillResponse<string>>(nameof(CreateReadme), request.Request);
context.CallSubOrchestratorAsync<string>(nameof(AddToPR), new AddToPRRequest
{
Output = readmeResult.Output,
IssueOrchestrationId = request.InstanceId,
SubOrchestrationId = readmeResult.SuborchestrationId,
Extension = "md",
RunInSandbox = false,
Request = request.Request
});
return true;
}
[Function(nameof(AddToPR))]
public static async Task<string> AddToPR(
[OrchestrationTrigger] TaskOrchestrationContext context, AddToPRRequest request)
{
var saveScriptResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.SaveOutput), new SaveOutputRequest
{
Output = request.Output,
IssueOrchestrationId = request.IssueOrchestrationId,
SubOrchestrationId = request.SubOrchestrationId,
Extension = request.Extension,
Directory = "output",
FileName = request.RunInSandbox ? "run" : "readme"
});
if (request.RunInSandbox)
{
var newRequest = new RunInSandboxRequest
{
PrRequest = request,
SanboxOrchestrationId = context.InstanceId
};
var runScriptResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.RunInSandbox), newRequest);
bool containerTerminated = await context.WaitForExternalEvent<bool>(ContainerTerminated);
}
// this is not ideal, as the script might be still running and there might be files that are not yet generated
var commitResponse = await context.CallActivityAsync<bool>(nameof(PullRequestActivities.CommitToGithub), new GHCommitRequest
{
IssueOrchestrationId = request.IssueOrchestrationId,
SubOrchestrationId = request.SubOrchestrationId,
Directory = "output",
Org = request.Request.Org,
Repo = request.Request.Repo,
Branch = request.Request.Branch
});
return default;
}
}
}

View File

@@ -1,113 +0,0 @@
using System.Text.Json;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding;
using Microsoft.SemanticKernel.Connectors.Memory.Qdrant;
using Microsoft.SemanticKernel.Memory;
using Octokit.Webhooks;
using Octokit.Webhooks.AzureFunctions;
namespace KernelHttpServer;
public static class Program
{
public static void Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureGitHubWebhooks()
.ConfigureAppConfiguration(configuration =>
{
var config = configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
var builtConfig = config.Build();
})
.ConfigureServices(services =>
{
services.AddTransient((provider) => CreateKernel(provider));
services.AddScoped<GithubService>();
services.AddScoped<WebhookEventProcessor, SKWebHookEventProcessor>();
services.AddOptions<GithubOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("GithubOptions").Bind(settings);
});
services.AddOptions<AzureOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("AzureOptions").Bind(settings);
});
services.AddOptions<OpenAIOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("OpenAIOptions").Bind(settings);
});
services.AddOptions<QdrantOptions>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("QdrantOptions").Bind(settings);
});
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// return JSON with expected lowercase naming
services.Configure<JsonSerializerOptions>(options =>
{
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
services.AddHttpClient("FunctionsClient", client =>
{
var fqdn = Environment.GetEnvironmentVariable("FUNCTIONS_FQDN", EnvironmentVariableTarget.Process);
client.BaseAddress = new Uri($"{fqdn}/api/");
});
})
.ConfigureLogging(logging =>
{
logging.Services.Configure<LoggerFilterOptions>(options =>
{
LoggerFilterRule defaultRule = options.Rules.FirstOrDefault(rule => rule.ProviderName
== "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");
if (defaultRule is not null)
{
options.Rules.Remove(defaultRule);
}
});
})
.Build();
host.Run();
}
private static IKernel CreateKernel(IServiceProvider provider)
{
var openAiConfig = provider.GetService<IOptions<OpenAIOptions>>().Value;
var qdrantConfig = provider.GetService<IOptions<QdrantOptions>>().Value;
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(LogLevel.Debug)
.AddConsole()
.AddDebug();
});
var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient(qdrantConfig.Endpoint, qdrantConfig.VectorSize));
var embedingGeneration = new AzureTextEmbeddingGeneration(openAiConfig.EmbeddingDeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey);
var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration);
return new KernelBuilder()
.WithLoggerFactory(loggerFactory)
.WithAzureChatCompletionService(openAiConfig.DeploymentOrModelId, openAiConfig.Endpoint, openAiConfig.ApiKey, true, openAiConfig.ServiceId, true)
.WithMemory(semanticTextMemory)
.Build();
}
}

View File

@@ -1,5 +0,0 @@
# Demo
## How do I get started?
Check - [Getting started](../docs/github-flow-getting-started.md)

View File

@@ -1,36 +0,0 @@
using Microsoft.Extensions.Options;
using Octokit;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class GithubService
{
private readonly GithubOptions _githubSettings;
public GithubService(IOptions<GithubOptions> ghOptions)
{
_githubSettings = ghOptions.Value;
}
public async Task<GitHubClient> GetGitHubClient()
{
// Use GitHubJwt library to create the GitHubApp Jwt Token using our private certificate PEM file
var generator = new GitHubJwt.GitHubJwtFactory(
new GitHubJwt.StringPrivateKeySource(_githubSettings.AppKey),
new GitHubJwt.GitHubJwtFactoryOptions
{
AppIntegrationId = _githubSettings.AppId, // The GitHub App Id
ExpirationSeconds = 600 // 10 minutes is the maximum time allowed
}
);
var jwtToken = generator.CreateEncodedJwtToken();
var appClient = new GitHubClient(new ProductHeaderValue("SK-DEV-APP"))
{
Credentials = new Credentials(jwtToken, AuthenticationType.Bearer)
};
var response = await appClient.GitHubApps.CreateInstallationToken(_githubSettings.InstallationId);
return new GitHubClient(new ProductHeaderValue($"SK-DEV-APP-Installation{_githubSettings.InstallationId}"))
{
Credentials = new Credentials(response.Token)
};
}
}

View File

@@ -1,117 +0,0 @@
using System.Net.Http.Json;
using System.Text;
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Orchestration;
using Newtonsoft.Json;
using Octokit.Webhooks;
using Octokit.Webhooks.Events;
using Octokit.Webhooks.Events.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2007: Do not directly await a Task", Justification = "Durable functions")]
public class SKWebHookEventProcessor : WebhookEventProcessor
{
private readonly IKernel _kernel;
private readonly ILogger<SKWebHookEventProcessor> _logger;
private readonly GithubService _ghService;
private readonly IHttpClientFactory _httpClientFactory;
public SKWebHookEventProcessor(IKernel kernel, ILogger<SKWebHookEventProcessor> logger, GithubService ghService, IHttpClientFactory httpContextFactory)
{
_kernel = kernel;
_logger = logger;
_ghService = ghService;
_httpClientFactory = httpContextFactory;
}
protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action)
{
var ghClient = await _ghService.GetGitHubClient();
var org = issuesEvent.Organization.Login;
var repo = issuesEvent.Repository.Name;
var issueNumber = issuesEvent.Issue.Number;
var input = issuesEvent.Issue.Body;
if (issuesEvent.Action == IssuesAction.Opened)
{
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issuesEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
if (skillName == "Do" && functionName == "It")
{
var issueOrchestrationRequest = new IssueOrchestrationRequest
{
Number = issueNumber,
Org = org,
Repo = repo,
Input = input
};
var content = new StringContent(JsonConvert.SerializeObject(issueOrchestrationRequest), Encoding.UTF8, "application/json");
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
await httpClient.PostAsync("doit", content);
}
else
{
var result = await RunSkill(skillName, functionName, input);
await ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, result);
}
}
else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot)
{
var httpClient = _httpClientFactory.CreateClient("FunctionsClient");
var metadata = await httpClient.GetFromJsonAsync<IssueMetadata>($"metadata/{org}{repo}{issueNumber}");
var closeIssueRequest = new CloseIssueRequest { InstanceId = metadata.InstanceId, CommentId = metadata.CommentId.Value, Org = org, Repo = repo };
var content = new StringContent(JsonConvert.SerializeObject(closeIssueRequest), Encoding.UTF8, "application/json");
_ = await httpClient.PostAsync("close", content);
}
}
protected override async Task ProcessIssueCommentWebhookAsync(
WebhookHeaders headers,
IssueCommentEvent issueCommentEvent,
IssueCommentAction action)
{
// we only resond to non-bot comments
if (issueCommentEvent.Sender.Type.Value != UserType.Bot)
{
var ghClient = await _ghService.GetGitHubClient();
var org = issueCommentEvent.Organization.Login;
var repo = issueCommentEvent.Repository.Name;
var issueId = issueCommentEvent.Issue.Number;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issueCommentEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
var input = issueCommentEvent.Comment.Body;
var result = await RunSkill(skillName, functionName, input);
await ghClient.Issue.Comment.Create(org, repo, (int)issueId, result);
}
}
private async Task<string> RunSkill(string skillName, string functionName, string input)
{
var prompt = Skills.ForSkillAndFunction(skillName, functionName);
var function = _kernel.CreateSemanticFunction(prompt, new OpenAIRequestSettings { MaxTokens = 8000, Temperature = 0.4, TopP = 1 });
var interestingMemories = _kernel.Memory.SearchAsync("waf-pages", input, 2);
var wafContext = "Consider the following architectural guidelines:";
await foreach (var memory in interestingMemories)
{
wafContext += $"\n {memory.Metadata.Text}";
}
var context = new ContextVariables();
context.Set("input", input);
context.Set("wafContext", wafContext);
var result = await _kernel.RunAsync(context, function);
return result.ToString();
}
}

View File

@@ -1,9 +0,0 @@
public class AzureOptions
{
public string SubscriptionId { get; set; }
public string Location { get; set; }
public string ContainerInstancesResourceGroup { get; set; }
public string FilesShareName { get; set; }
public string FilesAccountName { get; set; }
public string FilesAccountKey { get; set; }
}

View File

@@ -1,6 +0,0 @@
public class GithubOptions
{
public string AppKey { get; set; }
public int AppId { get; set; }
public long InstallationId { get; set; }
}

View File

@@ -1,9 +0,0 @@
public class OpenAIOptions
{
public string ServiceType { get; set; }
public string ServiceId { get; set; }
public string DeploymentOrModelId { get; set; }
public string EmbeddingDeploymentOrModelId { get; set; }
public string Endpoint { get; set; }
public string ApiKey { get; set; }
}

View File

@@ -1,5 +0,0 @@
public class QdrantOptions
{
public string Endpoint { get; set; }
public int VectorSize { get; set; }
}

View File

@@ -1,57 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RepoRoot>$([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)'))))</RepoRoot>
<UserSecretsId>b4703849-9b3a-41c3-b1bb-5ede533963c4</UserSecretsId>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace></RootNamespace>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.0.2" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.0.0-preview4" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.14.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
<PackageReference Include="Octokit.Webhooks.AzureFunctions" Version="2.0.3" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.0.0-beta5" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="1.0.0-beta5" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage" Version="5.0.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Tables" Version="1.2.0" />
<PackageReference Include="Octokit" Version="7.1.0" />
<PackageReference Include="GitHubJwt" Version="0.0.6" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0-beta.1" />
<PackageReference Include="Azure.Storage.Files.Shares" Version="12.16.0-beta.1" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.1" />
<PackageReference Include="Azure.Identity" Version="1.10.0-beta.1" />
<ProjectReference Include="..\..\libs\Microsoft.AI.DevTeam.Skills\Microsoft.AI.DevTeam.Skills.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
<None Update="config\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,18 +0,0 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensions": {
"durableTask": {
"storageProvider": {
"type": "AzureStorage"
}
}
}
}

View File

@@ -1,31 +0,0 @@
{
"IsEncrypted": false,
"Host": {
"CORS": "*"
},
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"FUNCTIONS_FQDN": "http://localhost:7071",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsFeatureFlags": "EnableHttpProxying",
"SANDBOX_IMAGE" : "mcr.microsoft.com/dotnet/sdk:7.0",
"GithubOptions:AppKey": "",
"GithubOptions:AppId": "",
"GithubOptions:InstallationId": "",
"AzureOptions:SubscriptionId":"",
"AzureOptions:Location":"",
"AzureOptions:ContainerInstancesResourceGroup":"",
"AzureOptions:FilesShareName":"",
"AzureOptions:FilesAccountName":"",
"AzureOptions:FilesAccountKey":"",
"OpenAIOptions:ServiceType":"",
"OpenAIOptions:ServiceId":"",
"OpenAIOptions:DeploymentOrModelId":"",
"OpenAIOptions:EmbeddingDeploymentOrModelId":"",
"OpenAIOptions:Endpoint":"",
"OpenAIOptions:ApiKey":"",
"QdrantOptions:Endpoint":"http://qdrant:6333",
"QdrantOptions:VectorSize":"1536"
}
}

View File

@@ -1,52 +0,0 @@
{
"IsEncrypted": false,
"Host": {
"CORS": "*"
},
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
// For local development, keep the default value
// for Azure deployment, it will be injected as a variable in the bicep template
"FUNCTIONS_FQDN": "localhost:7071",
// For local development, keep the default value
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"AzureWebJobsFeatureFlags": "EnableHttpProxying",
// This is the container image used as a base for the sandbox
"SANDBOX_IMAGE" : "mcr.microsoft.com/devcontainers/universal:2-linux",
// The private key generated for the Github App
"GithubOptions__AppKey": "",
// The App Id for the Github App
"GithubOptions__AppId": "",
// The instalation ID for the Github App (once installed to a repo or an org)
"GithubOptions__InstallationId": "",
// Azure subscription id
"AzureOptions__SubscriptionId":"",
// Location for the deployed resources in Azure
"AzureOptions__Location":"",
// Resource group in Azure, where ACI sandbox instances are going to be created
"AzureOptions__ContainerInstancesResourceGroup":"",
// Azure storage file share name (doesn't work with Azurite)
"AzureOptions__FilesShareName":"",
// Azure storage file share account name
"AzureOptions__FilesAccountName":"",
// Azure storage file share account key
"AzureOptions__FilesAccountKey":"",
// If using Azure - AzureOpenAI
"OpenAIOptions__ServiceType":"",
// the service id of the OpenAI model you want to use
"OpenAIOptions__ServiceId":"",
// the deployment id of the OpenAI model you want to use
"OpenAIOptions__DeploymentOrModelId":"",
// the embedding deployment id for the semantic memory
"OpenAIOptions__EmbeddingDeploymentOrModelId":"",
// the endpoint for the provisioned OpenAI service
"OpenAIOptions__Endpoint":"",
// the key for the provisioned OpenAI service
"OpenAIOptions__ApiKey":"",
// if using Codespaces, keep the default value
"QdrantOptions__Endpoint":"http://qdrant:6333",
// keep default
"QdrantOptions__VectorSize":"1536"
}
}

View File

@@ -11,7 +11,8 @@ using Microsoft.SemanticKernel.Plugins.Memory;
using Microsoft.SemanticKernel.Reliability.Basic;
using Octokit.Webhooks;
using Octokit.Webhooks.AspNetCore;
using Orleans.Configuration;
using Azure.Identity;
using Microsoft.Extensions.Azure;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<WebhookEventProcessor, GithubWebHookProcessor>();
@@ -27,6 +28,14 @@ builder.Services.AddSingleton(s =>
var client = ghService.GetGitHubClient().Result;
return client;
});
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.UseCredential(new DefaultAzureCredential());
clientBuilder.AddArmClient(default);
});
builder.Services.AddSingleton<GithubAuthService>();
builder.Services.AddApplicationInsightsTelemetry();
@@ -63,71 +72,14 @@ builder.Services.AddSingleton<IAnalyzeCode, CodeAnalyzer>();
builder.Host.UseOrleans(siloBuilder =>
{
if (builder.Environment.IsDevelopment())
{
var connectionString = builder.Configuration.GetValue<string>("AzureOptions:CosmosConnectionString");
siloBuilder.UseCosmosReminderService( o =>
{
o.ConfigureCosmosClient(connectionString);
o.ContainerName = "reminders";
o.DatabaseName = "devteam";
o.IsResourceCreationEnabled = true;
});
siloBuilder.AddCosmosGrainStorage(
name: "messages",
configureOptions: o =>
{
o.ConfigureCosmosClient(connectionString);
o.ContainerName = "persistence";
o.DatabaseName = "devteam";
o.IsResourceCreationEnabled = true;
});
siloBuilder.UseLocalhostClustering();
}
else
{
var cosmosDbconnectionString = builder.Configuration.GetValue<string>("AzureOptions:CosmosConnectionString");
siloBuilder.Configure<ClusterOptions>(options =>
{
options.ClusterId = "ai-dev-cluster";
options.ServiceId = "ai-dev-cluster";
});
siloBuilder.Configure<SiloMessagingOptions>(options =>
{
options.ResponseTimeout = TimeSpan.FromMinutes(3);
options.SystemResponseTimeout = TimeSpan.FromMinutes(3);
});
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 =>
{
o.ConfigureCosmosClient(cosmosDbconnectionString);
o.ContainerName = "devteam";
o.DatabaseName = "reminders";
o.IsResourceCreationEnabled = true;
});
siloBuilder.AddCosmosGrainStorage(
name: "messages",
configureOptions: o =>
{
o.ConfigureCosmosClient(cosmosDbconnectionString);
o.ContainerName = "devteam";
o.DatabaseName = "persistence";
o.IsResourceCreationEnabled = true;
});
}
siloBuilder.UseLocalhostClustering()
.AddMemoryStreams("StreamProvider")
.AddMemoryGrainStorage("PubSubStore")
.AddMemoryGrainStorage("messages");
siloBuilder.UseInMemoryReminderService();
siloBuilder.UseDashboard(x => x.HostSelf = true);
});
builder.Services.Configure<JsonSerializerOptions>(options =>
@@ -136,13 +88,13 @@ builder.Services.Configure<JsonSerializerOptions>(options =>
});
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints =>
app.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapGitHubWebhooks();
});
app.Map("/dashboard", x => x.UseOrleansDashboard());
app.Run();
@@ -185,7 +137,8 @@ static IKernel CreateKernel(IServiceProvider provider)
return new KernelBuilder()
.WithLoggerFactory(loggerFactory)
.WithAzureChatCompletionService(openAiConfig.DeploymentOrModelId, openAIClient)
.WithRetryBasic(new BasicRetryConfig {
.WithRetryBasic(new BasicRetryConfig
{
MaxRetryCount = 5,
UseExponentialBackoff = true
}).Build();

View File

@@ -2,43 +2,59 @@ using Microsoft.AI.DevTeam;
using Microsoft.AI.DevTeam.Skills;
using Octokit.Webhooks;
using Octokit.Webhooks.Events;
using Octokit.Webhooks.Events.CommitComment;
using Octokit.Webhooks.Events.IssueComment;
using Octokit.Webhooks.Events.Issues;
using Octokit.Webhooks.Models;
using Orleans.Runtime;
public sealed class GithubWebHookProcessor : WebhookEventProcessor
{
private readonly ILogger<GithubWebHookProcessor> _logger;
private readonly IGrainFactory _grains;
private readonly IClusterClient _client;
private readonly IManageGithub _ghService;
private readonly IManageAzure _azService;
public GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logger, IGrainFactory grains, IManageGithub ghService, IManageAzure azService)
public GithubWebHookProcessor(ILogger<GithubWebHookProcessor> logger,
IClusterClient client, IManageGithub ghService, IManageAzure azService)
{
_logger = logger;
_grains = grains;
_client = client;
_ghService = ghService;
_azService = azService;
}
protected override async Task ProcessIssuesWebhookAsync(WebhookHeaders headers, IssuesEvent issuesEvent, IssuesAction action)
{
var org = issuesEvent.Organization.Login;
var repo = issuesEvent.Repository.Name;
var issueNumber = issuesEvent.Issue.Number;
var input = issuesEvent.Issue.Body;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issuesEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
var suffix = $"{org}-{repo}";
if (issuesEvent.Action == IssuesAction.Opened)
try
{
await HandleNewAsk(issueNumber, skillName, functionName, suffix, input, org, repo);
_logger.LogInformation("Processing issue event");
var org = issuesEvent.Organization.Login;
var repo = issuesEvent.Repository.Name;
var issueNumber = issuesEvent.Issue.Number;
var input = issuesEvent.Issue.Body;
// 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]);
var skillName = labels.Keys.Where(k=>k != "Parent").FirstOrDefault();
long? parentNumber = labels.ContainsKey("Parent") ? long.Parse(labels["Parent"]) : null;
var suffix = $"{org}-{repo}";
if (issuesEvent.Action == IssuesAction.Opened)
{
_logger.LogInformation("Processing HandleNewAsk");
await HandleNewAsk(issueNumber,parentNumber, 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, parentNumber,skillName, labels[skillName], suffix, org, repo);
}
}
else if (issuesEvent.Action == IssuesAction.Closed && issuesEvent.Issue.User.Type.Value == UserType.Bot)
catch (System.Exception)
{
await HandleClosingIssue(issueNumber, skillName, functionName, suffix, org, repo);
_logger.LogError("Processing issue event");
}
}
@@ -47,198 +63,96 @@ public sealed class GithubWebHookProcessor : WebhookEventProcessor
IssueCommentEvent issueCommentEvent,
IssueCommentAction action)
{
var org = issueCommentEvent.Organization.Login;
var repo = issueCommentEvent.Repository.Name;
var issueNumber = issueCommentEvent.Issue.Number;
var input = issueCommentEvent.Issue.Body;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var labels = issueCommentEvent.Issue.Labels.First().Name.Split(".");
var skillName = labels[0];
var functionName = labels[1];
var suffix = $"{org}-{repo}";
// we only resond to non-bot comments
if (issueCommentEvent.Sender.Type.Value != UserType.Bot)
try
{
await HandleNewAsk(issueNumber, skillName, functionName, suffix, input, org, repo);
_logger.LogInformation("Processing issue comment event");
var org = issueCommentEvent.Organization.Login;
var repo = issueCommentEvent.Repository.Name;
var issueNumber = issueCommentEvent.Issue.Number;
var input = issueCommentEvent.Issue.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.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
if (issueCommentEvent.Sender.Type.Value != UserType.Bot)
{
await HandleNewAsk(issueNumber, parentNumber, skillName, labels[skillName], suffix, input, org, repo);
}
}
catch (System.Exception ex)
{
_logger.LogError("Processing issue comment event");
}
}
// TODO: implement
protected override Task ProcessPushWebhookAsync(WebhookHeaders headers, PushEvent pushEvent)
{
var org = pushEvent.Organization.Login;
var repo = pushEvent.Repository.Name;
// Assumes the label follows the following convention: Skill.Function example: PM.Readme
var suffix = $"{org}-{repo}";
var ingester = _grains.GetGrain<IIngestRepo>(suffix);
return Task.CompletedTask;
}
private async Task HandleClosingIssue(long issueNumber, string skillName, string functionName, string suffix, string org, string repo)
private async Task HandleClosingIssue(long issueNumber, long? parentNumber, string skillName, string functionName, string suffix, string org, string repo)
{
if (skillName == nameof(PM) && functionName == nameof(PM.Readme))
var streamProvider = _client.GetStreamProvider("StreamProvider");
var streamId = StreamId.Create(Consts.MainNamespace, suffix+issueNumber.ToString());
var stream = streamProvider.GetStream<Event>(streamId);
var eventType = (skillName, functionName) switch
{
(nameof(PM), nameof(PM.Readme)) => EventType.ReadmeChainClosed,
(nameof(DevLead), nameof(DevLead.Plan)) => EventType.DevPlanChainClosed,
(nameof(Developer), nameof(Developer.Implement)) => EventType.CodeChainClosed,
_ => EventType.NewAsk
};
var data = new Dictionary<string, string>
{
await HandleClosingReadme(issueNumber, suffix, org, repo);
}
else if (skillName == nameof(DevLead) && functionName == nameof(DevLead.Plan))
{
await HandleClosingDevPlan(issueNumber, suffix, org, repo);
}
else if (skillName == nameof(Developer) && functionName == nameof(Developer.Implement))
{
await HandleClosingDevImplement(issueNumber, suffix, org, repo);
}
else { } // something went wrong
}
private async Task HandleClosingDevImplement(long issueNumber, string suffix, string org, string repo)
{
var dev = _grains.GetGrain<IDevelopCode>(issueNumber, suffix);
var code = await dev.GetLastMessage();
var lookup = _grains.GetGrain<ILookupMetadata>(suffix);
var parentIssue = await lookup.GetMetadata((int)issueNumber);
await _azService.Store(new SaveOutputRequest
{
ParentIssueNumber = parentIssue.IssueNumber,
IssueNumber = (int)issueNumber,
Output = code,
Extension = "sh",
Directory = "output",
FileName = "run",
Org = org,
Repo = repo
});
var sandboxRequest = new SandboxRequest
{
Org = org,
Repo = repo,
IssueNumber = (int)issueNumber,
ParentIssueNumber = parentIssue.IssueNumber
};
await _azService.RunInSandbox(sandboxRequest);
var commitRequest = new CommitRequest
{
Dir = "output",
Org = org,
Repo = repo,
ParentNumber = parentIssue.IssueNumber,
Number = (int)issueNumber,
Branch = $"sk-{parentIssue.IssueNumber}"
};
var markTaskCompleteRequest = new MarkTaskCompleteRequest
{
Org = org,
Repo = repo,
CommentId = parentIssue.CommentId
{ "org", org },
{ "repo", repo },
{ "issueNumber", issueNumber.ToString() },
{ "parentNumber", parentNumber?.ToString()}
};
var sandbox = _grains.GetGrain<IManageSandbox>(issueNumber, suffix);
await sandbox.ScheduleCommitSandboxRun(commitRequest, markTaskCompleteRequest, sandboxRequest);
}
private async Task HandleClosingDevPlan(long issueNumber, string suffix, string org, string repo)
{
var devLead = _grains.GetGrain<ILeadDevelopment>(issueNumber, suffix);
var lookup = _grains.GetGrain<ILookupMetadata>(suffix);
var parentIssue = await lookup.GetMetadata((int)issueNumber);
var conductor = _grains.GetGrain<IOrchestrateWorkflows>(parentIssue.IssueNumber, suffix);
var plan = await devLead.GetLatestPlan();
await conductor.ImplementationFlow(plan, org, repo, parentIssue.IssueNumber);
await _ghService.MarkTaskComplete(new MarkTaskCompleteRequest
await stream.OnNextAsync(new Event
{
Org = org,
Repo = repo,
CommentId = parentIssue.CommentId
Type = eventType,
Data = data
});
}
private async Task HandleClosingReadme(long issueNumber, string suffix, string org, string repo)
private async Task HandleNewAsk(long issueNumber, long? parentNumber, string skillName, string functionName, string suffix, string input, string org, string repo)
{
var pm = _grains.GetGrain<IManageProduct>(issueNumber, suffix);
var readme = await pm.GetLastMessage();
var lookup = _grains.GetGrain<ILookupMetadata>(suffix);
var parentIssue = await lookup.GetMetadata((int)issueNumber);
await _azService.Store(new SaveOutputRequest
try
{
ParentIssueNumber = parentIssue.IssueNumber,
IssueNumber = (int)issueNumber,
Output = readme,
Extension = "md",
Directory = "output",
FileName = "readme",
Org = org,
Repo = repo
});
await _ghService.CommitToBranch(new CommitRequest
{
Dir = "output",
Org = org,
Repo = repo,
ParentNumber = parentIssue.IssueNumber,
Number = (int)issueNumber,
Branch = $"sk-{parentIssue.IssueNumber}"
});
await _ghService.MarkTaskComplete(new MarkTaskCompleteRequest
{
Org = org,
Repo = repo,
CommentId = parentIssue.CommentId
});
}
_logger.LogInformation("Handling new ask");
var streamProvider = _client.GetStreamProvider("StreamProvider");
var streamId = StreamId.Create(Consts.MainNamespace, suffix+issueNumber.ToString());
var stream = streamProvider.GetStream<Event>(streamId);
private async Task HandleNewAsk(long issueNumber, string skillName, string functionName, string suffix, string input, string org, string repo)
{
if (skillName == "Do" && functionName == "It")
{
var conductor = _grains.GetGrain<IOrchestrateWorkflows>(issueNumber, suffix);
await conductor.InitialFlow(input, org, repo, issueNumber);
}
else if (skillName == "Repo" && functionName == "Ingest")
{
var ingestor = _grains.GetGrain<IIngestRepo>(suffix);
await ingestor.IngestionFlow(org, repo, "main");
}
else if (skillName == nameof(PM) && functionName == nameof(PM.Readme))
{
var pm = _grains.GetGrain<IManageProduct>(issueNumber, suffix);
var readme = await pm.CreateReadme(input);
await _ghService.PostComment(new PostCommentRequest
var eventType = (skillName, functionName) switch
{
Org = org,
Repo = repo,
Number = (int)issueNumber,
Content = string.IsNullOrEmpty(readme)? "Sorry, something went wrong": readme
("Do", "It") => EventType.NewAsk,
(nameof(PM), nameof(PM.Readme)) => EventType.ReadmeRequested,
(nameof(DevLead), nameof(DevLead.Plan)) => EventType.DevPlanRequested,
(nameof(Developer), nameof(Developer.Implement)) => EventType.CodeGenerationRequested,
_ => EventType.NewAsk
};
var data = new Dictionary<string, string>
{
{ "org", org },
{ "repo", repo },
{ "issueNumber", issueNumber.ToString() },
{ "parentNumber", parentNumber?.ToString()}
};
await stream.OnNextAsync(new Event
{
Type = eventType,
Message = input,
Data = data
});
}
else if (skillName == nameof(DevLead) && functionName == nameof(DevLead.Plan))
catch (System.Exception)
{
var devLead = _grains.GetGrain<ILeadDevelopment>(issueNumber, suffix);
var plan = await devLead.CreatePlan(input);
await _ghService.PostComment(new PostCommentRequest
{
Org = org,
Repo = repo,
Number = (int)issueNumber,
Content = string.IsNullOrEmpty(plan)? "Sorry, something went wrong":plan
});
_logger.LogError("Handling new ask");
}
else if (skillName == nameof(Developer) && functionName == nameof(Developer.Implement))
{
var dev = _grains.GetGrain<IDevelopCode>(issueNumber, suffix);
var code = await dev.GenerateCode(input);
await _ghService.PostComment(new PostCommentRequest
{
Org = org,
Repo = repo,
Number = (int)issueNumber,
Content = string.IsNullOrEmpty(code)? "Sorry, something went wrong":code
});
}
else { }// something went wrong
}
}
}

View File

@@ -5,7 +5,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServerGarbageCollection>true</ServerGarbageCollection>
@@ -16,12 +16,15 @@
<ItemGroup>
<PackageReference Include="Octokit.Webhooks.AspNetCore" Version="2.0.3" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Memory.Qdrant" Version="1.0.0-beta5" />
<PackageReference Include="Microsoft.Orleans.Server" Version="7.2.1" />
<PackageReference Include="Microsoft.Orleans.Persistence.Cosmos" Version="7.2.1" />
<PackageReference Include="Microsoft.Orleans.Clustering.Cosmos" Version="7.2.1" />
<PackageReference Include="Microsoft.Orleans.Server" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Persistence.Cosmos" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Clustering.Cosmos" Version="8.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.Orleans.Reminders.Cosmos" Version="7.2.1" />
<PackageReference Include="Microsoft.Orleans.Reminders.Cosmos" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.Orleans.Streaming.EventHubs" Version="8.0.0" />
<PackageReference Include="OrleansDashboard" Version="7.2.2" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.7.2" />
</ItemGroup>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>waf_import</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0</TargetFrameworks>
<TargetFrameworks>net8.0</TargetFrameworks>
<Description>
Activities for calling Semantic Kernel SDK
</Description>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -1,26 +0,0 @@
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class Architect : SemanticPersona, IArchitectSolutions
{
public Architect( [PersistentState("state", "messages")]IPersistentState<SemanticPersonaState> state) : base(state)
{
}
public Task<string> GenerateProjectStructure(string ask)
{
throw new NotImplementedException();
}
public Task<string> ReviewPlan(string plan)
{
throw new NotImplementedException();
}
}
public interface IArchitectSolutions : IGrainWithIntegerCompoundKey
{
Task<string> ReviewPlan(string plan);
Task<string> GenerateProjectStructure(string ask);
}

View File

@@ -1,82 +0,0 @@
using Microsoft.AI.DevTeam.Skills;
using Orleans.Concurrency;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class Conductor : Grain, IOrchestrateWorkflows
{
private readonly IManageGithub _ghService;
public Conductor( IManageGithub ghService)
{
_ghService = ghService;
}
public async Task InitialFlow(string input, string org, string repo, long parentNumber)
{
await _ghService.CreateBranch(new CreateBranchRequest
{
Org = org,
Repo = repo,
Branch = $"sk-{parentNumber}"
});
var pmIssue = await _ghService.CreateIssue(new CreateIssueRequest
{
Label = $"{nameof(PM)}.{nameof(PM.Readme)}",
Org = org,
Repo = repo,
Input = input,
ParentNumber = parentNumber
});
var devLeadIssue = await _ghService.CreateIssue(new CreateIssueRequest
{
Label = $"{nameof(DevLead)}.{nameof(DevLead.Plan)}",
Org = org,
Repo = repo,
Input = input,
ParentNumber = parentNumber
});
var suffix = $"{org}-{repo}";
var lookup = GrainFactory.GetGrain<ILookupMetadata>(suffix);
var metadataList = new List<StoreMetadataPairs>{
new StoreMetadataPairs
{
Key = pmIssue.IssueNumber,
Value = new NewIssueResponse { CommentId = pmIssue.CommentId, IssueNumber = (int)parentNumber}
},
new StoreMetadataPairs
{
Key = devLeadIssue.IssueNumber,
Value = new NewIssueResponse { CommentId = devLeadIssue.CommentId, IssueNumber = (int)parentNumber}
}
};
await lookup.StoreMetadata(metadataList);
// await githubActor.CreatePR(); // TODO: this should happen when all tasks are done?
}
public async Task ImplementationFlow(DevLeadPlanResponse plan, string org, string repo, int parentNumber)
{
var suffix = $"{org}-{repo}";
var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt));
var lookup = GrainFactory.GetGrain<ILookupMetadata>(suffix);
var metadataList = new List<StoreMetadataPairs>();
foreach(var prompt in prompts)
{
var issue = await _ghService.CreateIssue(new CreateIssueRequest
{
Label = $"{nameof(Developer)}.{nameof(Developer.Implement)}",
Org = org,
Repo = repo,
Input = prompt,
ParentNumber = parentNumber
});
metadataList.Add(new StoreMetadataPairs
{
Key = issue.IssueNumber,
Value = new NewIssueResponse { CommentId = issue.CommentId, IssueNumber = (int)parentNumber}
});
}
await lookup.StoreMetadata(metadataList);
}
}

View File

@@ -1,11 +0,0 @@
using Orleans.Concurrency;
namespace Microsoft.AI.DevTeam;
public interface IOrchestrateWorkflows : IGrainWithIntegerCompoundKey
{
[OneWay]
Task InitialFlow(string input, string org, string repo, long parentNumber);
[OneWay]
Task ImplementationFlow(DevLeadPlanResponse plan, string org, string repo, int parentNumber);
}

View File

@@ -1,98 +0,0 @@
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Orleans.Runtime;
using System.Text.Json;
namespace Microsoft.AI.DevTeam;
public class DeveloperLead : SemanticPersona, ILeadDevelopment
{
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly ILogger<DeveloperLead> _logger;
protected override string MemorySegment => "dev-lead-memory";
public DeveloperLead([PersistentState("state", "messages")] IPersistentState<SemanticPersonaState> state,IKernel kernel, ISemanticTextMemory memory, ILogger<DeveloperLead> logger) : base(state)
{
_kernel = kernel;
_memory = memory;
_logger = logger;
}
public async Task<string> CreatePlan(string ask)
{
try
{
var function = _kernel.CreateSemanticFunction(DevLead.Plan, new OpenAIRequestSettings { MaxTokens = 15000, Temperature = 0.4, TopP = 1 });
var context = new ContextVariables();
context.Set("input", ask);
if (_state.State.History == null) _state.State.History = new List<ChatHistoryItem>();
_state.State.History.Add(new ChatHistoryItem
{
Message = ask,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.User
});
await AddWafContext(_memory, ask, context);
context.Set("input", ask);
var result = await _kernel.RunAsync(context, function);
var resultMessage = result.ToString();
_state.State.History.Add(new ChatHistoryItem
{
Message = resultMessage,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.Agent
});
await _state.WriteStateAsync();
return resultMessage;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating development plan");
return default;
}
}
public Task<DevLeadPlanResponse> GetLatestPlan()
{
var plan = _state.State.History.Last().Message;
var response = JsonSerializer.Deserialize<DevLeadPlanResponse>(plan);
return Task.FromResult(response);
}
}
[GenerateSerializer]
public class DevLeadPlanResponse
{
[Id(0)]
public List<Step> steps { get; set; }
}
[GenerateSerializer]
public class Step
{
[Id(0)]
public string description { get; set; }
[Id(1)]
public string step { get; set; }
[Id(2)]
public List<Subtask> subtasks { get; set; }
}
[GenerateSerializer]
public class Subtask
{
[Id(0)]
public string subtask { get; set; }
[Id(1)]
public string prompt { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace Microsoft.AI.DevTeam;
public interface ILeadDevelopment: IGrainWithIntegerCompoundKey, IChatHistory
{
Task<string> CreatePlan(string ask);
Task<DevLeadPlanResponse> GetLatestPlan();
}

View File

@@ -1,7 +0,0 @@
namespace Microsoft.AI.DevTeam;
public interface IDevelopCode : IGrainWithIntegerCompoundKey, IChatHistory, IUnderstand
{
Task<string> GenerateCode(string ask);
Task<string> ReviewPlan(string plan);
}

View File

@@ -1,9 +0,0 @@
using Orleans.Concurrency;
namespace Microsoft.AI.DevTeam;
public interface IIngestRepo : IGrainWithStringKey
{
[OneWay]
Task IngestionFlow(string org, string repo, string branch);
}

View File

@@ -1,48 +0,0 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Octokit;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class Ingester : SemanticPersona, IIngestRepo
{
protected override string MemorySegment => "code-analysis";
private readonly IManageGithub _ghService;
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly IAnalyzeCode _codeAnalyzer;
public Ingester([PersistentState("state", "messages")] IPersistentState<SemanticPersonaState> state, IManageGithub ghService, IKernel kernel, ISemanticTextMemory memory, IAnalyzeCode codeAnalyzer) : base(state)
{
_ghService = ghService;
_kernel = kernel;
_memory = memory;
_codeAnalyzer = codeAnalyzer;
}
public async Task IngestionFlow(string org, string repo, string branch)
{
var suffix = $"{org}-{repo}";
var language = await _ghService.GetMainLanguage(org, repo);
var files = await _ghService.GetFiles(org, repo, branch, Language.Filters[language]);
var dev = GrainFactory.GetGrain<IDevelopCode>(0, suffix);
foreach (var file in files)
{
var codeAnalysis = await _codeAnalyzer.Analyze(file.Content);
codeAnalysis.ToList().ForEach(async c =>
await _memory.SaveInformationAsync(MemorySegment, c.CodeBlock, Guid.NewGuid().ToString(), c.Meaning));
// TODO: do something with the result
await dev.BuildUnderstanding(file.Content);
}
}
}
public static class Language
{
public static Dictionary<string, Func<RepositoryContent, bool>> Filters = new Dictionary<string, Func<RepositoryContent, bool>> {
{"C#", f => f.Name.EndsWith(".cs") }
};
}

View File

@@ -1,7 +0,0 @@
namespace Microsoft.AI.DevTeam;
public interface ILookupMetadata : IGrainWithStringKey
{
Task<NewIssueResponse> GetMetadata(int key);
Task StoreMetadata(List<StoreMetadataPairs> pairs);
}

View File

@@ -1,42 +0,0 @@
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class Lookup : Grain, ILookupMetadata
{
protected readonly IPersistentState<ConductorLookup> _state;
public Lookup([PersistentState("state", "messages")] IPersistentState<ConductorLookup> state)
{
_state = state;
}
public Task<NewIssueResponse> GetMetadata(int key)
{
return Task.FromResult(_state.State.Metadata[key]);
}
public Task StoreMetadata(List<StoreMetadataPairs> pairs)
{
if(_state.State.Metadata == null) _state.State.Metadata = new Dictionary<int, NewIssueResponse>();
foreach(var pair in pairs)
{
_state.State.Metadata[pair.Key] = pair.Value;
}
return _state.WriteStateAsync();
}
}
[Serializable]
public class ConductorLookup
{
public Dictionary<int, NewIssueResponse> Metadata { get; set; }
}
[GenerateSerializer]
public class StoreMetadataPairs
{
[Id(0)]
public int Key { get; set; }
[Id(1)]
public NewIssueResponse Value { get; set; }
}

View File

@@ -1,6 +0,0 @@
namespace Microsoft.AI.DevTeam;
public interface IManageProduct : IGrainWithIntegerCompoundKey, IChatHistory
{
Task<string> CreateReadme(string ask);
}

View File

@@ -1,58 +0,0 @@
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class ProductManager : SemanticPersona, IManageProduct
{
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly ILogger<ProductManager> _logger;
protected override string MemorySegment => "pm-memory";
public ProductManager([PersistentState("state", "messages")] IPersistentState<SemanticPersonaState> state, IKernel kernel, ISemanticTextMemory memory, ILogger<ProductManager> logger) : base(state)
{
_kernel = kernel;
_memory = memory;
_logger = logger;
}
public async Task<string> CreateReadme(string ask)
{
try
{
var function = _kernel.CreateSemanticFunction(PM.Readme, new OpenAIRequestSettings { MaxTokens = 10000, Temperature = 0.6, TopP = 1 });
var context = new ContextVariables();
context.Set("input", ask);
if (_state.State.History == null) _state.State.History = new List<ChatHistoryItem>();
_state.State.History.Add(new ChatHistoryItem
{
Message = ask,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.User
});
await AddWafContext(_memory, ask, context);
context.Set("input", ask);
var result = await _kernel.RunAsync(context, function);
var resultMessage = result.ToString();
_state.State.History.Add(new ChatHistoryItem
{
Message = resultMessage,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.Agent
});
await _state.WriteStateAsync();
return resultMessage;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating readme");
return default;
}
}
}

View File

@@ -1,6 +0,0 @@
namespace Microsoft.AI.DevTeam;
public interface IManageSandbox : IGrainWithIntegerCompoundKey
{
Task ScheduleCommitSandboxRun(CommitRequest commitRequest, MarkTaskCompleteRequest markTaskCompleteRequest, SandboxRequest sandboxRequest);
}

View File

@@ -1,77 +0,0 @@
using Orleans.Runtime;
using Orleans.Timers;
namespace Microsoft.AI.DevTeam;
public class Sandbox : Grain, IManageSandbox, IRemindable
{
private const string ReminderName = "SandboxRunReminder";
private readonly IManageGithub _ghService;
private readonly IManageAzure _azService;
private readonly IReminderRegistry _reminderRegistry;
private IGrainReminder? _reminder;
protected readonly IPersistentState<SandboxMetadata> _state;
public Sandbox([PersistentState("state", "messages")] IPersistentState<SandboxMetadata> state, IManageGithub ghService,
IReminderRegistry reminderRegistry, IManageAzure azService)
{
_ghService = ghService;
_reminderRegistry = reminderRegistry;
_azService = azService;
_state = state;
}
public async Task ScheduleCommitSandboxRun(CommitRequest commitRequest, MarkTaskCompleteRequest markTaskCompleteRequest, SandboxRequest sandboxRequest)
{
await StoreState(commitRequest, markTaskCompleteRequest, sandboxRequest);
_reminder = await _reminderRegistry.RegisterOrUpdateReminder(
callingGrainId: this.GetGrainId(),
reminderName: ReminderName,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromMinutes(1));
}
async Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
if (!_state.State.IsCompleted)
{
var sandboxId = $"sk-sandbox-{_state.State.SandboxRequest.Org}-{_state.State.SandboxRequest.Repo}-{_state.State.SandboxRequest.ParentIssueNumber}-{_state.State.SandboxRequest.IssueNumber}";
if (await _azService.IsSandboxCompleted(sandboxId))
{
await _azService.DeleteSandbox(sandboxId);
await _ghService.CommitToBranch(_state.State.CommitRequest);
await _ghService.MarkTaskComplete(_state.State.MarkTaskCompleteRequest);
await Cleanup();
}
}
else
{
await Cleanup();
}
}
private async Task StoreState(CommitRequest commitRequest, MarkTaskCompleteRequest markTaskCompleteRequest, SandboxRequest sandboxRequest)
{
_state.State.CommitRequest = commitRequest;
_state.State.MarkTaskCompleteRequest = markTaskCompleteRequest;
_state.State.SandboxRequest = sandboxRequest;
_state.State.IsCompleted = false;
await _state.WriteStateAsync();
}
private async Task Cleanup()
{
_state.State.IsCompleted = true;
await _reminderRegistry.UnregisterReminder(
this.GetGrainId(), _reminder);
await _state.WriteStateAsync();
}
}
public class SandboxMetadata
{
public CommitRequest CommitRequest { get; set; }
public MarkTaskCompleteRequest MarkTaskCompleteRequest { get; set; }
public SandboxRequest SandboxRequest { get; set; }
public bool IsCompleted { get; set; }
}

View File

@@ -1,75 +0,0 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public abstract class SemanticPersona : Grain, IChatHistory
{
public SemanticPersona(
[PersistentState("state", "messages")] IPersistentState<SemanticPersonaState> state)
{
_state = state;
}
protected virtual string MemorySegment { get; set; }
protected List<ChatHistoryItem> History { get; set; }
protected readonly IPersistentState<SemanticPersonaState> _state;
public async Task<string> GetLastMessage()
{
return _state.State.History.Last().Message;
}
protected async Task AddWafContext(ISemanticTextMemory memory, string ask, ContextVariables context)
{
var interestingMemories = memory.SearchAsync("waf-pages", ask, 2);
var wafContext = "Consider the following architectural guidelines:";
await foreach (var m in interestingMemories)
{
wafContext += $"\n {m.Metadata.Text}";
}
context.Set("wafContext", wafContext);
}
}
public interface IChatHistory
{
Task<string> GetLastMessage();
}
public interface IUnderstand
{
Task<UnderstandingResult> BuildUnderstanding(string content);
}
[GenerateSerializer]
public class UnderstandingResult {
[Id(0)]
public string NewUnderstanding { get; set; }
[Id(1)]
public string Explanation { get; set; }
}
[Serializable]
public class ChatHistoryItem
{
public string Message { get; set; }
public ChatUserType UserType { get; set; }
public int Order { get; set; }
}
public class SemanticPersonaState
{
public List<ChatHistoryItem> History { get; set; }
public string Understanding { get; set; }
}
public enum ChatUserType
{
System,
User,
Agent
}

View File

@@ -1,27 +0,0 @@
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public class Tester : SemanticPersona, ITestCode
{
public Tester(
[PersistentState("state", "messages")]IPersistentState<SemanticPersonaState> state) : base(state)
{
}
public Task<string> ReviewPlan(string plan)
{
throw new NotImplementedException();
}
public Task<string> TestCode(string ask)
{
throw new NotImplementedException();
}
}
public interface ITestCode : IGrainWithIntegerCompoundKey
{
Task<string> TestCode(string ask);
Task<string> ReviewPlan(string plan);
}

View File

@@ -0,0 +1,25 @@
using Orleans.Runtime;
using Orleans.Streams;
namespace Microsoft.AI.DevTeam;
public abstract class Agent : Grain, IGrainWithStringKey
{
public abstract Task HandleEvent(Event item, StreamSequenceToken? token);
protected async Task PublishEvent(string ns, string id, Event item)
{
var streamProvider = this.GetStreamProvider("StreamProvider");
var streamId = StreamId.Create(ns, id);
var stream = streamProvider.GetStream<Event>(streamId);
await stream.OnNextAsync(item);
}
public async override Task OnActivateAsync(CancellationToken cancellationToken)
{
var streamProvider = this.GetStreamProvider("StreamProvider");
var streamId = StreamId.Create(Consts.MainNamespace, this.GetPrimaryKeyString());
var stream = streamProvider.GetStream<Event>(streamId);
await stream.SubscribeAsync(HandleEvent);
}
}

View File

@@ -0,0 +1,75 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Orleans.Runtime;
namespace Microsoft.AI.DevTeam;
public abstract class AiAgent : Agent
{
public AiAgent(
[PersistentState("state", "messages")] IPersistentState<AgentState> state)
{
_state = state;
}
protected readonly IPersistentState<AgentState> _state;
protected async Task<ContextVariables> CreateWafContext(ISemanticTextMemory memory, string ask)
{
var context = new ContextVariables();
var interestingMemories = memory.SearchAsync("waf-pages", ask, 2);
var wafContext = "Consider the following architectural guidelines:";
await foreach (var m in interestingMemories)
{
wafContext += $"\n {m.Metadata.Text}";
}
context.Set("input", ask);
context.Set("wafContext", wafContext);
return context;
}
protected void AddToHistory(string message, ChatUserType userType)
{
if (_state.State.History == null) _state.State.History = new List<ChatHistoryItem>();
_state.State.History.Add(new ChatHistoryItem
{
Message = message,
Order = _state.State.History.Count + 1,
UserType = userType
});
}
protected async Task<string> CallFunction(string template, string ask, IKernel kernel, ISemanticTextMemory memory)
{
var function = kernel.CreateSemanticFunction(template, new OpenAIRequestSettings { MaxTokens = 15000, Temperature = 0.8, TopP = 1 });
var context = await CreateWafContext(memory, ask);
var result = (await kernel.RunAsync(context, function)).ToString();
AddToHistory(ask, ChatUserType.User);
AddToHistory(result, ChatUserType.Agent);
await _state.WriteStateAsync();
return result;
}
}
[Serializable]
public class ChatHistoryItem
{
public string Message { get; set; }
public ChatUserType UserType { get; set; }
public int Order { get; set; }
}
public class AgentState
{
public List<ChatHistoryItem> History { get; set; }
public string Understanding { get; set; }
}
public enum ChatUserType
{
System,
User,
Agent
}

View File

@@ -0,0 +1,71 @@
using Orleans.Runtime;
using Orleans.Streams;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class AzureGenie : Agent
{
private readonly IManageAzure _azureService;
public AzureGenie( IManageAzure azureService)
{
_azureService = azureService;
}
public override async Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch (item.Type)
{
case EventType.ReadmeCreated:
{
var parentNumber = long.Parse(item.Data["parentNumber"]);
var issueNumber = long.Parse(item.Data["issueNumber"]);
await Store(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "readme", "md", "output", item.Message);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = EventType.ReadmeStored,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "parentNumber", item.Data["parentNumber"] }
}
});
}
break;
case EventType.CodeCreated:
{
var parentNumber = long.Parse(item.Data["parentNumber"]);
var issueNumber = long.Parse(item.Data["issueNumber"]);
await Store(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "run", "sh", "output", item.Message);
await RunInSandbox(item.Data["org"], item.Data["repo"], parentNumber, issueNumber);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = EventType.SandboxRunCreated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "parentNumber", item.Data["parentNumber"] }
}
});
}
break;
default:
break;
}
}
public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output)
{
await _azureService.Store(org, repo, parentIssueNumber, issueNumber, filename, extension, dir, output);
}
public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber)
{
await _azureService.RunInSandbox(org, repo, parentIssueNumber, issueNumber);
}
}

View File

@@ -5,51 +5,65 @@ using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Orchestration;
using Orleans.Runtime;
using Orleans.Streams;
namespace Microsoft.AI.DevTeam;
public class Dev : SemanticPersona, IDevelopCode
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Dev : AiAgent, IDevelopApps
{
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly ILogger<Dev> _logger;
protected override string MemorySegment => "dev-memory";
public Dev([PersistentState("state", "messages")] IPersistentState<SemanticPersonaState> state, IKernel kernel, ISemanticTextMemory memory, ILogger<Dev> logger) : base(state)
public Dev([PersistentState("state", "messages")] IPersistentState<AgentState> state, IKernel kernel, ISemanticTextMemory memory, ILogger<Dev> logger) : base(state)
{
_kernel = kernel;
_memory = memory;
_logger = logger;
}
public async override Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch (item.Type)
{
case EventType.CodeGenerationRequested:
var code = await GenerateCode(item.Message);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = EventType.CodeGenerated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "code", code }
},
Message = code
});
break;
case EventType.CodeChainClosed:
var lastCode = _state.State.History.Last().Message;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = EventType.CodeCreated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "code", lastCode },
{ "parentNumber", item.Data["parentNumber"] }
},
Message = lastCode
});
break;
default:
break;
}
}
public async Task<string> GenerateCode(string ask)
{
try
{
var function = _kernel.CreateSemanticFunction(Developer.Implement, new OpenAIRequestSettings { MaxTokens = 15000, Temperature = 0.8, TopP = 1 });
var context = new ContextVariables();
if (_state.State.History == null) _state.State.History = new List<ChatHistoryItem>();
_state.State.History.Add(new ChatHistoryItem
{
Message = ask,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.User
});
await AddWafContext(_memory, ask, context);
context.Set("input", ask);
var result = await _kernel.RunAsync(context, function);
var resultMessage = result.ToString();
_state.State.History.Add(new ChatHistoryItem
{
Message = resultMessage,
Order = _state.State.History.Count + 1,
UserType = ChatUserType.Agent
});
await _state.WriteStateAsync();
return resultMessage;
return await CallFunction(Developer.Implement, ask, _kernel, _memory);
}
catch (Exception ex)
{
@@ -58,13 +72,6 @@ public class Dev : SemanticPersona, IDevelopCode
}
}
public Task<string> ReviewPlan(string plan)
{
throw new NotImplementedException();
}
public async Task<UnderstandingResult> BuildUnderstanding(string content)
{
try
@@ -98,4 +105,18 @@ public class Dev : SemanticPersona, IDevelopCode
return default;
}
}
}
public interface IDevelopApps
{
public Task<string> GenerateCode(string ask);
}
[GenerateSerializer]
public class UnderstandingResult
{
[Id(0)]
public string NewUnderstanding { get; set; }
[Id(1)]
public string Explanation { get; set; }
}

View File

@@ -0,0 +1,112 @@
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
using Orleans.Streams;
using System.Text.Json;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class DeveloperLead : AiAgent, ILeadDevelopers
{
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly ILogger<DeveloperLead> _logger;
private readonly IManageGithub _ghService;
public DeveloperLead([PersistentState("state", "messages")] IPersistentState<AgentState> state, IKernel kernel, ISemanticTextMemory memory, ILogger<DeveloperLead> logger, IManageGithub ghService) : base(state)
{
_kernel = kernel;
_memory = memory;
_logger = logger;
_ghService = ghService;
}
public async override Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch (item.Type)
{
case EventType.DevPlanRequested:
var plan = await CreatePlan(item.Message);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = EventType.DevPlanGenerated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "plan", plan }
},
Message = plan
});
break;
case EventType.DevPlanChainClosed:
var latestPlan = _state.State.History.Last().Message;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = EventType.DevPlanCreated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{"parentNumber", item.Data["parentNumber"]},
{ "plan", latestPlan }
},
Message = latestPlan
});
break;
default:
break;
}
}
public async Task<string> CreatePlan(string ask)
{
try
{
return await CallFunction(DevLead.Plan, ask, _kernel, _memory);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating development plan");
return default;
}
}
}
public interface ILeadDevelopers
{
public Task<string> CreatePlan(string ask);
}
[GenerateSerializer]
public class DevLeadPlanResponse
{
[Id(0)]
public List<Step> steps { get; set; }
}
[GenerateSerializer]
public class Step
{
[Id(0)]
public string description { get; set; }
[Id(1)]
public string step { get; set; }
[Id(2)]
public List<Subtask> subtasks { get; set; }
}
[GenerateSerializer]
public class Subtask
{
[Id(0)]
public string subtask { get; set; }
[Id(1)]
public string prompt { get; set; }
}

View File

@@ -0,0 +1,95 @@
using System.Text.Json;
using Microsoft.AI.DevTeam.Skills;
using Octokit;
using Orleans.Concurrency;
using Orleans.Runtime;
using Orleans.Streams;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Hubber : Agent
{
private readonly IManageGithub _ghService;
public Hubber(IManageGithub ghService)
{
_ghService = ghService;
}
public override async Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch (item.Type)
{
case EventType.NewAsk:
{
var parentNumber = long.Parse(item.Data["issueNumber"]);
var pmIssue = await CreateIssue(item.Data["org"], item.Data["repo"], item.Message, $"{nameof(PM)}.{nameof(PM.Readme)}", parentNumber);
var devLeadIssue = await CreateIssue(item.Data["org"], item.Data["repo"], item.Message, $"{nameof(DevLead)}.{nameof(DevLead.Plan)}", parentNumber);
await PostComment(item.Data["org"], item.Data["repo"], parentNumber, $" - #{pmIssue} - tracks {nameof(PM)}.{nameof(PM.Readme)}");
await PostComment(item.Data["org"], item.Data["repo"], parentNumber, $" - #{devLeadIssue} - tracks {nameof(DevLead)}.{nameof(DevLead.Plan)}");
await CreateBranch(item.Data["org"], item.Data["repo"], $"sk-{parentNumber}");
}
break;
case EventType.ReadmeGenerated:
case EventType.DevPlanGenerated:
case EventType.CodeGenerated:
await PostComment(item.Data["org"], item.Data["repo"], long.Parse(item.Data["issueNumber"]), item.Message);
break;
case EventType.DevPlanCreated:
{
var plan = JsonSerializer.Deserialize<DevLeadPlanResponse>(item.Data["plan"]);
var prompts = plan.steps.SelectMany(s => s.subtasks.Select(st => st.prompt));
var parentNumber = long.Parse(item.Data["parentNumber"]);
foreach (var prompt in prompts)
{
var functionName = $"{nameof(Developer)}.{nameof(Developer.Implement)}";
var issue = await CreateIssue(item.Data["org"], item.Data["repo"], prompt, functionName, parentNumber);
var commentBody = $" - #{issue} - tracks {functionName}";
await PostComment(item.Data["org"], item.Data["repo"], parentNumber, commentBody);
}
}
break;
case EventType.ReadmeStored:
{
var parentNumber = long.Parse(item.Data["parentNumber"]);
var issueNumber = long.Parse(item.Data["issueNumber"]);
var branch = $"sk-{parentNumber}";
await CommitToBranch(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "output", branch);
await CreatePullRequest(item.Data["org"], item.Data["repo"], parentNumber, branch);
}
break;
case EventType.SandboxRunFinished:
{
var parentNumber = long.Parse(item.Data["parentNumber"]);
var issueNumber = long.Parse(item.Data["issueNumber"]);
var branch = $"sk-{parentNumber}";
await CommitToBranch(item.Data["org"], item.Data["repo"], parentNumber, issueNumber, "output", branch);
}
break;
default:
break;
}
}
public async Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber)
{
return await _ghService.CreateIssue(org, repo, input, function, parentNumber);
}
public async Task PostComment(string org, string repo, long issueNumber, string comment)
{
await _ghService.PostComment(org, repo, issueNumber, comment);
}
public async Task CreateBranch(string org, string repo, string branch)
{
await _ghService.CreateBranch(org, repo, branch);
}
public async Task CreatePullRequest(string org, string repo, long issueNumber, string branch)
{
await _ghService.CreatePR(org, repo, issueNumber, branch);
}
public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch)
{
await _ghService.CommitToBranch(org, repo, parentNumber, issueNumber, rootDir, branch);
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.AI.DevTeam.Skills;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Memory;
using Orleans.Runtime;
using Orleans.Streams;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class ProductManager : AiAgent, IManageProducts
{
private readonly IKernel _kernel;
private readonly ISemanticTextMemory _memory;
private readonly ILogger<ProductManager> _logger;
public ProductManager([PersistentState("state", "messages")] IPersistentState<AgentState> state, IKernel kernel, ISemanticTextMemory memory, ILogger<ProductManager> logger) : base(state)
{
_kernel = kernel;
_memory = memory;
_logger = logger;
}
public async override Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch (item.Type)
{
case EventType.ReadmeRequested:
var readme = await CreateReadme(item.Message);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = EventType.ReadmeGenerated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "readme", readme }
},
Message = readme
});
break;
case EventType.ReadmeChainClosed:
var lastReadme = _state.State.History.Last().Message;
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event {
Type = EventType.ReadmeCreated,
Data = new Dictionary<string, string> {
{ "org", item.Data["org"] },
{ "repo", item.Data["repo"] },
{ "issueNumber", item.Data["issueNumber"] },
{ "readme", lastReadme },
{ "parentNumber", item.Data["parentNumber"] }
},
Message = lastReadme
});
break;
default:
break;
}
}
public async Task<string> CreateReadme(string ask)
{
try
{
return await CallFunction(PM.Readme, ask, _kernel, _memory);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating readme");
return default;
}
}
}
public interface IManageProducts
{
public Task<string> CreateReadme(string ask);
}

View File

@@ -0,0 +1,104 @@
using Orleans.Runtime;
using Orleans.Streams;
using Orleans.Timers;
namespace Microsoft.AI.DevTeam;
[ImplicitStreamSubscription(Consts.MainNamespace)]
public class Sandbox : Agent, IRemindable
{
private const string ReminderName = "SandboxRunReminder";
private readonly IManageAzure _azService;
private readonly IReminderRegistry _reminderRegistry;
private IGrainReminder? _reminder;
protected readonly IPersistentState<SandboxMetadata> _state;
public Sandbox([PersistentState("state", "messages")] IPersistentState<SandboxMetadata> state,
IReminderRegistry reminderRegistry, IManageAzure azService)
{
_reminderRegistry = reminderRegistry;
_azService = azService;
_state = state;
}
public override async Task HandleEvent(Event item, StreamSequenceToken? token)
{
switch(item.Type)
{
case EventType.SandboxRunCreated:
var org = item.Data["org"];
var repo = item.Data["repo"];
var parentIssueNumber = long.Parse(item.Data["parentNumber"]);
var issueNumber = long.Parse(item.Data["issueNumber"]);
await ScheduleCommitSandboxRun(org, repo, parentIssueNumber, issueNumber);
break;
default:
break;
}
}
public async Task ScheduleCommitSandboxRun(string org, string repo, long parentIssueNumber, long issueNumber)
{
await StoreState(org, repo, parentIssueNumber, issueNumber);
_reminder = await _reminderRegistry.RegisterOrUpdateReminder(
callingGrainId: this.GetGrainId(),
reminderName: ReminderName,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromMinutes(1));
}
async Task IRemindable.ReceiveReminder(string reminderName, TickStatus status)
{
if (!_state.State.IsCompleted)
{
var sandboxId = $"sk-sandbox-{_state.State.Org}-{_state.State.Repo}-{_state.State.ParentIssueNumber}-{_state.State.IssueNumber}";
if (await _azService.IsSandboxCompleted(sandboxId))
{
await _azService.DeleteSandbox(sandboxId);
await PublishEvent(Consts.MainNamespace, this.GetPrimaryKeyString(), new Event
{
Type = EventType.SandboxRunFinished,
Data = new Dictionary<string, string> {
{ "org", _state.State.Org },
{ "repo", _state.State.Repo },
{ "issueNumber", _state.State.IssueNumber.ToString() },
{ "parentNumber", _state.State.ParentIssueNumber.ToString() }
}
});
await Cleanup();
}
}
else
{
await Cleanup();
}
}
private async Task StoreState(string org, string repo, long parentIssueNumber, long issueNumber)
{
_state.State.Org = org;
_state.State.Repo = repo;
_state.State.ParentIssueNumber = parentIssueNumber;
_state.State.IssueNumber = issueNumber;
_state.State.IsCompleted = false;
await _state.WriteStateAsync();
}
private async Task Cleanup()
{
_state.State.IsCompleted = true;
await _reminderRegistry.UnregisterReminder(
this.GetGrainId(), _reminder);
await _state.WriteStateAsync();
}
}
public class SandboxMetadata
{
public string Org { get; set; }
public string Repo { get; set; }
public long ParentIssueNumber { get; set; }
public long IssueNumber { get; set; }
public bool IsCompleted { get; set; }
}

View File

@@ -0,0 +1,31 @@
[GenerateSerializer]
public class Event
{
[Id(0)]
public EventType Type { get; set; }
[Id(1)]
public string Message { get; set; }
[Id(2)]
public Dictionary<string,string> Data { get; set; }
}
public enum EventType
{
NewAsk,
ReadmeChainClosed,
CodeChainClosed,
CodeGenerationRequested,
DevPlanRequested,
ReadmeGenerated,
DevPlanGenerated,
CodeGenerated,
DevPlanChainClosed,
ReadmeRequested,
ReadmeStored,
SandboxRunFinished,
ReadmeCreated,
CodeCreated,
DevPlanCreated,
SandboxRunCreated
}

View File

@@ -1,26 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.2.1" />
<PackageReference Include="Microsoft.Orleans.Runtime" Version="7.2.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.0.0-beta5" />
<PackageReference Include="Octokit" Version="7.1.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Runtime" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.0.0-beta5" />
<PackageReference Include="Octokit" Version="10.0.0" />
<PackageReference Include="GitHubJwt" Version="0.0.6" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0-beta.1" />
<PackageReference Include="Azure.ResourceManager.ContainerInstance" Version="1.2.0" />
<PackageReference Include="Azure.Storage.Files.Shares" Version="12.16.0-beta.1" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.1" />
<PackageReference Include="Azure.Identity" Version="1.10.0-beta.1" />
<PackageReference Include="Microsoft.Orleans.Reminders" Version="7.2.1" />
<PackageReference Include="Azure.Identity" Version="1.11.0-beta.1" />
<PackageReference Include="Microsoft.Orleans.Reminders" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Streaming" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libs\Microsoft.AI.DevTeam.Skills\Microsoft.AI.DevTeam.Skills.csproj" />
<ProjectReference
Include="..\..\libs\Microsoft.AI.DevTeam.Skills\Microsoft.AI.DevTeam.Skills.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Microsoft.AI.DevTeam;
public static class Consts
{
public const string MainNamespace = "DevPersonas";
}

View File

@@ -17,20 +17,21 @@ public class AzureService : IManageAzure
{
private readonly AzureOptions _azSettings;
private readonly ILogger<AzureService> _logger;
private readonly ArmClient _client;
public AzureService(IOptions<AzureOptions> azOptions, ILogger<AzureService> logger)
public AzureService(IOptions<AzureOptions> azOptions, ILogger<AzureService> logger, ArmClient client)
{
_azSettings = azOptions.Value;
_logger = logger;
_client = client;
}
public async Task DeleteSandbox(string sandboxId)
{
try
{
var client = new ArmClient(new DefaultAzureCredential());
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
var containerGroup = await collection.GetAsync(sandboxId);
@@ -47,9 +48,8 @@ public class AzureService : IManageAzure
{
try
{
var client = new ArmClient(new DefaultAzureCredential());
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var collection = resourceGroupResource.GetContainerGroups();
var containerGroup = await collection.GetAsync(sandboxId);
@@ -63,18 +63,14 @@ public class AzureService : IManageAzure
}
}
public async Task RunInSandbox(SandboxRequest request)
public async Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber)
{
try
{
var client = string.IsNullOrEmpty(_azSettings.ManagedIdentity) ?
new ArmClient(new AzureCliCredential())
: new ArmClient(new ManagedIdentityCredential(_azSettings.ManagedIdentity));
var runId = $"sk-sandbox-{request.Org}-{request.Repo}-{request.ParentIssueNumber}-{request.IssueNumber}";
var runId = $"sk-sandbox-{org}-{repo}-{parentIssueNumber}-{issueNumber}";
var resourceGroupResourceId = ResourceGroupResource.CreateResourceIdentifier(_azSettings.SubscriptionId, _azSettings.ContainerInstancesResourceGroup);
var resourceGroupResource = client.GetResourceGroupResource(resourceGroupResourceId);
var scriptPath = $"/azfiles/output/{request.Org}-{request.Repo}/{request.ParentIssueNumber}/{request.IssueNumber}/run.sh";
var resourceGroupResource = _client.GetResourceGroupResource(resourceGroupResourceId);
var scriptPath = $"/azfiles/output/{org}-{repo}/{parentIssueNumber}/{issueNumber}/run.sh";
var collection = resourceGroupResource.GetContainerGroups();
var data = new ContainerGroupData(new AzureLocation(_azSettings.Location), new ContainerInstanceContainer[]
{
@@ -113,33 +109,33 @@ public class AzureService : IManageAzure
}
public async Task Store(SaveOutputRequest request)
public async Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output)
{
try
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var parentDirName = $"{request.Directory}/{request.Org}-{request.Repo}";
var parentDirName = $"{dir}/{org}-{repo}";
var fileName = $"{request.FileName}.{request.Extension}";
var fileName = $"{filename}.{extension}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
await share.CreateIfNotExistsAsync();
await share.GetDirectoryClient($"{request.Directory}").CreateIfNotExistsAsync(); ;
await share.GetDirectoryClient($"{dir}").CreateIfNotExistsAsync(); ;
var parentDir = share.GetDirectoryClient(parentDirName);
await parentDir.CreateIfNotExistsAsync();
var parentIssueDir = parentDir.GetSubdirectoryClient($"{request.ParentIssueNumber}");
var parentIssueDir = parentDir.GetSubdirectoryClient($"{parentIssueNumber}");
await parentIssueDir.CreateIfNotExistsAsync();
var directory = parentIssueDir.GetSubdirectoryClient($"{request.IssueNumber}");
var directory = parentIssueDir.GetSubdirectoryClient($"{issueNumber}");
await directory.CreateIfNotExistsAsync();
var file = directory.GetFileClient(fileName);
// hack to enable script to save files in the same directory
var cwdHack = "#!/bin/bash\n cd $(dirname $0)";
var output = request.Extension == "sh" ? request.Output.Replace("#!/bin/bash", cwdHack) : request.Output;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(output)))
var contents = extension == "sh" ? output.Replace("#!/bin/bash", cwdHack) : output;
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
{
await file.CreateAsync(stream.Length);
await file.UploadRangeAsync(
@@ -151,39 +147,13 @@ public class AzureService : IManageAzure
{
_logger.LogError(ex, "Error storing output");
}
}
}
public interface IManageAzure
{
Task Store(SaveOutputRequest request);
Task RunInSandbox(SandboxRequest request);
Task Store(string org, string repo, long parentIssueNumber, long issueNumber, string filename, string extension, string dir, string output);
Task RunInSandbox(string org, string repo, long parentIssueNumber, long issueNumber);
Task<bool> IsSandboxCompleted(string sandboxId);
Task DeleteSandbox(string sandboxId);
}
public class SaveOutputRequest
{
public int ParentIssueNumber { get; set; }
public int IssueNumber { get; set; }
public string Output { get; set; }
public string Extension { get; set; }
public string Directory { get; set; }
public string FileName { get; set; }
public string Org { get; set; }
public string Repo { get; set; }
}
[GenerateSerializer]
public class SandboxRequest
{
[Id(0)]
public string Org { get; set; }
[Id(1)]
public string Repo { get; set; }
[Id(2)]
public int IssueNumber { get; set; }
[Id(3)]
public int ParentIssueNumber { get; set; }
}

View File

@@ -23,13 +23,13 @@ public class GithubService : IManageGithub
_httpClient = httpClient;
}
public async Task CommitToBranch(CommitRequest request)
public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch)
{
try
{
var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net";
var dirName = $"{request.Dir}/{request.Org}-{request.Repo}/{request.ParentNumber}/{request.Number}";
var dirName = $"{rootDir}/{org}-{repo}/{parentNumber}/{issueNumber}";
var share = new ShareClient(connectionString, _azSettings.FilesShareName);
var directory = share.GetDirectoryClient(dirName);
@@ -53,8 +53,8 @@ public class GithubService : IManageGithub
var value = reader.ReadToEnd();
await _ghClient.Repository.Content.CreateFile(
request.Org, request.Repo, filePath,
new CreateFileRequest($"Commit message", value, request.Branch)); // TODO: add more meaningfull commit message
org, repo, filePath,
new CreateFileRequest($"Commit message", value, branch)); // TODO: add more meaningfull commit message
}
}
catch (Exception ex)
@@ -75,12 +75,12 @@ public class GithubService : IManageGithub
}
}
public async Task CreateBranch(CreateBranchRequest request)
public async Task CreateBranch(string org, string repo, string branch)
{
try
{
var ghRepo = await _ghClient.Repository.Get(request.Org, request.Repo);
await _ghClient.Git.Reference.CreateBranch(request.Org, request.Repo, request.Branch, ghRepo.DefaultBranch);
var ghRepo = await _ghClient.Repository.Get(org, repo);
await _ghClient.Git.Reference.CreateBranch(org, repo, branch, ghRepo.DefaultBranch);
}
catch (Exception ex)
{
@@ -103,24 +103,18 @@ public class GithubService : IManageGithub
}
}
public async Task<NewIssueResponse> CreateIssue(CreateIssueRequest request)
public async Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber)
{
try
{
var newIssue = new NewIssue($"{request.Label} chain for #{request.ParentNumber}")
var newIssue = new NewIssue($"{function} chain for #{parentNumber}")
{
Body = request.Input,
};
newIssue.Labels.Add(request.Label);
var issue = await _ghClient.Issue.Create(request.Org, request.Repo, newIssue);
var commentBody = $" - [ ] #{issue.Number} - tracks {request.Label}";
var comment = await _ghClient.Issue.Comment.Create(request.Org, request.Repo, (int)request.ParentNumber, commentBody);
return new NewIssueResponse
{
IssueNumber = issue.Number,
CommentId = comment.Id
Body = input,
};
newIssue.Labels.Add(function);
newIssue.Labels.Add($"Parent.{parentNumber}");
var issue = await _ghClient.Issue.Create(org, repo, newIssue);
return issue.Number;
}
catch (Exception ex)
{
@@ -129,46 +123,43 @@ public class GithubService : IManageGithub
}
}
public async Task CreatePR(CreatePRRequest request)
public async Task CreatePR(string org, string repo, long number, string branch)
{
try
{
var ghRepo = await _ghClient.Repository.Get(request.Org, request.Repo);
await _ghClient.PullRequest.Create(request.Org, request.Repo, new NewPullRequest($"New app #{request.Number}", request.Branch, ghRepo.DefaultBranch));
var ghRepo = await _ghClient.Repository.Get(org, repo);
await _ghClient.PullRequest.Create(org, repo, new NewPullRequest($"New app #{number}", branch, ghRepo.DefaultBranch));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating PR");
}
}
public async Task MarkTaskComplete(MarkTaskCompleteRequest request)
public async Task MarkTaskComplete(string org, string repo, int commentId)
{
try
{
var comment = await _ghClient.Issue.Comment.Get(request.Org, request.Repo, request.CommentId);
var comment = await _ghClient.Issue.Comment.Get(org, repo, commentId);
var updatedComment = comment.Body.Replace("[ ]", "[x]");
await _ghClient.Issue.Comment.Update(request.Org, request.Repo, request.CommentId, updatedComment);
await _ghClient.Issue.Comment.Update(org, repo, commentId, updatedComment);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error marking task complete");
}
}
public async Task PostComment(PostCommentRequest request)
public async Task PostComment(string org, string repo, long issueNumber, string comment)
{
try
{
await _ghClient.Issue.Comment.Create(request.Org, request.Repo, request.Number, request.Content);
await _ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, comment);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error posting comment");
}
}
public async Task<IEnumerable<FileResponse>> GetFiles(string org, string repo, string branch, Func<RepositoryContent, bool> filter)
@@ -225,66 +216,18 @@ public class FileResponse
public interface IManageGithub
{
Task<NewIssueResponse> CreateIssue(CreateIssueRequest request);
Task MarkTaskComplete(MarkTaskCompleteRequest request);
Task<int> CreateIssue(string org, string repo, string input, string function, long parentNumber);
Task MarkTaskComplete(string org, string repo, int commentId);
Task CreatePR(CreatePRRequest request);
Task CreateBranch(CreateBranchRequest request);
Task CommitToBranch(CommitRequest request);
Task CreatePR(string org, string repo, long number, string branch);
Task CreateBranch(string org, string repo, string branch);
Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch);
Task PostComment(PostCommentRequest request);
Task PostComment(string org, string repo, long issueNumber, string comment);
Task<IEnumerable<FileResponse>> GetFiles(string org, string repo, string branch, Func<RepositoryContent, bool> filter);
Task<string> GetMainLanguage(string org, string repo);
}
[GenerateSerializer]
public class MarkTaskCompleteRequest
{
[Id(0)]
public string Org { get; set; }
[Id(1)]
public string Repo { get; set; }
[Id(2)]
public int CommentId { get; set; }
}
[GenerateSerializer]
public class CreateIssueRequest
{
[Id(0)]
public string Input { get; set; }
[Id(1)]
public string Label { get; set; }
[Id(2)]
public long ParentNumber { get; set; }
[Id(3)]
public string Org { get; set; }
[Id(4)]
public string Repo { get; set; }
}
public class CreateBranchRequest
{
public string Org { get; set; }
public string Repo { get; set; }
public string Branch { get; set; }
}
public class CreatePRRequest
{
public string Org { get; set; }
public string Repo { get; set; }
public string Branch { get; set; }
public int Number { get; set; }
}
public class PostCommentRequest
{
public string Org { get; set; }
public string Repo { get; set; }
public string Content { get; set; }
public int Number { get; set; }
}
[GenerateSerializer]
public class NewIssueResponse
{
@@ -293,20 +236,3 @@ public class NewIssueResponse
[Id(1)]
public int CommentId { get; set; }
}
[GenerateSerializer]
public class CommitRequest
{
[Id(0)]
public string Dir { get; set; }
[Id(1)]
public string Org { get; set; }
[Id(2)]
public string Repo { get; set; }
[Id(3)]
public int ParentNumber { get; set; }
[Id(4)]
public int Number { get; set; }
[Id(5)]
public string Branch { get; set; }
}