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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-