diff --git a/eng/Versions.props b/eng/Versions.props index c2eb8b03d5a..8c1ba5cfc0a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -154,7 +154,7 @@ 11.6.0 1.37.0-preview 1.37.0 - 5.0.7 + 5.1.5 2.2.0-beta.1 0.1.9 6.0.1 diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index e6fb9d4dafb..84eb7b99e8e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -47,7 +48,7 @@ public static void AddMessages(this IList list, ChatResponse respon /// is . /// is . /// - /// As part of combining into a series of instances, tne + /// As part of combining into a series of instances, the /// method may use to determine message boundaries, as well as coalesce /// contiguous items where applicable, e.g. multiple /// instances in a row may be combined into a single . @@ -65,6 +66,33 @@ public static void AddMessages(this IList list, IEnumerableConverts the into a instance and adds it to . + /// The destination list to which the newly constructed message should be added. + /// The instance to convert to a message and add to the list. + /// A predicate to filter which gets included in the message. + /// is . + /// is . + /// + /// If the has no content, or all its content gets excluded by , then + /// no will be added to the . + /// + public static void AddMessages(this IList list, ChatResponseUpdate update, Func? filter = null) + { + _ = Throw.IfNull(list); + _ = Throw.IfNull(update); + + var contentsList = filter is null ? update.Contents : update.Contents.Where(filter).ToList(); + if (contentsList.Count > 0) + { + list.Add(new ChatMessage(update.Role ?? ChatRole.Assistant, contentsList) + { + AuthorName = update.AuthorName, + RawRepresentation = update.RawRepresentation, + AdditionalProperties = update.AdditionalProperties, + }); + } + } + /// Converts the into instances and adds them to . /// The list to which the newly constructed messages should be added. /// The instances to convert to messages and add to the list. diff --git a/src/ProjectTemplates/.gitignore b/src/ProjectTemplates/.gitignore index cfa143cb64d..706de75b916 100644 --- a/src/ProjectTemplates/.gitignore +++ b/src/ProjectTemplates/.gitignore @@ -6,6 +6,9 @@ package-lock.json # Don't track files generated for debugging templates locally. */src/**/*.csproj +*/src/**/NuGet.config +*/src/**/Directory.Build.targets +*/src/**/ingestioncache.db # launchSettings.json files are required for the templates. !launchSettings.json diff --git a/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj b/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj index 03a3ba71b3a..cead17cde9e 100644 --- a/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj +++ b/src/ProjectTemplates/GenerateTemplateContent/GenerateTemplateContent.csproj @@ -1,9 +1,8 @@ - - netstandard2.0 + true <_GeneratedContentPropertiesHashFile>$(IntermediateOutputPath)$(MSBuildProjectName).content.g.cache @@ -24,7 +23,8 @@ in the _GenerateContent target. This hash is used to determine if the generated content needs to be re-generated. --> - + @@ -45,6 +45,12 @@ Inputs="$(MSBuildAllProjects);$(_GeneratedContentPropertiesHashFile);@(GeneratedContent)" Outputs="@(GeneratedContent->'%(OutputPath)')"> + + + + + + + + diff --git a/src/ProjectTemplates/GeneratedContent.props b/src/ProjectTemplates/GeneratedContent.props deleted file mode 100644 index f69ac2d5979..00000000000 --- a/src/ProjectTemplates/GeneratedContent.props +++ /dev/null @@ -1,34 +0,0 @@ - - - - <_MicrosoftExtensionsAIVersion>9.3.0-preview.1.25114.11 - - - - $(GeneratedContentProperties); - OllamaSharpVersion=$(OllamaSharpVersion); - OpenAIVersion=$(OpenAIVersion); - AzureAIProjectsVersion=$(AzureAIProjectsVersion); - AzureAIOpenAIVersion=$(AzureAIOpenAIVersion); - AzureIdentityVersion=$(AzureIdentityVersion); - MicrosoftEntityFrameworkCoreSqliteVersion=$(MicrosoftEntityFrameworkCoreSqliteVersion); - MicrosoftExtensionsAIVersion=$(_MicrosoftExtensionsAIVersion); - MicrosoftSemanticKernelCoreVersion=$(MicrosoftSemanticKernelCoreVersion); - PdfPigVersion=$(PdfPigVersion); - SystemLinqAsyncVersion=$(SystemLinqAsyncVersion); - AzureSearchDocumentsVersion=$(AzureSearchDocumentsVersion); - MicrosoftSemanticKernelConnectorsAzureAISearchVersion=$(MicrosoftSemanticKernelConnectorsAzureAISearchVersion); - - - - - <_ChatWithCustomDataWebContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\ChatWithCustomData\ChatWithCustomData.Web-CSharp\ - - - - - - - diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets new file mode 100644 index 00000000000..d84b8d994d4 --- /dev/null +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -0,0 +1,72 @@ + + + + <_ChatWithCustomDataWebContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\ChatWithCustomData\ChatWithCustomData.Web-CSharp\ + + + + + + 9.3.0-preview.1.25161.3 + 9.0.3 + + + false + false + + + $(TemplatePinnedMicrosoftExtensionsAIVersion) + $(TemplatePinnedMicrosoftEntityFrameworkCoreSqliteVersion) + + + $(Version) + $(MicrosoftEntityFrameworkCoreSqliteVersion) + + <_TemplateUsingJustBuiltPackages Condition="'$(TemplateMicrosoftExtensionsAIVersion)' == '$(Version)'">true + + + + $(GeneratedContentProperties); + + + ArtifactsShippingPackagesDir=$(ArtifactsShippingPackagesDir); + + + OllamaSharpVersion=$(OllamaSharpVersion); + OpenAIVersion=$(OpenAIVersion); + AzureAIProjectsVersion=$(AzureAIProjectsVersion); + AzureAIOpenAIVersion=$(AzureAIOpenAIVersion); + AzureIdentityVersion=$(AzureIdentityVersion); + MicrosoftEntityFrameworkCoreSqliteVersion=$(TemplateMicrosoftEntityFrameworkCoreSqliteVersion); + MicrosoftExtensionsAIVersion=$(TemplateMicrosoftExtensionsAIVersion); + MicrosoftSemanticKernelCoreVersion=$(MicrosoftSemanticKernelCoreVersion); + PdfPigVersion=$(PdfPigVersion); + SystemLinqAsyncVersion=$(SystemLinqAsyncVersion); + AzureSearchDocumentsVersion=$(AzureSearchDocumentsVersion); + MicrosoftSemanticKernelConnectorsAzureAISearchVersion=$(MicrosoftSemanticKernelConnectorsAzureAISearchVersion); + + + + + + + + <_GeneratedContentEnablingJustBuiltPackages + Include="$(_ChatWithCustomDataWebContentRoot)NuGet.config.in" + OutputPath="$(_ChatWithCustomDataWebContentRoot)NuGet.config" /> + <_GeneratedContentEnablingJustBuiltPackages + Include="$(_ChatWithCustomDataWebContentRoot)Directory.Build.targets.in" + OutputPath="$(_ChatWithCustomDataWebContentRoot)Directory.Build.targets" /> + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 2762f3148df..34dcf90629b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -35,16 +35,21 @@ + + **\package-lock.json; + **\ingestioncache.db; + **\NuGet.config; + **\Directory.Build.targets;" /> diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/ChatWithCustomData.Web-CSharp.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/ChatWithCustomData.Web-CSharp.csproj.in index 707e0a73855..92cb05131b3 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/ChatWithCustomData.Web-CSharp.csproj.in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/ChatWithCustomData.Web-CSharp.csproj.in @@ -24,7 +24,7 @@ - + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Components/Pages/Chat/Chat.razor b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Components/Pages/Chat/Chat.razor index 1858ce1cbab..b17de180fc1 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Components/Pages/Chat/Chat.razor +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Components/Pages/Chat/Chat.razor @@ -67,23 +67,25 @@ // aren't supported because Ollama will not support both streaming and using Tools currentResponseCancellation = new(); var response = await ChatClient.GetResponseAsync(messages, chatOptions, currentResponseCancellation.Token); - currentResponseMessage = response.Message; - ChatMessageItem.NotifyChanged(currentResponseMessage); + + // Store responses in the conversation, and begin getting suggestions + messages.AddMessages(response); #else*@ // Stream and display a new response from the IChatClient var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var chunk in ChatClient.GetStreamingResponseAsync(messages, chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) { - responseText.Text += chunk.Text; + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; ChatMessageItem.NotifyChanged(currentResponseMessage); } -@*#endif*@ // Store the final response in the conversation, and begin getting suggestions messages.Add(currentResponseMessage!); currentResponseMessage = null; +@*#endif*@ chatSuggestions?.Update(messages); } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Directory.Build.targets.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Directory.Build.targets.in new file mode 100644 index 00000000000..8fe33082cd6 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/Directory.Build.targets.in @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/NuGet.config.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/NuGet.config.in new file mode 100644 index 00000000000..8ad880bce31 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/ChatWithCustomData/ChatWithCustomData.Web-CSharp/NuGet.config.in @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/ProjectTemplates/README.md b/src/ProjectTemplates/README.md index b78f1361a32..70940042158 100644 --- a/src/ProjectTemplates/README.md +++ b/src/ProjectTemplates/README.md @@ -11,7 +11,17 @@ To update project template JavaScript dependencies: To add a new dependency, run `npm install ` and update the `scripts` section in `package.json` to specify how the new dependency should be copied into its template. -# Installing the templates locally +# Running AI templates + +By default the templates use just-built versions of `Microsoft.Extensions.AI*` packages, so NuGet packages must be produced before the templates can be run: +```sh +.\build.cmd -vs AI -noLaunch # Generate an SDK.sln for Microsoft.Extensions.AI* projects +.\build.cmd -build -pack # Build a NuGet package for each project +``` + +Alternatively, you can override the `TemplateMicrosoftExtensionsAIVersion` property (defined in the `GeneratedContent.targets` file in this directory) with a publicly-available version. This will disable the template generation logic that utilizes locally-built `Microsoft.Extensions.AI*` packages. + +## Installing the templates locally First, create the template NuGet package by running the following from the repo root: ```pwsh @@ -37,3 +47,21 @@ Finally, create a project from the template and run it: dotnet new aichatweb [-A ] [-V ] dotnet run ``` + +## Running the templates directly within the repo + +The project templates are structured in a way that allows them to be run directly within the repo. + +**Note:** For the following commands to succeed, you'll need to either install a compatible .NET SDK globally or prepend the repo's generated `.dotnet` folder to the PATH environment variable. + +Navigate to the `Microsoft.Extensions.AI.Templates` folder and run: +```sh +dotnet build +``` + +This will generate the necessary template content to build and run AI templates from within this repo. + +Now, you can navigate to a folder containing a template's `.csproj` file and run: +```sh +dotnet run +``` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AichatwebTemplatesTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AichatwebTemplatesTests.cs index 26c4c47f0fd..7953003cf63 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AichatwebTemplatesTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AichatwebTemplatesTests.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.IO; +using System.Linq; using System.Threading.Tasks; -using EmptyFiles; using Microsoft.Extensions.AI.Templates.IntegrationTests; using Microsoft.Extensions.AI.Templates.Tests; using Microsoft.Extensions.Logging; @@ -16,6 +16,21 @@ namespace Microsoft.Extensions.AI.Templates.InegrationTests; public class AichatwebTemplatesTests : TestBase { + // Keep the exclude patterns below in sync with those in Microsoft.Extensions.AI.Templates.csproj. + private static readonly string[] _verificationExcludePatterns = [ + "**/bin/**", + "**/obj/**", + "**/node_modules/**", + "**/*.user", + "**/*.in", + "**/*.out.js", + "**/*.generated.css", + "**/package-lock.json", + "**/ingestioncache.db", + "**/NuGet.config", + "**/Directory.Build.targets", + ]; + private readonly ILogger _log; public AichatwebTemplatesTests(ITestOutputHelper log) @@ -34,8 +49,9 @@ public async Task BasicTest() // Get the template location string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.Extensions.AI.Templates", "src", "ChatWithCustomData"); - // Treat *.in files as text, see https://github.com/VerifyTests/EmptyFiles#istext - FileExtensions.AddTextExtension(".in"); + var verificationExcludePatterns = Path.DirectorySeparatorChar is '/' + ? _verificationExcludePatterns + : _verificationExcludePatterns.Select(p => p.Replace('/', Path.DirectorySeparatorChar)).ToArray(); TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) { @@ -44,6 +60,7 @@ public async Task BasicTest() OutputDirectory = workingDir, DoNotPrependCallerMethodNameToScenarioName = true, ScenarioName = "Basic", + VerificationExcludePatterns = verificationExcludePatterns, } .WithCustomScrubbers( ScrubbersDefinition.Empty.AddScrubber((path, content) => @@ -54,6 +71,7 @@ public async Task BasicTest() filePath.EndsWith("aichatweb/aichatweb.csproj.in")) { content.ScrubByRegex("(.*)<\\/UserSecretsId>", "secret"); + content.ScrubByRegex("\"(\\d*\\.\\d*\\.\\d*)-(dev|ci)\"", "\"$1\""); } if (filePath.EndsWith("aichatweb/Properties/launchSettings.json")) diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor index d8c95dc16f2..77626e2d565 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -66,9 +66,10 @@ var responseText = new TextContent(""); currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); currentResponseCancellation = new(); - await foreach (var chunk in ChatClient.GetStreamingResponseAsync(messages, chatOptions, currentResponseCancellation.Token)) + await foreach (var update in ChatClient.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) { - responseText.Text += chunk.Text; + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; ChatMessageItem.NotifyChanged(currentResponseMessage); } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj index acb773dfc7e..a73d885e592 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj.in b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj.in deleted file mode 100644 index bbe0057572b..00000000000 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb.Basic.verified/aichatweb/aichatweb.csproj.in +++ /dev/null @@ -1,38 +0,0 @@ - - - - net9.0 - enable - enable - secret - - - - - - - - - - - - - - - - - - -