Blog

  • UMAI Bilisim
  • 04.08.2026
  • Teknoloji, Semantic Kernel, AI, Agent
  • 0 Comment(s)

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

Entity Framework Core ile Konuşma Geçmişini ve Ürünleri PostgreSQL'e Taşımak

İkinci makalede ProductsPlugin'i in-memory bir liste üzerinden çalıştırmış, konuşma geçmişini de ChatHistory içinde bellekte tutmuştuk. Bu bölümde ikisini birlikte PostgreSQL'e taşıyoruz — hem ürün verisini hem de session/mesaj geçmişini aynı veritabanında, Entity Framework Core üzerinden yönetiyoruz.


Neden Entity Framework Core?

Ham SQL/ADO.NET yerine EF Core kullanıldığında:

  • Migration'lar sayesinde şema değişiklikleri kod ile birlikte versiyonlanır.
  • ProductModel, SessionModel, MessageModel gibi sınıflar hem C## tarafında hem veritabanı tarafında tek bir kaynaktan beslenir.
  • LINQ ile yazılan sorgular SQL enjeksiyonuna karşı doğal olarak güvenlidir.

Paketler

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Design paketi migration komutlarını (dotnet ef migrations add vb.) çalıştırabilmek için gereklidir.


appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=192.168.5.200;Port=5433;Database=umaichatdb;Username=vector;Password=vector_pass"
  }
}

Connection string artık kod içinde değil, konfigürasyon dosyasından okunuyor.


Modeller

ProductModel artık isim/fiyat dışında ürünü tanıtan alanlar da taşıyor:

// Models/ProductModel.cs
public class ProductModel
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("description")]
    public string? Description { get; set; }

    [JsonPropertyName("detail")]
    public string? Detail { get; set; }

    [JsonPropertyName("price")]
    public decimal? Price { get; set; }

    [JsonPropertyName("inStock")]
    public int? InStock { get; set; }
}

SessionModel ve MessageModel arasında bire çok (one-to-many) bir ilişki kuruyoruz — bir session'ın birden çok mesajı olabilir:

// Models/SessionModel.cs
public class SessionModel
{
    public Guid Id { get; set; }
    public DateTime CreatedAt { get; set; }

    public ICollection<MessageModel> Messages { get; set; } = new List<MessageModel>();
}
// Models/MessageModel.cs
public class MessageModel
{
    public long Id { get; set; }
    public Guid SessionId { get; set; }
    public string Role { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }

    public SessionModel? Session { get; set; }
}

ApplicationDbContext ve Seed Data

DbContext, tabloları ve aralarındaki ilişkileri OnModelCreating içinde tanımlıyor. Aşağıda ProductModel için tablo eşlemesi ve HasData ile eklenen seed verisi örneği yer alıyor:

public DbSet<ProductModel> Products => Set<ProductModel>();
public DbSet<SessionModel> Sessions => Set<SessionModel>();
public DbSet<MessageModel> Messages => Set<MessageModel>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ProductModel>(entity =>
    {
        entity.HasKey(x => x.Id);
        entity.ToTable("products");

        entity.Property(x => x.Name).HasColumnName("name").HasMaxLength(200);
        entity.Property(x => x.Price).HasColumnName("price").HasColumnType("decimal(18,2)");
        entity.Property(x => x.InStock).HasColumnName("in_stock");

        entity.HasData(
            new ProductModel
            {
                Id = 1,
                Name = "Masa Lambası",
                Description = "Masaüstünüzde şık ve modern bir aydınlatma çözümü",
                Price = 300,
                InStock = 21
            }
        );
    });

    modelBuilder.Entity<SessionModel>(entity =>
    {
        entity.HasKey(x => x.Id);
        entity.ToTable("sessions");

        entity.HasMany(x => x.Messages)
            .WithOne(x => x.Session)
            .HasForeignKey(x => x.SessionId)
            .OnDelete(DeleteBehavior.Cascade);
    });
}

HasData(...) ile tanımlanan kayıtlar, migration çalıştırıldığında otomatik olarak veritabanına yazılır. OnDelete(DeleteBehavior.Cascade) ile bir session silindiğinde ona bağlı mesajlar da otomatik silinir.


Migration'lar

dotnet ef migrations add InitialCreate
dotnet ef migrations add SeedProducts
dotnet ef migrations add AddChatHistory
dotnet ef database update

