Blog

  • UMAI Bilisim
  • 10.09.2026
  • Teknoloji, Semantic Kernel, AI, Agent
  • 0 Yorum

Yerel Yapay Zekâ Agent Geliştirmeye Başlangıç - 4

Semantic Kernel ile MVC Chat Arayüzü Oluşturmak

Önceki bölümlerde Ollama üzerinde çalışan yerel modelimizi Semantic Kernel ile buluşturduk. Ürün sorgulama fonksiyonlarını plugin olarak ekledik ve konuşma geçmişini PostgreSQL'e taşıdık.

Bu bölümde artık konsol ekranından çıkıp basit bir web arayüzüne geçiyoruz. Henüz Web API, React veya ayrı bir frontend katmanı oluşturmuyoruz. ASP.NET Core MVC'nin Controller + View yaklaşımı bu aşama için yeterli:

Tarayıcı -> HomeController -> ChatService -> Semantic Kernel -> ProductsPlugin
                                      |
                                      -> PostgreSQL Chat History

Amaç, çalışan uygulamanın davranışını değiştirmeden kullanıcıya tarayıcıdan soru sorabileceği küçük ve anlaşılır bir ekran sunmak.


Console Uygulamasından MVC'ye Geçiş

Mevcut proje konsol uygulaması olarak oluşturulmuştu. Önce proje SDK'sını ASP.NET Core web SDK ile değiştiriyoruz:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Microsoft.NET.Sdk.Web, ASP.NET Core MVC ve Razor View altyapısını sağlar. Entity Framework Core, Semantic Kernel ve PostgreSQL paketlerimiz aynı şekilde kullanılmaya devam eder.

Konsol uygulamasındaki while döngüsünün yerini MVC route yapısı alır:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Bu tanım, / isteğini HomeController içindeki Index action'ına yönlendirir.


Dependency Injection Ayarları

Program.cs içinde veritabanı, repository, kernel ve chat servisini uygulamanın servis container'ına kaydediyoruz:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddScoped<IChatHistoryRepository,
    PostgresChatHistoryRepository>();
builder.Services.AddScoped<ChatService>();
builder.Services.AddControllersWithViews();

DbContext ve repository Scoped olarak tanımlanır; her HTTP isteğinde uygun yaşam döngüsüne sahip yeni bir scope kullanılır. Kernel ve model bağlantısı ise uygulama boyunca paylaşılabilir:

builder.Services.AddSingleton<Kernel>(serviceProvider =>
{
    var kernelBuilder = Kernel.CreateBuilder();

    kernelBuilder.AddOllamaChatCompletion(
        configuration["OllamaSettings:Local:Model"] ?? OllamaModels.Qwen25_14B,
        new Uri(configuration["OllamaSettings:Local:Endpoint"]
            ?? "http://localhost:11434"));

    var kernel = kernelBuilder.Build();
    kernel.Plugins.AddFromType<ProductsPlugin>(
        serviceProvider: serviceProvider);
    return kernel;
});

builder.Services.AddSingleton<IChatCompletionService>(serviceProvider =>
    serviceProvider.GetRequiredService<Kernel>()
        .GetRequiredService<IChatCompletionService>());

Burada önce kernel oluşturuluyor, sonra mevcut ProductsPlugin kernel'e ekleniyor. Böylece MVC ekranından gelen soru, önceki makalelerdeki tool calling altyapısını kullanmaya devam ediyor.


ChatService: İş Mantığını Controller'dan Ayırmak

Controller'ın doğrudan Semantic Kernel çağırmasını istemiyoruz. Bu nedenle Services/ChatService.cs dosyasında sohbet akışını topluyoruz:

public async Task<string> AskAsync(Guid sessionId, string question,
    CancellationToken cancellationToken = default)
{
    if (sessionId == Guid.Empty)
        throw new ArgumentException("Geçerli bir oturum bulunamadı.");

    if (string.IsNullOrWhiteSpace(question))
        throw new ArgumentException("Soru boş bırakılamaz.");

    var history = await _historyRepository.LoadHistoryAsync(
        sessionId, SystemPrompt);

    history.AddUserMessage(question);
    await _historyRepository.SaveMessageAsync(
        sessionId, AuthorRole.User, question);

    var response = await _completionService.GetChatMessageContentAsync(
        history,
        kernel: _kernel,
        executionSettings: new PromptExecutionSettings
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
        },
        cancellationToken: cancellationToken);

    var answer = response.Content ?? "Cevap üretilemedi.";
    await _historyRepository.SaveMessageAsync(
        sessionId, AuthorRole.Assistant, answer);

    return answer;
}

Akışın sırası önemlidir. Önce geçmiş yüklenir, sonra yeni soru geçmişe ve veritabanına eklenir. Kernel çalışırken ProductsPlugin gerekirse ürün fonksiyonlarından birini çağırır. Son cevap da kalıcı hale getirilir.

Bu sayede aynı session ile yeni bir soru sorulduğunda model önceki mesajları hatırlayabilir.


Controller: GET ve POST Akışları

HomeController iki temel action içeriyor. GET action yeni bir session oluşturuyor veya mevcut session'ın mesajlarını yüklüyor:

[HttpGet]
public async Task<IActionResult> Index(Guid? sessionId)
{
    var id = sessionId.GetValueOrDefault();

    if (id == Guid.Empty ||
        !await _dbContext.Sessions.AnyAsync(x => x.Id == id))
    {
        id = await _historyRepository.CreateSessionAsync();
    }

    return View(await BuildViewModelAsync(id));
}

