diff --git a/.devcontainer/startup.sh b/.devcontainer/startup.sh index 28bf98e1c..f71e38436 100644 --- a/.devcontainer/startup.sh +++ b/.devcontainer/startup.sh @@ -2,4 +2,6 @@ curl -k https://localhost:8081/_explorer/emulator.pem > ~/emulatorcert.crt sudo cp ~/emulatorcert.crt /usr/local/share/ca-certificates/ -sudo update-ca-certificates \ No newline at end of file +sudo update-ca-certificates +sleep 10 +dotnet build util/seed-memory/seed-memory.csproj && dotnet util/seed-memory/bin/Debug/net7.0/seed-memory.dll \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..7347a7fbb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/cli/Program.cs b/cli/Program.cs index 1a4d03858..f6da1b116 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -2,6 +2,9 @@ using System.CommandLine; using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; +using Microsoft.SemanticKernel.Connectors.Memory.Qdrant; +using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Orchestration; using skills; @@ -10,9 +13,7 @@ class Program static async Task Main(string[] args) { var kernelSettings = KernelSettings.LoadSettings(); - var kernelConfig = new KernelConfig(); - kernelConfig.AddCompletionBackend(kernelSettings); using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { @@ -22,8 +23,14 @@ class Program .AddDebug(); }); + var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant", 1536, port: 6333)); + var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey); + var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration); + var kernel = new KernelBuilder() .WithLogger(loggerFactory.CreateLogger()) + .WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true) + .WithMemory(semanticTextMemory) .WithConfiguration(kernelConfig).Build(); @@ -108,6 +115,12 @@ class Program public static async Task CallFunction(string skillName, string functionName, string input, IKernel kernel) { Console.WriteLine($"Calling skill '{skillName}' function '{functionName}' with input '{input}'"); + 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 skillConfig = SemanticFunctionConfig.ForSkillAndFunction(skillName, functionName); var function = kernel.CreateSemanticFunction(skillConfig.PromptTemplate, skillConfig.Name, skillConfig.SkillName, skillConfig.Description, skillConfig.MaxTokens, skillConfig.Temperature, @@ -115,6 +128,7 @@ class Program 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(answer.ToString()) : (T)(object)answer.ToString(); diff --git a/cli/cli.csproj b/cli/cli.csproj index f72563fc6..87703ee95 100644 --- a/cli/cli.csproj +++ b/cli/cli.csproj @@ -2,9 +2,8 @@ Exe - net8.0 + net7.0 enable - true enable @@ -13,7 +12,8 @@ - + + diff --git a/cli/config/KernelSettings.cs b/cli/config/KernelSettings.cs index 5d01fb69d..754fa84ac 100644 --- a/cli/config/KernelSettings.cs +++ b/cli/config/KernelSettings.cs @@ -16,6 +16,8 @@ internal class KernelSettings [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; diff --git a/util/prompts/Initial Repo Prompt.txt b/cli/util/Initial Repo Prompt.txt similarity index 100% rename from util/prompts/Initial Repo Prompt.txt rename to cli/util/Initial Repo Prompt.txt diff --git a/util/prompts/InitialRepoPromptOutput.sh b/cli/util/InitialRepoPromptOutput.sh similarity index 100% rename from util/prompts/InitialRepoPromptOutput.sh rename to cli/util/InitialRepoPromptOutput.sh diff --git a/cli/util/ToDoListSamplePrompt.txt b/cli/util/ToDoListSamplePrompt.txt index e8fe720b8..842d4c952 100644 --- a/cli/util/ToDoListSamplePrompt.txt +++ b/cli/util/ToDoListSamplePrompt.txt @@ -2,4 +2,5 @@ I'd like to build a typical Todo List Application: a simple productivity tool th 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/sk-azfunc-server/sk-csharp-azure-functions.csproj b/sk-azfunc-server/sk-csharp-azure-functions.csproj index a649fdfc5..602415f86 100644 --- a/sk-azfunc-server/sk-csharp-azure-functions.csproj +++ b/sk-azfunc-server/sk-csharp-azure-functions.csproj @@ -23,7 +23,7 @@ - + diff --git a/sk-dev-team.sln b/sk-dev-team.sln index 190f2328c..1ca548760 100644 --- a/sk-dev-team.sln +++ b/sk-dev-team.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "skills", "skills\skills.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cli", "cli\cli.csproj", "{C0073CEA-8A1B-43BC-BADE-F6323CDFA853}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{16A0621E-E1B8-4737-9B3D-08EB9CEF87B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "seed-memory", "util\seed-memory\seed-memory.csproj", "{94C4BF5F-50BF-41CF-8B94-11F39CA430FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,10 @@ Global {C0073CEA-8A1B-43BC-BADE-F6323CDFA853}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0073CEA-8A1B-43BC-BADE-F6323CDFA853}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0073CEA-8A1B-43BC-BADE-F6323CDFA853}.Release|Any CPU.Build.0 = Release|Any CPU + {94C4BF5F-50BF-41CF-8B94-11F39CA430FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94C4BF5F-50BF-41CF-8B94-11F39CA430FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94C4BF5F-50BF-41CF-8B94-11F39CA430FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94C4BF5F-50BF-41CF-8B94-11F39CA430FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -34,4 +42,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {30612385-E4F4-4FD9-B648-45AF74CB4915} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {94C4BF5F-50BF-41CF-8B94-11F39CA430FA} = {16A0621E-E1B8-4737-9B3D-08EB9CEF87B0} + EndGlobalSection EndGlobal diff --git a/skills/DevLead.cs b/skills/DevLead.cs index 2f84dc613..9ef4b92a4 100644 --- a/skills/DevLead.cs +++ b/skills/DevLead.cs @@ -12,11 +12,12 @@ public static class DevLead { Please output a JSON data structure with a list of steps and a description of each step, and the steps or subtasks that each requires, and the LLM prompts for each subtask. Do not output any other text. Input: {{$input}} + {{$wafContext}} """, Name = nameof(Plan), SkillName = nameof(DevLead), Description = "From a simple description of an application output a development plan for building the application.", - MaxTokens = 4096, + MaxTokens = 6500, Temperature = 0.0, TopP = 0.0, PPenalty = 0.0, diff --git a/skills/Developer.cs b/skills/Developer.cs index fd63cc29b..379d9ab67 100644 --- a/skills/Developer.cs +++ b/skills/Developer.cs @@ -10,11 +10,12 @@ public static class Developer { Make specific choices about implementation. Do not offer a range of options. Use comments in the code to describe the intent. Do not include other text other than code and code comments. Input: {{$input}} + {{$wafContext}} """, Name = nameof(Implement), SkillName = nameof(Developer), Description = "From a description of a coding task out put the code or scripts necessary to complete the task.", - MaxTokens = 4096, + MaxTokens = 6500, Temperature = 0.0, TopP = 0.0, PPenalty = 0.0, diff --git a/skills/PM.cs b/skills/PM.cs index 766e4b8e4..4f91c420e 100644 --- a/skills/PM.cs +++ b/skills/PM.cs @@ -9,11 +9,12 @@ public static class PM You may include commands to build the applications but do not run them. Do not include any git commands. Input: {{$input}} + {{$wafContext}} """, Name = nameof(BootstrapProject), SkillName = nameof(PM), Description = "Bootstrap a new project", - MaxTokens = 7000, + MaxTokens = 6500, Temperature = 0.0, TopP = 0.0, PPenalty = 0.0, @@ -27,11 +28,12 @@ public static class PM Do not describe how to create the application. Write the README as if it were documenting the features and architecture of the application. You may include instructions for how to run the application. Input: {{$input}} + {{$wafContext}} """, Name = nameof(Readme), SkillName = nameof(PM), Description = "From a simple description output a README.md file for a GitHub repository.", - MaxTokens = 7600, + MaxTokens = 6500, Temperature = 0.0, TopP = 0.0, PPenalty = 0.0, diff --git a/skills/skills.csproj b/skills/skills.csproj index 351d9eceb..140c3454d 100644 --- a/skills/skills.csproj +++ b/skills/skills.csproj @@ -8,6 +8,6 @@ - + diff --git a/util/seed-memory/Dockerfile b/util/seed-memory/Dockerfile new file mode 100644 index 000000000..3de5d1359 --- /dev/null +++ b/util/seed-memory/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["util/seed-memory/seed-memory.csproj", "util/seed-memory/"] +RUN dotnet restore "util/seed-memory/seed-memory.csproj" +COPY . . +WORKDIR "/src/util/seed-memory" +RUN dotnet build "seed-memory.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "seed-memory.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "seed-memory.dll"] diff --git a/util/seed-memory/Program.cs b/util/seed-memory/Program.cs new file mode 100644 index 000000000..73cc3de3f --- /dev/null +++ b/util/seed-memory/Program.cs @@ -0,0 +1,57 @@ +using UglyToad.PdfPig; +using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Text; +using Microsoft.Extensions.Logging; +using System.Text; +using Microsoft.SemanticKernel.Connectors.Memory.Qdrant; +using Microsoft.SemanticKernel.Connectors.AI.OpenAI.TextEmbedding; +using Microsoft.SemanticKernel.Memory; +using System.Reflection; + +class Program +{ + static string WafFileName = "azure-well-architected.pdf"; + static async Task Main(string[] args) + { + var kernelSettings = KernelSettings.LoadSettings(); + var kernelConfig = new KernelConfig(); + + using ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder + .SetMinimumLevel(kernelSettings.LogLevel ?? LogLevel.Warning) + .AddConsole() + .AddDebug(); + }); + + var memoryStore = new QdrantMemoryStore(new QdrantVectorDbClient("http://qdrant", 1536, port: 6333)); + var embedingGeneration = new AzureTextEmbeddingGeneration(kernelSettings.EmbeddingDeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey); + var semanticTextMemory = new SemanticTextMemory(memoryStore, embedingGeneration); + + var kernel = new KernelBuilder() + .WithLogger(loggerFactory.CreateLogger()) + .WithAzureChatCompletionService(kernelSettings.DeploymentOrModelId, kernelSettings.Endpoint, kernelSettings.ApiKey, true, kernelSettings.ServiceId, true) + .WithMemory(semanticTextMemory) + .WithConfiguration(kernelConfig).Build(); + await ImportDocumentAsync(kernel, WafFileName); + } + + public static async Task ImportDocumentAsync(IKernel kernel, string filename) + { + var currentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + var filePath = Path.Combine(currentDirectory, filename); + using var pdfDocument = PdfDocument.Open(File.OpenRead(filePath)); + var pages = pdfDocument.GetPages(); + foreach (var page in pages) + { + var text = ContentOrderTextExtractor.GetText(page); + var descr = text.Take(100); + await kernel.Memory.SaveInformationAsync( + collection: "waf-pages", + text: text, + id: $"{Guid.NewGuid()}", + description: $"Document: {descr}"); + } + } +} \ No newline at end of file diff --git a/util/seed-memory/azure-well-architected.pdf b/util/seed-memory/azure-well-architected.pdf new file mode 100644 index 000000000..25dfc501a Binary files /dev/null and b/util/seed-memory/azure-well-architected.pdf differ diff --git a/util/seed-memory/config/KernelSettings.cs b/util/seed-memory/config/KernelSettings.cs new file mode 100644 index 000000000..042945c86 --- /dev/null +++ b/util/seed-memory/config/KernelSettings.cs @@ -0,0 +1,93 @@ +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"; + public const string Qdrant = "QDRANT"; + + [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; } + + /// + /// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets. + /// + 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); + } + } + + /// + /// Load the kernel settings from the specified configuration file if it exists. + /// + 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() + ?? throw new InvalidDataException($"Invalid semantic kernel settings in '{configFile}', please provide configuration settings using instructions in the README."); + } + + /// + /// Load the kernel settings from user secrets. + /// + internal static KernelSettings FromUserSecrets() + { + var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + + return configuration.Get() + ?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README."); + } +} diff --git a/util/seed-memory/seed-memory.csproj b/util/seed-memory/seed-memory.csproj new file mode 100644 index 000000000..e36401b37 --- /dev/null +++ b/util/seed-memory/seed-memory.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + waf_import + enable + enable + + + + + + + + + + + + + + + PreserveNewest + + +