Migration dosyalarının içeriği EF Core tarafından OnModelCreating'e bakılarak otomatik üretilir, elle düzenlenmez.


Konuşma Geçmişi Repository'si

Repository arayüzü önceki makaledeki tasarımla aynı kalıyor, sadece implementasyon artık EF Core üzerinden çalışıyor:

public interface IChatHistoryRepository
{
    Task EnsureSchemaAsync();
    Task<Guid> CreateSessionAsync();
    Task<ChatHistory> LoadHistoryAsync(Guid sessionId, string systemPrompt);
    Task SaveMessageAsync(Guid sessionId, AuthorRole role, string content);
}

CreateSessionAsync ve SaveMessageAsync örnekleri:

public async Task<Guid> CreateSessionAsync()
{
    var session = new SessionModel
    {
        Id = Guid.NewGuid(),
        CreatedAt = DateTime.UtcNow
    };

    _dbContext.Sessions.Add(session);
    await _dbContext.SaveChangesAsync();

    return session.Id;
}

public async Task SaveMessageAsync(Guid sessionId, AuthorRole role, string content)
{
    if (string.IsNullOrWhiteSpace(content)) return;

    var message = new MessageModel
    {
        SessionId = sessionId,
        Role = role.Label.ToLowerInvariant(),
        Content = content,
        CreatedAt = DateTime.UtcNow
    };

    _dbContext.Messages.Add(message);
    await _dbContext.SaveChangesAsync();
}

Konuşma geçmişi yüklenirken DB'deki mesajlar sırayla ChatHistory'ye ekleniyor:

public async Task<ChatHistory> LoadHistoryAsync(Guid sessionId, string systemPrompt)
{
    var history = new ChatHistory();
    history.AddSystemMessage(systemPrompt);

    var messages = await _dbContext.Messages
        .Where(m => m.SessionId == sessionId)
        .OrderBy(m => m.Id)
        .ToListAsync();

    foreach (var message in messages)
    {
        if (message.Role == "user")
            history.AddUserMessage(message.Content);
        else if (message.Role == "assistant")
            history.AddAssistantMessage(message.Content);
    }

    return history;
}

ProductsPlugin — EF Core Sürümü

ProductsPlugin artık ApplicationDbContext üzerinden LINQ sorguları çalıştırıyor. Temel fonksiyonlar önceki makaledekiyle aynı mantıkta, sadece veri kaynağı değişti:

[KernelFunction("get_products")]
[Description("Tüm ürünlerin listesini ID ve isim bilgisi ile getirir")]
public async Task<List<ProductModel>> GetProductsAsync()
{
    return await _dbContext.Products
        .Select(p => new ProductModel { Id = p.Id, Name = p.Name })
        .ToListAsync();
}

[KernelFunction("get_detail")]
[Description("Belirtilen ID'ye sahip ürünün tüm detaylarını getirir")]
public async Task<ProductModel?> GetDetailAsync(
    [Description("Ürün ID'si, örn: 1")] int id)
{
    return await _dbContext.Products
        .FirstOrDefaultAsync(p => p.Id == id);
}

Bunlara ek olarak, modelin ürünlerle ilgili daha spesifik sorulara da cevap verebilmesi için arama, filtreleme ve istatistik fonksiyonları eklendi. Arama fonksiyonu örneği:

[KernelFunction("search_products_by_name")]
[Description("İsmi verilen anahtar kelimeyi içeren ürünleri arar (büyük/küçük harf duyarsız)")]
public async Task<List<ProductModel>> SearchByNameAsync(
    [Description("Aranacak kelime, örn: 'masa', 'lamba'")] string keyword)
{
    return await _dbContext.Products
        .Where(p => EF.Functions.ILike(p.Name ?? "", $"%{keyword}%"))
        .OrderBy(p => p.Id)
        .ToListAsync();
}

EF.Functions.ILike, PostgreSQL'in ILIKE operatörünü LINQ'e taşıyan yöntemdir; case-insensitive arama için ayrıca .ToLower() çağırmaya gerek bırakmaz.

Fiyat aralığı filtreleme örneği:

[KernelFunction("get_products_by_price_range")]
[Description("Belirtilen fiyat aralığındaki ürünleri listeler")]
public async Task<List<ProductModel>> GetProductsByPriceRangeAsync(
    [Description("En düşük fiyat (TL), örn: 200")] decimal minPrice,
    [Description("En yüksek fiyat (TL), örn: 600")] decimal maxPrice)
{
    return await _dbContext.Products
        .Where(p => p.Price >= minPrice && p.Price <= maxPrice)
        .OrderBy(p => p.Price)
        .ToListAsync();
}

