Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/packages/ProjectTemplates.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageVersion Include="CommunityToolkit.Aspire.Hosting.Ollama" Version="13.0.0-beta.444" />
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0-beta.444" />
<PackageVersion Include="MessagePack" Version="2.5.301" />
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="1.2.3" />
<PackageVersion Include="Microsoft.Agents.AI" Version="1.3.0" />
<PackageVersion Include="Microsoft.Agents.AI.DevUI" Version="1.3.0-preview.260423.1" />
<PackageVersion Include="Microsoft.Agents.AI.Hosting" Version="1.3.0-preview.260423.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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"
}
Comment on lines +153 to 157
// {
// "choice": "azureaifoundry",
Expand Down Expand Up @@ -223,6 +228,10 @@
"type": "computed",
"value": "(AiServiceProvider == \"ollama\")"
},
"IsFoundryLocal": {
"type": "computed",
"value": "(AiServiceProvider == \"foundrylocal\")"
},
"IsAzureAIFoundry": {
"type": "computed",
"value": "(AiServiceProvider == \"azureaifoundry\")"
Expand Down Expand Up @@ -317,6 +326,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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<PackageReference Include="OllamaSharp" Version="${PackageVersion:OllamaSharp}" />
#elif ((IsGHModels || IsOpenAI) && !IsAspire)
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="${PackageVersion:Microsoft.Extensions.AI.OpenAI}" />
#elif (IsFoundryLocal && !IsAspire)
<PackageReference Include="Microsoft.AI.Foundry.Local" Version="${PackageVersion:Microsoft.AI.Foundry.Local}" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="${PackageVersion:Microsoft.Extensions.AI.OpenAI}" />
#elif (IsAzureAIFoundry)
<PackageReference Include="Azure.AI.Projects" Version="${PackageVersion:Azure.AI.Projects}" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="${PackageVersion:Microsoft.Extensions.AI.OpenAI}" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#if (IsGHModels || IsOpenAI || (IsAzureOpenAI && !IsManagedIdentity))
#if (IsGHModels || 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)
Expand All @@ -11,7 +15,7 @@
using Microsoft.Extensions.AI;
#if (IsOllama)
using OllamaSharp;
#elif (IsGHModels || IsOpenAI || IsAzureOpenAI)
#elif (IsGHModels || IsOpenAI || IsFoundryLocal || IsAzureOpenAI)
using OpenAI;
#endif
using AIChatWeb_CSharp.Web.Components;
Expand Down Expand Up @@ -40,6 +44,47 @@
"llama3.2");
IEmbeddingGenerator<string, Embedding<float>> 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<string> EnsureFoundryModelAsync(string alias)
{
var model = await foundryCatalog.GetModelAsync(alias)
?? throw new InvalidOperationException(
$"Foundry Local model '{alias}' was not found in the catalog. Run 'foundry model list' to see available models.");
if (!await model.IsCachedAsync())
{
Console.WriteLine($"Foundry Local: downloading model '{alias}' (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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*"
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public AIChatWebSnapshotTests(ITestOutputHelper log)
[InlineData /* Defaults: --provider=githubmodels --vector-store=local */]
[InlineData("--provider=ollama", "--vector-store=qdrant")]
[InlineData("--provider=openai", "--vector-store=azureaisearch")]
[InlineData("--provider=foundrylocal", "--vector-store=local", "--webHttpPort", "9996", "--webHttpsPort", "9995")]
[InlineData("--aspire")]
[InlineData("--aspire", "--provider=azureopenai", "--vector-store=azureaisearch")]
public async Task RunSnapshotTests(params string[] templateArgs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["aichatweb.styles.css"]" />
<ImportMap />
<HeadOutlet @rendermode="@renderMode" />
</head>

<body>
<Routes @rendermode="@renderMode" />
<script src="@Assets["app.js"]" type="module"></script>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>

</html>

@code {
private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@inherits LayoutComponentBase

@Body

<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="surveyContainer">
<div class="tool-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" />
</svg>
</div>

<div>
How well is this template working for you? Please take a
<a target="_blank" href="https://aka.ms/dotnet-chat-templatePreview2-survey">brief survey</a>
and tell us what you think.
</div>
</div>
Loading
Loading