BlazorSemantic Kernel

Creating a Multi-Model AI Chat with Blazor and Semantic Kernel

Creating a Multi-Model AI Chat with Blazor and Semantic Kernel

By now, most people have interacted with large language models such as OpenAI’s ChatGPT, Google’s Gemini, or DeepSeek. Beyond replacing traditional web search, these AI-powered chats are increasingly being integrated into existing systems and applications.

One common characteristic of these models is that they all provide SDKs, allowing developers to integrate them easily into their applications.

What about .NET developers? While Python is largely the number one language for AI, Microsoft provides Semantic Kernel (which also exists for Python and Java), a unified framework for building AI and agentic apps.

This tutorial presents how to use Semantic Kernel and Blazor Server to build an AI multi-model chat application. The chat is multi-model because it is a multi-session chat where each session can use a model from Azure AI Foundry or one hosted locally.

This tutorial answers the following questions:

  • Which model to use?
  • Where and how to host a model?
  • How can I integrate Semantic Kernel with Blazor?
  • How can I write a chat component using a model in Azure Foundry?
  • How can I write a chat component using a model hosted locally with Ollama?
  • How to make the Blazor app interact with the user and show the response of the model in a fashionable way?

Model Selection

There is no definite answer to this question. A lot of factors are needed for this decision: the cost, the reputation, the model type, and most importantly, the requirements. In our case, all we need is chat completions. Hence, we will use two models: GPT-4 (in Azure AI Foundry) and Qwen.

Model Hosting

Here, a lot of factors determine the choice, but you can host the model locally, deploy it to on-premise infrastructure, use the model as a MaaS (Model as a Service), or use AI cloud services such as Microsoft Foundry.

Hosting a model locally can be a good option for starter development or for POCs. In this tutorial, we will use the Qwen model deployed using Ollama. Ollama is a software that makes it very easy to install and run models locally. For example, the capture below shows the usage of the Qwen model after running the ollama ps command. Obviously, the capacity and the efficiency of the model depend on the compute power of your machine (CPU + GPU). Consider using "small" models for POCs when hosting locally.

Output of the ollama ps command showing the Qwen model running locally

Models can also be deployed to the company data center. Here also, scaling and efficiency depend on the availability of compute and GPU resources.

Most of the models also offer APIs to be consumed by developers (MaaS). Most of the time, developers just need an API key to start using the model. The issue with this approach is that it couples your system to a single model.

Finally, Microsoft Foundry is a cloud-based platform that allows developers to use models without thinking about the hosting. It also offers enterprise features such as agent orchestration, workflows, and compliance.

Please note that there are other services such as GitHub Models or Hugging Face that allow you to use models with eventually low/no cost.

Integrate Semantic Kernel with Blazor Server

The integration of Semantic Kernel with any ASP.NET app (including Blazor Server) is straightforward. You need to install the Microsoft.SemanticKernel package.

dotnet add package Microsoft.SemanticKernel

The Core is the foundation for Semantic Kernel, but it is not enough. We need to add the connectors for the models we need. In our case, we need Ollama and AzureOpenAI.

dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
dotnet add package Microsoft.SemanticKernel.Connectors.Ollama

Ollama needs to be installed on your machine. Once there, you need to pull the model you want to use locally. For example, if you want to run Qwen, run:

ollama pull qwen:14b

All you need for Azure OpenAI is an endpoint and an API Key. For more details, see this.

The final element is to enable Semantic Kernel in your web app. For this, add the following to your bootstrap code:

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();


builder.Services
    .AddKernel()
    .AddAzureOpenAIChatCompletion(builder.Configuration["AzureOpenAI:DeploymentName"]!,
        builder.Configuration["AzureOpenAI:Endpoint"]!, builder.Configuration["AzureOpenAI:ApiKey"]!);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

The AI Components

The Chat Abstraction

The chat service abstraction is very simple:

public interface IChatService
{
    IAsyncEnumerable<string> GetStreamingResponseAsync(string prompt, CancellationToken cancellationToken);
}

It returns a result of type IAsyncEnumerable<string> to show the model's answer progressively and make the user experience better.

The Azure OpenAI Chat Implementation

public class AzureOpenAiChatService(IChatCompletionService chat) : IChatService
{
    private ChatHistory _history = new("You are a helpful assistant that provides concise and accurate information.");
    private readonly ChatHistorySummarizationReducer _reducer = new(chat, 2, 2);


    public async IAsyncEnumerable<string> GetStreamingResponseAsync(string prompt,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(prompt))
        {
            yield break;
        }

        var currentAssistantMessage = new StringBuilder();

        _history.AddUserMessage(prompt);
        await foreach (var elt in chat.GetStreamingChatMessageContentsAsync(_history,
                           cancellationToken: cancellationToken))
        {
            if (string.IsNullOrWhiteSpace(elt.Content)) continue;
            currentAssistantMessage.Append(elt.Content);
            yield return elt.Content;
        }
        
        _history.AddAssistantMessage(currentAssistantMessage.ToString());

        _history = await _history.ReduceAsync(_reducer, cancellationToken);
    }
}

Please note:

  • we keep the session chat history in the ChatHistory object
  • We have set a system prompt to specify how the responses should be
  • Every time, we append the model response to the history
  • We use a history reducer to minimize the number of tokens we send to the model. This is very important for cost optimization.
  • The chat completion service is injected to the chat using Asp.NET DI

