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 @@ -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)",
Expand All @@ -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",
Expand Down Expand Up @@ -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\")"
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -62,7 +56,7 @@
.WithReference(embeddings)
.WaitFor(chat)
.WaitFor(embeddings);
#elif (IsOpenAI || IsGHModels)
#elif (IsOpenAI)
webApp.WithReference(openai);
#else // IsAzureOpenAI
webApp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,29 @@
<UserSecretsId>d5681fae-b21b-4114-b781-48180f08c0c4</UserSecretsId>
</PropertyGroup>

<!--#if (IsFoundryLocal) -->
<PropertyGroup>
<!-- Foundry Local ships native runtime assets per RID, so restore must be RID-aware for the build to resolve them. -->
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>

<!--#endif -->
<ItemGroup>
<!--#if (IsOllama && IsAspire)
<PackageReference Include="CommunityToolkit.Aspire.OllamaSharp" Version="${PackageVersion:CommunityToolkit.Aspire.OllamaSharp}" />
#endif -->
<!--#if (IsOllama)
<PackageReference Include="OllamaSharp" Version="${PackageVersion:OllamaSharp}" />
#elif ((IsGHModels || IsOpenAI) && !IsAspire)
#elif (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}" />
#endif -->
<!--#if ((IsAzureOpenAI || IsOpenAI || IsGHModels) && IsAspire) -->
<!--#if ((IsAzureOpenAI || IsOpenAI) && IsAspire) -->
<PackageReference Include="Aspire.Azure.AI.OpenAI" Version="${PackageVersion:Aspire.Azure.AI.OpenAI}" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="${PackageVersion:Microsoft.Extensions.AI.OpenAI}" />
<!--#endif -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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;
Expand All @@ -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<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 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<your-project-directory>>
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

Expand Down Expand Up @@ -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
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 All @@ -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
Comment on lines +22 to +30

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamsitnik Does this match your understanding that only Qdrant uses Guid keys?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that Azure Foundry local provides an embedding generator (hence to proper VectorDimensions change above) but I don't think it provides any vector store. So we should not change the Key here.


[VectorStoreData(StorageName = "documentid")]
[JsonPropertyName("documentid")]
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": "*"
}
Loading