Form gönderildiğinde POST action chat servisini çağırıyor. Başarılı işlemden sonra Redirect-After-Post deseniyle sayfaya dönüyoruz:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Ask(
    Guid sessionId,
    string question,
    CancellationToken cancellationToken)
{
    try
    {
        await _chatService.AskAsync(
            sessionId, question, cancellationToken);

        return RedirectToAction(nameof(Index), new { sessionId });
    }
    catch (ArgumentException exception)
    {
        return View(nameof(Index),
            await BuildViewModelAsync(sessionId, question, exception.Message));
    }
}

Redirect-After-Post, tarayıcı yenilendiğinde aynı formun tekrar gönderilmesini önler. ValidateAntiForgeryToken ise formun uygulamamız tarafından üretildiğini kontrol ederek CSRF saldırılarına karşı temel koruma sağlar.


ViewModel Neden Kullanıyoruz?

Razor View'a doğrudan DbContext veya entity koleksiyonu göndermek yerine, ekranın ihtiyacı olan modeli oluşturuyoruz:

public sealed class ChatViewModel
{
    public Guid SessionId { get; init; }
    public string Question { get; set; } = string.Empty;
    public string? Error { get; init; }
    public IReadOnlyList<MessageModel> Messages { get; init; } = [];
}

Bu model session kimliğini, formdaki soruyu, varsa hata mesajını ve ekranda gösterilecek mesajları taşır. İleride kullanıcı adı, yüklenme durumu veya ürün kartları eklemek istediğimizde ViewModel'i genişletebiliriz.


Razor View ile Basit Chat Ekranı

Views/Home/Index.cshtml dosyasında konuşma geçmişini listeliyor ve formu oluşturuyoruz:

@model ChatViewModel

<h1>UMAI Bilişim Ürün Asistanı</h1>

@foreach (var message in Model.Messages)
{
    <div class="message">
        <strong>@(message.Role == "user" ? "Siz" : "Cemile"):</strong>
        <div>@message.Content</div>
    </div>
}

<form asp-action="Ask" method="post">
    @Html.AntiForgeryToken()
    <input type="hidden" name="sessionId" value="@Model.SessionId" />
    <textarea name="question" placeholder="Sorunuzu yazın..."></textarea>
    <button type="submit">Gönder</button>
    <a asp-action="Index">Yeni konuşma</a>
</form>

Razor, @message.Content değerini HTML encode ederek ekrana basar. Bu, kullanıcı veya model cevabında HTML benzeri içerik bulunduğunda XSS riskini azaltan önemli bir varsayılan davranıştır.

İlk sürümde görünüm için ayrıca bir CSS framework kullanmadık. Basit CSS ile kullanıcı mesajlarını sağa, asistan mesajlarını sola yakın göstermek yeterli. Önceliğimiz görsel ayrıntıdan önce akışın çalışmasıdır.


Migration ve Çalıştırma

Uygulama başlarken mevcut migration'ları uyguluyoruz:

using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider
        .GetRequiredService<ApplicationDbContext>()
        .Database.MigrateAsync();
}

Ardından projeyi çalıştırıp tarayıcıdan http://localhost:5000 adresine gidebiliriz. Ollama'nın ayakta olması ve appsettings.json içindeki endpoint ile model adının doğru olması gerekir.

Örnek sorular:

  • “Hangi ürünleriniz var?”
  • “500 TL altındaki ürünler hangileri?”
  • “Stoğu tükenen ürün var mı?”

Model uygun gördüğünde ProductsPlugin içindeki ilgili fonksiyonu çağırır; cevap ve soru session'a bağlı olarak PostgreSQL'deki messages tablosunda tutulur.


Bu Aşamada Bilerek Eklenmeyenler

Bu sürüm basit bir tanıtım ve öğrenme arayüzüdür. Henüz şunları eklemedik:

  • Kullanıcı girişi ve yetkilendirme
  • Gerçek zamanlı streaming cevap
  • Rate limiting
  • Session'ın kullanıcıya bağlanması
  • React veya mobil istemci

Özellikle sessionId'yi yalnızca URL veya hidden input üzerinden almak, kimlik doğrulamalı production uygulaması için yeterli değildir. Gerçek sistemde session, giriş yapan kullanıcı veya tenant ile ilişkilendirilmeli; başka kullanıcıların geçmişine erişim engellenmelidir.


Sonuç

Bu bölümde önceki makalelerde oluşturduğumuz altyapıyı bozmadan konsol uygulamasından MVC tabanlı basit bir web chat ekranına geçtik:

  • HomeController HTTP akışını yönetiyor.
  • ChatService Semantic Kernel ve konuşma geçmişini yönetiyor.
  • ProductsPlugin aynı tool calling mantığıyla çalışıyor.
  • Razor View mesajları gösterip yeni soru gönderiyor.
  • PostgreSQL session ve mesaj geçmişini koruyor.

Bu küçük arayüz, sonraki adımlar için sağlam bir temel oluşturuyor. Bir sonraki bölümde bu chat ekranını RAG ile genişletip ürün tablosu dışındaki dokümanlardan da kaynaklı cevaplar üretmeyi inceleyebiliriz.

Github: https://github.com/xkursatx/MSSemantic.git

Yorum Bırak

Yorumunuz admin onayından sonra yayınlanacaktır.

Güvenlik sorusu