The Ollama Chat Implementation

public class OllamaChatService : IChatService
{
    private readonly ILogger<OllamaChatService> _logger;
    private readonly IChatCompletionService _chat;
    private readonly ChatHistory _history;

    public OllamaChatService(IConfiguration config, ILogger<OllamaChatService> logger)
    {
        _logger = logger;
        var model = config["Ollama:ModelId"] ?? throw new InvalidOperationException("Model");
        var endpoint = config["Ollama:Endpoint"] ?? throw new InvalidOperationException("Endpoint");
        _logger.LogInformation($"Ollama Chat Service started at {endpoint}");
        var client = new OllamaApiClient(new Uri(endpoint), model);
        _chat = client.AsChatCompletionService();
        _history = new ChatHistory("Be concise, clear, professional and creative when answering.");
    }

  

    public async IAsyncEnumerable<string> GetStreamingResponseAsync(string prompt,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(prompt))
        {
            yield break;
        }

        _history.AddUserMessage(prompt);
        
        var currentAssistantMessage = new StringBuilder();

        await foreach (var element in _chat.GetStreamingChatMessageContentsAsync(_history,
                           cancellationToken: cancellationToken))
        {
            if (string.IsNullOrEmpty(element.Content)) continue;
            currentAssistantMessage.Append(element.Content);
            yield return element.Content;
        }
        
        _history.AddAssistantMessage(currentAssistantMessage.ToString());
    }
}

The implementation is very similar except:

  • We create the chat completion manually instead of using DI:
  var client = new OllamaApiClient(new Uri(endpoint), model);
        _chat = client.AsChatCompletionService();
  • We do not use history reducers since the model runs locally

The UI Components

The Model Selector

The model selector asks the user to choose the model before starting a chat session.

The model selector letting the user choose Azure OpenAI or a local Ollama model before starting a chat

The two chat implementations are registered using keyed dependency injection. The model selector instantiates the chat using the key and starts a new session.

 private void OnModelSelected(string model)
    {
        _model = model;
        _chat = ServiceProvider.GetRequiredKeyedService<IChatService>(model);
    }

The Chat Component

This component:

  • Shows the conversation history
  • Allows the user to ask a question
  • Shows progressively the model response
  • Shows a thinking indicator

The SendMessage method

private async Task SendMessage()
    {
        if (string.IsNullOrWhiteSpace(_userInput)) return;

        // 1. Add User Message
        _messages.Add(new ChatMessage { Content = _userInput, IsUser = true });
        var prompt = _userInput;
        _userInput = "";
        
        // 2. Prepare AI Response Placeholder
        var aiMessage = new ChatMessage { Content = "", IsUser = false };
        _messages.Add(aiMessage);
        // enable thinking indicator
        _isTyping = true;
        
        // 3. Force UI Update & Scroll
        StateHasChanged();
        await ScrollToBottom();

        // 4. Stream Response
        _cts = new CancellationTokenSource();
        try
        {
            await foreach (var chunk in Chat.GetStreamingResponseAsync(prompt, _cts.Token))
            {
                // show progressively the model output
                aiMessage.Content += chunk;
                StateHasChanged();
                await ScrollToBottom();
            }
        }
        finally
        {
            // disable thinking indicator
            _isTyping = false;
            StateHasChanged();
            await ScrollToBottom();
        }
    }

The UI

 <div id="chat-container" class="flex-1 overflow-y-auto p-4 space-y-6 scroll-smooth">
        @foreach (var message in _messages)
        {
            <div class="flex w-full @(message.IsUser ? "justify-start" : "justify-end")">
                <div class="max-w-[80%] rounded-2xl px-5 py-3 shadow-sm 
                            @(message.IsUser 
                                ? "bg-blue-600 text-white rounded-tl-none" 
                                : "bg-white border border-gray-200 text-gray-800 rounded-tr-none")">
                    
                    @if (message.IsUser)
                    {
                        <p>@message.Content</p>
                    }
                    else
                    {
                        <div class="chat-answer prose prose-sm max-w-none 
                                    dark:prose-invert 
                                    prose-p:my-1 prose-headings:my-2 prose-pre:bg-gray-800 prose-pre:text-gray-100">
                            @((MarkupString)Markdown.ToHtml(message.Content, _pipeline))
                        </div>
                    }
                </div>
            </div>
        }
        
        @if (_isTyping)
        {
            <div class="flex w-full justify-end animate-pulse">
                <div class="bg-gray-200 rounded-full px-4 py-2 text-xs text-gray-500">
                    AI is thinking...
                </div>
            </div>
        }
    </div>

Notes

  • We are using Markdig package as the model output uses markdown format
  • We use aspire for the app telemetry
  • The app uses tailwind CSS for styling
  • You need an Azure Subscription and a deployed model in Azure AI foundry if you want to use the AzureOpenAIChat

Captures

Chat Tab

The chat tab showing a conversation with the AI model

Multi-Session Chats

Multiple chat sessions open at once, each using a different model

Source Code

The code of this app can be obtained here: https://github.com/nubiquest-blogs/2025-12-multi-model-ai-chat-blazor

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.