İstatistik fonksiyonu örneği:

[KernelFunction("get_average_price")]
[Description("Tüm ürünlerin ortalama fiyatını hesaplar")]
public async Task<decimal> GetAveragePriceAsync()
{
    var average = await _dbContext.Products.AverageAsync(p => p.Price);
    return average ?? 0;
}

Bu desende toplam on iki fonksiyon tanımlandı (arama, fiyat filtreleme, stok durumu, en ucuz/en pahalı ürün, ürün sayısı, ortalama fiyat gibi). Her biri tek bir LINQ sorgusu; modelin hangi fonksiyonu ne zaman çağıracağını [Description] attribute'ları belirliyor, ayrıca bir yönlendirme kodu yazmaya gerek kalmıyor.


Program.cs — Çoklu AI Provider Desteği

Uygulama artık tek bir model sağlayıcısına bağımlı değil. appsettings.json üzerinden dört farklı sağlayıcı arasında seçim yapılabiliyor:

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .Build();

var aiProvider = configuration["AiProvider"] ?? "LocalOllama";

DbContext ve repository kurulumu:

var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("DefaultConnection"));
var dbContext = new ApplicationDbContext(optionsBuilder.Options);

await dbContext.Database.MigrateAsync();

var chatRepository = new PostgresChatHistoryRepository(dbContext);
var sessionId = await chatRepository.CreateSessionAsync();

Provider seçimi bir switch ile yapılıyor. Örnek olarak LocalOllama ve OpenAI case'leri:

var kernelBuilder = Kernel.CreateBuilder();

switch (aiProvider)
{
    case "LocalOllama":
        kernelBuilder.AddOllamaChatCompletion(
            modelId: localOllamaModel,
            endpoint: new Uri(localOllamaEndpoint));
        break;

    case "OpenAI":
        kernelBuilder.AddOpenAIChatCompletion(
            modelId: openAiModel,
            apiKey: openAiApiKey);
        break;

    // CloudOllama ve AzureOpenAI için benzer case'ler eklenebilir
}

kernelBuilder.Plugins.AddFromObject(new ProductsPlugin(dbContext));

var kernel = kernelBuilder.Build();

API key gerektiren uzak bir Ollama sunucusuna (örn. Ollama Cloud) bağlanırken, AddOllamaChatCompletion doğrudan bir apiKey parametresi almaz. Bunun yerine Authorization header'ı enjekte edilmiş özel bir HttpClient verilir:

var cloudHttpClient = new HttpClient
{
    BaseAddress = new Uri(cloudOllamaEndpoint)
};
cloudHttpClient.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", cloudOllamaApiKey);

kernelBuilder.AddOllamaChatCompletion(
    modelId: cloudOllamaModel,
    httpClient: cloudHttpClient);

ProductsPlugin artık AddFromType yerine AddFromObject ile ekleniyor, çünkü constructor artık ApplicationDbContext parametresi alıyor.


System Prompt

chatMessages.AddSystemMessage("""
Sen UMAI Bilişim'in yapay zeka ürün asistanısın.
Adın Cemile.
Sadece ve sadece UMAI Bilişim ürünleri hakkında bilgi verebilirsin.
Kullanıcı konuyu değiştirse bile sadece UMAI Bilişim ürünleri hakkında cevap vereceksin.

Kurallar:
- Türkçe, İngilizce ve İspanyolca cevap verebilirsin.
- Gerektiğinde plugin kullan.
- Emin olmadığın veya kaynağı olmayan bilgileri uydurma.
""");

Önceki makaledeki prompt'a göre iki değişiklik var: asistan artık üç dilde (TR/EN/ES) cevap verebiliyor, ve konu dışına çıkılsa bile sadece UMAI Bilişim ürünleri hakkında konuşacak şekilde sınırlandırıldı.


Sonuç

Artık hem konuşma geçmişi hem de ürün verisi PostgreSQL'de, Entity Framework Core üzerinden yönetiliyor. ProductsPlugin daha zengin sorulara cevap verebiliyor, ve uygulama tek bir model sağlayıcısına bağımlı değil.

Serinin bir sonraki bölümünde bu yapı web arayüzüne (Controller/View) taşınacak.

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

Leave a Comment

Your comment will be published after admin approval.

5 + 9 = ?