diff --git a/eng/packages/ProjectTemplates.props b/eng/packages/ProjectTemplates.props index eb053b5270d..385410facee 100644 --- a/eng/packages/ProjectTemplates.props +++ b/eng/packages/ProjectTemplates.props @@ -19,6 +19,7 @@ + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json index 4058affe004..51a6aac70d4 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json @@ -128,18 +128,13 @@ "type": "parameter", "displayName": "_AI service provider", "datatype": "choice", - "defaultValue": "githubmodels", + "defaultValue": "foundrylocal", "choices": [ { "choice": "azureopenai", "displayName": "Azure OpenAI", "description": "Uses Azure OpenAI service" }, - { - "choice": "githubmodels", - "displayName": "GitHub Models", - "description": "Uses GitHub Models" - }, { "choice": "ollama", "displayName": "Ollama (for local development)", @@ -149,6 +144,11 @@ "choice": "openai", "displayName": "OpenAI Platform", "description": "Uses the OpenAI Platform" + }, + { + "choice": "foundrylocal", + "displayName": "Foundry Local (on-device)", + "description": "Uses Foundry Local with the qwen3-4b and qwen3-embedding-0.6b models" } // { // "choice": "azureaifoundry", @@ -215,14 +215,14 @@ "type": "computed", "value": "(AiServiceProvider == \"openai\")" }, - "IsGHModels": { - "type": "computed", - "value": "(AiServiceProvider == \"githubmodels\")" - }, "IsOllama": { "type": "computed", "value": "(AiServiceProvider == \"ollama\")" }, + "IsFoundryLocal": { + "type": "computed", + "value": "(AiServiceProvider == \"foundrylocal\")" + }, "IsAzureAIFoundry": { "type": "computed", "value": "(AiServiceProvider == \"azureaifoundry\")" @@ -317,6 +317,38 @@ }, "replaces": "all-minilm" }, + "FoundryLocalChatModelDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "qwen3-4b" + } + }, + "FoundryLocalEmbeddingModelDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "qwen3-embedding-0.6b" + } + }, + "FoundryLocalChatModel": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "ChatModel", + "fallbackVariableName": "FoundryLocalChatModelDefault" + }, + "replaces": "qwen3-4b" + }, + "FoundryLocalEmbeddingModel": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "EmbeddingModel", + "fallbackVariableName": "FoundryLocalEmbeddingModelDefault" + }, + "replaces": "qwen3-embedding-0.6b" + }, "webHttpPort": { "type": "parameter", "datatype": "integer", diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs index 3304884fa78..fd577fedc2c 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs @@ -1,17 +1,11 @@ var builder = DistributedApplication.CreateBuilder(args); #if (IsOllama) // ASPIRE PARAMETERS -#elif (IsOpenAI || IsGHModels) +#elif (IsOpenAI) // You will need to set the connection string to your own value // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory -#if (IsOpenAI) // dotnet user-secrets set ConnectionStrings:openai "Key=YOUR-API-KEY" -#elif (IsGHModels) -// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" -#else // IsAzureOpenAI -// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY" -#endif var openai = builder.AddConnectionString("openai"); #else // IsAzureOpenAI @@ -62,7 +56,7 @@ .WithReference(embeddings) .WaitFor(chat) .WaitFor(embeddings); -#elif (IsOpenAI || IsGHModels) +#elif (IsOpenAI) webApp.WithReference(openai); #else // IsAzureOpenAI webApp diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in index 3d8811c209e..e490183ab4b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in @@ -7,19 +7,29 @@ d5681fae-b21b-4114-b781-48180f08c0c4 + + + + win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64 + + + - + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs index 31442718f1f..7ce9d732627 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs @@ -1,5 +1,9 @@ +#if (IsFoundryLocal) +// Foundry Local has no .NET Aspire hosting integration and is only supported without Aspire. +#error Foundry Local is not supported with .NET Aspire. Re-create the project without --aspire and with a non-Qdrant vector store (Qdrant requires Aspire). +#endif using Microsoft.Extensions.AI; -#if (IsOpenAI || IsGHModels) +#if (IsOpenAI) using OpenAI; #endif using AIChatWeb_CSharp.Web.Components; @@ -19,7 +23,7 @@ builder.AddOllamaApiClient("embeddings") .AddEmbeddingGenerator(); #elif (IsAzureAIFoundry) -#else // (IsOpenAI || IsAzureOpenAI || IsGHModels) +#else // (IsOpenAI || IsAzureOpenAI) #if (IsOpenAI) var openai = builder.AddOpenAIClient("openai"); #else diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs index 89a231105e9..c386794ad7b 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs @@ -1,8 +1,12 @@ -#if (IsGHModels || IsOpenAI || (IsAzureOpenAI && !IsManagedIdentity)) +#if (IsOpenAI || IsFoundryLocal || (IsAzureOpenAI && !IsManagedIdentity)) using System.ClientModel; #elif (IsAzureOpenAI && IsManagedIdentity) using System.ClientModel.Primitives; #endif +#if (IsFoundryLocal) +using Microsoft.AI.Foundry.Local; +using Microsoft.Extensions.Logging.Abstractions; +#endif #if (IsAzureAISearch && !IsManagedIdentity) using Azure; #elif (IsManagedIdentity) @@ -11,7 +15,7 @@ using Microsoft.Extensions.AI; #if (IsOllama) using OllamaSharp; -#elif (IsGHModels || IsOpenAI || IsAzureOpenAI) +#elif (IsOpenAI || IsFoundryLocal || IsAzureOpenAI) using OpenAI; #endif using AIChatWeb_CSharp.Web.Components; @@ -21,25 +25,52 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -#if (IsGHModels) -// You will need to set the endpoint and key to your own values -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set GitHubModels:Token YOUR-GITHUB-TOKEN -var credential = new ApiKeyCredential(builder.Configuration["GitHubModels:Token"] ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token. See the README for details.")); -var openAIOptions = new OpenAIClientOptions() -{ - Endpoint = new Uri("https://models.inference.ai.azure.com") -}; - -var ghModelsClient = new OpenAIClient(credential, openAIOptions); -var chatClient = ghModelsClient.GetChatClient("gpt-4o-mini").AsIChatClient(); -var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); -#elif (IsOllama) +#if (IsOllama) IChatClient chatClient = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.2"); IEmbeddingGenerator> embeddingGenerator = new OllamaApiClient(new Uri("http://localhost:11434"), "all-minilm"); +#elif (IsFoundryLocal) +var chatAlias = builder.Configuration["FoundryLocal:ChatModel"] ?? "qwen3-4b"; +var embeddingAlias = builder.Configuration["FoundryLocal:EmbeddingModel"] ?? "qwen3-embedding-0.6b"; +var foundryServiceUrl = builder.Configuration["FoundryLocal:ServiceUrl"] ?? "http://127.0.0.1:5273"; + +await FoundryLocalManager.CreateAsync(new Configuration +{ + AppName = "AIChatWeb-CSharp", + Web = new Configuration.WebService { Urls = foundryServiceUrl } +}, NullLogger.Instance); +var foundryManager = FoundryLocalManager.Instance; +await foundryManager.StartWebServiceAsync(); +var foundryCatalog = await foundryManager.GetCatalogAsync(); + +async Task EnsureFoundryModelAsync(string modelAlias) +{ + var model = await foundryCatalog.GetModelAsync(modelAlias) + ?? throw new InvalidOperationException( + $"Foundry Local model '{modelAlias}' was not found in the catalog. Run 'foundry model list' to see available models."); + if (!await model.IsCachedAsync()) + { + Console.WriteLine($"Foundry Local: downloading model '{modelAlias}' (first run only)..."); + await model.DownloadAsync(_ => { }); + } + if (!await model.IsLoadedAsync()) + { + await model.LoadAsync(); + } + return model.Id; +} + +var chatModelId = await EnsureFoundryModelAsync(chatAlias); +var embeddingModelId = await EnsureFoundryModelAsync(embeddingAlias); + +var foundryEndpointUrl = foundryManager.Urls?.FirstOrDefault() ?? foundryServiceUrl; +var foundryEndpoint = new Uri($"{foundryEndpointUrl.TrimEnd('/')}/v1"); +var foundryClient = new OpenAIClient( + new ApiKeyCredential("unused"), + new OpenAIClientOptions { Endpoint = foundryEndpoint }); +var chatClient = foundryClient.GetChatClient(chatModelId).AsIChatClient(); +var embeddingGenerator = foundryClient.GetEmbeddingClient(embeddingModelId).AsIEmbeddingGenerator(); #elif (IsOpenAI) // You will need to set the endpoint and key to your own values // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md index 14677ca5372..0290f72ceef 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md @@ -21,32 +21,6 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See #### ---#endif # Configure the AI Model Provider -#### ---#if (IsGHModels) -To use models hosted by GitHub Models, you will need to create a GitHub personal access token with `models:read` permissions, but no other scopes or permissions. See [Prototyping with AI models](https://docs.github.com/github-models/prototyping-with-ai-models) and [Managing your personal access tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) in the GitHub Docs for more information. - -#### ---#if (hostIdentifier == "vs") -Configure your token for this project using .NET User Secrets: - -1. In Visual Studio, right-click on your project in the Solution Explorer and select "Manage User Secrets". -2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following key and value: - - ```json - { - "GitHubModels:Token": "YOUR-TOKEN" - } - ``` -#### ---#else -From the command line, configure your token for this project using .NET User Secrets by running the following commands: - -```sh -cd <> -dotnet user-secrets set GitHubModels:Token YOUR-TOKEN -``` -#### ---#endif - -Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models). - -#### ---#endif #### ---#if (IsOpenAI) ## Using OpenAI @@ -92,6 +66,34 @@ ollama pull all-minilm ### 3. Learn more about Ollama Once the models are installed, you can start using them in your application. Refer to the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/README.md) for detailed instructions on how to explore models locally. +#### ---#endif +#### ---#if (IsFoundryLocal) +## Setting up a local environment using Foundry Local +This project is configured to use Foundry Local, which runs models on your workstation through a local OpenAI-compatible endpoint. It does not need an API key. + +### 1. Install Foundry Local +Install Foundry Local for your operating system by following the [Foundry Local documentation](https://learn.microsoft.com/azure/ai-foundry/foundry-local/). + +### 2. Run the app +The app starts the Foundry Local service for you. On first run, it downloads the configured models, then loads them into the local service. + +The default chat model alias is `qwen3-4b`. The default embedding model alias is `qwen3-embedding-0.6b`. + +### 3. Override model aliases or the service URL +You can change the defaults in `appsettings.json`, `appsettings.Development.json`, or user secrets: + +```json +{ + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } +} +``` + +Use `FoundryLocal:ServiceUrl` if another local process already uses the default port. + #### ---#endif #### ---#if (IsAzureOpenAI) ## Using Azure OpenAI diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs index 60e6b5684e4..3acc0ba26de 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs @@ -7,6 +7,8 @@ public class IngestedChunk { #if (IsOllama) public const int VectorDimensions = 384; // 384 is the default vector size for the all-minilm embedding model +#elif (IsFoundryLocal) + public const int VectorDimensions = 1024; // 1024 is the default vector size for the qwen3-embedding-0.6b embedding model #else public const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model #endif @@ -17,9 +19,15 @@ public class IngestedChunk #endif public const string CollectionName = "data-AIChatWeb-CSharp.Web-chunks"; +#if (IsQdrant) [VectorStoreKey(StorageName = "key")] [JsonPropertyName("key")] public required Guid Key { get; set; } +#else + [VectorStoreKey(StorageName = "key")] + [JsonPropertyName("key")] + public required string Key { get; set; } +#endif [VectorStoreData(StorageName = "documentid")] [JsonPropertyName("documentid")] diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json index d7b2fc5dca0..5dde9fc2245 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json @@ -6,4 +6,11 @@ "Microsoft.EntityFrameworkCore": "Warning" } } +//#if (IsFoundryLocal) + ,"FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } +//#endif } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json index 46bdb452246..ca99e211be7 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json @@ -6,5 +6,12 @@ "Microsoft.EntityFrameworkCore": "Warning" } }, +//#if (IsFoundryLocal) + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + }, +//#endif "AllowedHosts": "*" } diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md index 0fff810489c..fc3071a5e80 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md @@ -20,32 +20,6 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See # Configure the AI Model Provider -#### ---#if (IsGHModels) -## Using GitHub Models -To use models hosted by GitHub Models, you will need to create a GitHub personal access token. The token should not have any scopes or permissions. See [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). - -#### ---#if (hostIdentifier == "vs") -Configure your token for this project using .NET User Secrets: - -1. In Visual Studio, right-click on the AIChatWeb-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets". -2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following key and value: - - ```json - { - "ConnectionStrings:openai": "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" - } - ``` -#### ---#else -From the command line, configure your token for this project using .NET User Secrets by running the following commands: - -```sh -cd AIChatWeb-CSharp.AppHost -dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" -``` -#### ---#endif - -Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models). -#### ---#endif #### ---#if (IsOpenAI) ## Using OpenAI diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs index 513f4cf0394..b16ab3c4fc9 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs @@ -38,7 +38,7 @@ public AIChatWebExecutionTests(TemplateExecutionTestFixture fixture, ITestOutput public static IEnumerable GetSupportedProjectConfigurations() { (string name, string[] values)[] allOptionValues = [ - ("--provider", ["azureopenai", "githubmodels", "ollama", "openai" /*, "azureaifoundry" */]), + ("--provider", ["azureopenai", "ollama", "openai", "foundrylocal" /*, "azureaifoundry" */]), ("--vector-store", ["azureaisearch", "local", "qdrant"]), ("--aspire", ["true", "false"]), ("--managed-identity", ["true", "false"]), @@ -69,6 +69,13 @@ public static IEnumerable GetSupportedProjectConfigurations() continue; } + // Foundry Local is only supported without Aspire (Qdrant requires Aspire, so it's unsupported too) + if (HasOption(args, "--provider", "foundrylocal") && + (HasOption(args, "--aspire") || HasOption(args, "--vector-store", "qdrant"))) + { + continue; + } + yield return args; } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs index dbe71cdbdef..8d4babef5bb 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebSnapshotTests.cs @@ -29,10 +29,11 @@ public AIChatWebSnapshotTests(ITestOutputHelper log) } [Theory] - [InlineData /* Defaults: --provider=githubmodels --vector-store=local */] + [InlineData /* Defaults: --provider=foundrylocal --vector-store=local */] [InlineData("--provider=ollama", "--vector-store=qdrant")] [InlineData("--provider=openai", "--vector-store=azureaisearch")] - [InlineData("--aspire")] + [InlineData("--provider=foundrylocal", "--vector-store=local", "--webHttpPort", "9996", "--webHttpsPort", "9995")] + [InlineData("--aspire", "--provider=openai")] [InlineData("--aspire", "--provider=azureopenai", "--vector-store=azureaisearch")] public async Task RunSnapshotTests(params string[] templateArgs) { diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_aoai_aais.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_aoai_aais.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index 2d93db8fd94..44aa90f00bd 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_aoai_aais.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_aoai_aais.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -11,7 +11,7 @@ public class IngestedChunk [VectorStoreKey(StorageName = "key")] [JsonPropertyName("key")] - public required Guid Key { get; set; } + public required string Key { get; set; } [VectorStoreData(StorageName = "documentid")] [JsonPropertyName("documentid")] diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/README.md similarity index 78% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/README.md rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/README.md index 94c542fda7f..e587d7a5f6c 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/README.md @@ -15,18 +15,17 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See # Configure the AI Model Provider -## Using GitHub Models -To use models hosted by GitHub Models, you will need to create a GitHub personal access token. The token should not have any scopes or permissions. See [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). +## Using OpenAI -From the command line, configure your token for this project using .NET User Secrets by running the following commands: +To call the OpenAI REST API, you will need an API key. To obtain one, first [create a new OpenAI account](https://platform.openai.com/signup) or [log in](https://platform.openai.com/login). Next, navigate to the API key page and select "Create new secret key", optionally naming the key. Make sure to save your API key somewhere safe and do not share it with anyone. + +From the command line, configure your API key for this project using .NET User Secrets by running the following commands: ```sh cd aichatweb.AppHost -dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" +dotnet user-secrets set ConnectionStrings:openai "Key=YOUR-API-KEY" ``` -Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models). - # Running the application ## Using Visual Studio diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/AppHost.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/AppHost.cs similarity index 85% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/AppHost.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/AppHost.cs index bf116a0c47e..96b05518036 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/AppHost.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/AppHost.cs @@ -3,7 +3,7 @@ // You will need to set the connection string to your own value // You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: // cd this-project-directory -// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" +// dotnet user-secrets set ConnectionStrings:openai "Key=YOUR-API-KEY" var openai = builder.AddConnectionString("openai"); var markitdown = builder.AddContainer("markitdown", "mcp/markitdown") diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/Properties/launchSettings.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/aichatweb.AppHost.csproj diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/appsettings.Development.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/appsettings.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.AppHost/appsettings.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.AppHost/appsettings.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.ServiceDefaults/Extensions.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.ServiceDefaults/aichatweb.ServiceDefaults.csproj diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/App.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/App.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/App.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/App.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/LoadingSpinner.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/MainLayout.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Layout/SurveyPrompt.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/Chat.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatCitation.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatHeader.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatInput.razor.js diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageItem.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatMessageList.razor.js diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Chat/ChatSuggestions.razor.css diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Pages/Error.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Routes.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Routes.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/Routes.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/Routes.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/_Imports.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/_Imports.razor similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Components/_Imports.razor rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Components/_Imports.razor diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Program.cs similarity index 96% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Program.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Program.cs index e47bad71cc8..08772380700 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Program.cs @@ -8,7 +8,7 @@ builder.AddServiceDefaults(); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -var openai = builder.AddAzureOpenAIClient("openai"); +var openai = builder.AddOpenAIClient("openai"); openai.AddChatClient("gpt-4o-mini") .UseFunctionInvocation() .UseOpenTelemetry(configure: c => diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Properties/launchSettings.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs similarity index 95% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs index af609ea239e..2f1ca15090a 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/IngestedChunk.cs @@ -11,7 +11,7 @@ public class IngestedChunk [VectorStoreKey(StorageName = "key")] [JsonPropertyName("key")] - public required Guid Key { get; set; } + public required string Key { get; set; } [VectorStoreData(StorageName = "documentid")] [JsonPropertyName("documentid")] diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/Ingestion/DataIngestor.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/Ingestion/DocumentReader.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/Ingestion/DocumentReader.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/Ingestion/DocumentReader.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/Ingestion/DocumentReader.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/Services/SemanticSearch.cs diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/aichatweb.Web.csproj diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/appsettings.Development.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/appsettings.Development.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/appsettings.Development.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/appsettings.json similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.Web/appsettings.json rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.Web/appsettings.json diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.sln b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.sln similarity index 100% rename from test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A.verified/aichatweb/aichatweb.sln rename to test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.A_oai.verified/aichatweb/aichatweb.sln diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Program.cs index 27e50372647..d0da26ad6cf 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Program.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Program.cs @@ -1,4 +1,6 @@ using System.ClientModel; +using Microsoft.AI.Foundry.Local; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.AI; using OpenAI; using aichatweb.Components; @@ -8,19 +10,46 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents().AddInteractiveServerComponents(); -// You will need to set the endpoint and key to your own values -// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line: -// cd this-project-directory -// dotnet user-secrets set GitHubModels:Token YOUR-GITHUB-TOKEN -var credential = new ApiKeyCredential(builder.Configuration["GitHubModels:Token"] ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token. See the README for details.")); -var openAIOptions = new OpenAIClientOptions() +var chatAlias = builder.Configuration["FoundryLocal:ChatModel"] ?? "qwen3-4b"; +var embeddingAlias = builder.Configuration["FoundryLocal:EmbeddingModel"] ?? "qwen3-embedding-0.6b"; +var foundryServiceUrl = builder.Configuration["FoundryLocal:ServiceUrl"] ?? "http://127.0.0.1:5273"; + +await FoundryLocalManager.CreateAsync(new Configuration +{ + AppName = "aichatweb", + Web = new Configuration.WebService { Urls = foundryServiceUrl } +}, NullLogger.Instance); +var foundryManager = FoundryLocalManager.Instance; +await foundryManager.StartWebServiceAsync(); +var foundryCatalog = await foundryManager.GetCatalogAsync(); + +async Task EnsureFoundryModelAsync(string modelAlias) { - Endpoint = new Uri("https://models.inference.ai.azure.com") -}; + var model = await foundryCatalog.GetModelAsync(modelAlias) + ?? throw new InvalidOperationException( + $"Foundry Local model '{modelAlias}' was not found in the catalog. Run 'foundry model list' to see available models."); + if (!await model.IsCachedAsync()) + { + Console.WriteLine($"Foundry Local: downloading model '{modelAlias}' (first run only)..."); + await model.DownloadAsync(_ => { }); + } + if (!await model.IsLoadedAsync()) + { + await model.LoadAsync(); + } + return model.Id; +} + +var chatModelId = await EnsureFoundryModelAsync(chatAlias); +var embeddingModelId = await EnsureFoundryModelAsync(embeddingAlias); -var ghModelsClient = new OpenAIClient(credential, openAIOptions); -var chatClient = ghModelsClient.GetChatClient("gpt-4o-mini").AsIChatClient(); -var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); +var foundryEndpointUrl = foundryManager.Urls?.FirstOrDefault() ?? foundryServiceUrl; +var foundryEndpoint = new Uri($"{foundryEndpointUrl.TrimEnd('/')}/v1"); +var foundryClient = new OpenAIClient( + new ApiKeyCredential("unused"), + new OpenAIClientOptions { Endpoint = foundryEndpoint }); +var chatClient = foundryClient.GetChatClient(chatModelId).AsIChatClient(); +var embeddingGenerator = foundryClient.GetEmbeddingClient(embeddingModelId).AsIEmbeddingGenerator(); var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/README.md index cf7deff5878..75ad9811358 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/README.md +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/README.md @@ -6,14 +6,29 @@ This project is an AI chat application that demonstrates how to chat with custom > Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. # Configure the AI Model Provider -To use models hosted by GitHub Models, you will need to create a GitHub personal access token with `models:read` permissions, but no other scopes or permissions. See [Prototyping with AI models](https://docs.github.com/github-models/prototyping-with-ai-models) and [Managing your personal access tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) in the GitHub Docs for more information. +## Setting up a local environment using Foundry Local +This project is configured to use Foundry Local, which runs models on your workstation through a local OpenAI-compatible endpoint. It does not need an API key. -From the command line, configure your token for this project using .NET User Secrets by running the following commands: +### 1. Install Foundry Local +Install Foundry Local for your operating system by following the [Foundry Local documentation](https://learn.microsoft.com/azure/ai-foundry/foundry-local/). -```sh -cd <> -dotnet user-secrets set GitHubModels:Token YOUR-TOKEN +### 2. Run the app +The app starts the Foundry Local service for you. On first run, it downloads the configured models, then loads them into the local service. + +The default chat model alias is `qwen3-4b`. The default embedding model alias is `qwen3-embedding-0.6b`. + +### 3. Override model aliases or the service URL +You can change the defaults in `appsettings.json`, `appsettings.Development.json`, or user secrets: + +```json +{ + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } +} ``` -Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models). +Use `FoundryLocal:ServiceUrl` if another local process already uses the default port. diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Services/IngestedChunk.cs index 68af3ef20fb..6a3ff635187 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/Services/IngestedChunk.cs @@ -5,13 +5,13 @@ namespace aichatweb.Services; public class IngestedChunk { - public const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model + public const int VectorDimensions = 1024; // 1024 is the default vector size for the qwen3-embedding-0.6b embedding model public const string VectorDistanceFunction = DistanceFunction.CosineDistance; public const string CollectionName = "data-aichatweb-chunks"; [VectorStoreKey(StorageName = "key")] [JsonPropertyName("key")] - public required Guid Key { get; set; } + public required string Key { get; set; } [VectorStoreData(StorageName = "documentid")] [JsonPropertyName("documentid")] diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/aichatweb.csproj index 4359829d8ad..d7c1843dc34 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/aichatweb.csproj +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/aichatweb.csproj @@ -7,7 +7,13 @@ {00000000-0000-0000-0000-000000000000} + + + win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64 + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.Development.json index e22bd83cf3a..52a351f7c72 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.Development.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.Development.json @@ -6,4 +6,9 @@ "Microsoft.EntityFrameworkCore": "Warning" } } + ,"FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.json index d286041f99d..b16205b8eea 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.json +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb._defaults.verified/aichatweb/appsettings.json @@ -6,5 +6,10 @@ "Microsoft.EntityFrameworkCore": "Warning" } }, + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + }, "AllowedHosts": "*" } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/App.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/App.razor new file mode 100644 index 00000000000..40862eea8e7 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/App.razor @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + +@code { + private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor new file mode 100644 index 00000000000..116455ce45b --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor @@ -0,0 +1 @@ +
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor.css new file mode 100644 index 00000000000..d85b851a679 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/LoadingSpinner.razor.css @@ -0,0 +1,89 @@ +/* Used under CC0 license */ + +.lds-ellipsis { + color: #666; + animation: fade-in 1s; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + + .lds-ellipsis, + .lds-ellipsis div { + box-sizing: border-box; + } + +.lds-ellipsis { + margin: auto; + display: block; + position: relative; + width: 80px; + height: 80px; +} + + .lds-ellipsis div { + position: absolute; + top: 33.33333px; + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + + .lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; + } + + .lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; + } + + .lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; + } + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} + +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + + 100% { + transform: scale(0); + } +} + +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(24px, 0); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor new file mode 100644 index 00000000000..96fbbe6cc42 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor.css new file mode 100644 index 00000000000..60cec92d5e5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/MainLayout.razor.css @@ -0,0 +1,20 @@ +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor new file mode 100644 index 00000000000..77557f20173 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor @@ -0,0 +1,13 @@ +
+ + +
+ How well is this template working for you? Please take a + brief survey + and tell us what you think. +
+
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor.css new file mode 100644 index 00000000000..c939b902afb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Layout/SurveyPrompt.razor.css @@ -0,0 +1,20 @@ +.surveyContainer { + display: flex; + justify-content: center; + gap: 0.5rem; + font-size: 0.9em; + margin: 0.5rem auto -0.7rem auto; + max-width: 1024px; + color: #444; +} + + .surveyContainer a { + text-decoration: underline; + } + + .surveyContainer .tool-icon { + margin-top: 0.15rem; + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor new file mode 100644 index 00000000000..6fc5881c18f --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor @@ -0,0 +1,134 @@ +@page "/" +@using System.ComponentModel +@inject IChatClient ChatClient +@inject NavigationManager Nav +@inject SemanticSearch Search +@implements IDisposable + +Chat + + + + + +
To get started, try asking about these example documents. You can replace these with your own data and replace this message.
+ + +
+
+ +
+ + + @* Remove this line to eliminate the template survey message *@ +
+ +@code { + private const string SystemPrompt = @" + You are an assistant who answers questions about information you retrieve. + Do not answer questions about anything else. + Use only simple markdown to format your responses. + + Use the LoadDocuments tool to prepare for searches before answering any questions. + + Use the Search tool to find relevant information. When you do this, end your + reply with citations in the special XML format: + + exact quote here + + Always include the citation in your response if there are results. + + The quote must be max 5 words, taken word-for-word from the search result, and is the basis for why the citation is relevant. + Don't refer to the presence of citations; just emit these tags right at the end, with no surrounding text. + "; + + private int statefulMessageCount; + private readonly ChatOptions chatOptions = new(); + private readonly List messages = new(); + private CancellationTokenSource? currentResponseCancellation; + private ChatMessage? currentResponseMessage; + private ChatInput? chatInput; + private ChatSuggestions? chatSuggestions; + + protected override void OnInitialized() + { + statefulMessageCount = 0; + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.Tools = [ + AIFunctionFactory.Create(LoadDocumentsAsync), + AIFunctionFactory.Create(SearchAsync) + ]; + } + + private async Task AddUserMessageAsync(ChatMessage userMessage) + { + CancelAnyCurrentResponse(); + + // Add the user message to the conversation + messages.Add(userMessage); + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + + // Stream and display a new response from the IChatClient + var responseText = new TextContent(""); + currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); + currentResponseCancellation = new(); + await foreach (var update in ChatClient.GetStreamingResponseAsync(messages.Skip(statefulMessageCount), chatOptions, currentResponseCancellation.Token)) + { + messages.AddMessages(update, filter: c => c is not TextContent); + responseText.Text += update.Text; + chatOptions.ConversationId = update.ConversationId; + ChatMessageItem.NotifyChanged(currentResponseMessage); + } + + // Store the final response in the conversation, and begin getting suggestions + messages.Add(currentResponseMessage!); + statefulMessageCount = chatOptions.ConversationId is not null ? messages.Count : 0; + currentResponseMessage = null; + chatSuggestions?.Update(messages); + } + + private void CancelAnyCurrentResponse() + { + // If a response was cancelled while streaming, include it in the conversation so it's not lost + if (currentResponseMessage is not null) + { + messages.Add(currentResponseMessage); + } + + currentResponseCancellation?.Cancel(); + currentResponseMessage = null; + } + + private async Task ResetConversationAsync() + { + CancelAnyCurrentResponse(); + messages.Clear(); + messages.Add(new(ChatRole.System, SystemPrompt)); + chatOptions.ConversationId = null; + statefulMessageCount = 0; + chatSuggestions?.Clear(); + await chatInput!.FocusAsync(); + } + + [Description("Loads the documents needed for performing searches. Must be completed before a search can be executed, but only needs to be completed once.")] + private async Task LoadDocumentsAsync() + { + await InvokeAsync(StateHasChanged); + await Search.LoadDocumentsAsync(); + } + + [Description("Searches for information using a phrase or keyword. Relies on documents already being loaded.")] + private async Task> SearchAsync( + [Description("The phrase to search for.")] string searchPhrase, + [Description("If possible, specify the filename to search that file only. If not provided or empty, the search includes all files.")] string? filenameFilter = null) + { + await InvokeAsync(StateHasChanged); + var results = await Search.SearchAsync(searchPhrase, filenameFilter, maxResults: 5); + return results.Select(result => + $"{result.Text}"); + } + + public void Dispose() + => currentResponseCancellation?.Cancel(); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor.css new file mode 100644 index 00000000000..439b52ed18a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/Chat.razor.css @@ -0,0 +1,11 @@ +.chat-container { + position: sticky; + bottom: 0; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 1.5rem; + border-top-width: 1px; + background-color: #F3F4F6; + border-color: #E5E7EB; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor new file mode 100644 index 00000000000..667189beabd --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor @@ -0,0 +1,39 @@ +@using System.Web +@if (!string.IsNullOrWhiteSpace(viewerUrl)) +{ + + + + +
+
@File
+
@Quote
+
+
+} + +@code { + [Parameter] + public required string File { get; set; } + + [Parameter] + public string? Quote { get; set; } + + private string? viewerUrl; + + protected override void OnParametersSet() + { + viewerUrl = null; + + // If you ingest other types of content besides Markdown or PDF files, construct a URL to an appropriate viewer here + if (File.EndsWith(".md")) + { + viewerUrl = $"lib/markdown_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#:~:text={Uri.EscapeDataString(Quote ?? "")}"; + } + else if (File.EndsWith(".pdf")) + { + var search = Quote?.Trim('.', ',', ' ', '\n', '\r', '\t', '"', '\''); + viewerUrl = $"lib/pdf_viewer/viewer.html?file=/Data/{HttpUtility.UrlEncode(File)}#search={HttpUtility.UrlEncode(search)}&phrase=true"; + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor.css new file mode 100644 index 00000000000..0ca029b7e64 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatCitation.razor.css @@ -0,0 +1,37 @@ +.citation { + display: inline-flex; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 0.75rem; + padding-right: 0.75rem; + margin-top: 1rem; + margin-right: 1rem; + border-bottom: 2px solid #a770de; + gap: 0.5rem; + border-radius: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + background-color: #ffffff; +} + + .citation[href]:hover { + outline: 1px solid #865cb1; + } + + .citation svg { + width: 1.5rem; + height: 1.5rem; + } + + .citation:active { + background-color: rgba(0,0,0,0.05); + } + +.citation-content { + display: flex; + flex-direction: column; +} + +.citation-file { + font-weight: 600; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor new file mode 100644 index 00000000000..0e9d9c8894a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor @@ -0,0 +1,17 @@ +
+
+ +
+ +

aichatweb

+
+ +@code { + [Parameter] + public EventCallback OnNewChat { get; set; } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor.css new file mode 100644 index 00000000000..1fe16f7219d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatHeader.razor.css @@ -0,0 +1,25 @@ +.chat-header-container { + top: 0; + padding: 1.5rem; +} + +.chat-header-controls { + margin-bottom: 1.5rem; +} + +h1 { + overflow: hidden; + text-overflow: ellipsis; +} + +.new-chat-icon { + width: 1.25rem; + height: 1.25rem; + color: rgb(55, 65, 81); +} + +@media (min-width: 768px) { + .chat-header-container { + position: sticky; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor new file mode 100644 index 00000000000..e87ac6ccf47 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor @@ -0,0 +1,51 @@ +@inject IJSRuntime JS + + + + + +@code { + private ElementReference textArea; + private string? messageText; + + [Parameter] + public EventCallback OnSend { get; set; } + + public ValueTask FocusAsync() + => textArea.FocusAsync(); + + private async Task SendMessageAsync() + { + if (messageText is { Length: > 0 } text) + { + messageText = null; + await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, text)); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + try + { + var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js"); + await module.InvokeVoidAsync("init", textArea); + await module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.css new file mode 100644 index 00000000000..3b26c9af316 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.css @@ -0,0 +1,57 @@ +.input-box { + display: flex; + flex-direction: column; + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 8px; + padding: 0.5rem 0.75rem; + margin-top: 0.75rem; +} + + .input-box:focus-within { + outline: 2px solid #4152d5; + } + +textarea { + resize: none; + border: none; + outline: none; + flex-grow: 1; +} + + textarea:placeholder-shown + .tools { + --send-button-color: #aaa; + } + +.tools { + display: flex; + margin-top: 1rem; + align-items: center; +} + +.tool-icon { + width: 1.25rem; + height: 1.25rem; +} + +.send-button { + color: var(--send-button-color); + margin-left: auto; +} + + .send-button:hover { + color: black; + } + +.attach { + background-color: white; + border-style: dashed; + color: #888; + border-color: #888; + padding: 3px 8px; +} + + .attach:hover { + background-color: #f0f0f0; + color: black; + } diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.js new file mode 100644 index 00000000000..39e18ac7b74 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatInput.razor.js @@ -0,0 +1,43 @@ +export function init(elem) { + elem.focus(); + + // Auto-resize whenever the user types or if the value is set programmatically + elem.addEventListener('input', () => resizeToFit(elem)); + afterPropertyWritten(elem, 'value', () => resizeToFit(elem)); + + // Auto-submit the form on 'enter' keypress + elem.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + elem.dispatchEvent(new CustomEvent('change', { bubbles: true })); + elem.closest('form').dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true })); + } + }); +} + +function resizeToFit(elem) { + const lineHeight = parseFloat(getComputedStyle(elem).lineHeight); + + elem.rows = 1; + const numLines = Math.ceil(elem.scrollHeight / lineHeight); + elem.rows = Math.min(5, Math.max(1, numLines)); +} + +function afterPropertyWritten(target, propName, callback) { + const descriptor = getPropertyDescriptor(target, propName); + Object.defineProperty(target, propName, { + get: function () { + return descriptor.get.apply(this, arguments); + }, + set: function () { + const result = descriptor.set.apply(this, arguments); + callback(); + return result; + } + }); +} + +function getPropertyDescriptor(target, propertyName) { + return Object.getOwnPropertyDescriptor(target, propertyName) + || getPropertyDescriptor(Object.getPrototypeOf(target), propertyName); +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor new file mode 100644 index 00000000000..e45d92ab5f9 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor @@ -0,0 +1,107 @@ +@using System.Runtime.CompilerServices +@using System.Text.RegularExpressions +@using System.Linq + +@if (Message.Role == ChatRole.User) +{ +
+ @Message.Text +
+} +else if (Message.Role == ChatRole.Assistant) +{ + foreach (var content in Message.Contents) + { + if (content is TextContent { Text: { Length: > 0 } text }) + { +
+
+
+ + + +
+
+
Assistant
+
+ + + @foreach (var citation in citations ?? []) + { + + } +
+
+ } + else if (content is FunctionCallContent { Name: "LoadDocuments" }) + { + + } + else if (content is FunctionCallContent { Name: "Search" } fcc && fcc.Arguments?.TryGetValue("searchPhrase", out var searchPhrase) is true) + { + + } + } +} + +@code { + private static readonly ConditionalWeakTable SubscribersLookup = new(); + private static readonly Regex CitationRegex = new(@"(?.*?)", RegexOptions.NonBacktracking); + + private List<(string File, string Quote)>? citations; + + [Parameter, EditorRequired] + public required ChatMessage Message { get; set; } + + [Parameter] + public bool InProgress { get; set;} + + protected override void OnInitialized() + { + SubscribersLookup.AddOrUpdate(Message, this); + + if (!InProgress && Message.Role == ChatRole.Assistant && Message.Text is { Length: > 0 } text) + { + ParseCitations(text); + } + } + + public static void NotifyChanged(ChatMessage source) + { + if (SubscribersLookup.TryGetValue(source, out var subscriber)) + { + subscriber.StateHasChanged(); + } + } + + private void ParseCitations(string text) + { + var matches = CitationRegex.Matches(text); + citations = matches.Any() + ? matches.Select(m => (m.Groups["file"].Value, m.Groups["quote"].Value)).ToList() + : null; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor.css new file mode 100644 index 00000000000..10453454be8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageItem.razor.css @@ -0,0 +1,120 @@ +.user-message { + background: rgb(182 215 232); + align-self: flex-end; + min-width: 25%; + max-width: calc(100% - 5rem); + padding: 0.5rem 1.25rem; + border-radius: 0.25rem; + color: #1F2937; + white-space: pre-wrap; +} + +.assistant-message, .assistant-search { + display: grid; + grid-template-rows: min-content; + grid-template-columns: 2rem minmax(0, 1fr); + gap: 0.25rem; +} + +.assistant-message-header { + font-weight: 600; +} + +.assistant-message-text { + grid-column-start: 2; +} + +.assistant-message-icon { + display: flex; + justify-content: center; + align-items: center; + border-radius: 9999px; + width: 1.5rem; + height: 1.5rem; + color: #ffffff; + background: #9b72ce; +} + + .assistant-message-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.assistant-search-icon { + display: flex; + justify-content: center; + align-items: center; + width: 1.5rem; + height: 1.5rem; +} + + .assistant-search-icon svg { + width: 1rem; + height: 1rem; + } + +.assistant-search-content { + align-content: center; +} + +.assistant-search-phrase { + font-weight: 600; +} + +/* Default styling for markdown-formatted assistant messages */ +::deep ul { + list-style-type: disc; + margin-left: 1.5rem; +} + +::deep ol { + list-style-type: decimal; + margin-left: 1.5rem; +} + +::deep li { + margin: 0.5rem 0; +} + +::deep strong { + font-weight: 600; +} + +::deep h3 { + margin: 1rem 0; + font-weight: 600; +} + +::deep p + p { + margin-top: 1rem; +} + +::deep table { + margin: 1rem 0; +} + +::deep th { + text-align: left; + border-bottom: 1px solid silver; +} + +::deep th, ::deep td { + padding: 0.1rem 0.5rem; +} + +::deep th, ::deep tr:nth-child(even) { + background-color: rgba(0, 0, 0, 0.05); +} + +::deep pre > code { + background-color: white; + display: block; + padding: 0.5rem 1rem; + margin: 1rem 0; + overflow-x: auto; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor new file mode 100644 index 00000000000..d245f455f11 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor @@ -0,0 +1,42 @@ +@inject IJSRuntime JS + +
+ + @foreach (var message in Messages) + { + + } + + @if (InProgressMessage is not null) + { + + + } + else if (IsEmpty) + { +
@NoMessagesContent
+ } +
+
+ +@code { + [Parameter] + public required IEnumerable Messages { get; set; } + + [Parameter] + public ChatMessage? InProgressMessage { get; set; } + + [Parameter] + public RenderFragment? NoMessagesContent { get; set; } + + private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text)); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + // Activates the auto-scrolling behavior + await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js"); + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.css new file mode 100644 index 00000000000..6fbf083c7fa --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.css @@ -0,0 +1,22 @@ +.message-list-container { + margin: 2rem 1.5rem; + flex-grow: 1; +} + +.message-list { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.no-messages { + text-align: center; + font-size: 1.25rem; + color: #999; + margin-top: calc(40vh - 18rem); +} + +chat-messages > ::deep div:last-of-type { + /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */ + margin-bottom: 2rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.js b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.js new file mode 100644 index 00000000000..3de8de273b8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatMessageList.razor.js @@ -0,0 +1,34 @@ +// The following logic provides auto-scroll behavior for the chat messages list. +// If you don't want that behavior, you can simply not load this module. + +window.customElements.define('chat-messages', class ChatMessages extends HTMLElement { + static _isFirstAutoScroll = true; + + connectedCallback() { + this._observer = new MutationObserver(mutations => this._scheduleAutoScroll(mutations)); + this._observer.observe(this, { childList: true, attributes: true }); + } + + disconnectedCallback() { + this._observer.disconnect(); + } + + _scheduleAutoScroll(mutations) { + // Debounce the calls in case multiple DOM updates occur together + cancelAnimationFrame(this._nextAutoScroll); + this._nextAutoScroll = requestAnimationFrame(() => { + const addedUserMessage = mutations.some(m => Array.from(m.addedNodes).some(n => n.parentElement === this && n.classList?.contains('user-message'))); + const elem = this.lastElementChild; + if (ChatMessages._isFirstAutoScroll || addedUserMessage || this._elemIsNearScrollBoundary(elem, 300)) { + elem.scrollIntoView({ behavior: ChatMessages._isFirstAutoScroll ? 'instant' : 'smooth' }); + ChatMessages._isFirstAutoScroll = false; + } + }); + } + + _elemIsNearScrollBoundary(elem, threshold) { + const maxScrollPos = document.body.scrollHeight - window.innerHeight; + const remainingScrollDistance = maxScrollPos - window.scrollY; + return remainingScrollDistance < elem.offsetHeight + threshold; + } +}); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor new file mode 100644 index 00000000000..69ca922a8ce --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor @@ -0,0 +1,78 @@ +@inject IChatClient ChatClient + +@if (suggestions is not null) +{ +
+ @foreach (var suggestion in suggestions) + { + + } +
+} + +@code { + private static string Prompt = @" + Suggest up to 3 follow-up questions that I could ask you to help me complete my task. + Each suggestion must be a complete sentence, maximum 6 words. + Each suggestion must be phrased as something that I (the user) would ask you (the assistant) in response to your previous message, + for example 'How do I do that?' or 'Explain ...'. + If there are no suggestions, reply with an empty list. + "; + + private string[]? suggestions; + private CancellationTokenSource? cancellation; + + [Parameter] + public EventCallback OnSelected { get; set; } + + public void Clear() + { + suggestions = null; + cancellation?.Cancel(); + } + + public void Update(IReadOnlyList messages) + { + // Runs in the background and handles its own cancellation/errors + _ = UpdateSuggestionsAsync(messages); + } + + private async Task UpdateSuggestionsAsync(IReadOnlyList messages) + { + cancellation?.Cancel(); + cancellation = new CancellationTokenSource(); + + try + { + var response = await ChatClient.GetResponseAsync( + [.. ReduceMessages(messages), new(ChatRole.User, Prompt)], + cancellationToken: cancellation.Token); + if (!response.TryGetResult(out suggestions)) + { + suggestions = null; + } + + StateHasChanged(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + await DispatchExceptionAsync(ex); + } + } + + private async Task AddSuggestionAsync(string text) + { + await OnSelected.InvokeAsync(new(ChatRole.User, text)); + } + + private IEnumerable ReduceMessages(IReadOnlyList messages) + { + // Get any leading system messages, plus up to 5 user/assistant messages + // This should be enough context to generate suggestions without unnecessarily resending entire conversations when long + var systemMessages = messages.TakeWhile(m => m.Role == ChatRole.System); + var otherMessages = messages.Where((m, index) => m.Role == ChatRole.User || m.Role == ChatRole.Assistant).Where(m => !string.IsNullOrEmpty(m.Text)).TakeLast(5); + return systemMessages.Concat(otherMessages); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor.css b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor.css new file mode 100644 index 00000000000..dcc7ee8bd8a --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Chat/ChatSuggestions.razor.css @@ -0,0 +1,9 @@ +.suggestions { + text-align: right; + white-space: nowrap; + gap: 0.5rem; + justify-content: flex-end; + flex-wrap: wrap; + display: flex; + margin-bottom: 0.75rem; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Error.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Error.razor new file mode 100644 index 00000000000..576cc2d2f4d --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Routes.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Routes.razor new file mode 100644 index 00000000000..f756e19dfbc --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/_Imports.razor b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/_Imports.razor new file mode 100644 index 00000000000..9a70e8453ef --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.Extensions.AI +@using Microsoft.JSInterop +@using aichatweb +@using aichatweb.Components +@using aichatweb.Components.Layout +@using aichatweb.Services diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Program.cs new file mode 100644 index 00000000000..f9637ff2072 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Program.cs @@ -0,0 +1,82 @@ +using System.ClientModel; +using Microsoft.AI.Foundry.Local; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.AI; +using OpenAI; +using aichatweb.Components; +using aichatweb.Services; +using aichatweb.Services.Ingestion; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +var chatAlias = builder.Configuration["FoundryLocal:ChatModel"] ?? "qwen3-4b"; +var embeddingAlias = builder.Configuration["FoundryLocal:EmbeddingModel"] ?? "qwen3-embedding-0.6b"; +var foundryServiceUrl = builder.Configuration["FoundryLocal:ServiceUrl"] ?? "http://127.0.0.1:5273"; + +await FoundryLocalManager.CreateAsync(new Configuration +{ + AppName = "aichatweb", + Web = new Configuration.WebService { Urls = foundryServiceUrl } +}, NullLogger.Instance); +var foundryManager = FoundryLocalManager.Instance; +await foundryManager.StartWebServiceAsync(); +var foundryCatalog = await foundryManager.GetCatalogAsync(); + +async Task EnsureFoundryModelAsync(string modelAlias) +{ + var model = await foundryCatalog.GetModelAsync(modelAlias) + ?? throw new InvalidOperationException( + $"Foundry Local model '{modelAlias}' was not found in the catalog. Run 'foundry model list' to see available models."); + if (!await model.IsCachedAsync()) + { + Console.WriteLine($"Foundry Local: downloading model '{modelAlias}' (first run only)..."); + await model.DownloadAsync(_ => { }); + } + if (!await model.IsLoadedAsync()) + { + await model.LoadAsync(); + } + return model.Id; +} + +var chatModelId = await EnsureFoundryModelAsync(chatAlias); +var embeddingModelId = await EnsureFoundryModelAsync(embeddingAlias); + +var foundryEndpointUrl = foundryManager.Urls?.FirstOrDefault() ?? foundryServiceUrl; +var foundryEndpoint = new Uri($"{foundryEndpointUrl.TrimEnd('/')}/v1"); +var foundryClient = new OpenAIClient( + new ApiKeyCredential("unused"), + new OpenAIClientOptions { Endpoint = foundryEndpoint }); +var chatClient = foundryClient.GetChatClient(chatModelId).AsIChatClient(); +var embeddingGenerator = foundryClient.GetEmbeddingClient(embeddingModelId).AsIEmbeddingGenerator(); + +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "vector-store.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +builder.Services.AddSqliteVectorStore(_ => vectorStoreConnectionString); +builder.Services.AddSqliteCollection(IngestedChunk.CollectionName, vectorStoreConnectionString); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("ingestion_directory", new DirectoryInfo(Path.Combine(builder.Environment.WebRootPath, "Data"))); +builder.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging(); +builder.Services.AddEmbeddingGenerator(embeddingGenerator); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseAntiforgery(); + +app.UseStaticFiles(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Properties/launchSettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Properties/launchSettings.json new file mode 100644 index 00000000000..a7cc40a553c --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:9996", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:9995;http://localhost:9996", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/README.md new file mode 100644 index 00000000000..75ad9811358 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/README.md @@ -0,0 +1,34 @@ +# AI Chat with Custom Data + +This project is an AI chat application that demonstrates how to chat with custom data using an AI language model. Please note that this template is currently in an early preview stage. If you have feedback, please take a [brief survey](https://aka.ms/dotnet-chat-templatePreview2-survey). + +>[!NOTE] +> Before running this project you need to configure the API keys or endpoints for the providers you have chosen. See below for details specific to your choices. + +# Configure the AI Model Provider +## Setting up a local environment using Foundry Local +This project is configured to use Foundry Local, which runs models on your workstation through a local OpenAI-compatible endpoint. It does not need an API key. + +### 1. Install Foundry Local +Install Foundry Local for your operating system by following the [Foundry Local documentation](https://learn.microsoft.com/azure/ai-foundry/foundry-local/). + +### 2. Run the app +The app starts the Foundry Local service for you. On first run, it downloads the configured models, then loads them into the local service. + +The default chat model alias is `qwen3-4b`. The default embedding model alias is `qwen3-embedding-0.6b`. + +### 3. Override model aliases or the service URL +You can change the defaults in `appsettings.json`, `appsettings.Development.json`, or user secrets: + +```json +{ + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } +} +``` + +Use `FoundryLocal:ServiceUrl` if another local process already uses the default port. + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/IngestedChunk.cs new file mode 100644 index 00000000000..6a3ff635187 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/IngestedChunk.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Services; + +public class IngestedChunk +{ + public const int VectorDimensions = 1024; // 1024 is the default vector size for the qwen3-embedding-0.6b embedding model + public const string VectorDistanceFunction = DistanceFunction.CosineDistance; + public const string CollectionName = "data-aichatweb-chunks"; + + [VectorStoreKey(StorageName = "key")] + [JsonPropertyName("key")] + public required string Key { get; set; } + + [VectorStoreData(StorageName = "documentid")] + [JsonPropertyName("documentid")] + public required string DocumentId { get; set; } + + [VectorStoreData(StorageName = "content")] + [JsonPropertyName("content")] + public required string Text { get; set; } + + [VectorStoreData(StorageName = "context")] + [JsonPropertyName("context")] + public string? Context { get; set; } + + [VectorStoreVector(VectorDimensions, DistanceFunction = VectorDistanceFunction, StorageName = "embedding")] + [JsonPropertyName("embedding")] + public string? Vector => Text; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DataIngestor.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DataIngestor.cs new file mode 100644 index 00000000000..d97b986b694 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DataIngestor.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DataIngestion.Chunkers; +using Microsoft.Extensions.VectorData; +using Microsoft.ML.Tokenizers; + +namespace aichatweb.Services.Ingestion; + +public class DataIngestor( + ILogger logger, + ILoggerFactory loggerFactory, + VectorStore vectorStore, + IEmbeddingGenerator> embeddingGenerator) +{ + public async Task IngestDataAsync(DirectoryInfo directory, string searchPattern) + { + using var writer = new VectorStoreWriter(vectorStore, dimensionCount: IngestedChunk.VectorDimensions, new() + { + CollectionName = IngestedChunk.CollectionName, + DistanceFunction = IngestedChunk.VectorDistanceFunction, + IncrementalIngestion = false, + }); + + using var pipeline = new IngestionPipeline( + reader: new DocumentReader(directory), + chunker: new SemanticSimilarityChunker(embeddingGenerator, new(TiktokenTokenizer.CreateForModel("gpt-4o"))), + writer: writer, + loggerFactory: loggerFactory); + + await foreach (var result in pipeline.ProcessAsync(directory, searchPattern)) + { + logger.LogInformation("Completed processing '{id}'. Succeeded: '{succeeded}'.", result.DocumentId, result.Succeeded); + } + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DocumentReader.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DocumentReader.cs new file mode 100644 index 00000000000..38117fe8930 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/DocumentReader.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DataIngestion; + +namespace aichatweb.Services.Ingestion; + +internal sealed class DocumentReader(DirectoryInfo rootDirectory) : IngestionDocumentReader +{ + private readonly MarkdownReader _markdownReader = new(); + private readonly PdfPigReader _pdfReader = new(); + + public override Task ReadAsync(FileInfo source, string identifier, string? mediaType = null, CancellationToken cancellationToken = default) + { + if (Path.IsPathFullyQualified(identifier)) + { + // Normalize the identifier to its relative path + identifier = Path.GetRelativePath(rootDirectory.FullName, identifier); + } + + mediaType = GetCustomMediaType(source) ?? mediaType; + return base.ReadAsync(source, identifier, mediaType, cancellationToken); + } + + public override Task ReadAsync(Stream source, string identifier, string mediaType, CancellationToken cancellationToken = default) + => mediaType switch + { + "application/pdf" => _pdfReader.ReadAsync(source, identifier, mediaType, cancellationToken), + "text/markdown" => _markdownReader.ReadAsync(source, identifier, mediaType, cancellationToken), + _ => throw new InvalidOperationException($"Unsupported media type '{mediaType}'"), + }; + + private static string? GetCustomMediaType(FileInfo source) + => source.Extension switch + { + ".md" => "text/markdown", + _ => null + }; +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/PdfPigReader.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/PdfPigReader.cs new file mode 100644 index 00000000000..a7c9b3686d8 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/Ingestion/PdfPigReader.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DataIngestion; +using UglyToad.PdfPig; +using UglyToad.PdfPig.Content; +using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter; +using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor; + +namespace aichatweb.Services.Ingestion; + +internal sealed class PdfPigReader : IngestionDocumentReader +{ + public override Task ReadAsync(Stream source, string identifier, string mediaType, CancellationToken cancellationToken = default) + { + using var pdf = PdfDocument.Open(source); + var document = new IngestionDocument(identifier); + foreach (var page in pdf.GetPages()) + { + document.Sections.Add(GetPageSection(page)); + } + return Task.FromResult(document); + } + + private static IngestionDocumentSection GetPageSection(Page pdfPage) + { + var section = new IngestionDocumentSection + { + PageNumber = pdfPage.Number, + }; + + var letters = pdfPage.Letters; + var words = NearestNeighbourWordExtractor.Instance.GetWords(letters); + + foreach (var textBlock in DocstrumBoundingBoxes.Instance.GetBlocks(words)) + { + section.Elements.Add(new IngestionDocumentParagraph(textBlock.Text) + { + Text = textBlock.Text + }); + } + + return section; + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/SemanticSearch.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/SemanticSearch.cs new file mode 100644 index 00000000000..8072f8bcddb --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/Services/SemanticSearch.cs @@ -0,0 +1,27 @@ +using aichatweb.Services.Ingestion; +using Microsoft.Extensions.VectorData; + +namespace aichatweb.Services; + +public class SemanticSearch( + VectorStoreCollection vectorCollection, + [FromKeyedServices("ingestion_directory")] DirectoryInfo ingestionDirectory, + DataIngestor dataIngestor) +{ + private Task? _ingestionTask; + + public async Task LoadDocumentsAsync() => await ( _ingestionTask ??= dataIngestor.IngestDataAsync(ingestionDirectory, searchPattern: "*.*")); + + public async Task> SearchAsync(string text, string? documentIdFilter, int maxResults) + { + // Ensure documents have been loaded before searching + await LoadDocumentsAsync(); + + var nearest = vectorCollection.SearchAsync(text, maxResults, new VectorSearchOptions + { + Filter = documentIdFilter is { Length: > 0 } ? record => record.DocumentId == documentIdFilter : null, + }); + + return await nearest.Select(result => result.Record).ToListAsync(); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/aichatweb.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/aichatweb.csproj new file mode 100644 index 00000000000..d7c1843dc34 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/aichatweb.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + {00000000-0000-0000-0000-000000000000} + + + + + win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64 + + + + + + + + + + + + + + + + diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.Development.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.Development.json new file mode 100644 index 00000000000..9beb3ba3d46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.Development.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + } + ,"FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.json new file mode 100644 index 00000000000..ed957100477 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.foundryl_l_webHttpPort_9996_webHttpsPort_9995.verified/aichatweb/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "FoundryLocal": { + "ChatModel": "qwen3-4b", + "EmbeddingModel": "qwen3-embedding-0.6b", + "ServiceUrl": "http://127.0.0.1:5273" + }, + "AllowedHosts": "*" +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Services/IngestedChunk.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Services/IngestedChunk.cs index 348bb5d942f..e82be899337 100644 --- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Services/IngestedChunk.cs +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/aichatweb/aichatweb.oai_aais.verified/aichatweb/Services/IngestedChunk.cs @@ -11,7 +11,7 @@ public class IngestedChunk [VectorStoreKey(StorageName = "key")] [JsonPropertyName("key")] - public required Guid Key { get; set; } + public required string Key { get; set; } [VectorStoreData(StorageName = "documentid")] [JsonPropertyName("documentid")]