🧙‍♂️

Semantic Kernelを使ってC#でAI (2)

に公開

ASP.Net CoreにAIを

前回の記事で、Semantic Kernelを使うと非常に簡単にAIを導入することができることを感じてもらえたと思います。

今回はASP.Net CoreからSemantic KernelでAIを使うようにしてみましょう。

プログラムを作成する

サンプルは、こちら

プロジェクトを作成

プロジェクト作成
$ dotnet new webapp
$ dotnet add package Microsoft.SemanticKernel
$ dotnet add package Microsoft.SemanticKernel.Connectors.Ollama --prerelease
$ dotnet add package DotEnv.Core
$ dotnet add package Markdig

プログラム改変

メインプログラム

Program.cs
using DotEnv.Core;
using Microsoft.SemanticKernel;

new EnvLoader().Load();

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromHours(1);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

#pragma warning disable SKEXP0070 // 種類は、評価の目的でのみ提供されています。将来の更新で変更または削除されることがあります。続行するには、この診断を非表示にします。
builder.Services
    .AddKernel()
    .AddOllamaChatCompletion(Environment.GetEnvironmentVariable("OLLAMA_MODEL"), new Uri("http://localhost:11434"))
    ;
#pragma warning restore SKEXP0070 // 種類は、評価の目的でのみ提供されています。将来の更新で変更または削除されることがあります。続行するには、この診断を非表示にします。

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapStaticAssets();
app.MapRazorPages()
   .WithStaticAssets();

app.Run();

アプリ起動部分。
セッション使ってメモリキャッシュにhistory入れておくので、それ関連を追加してあります。

builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromHours(1);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

app.UseSession();

そして、Semantic Kernelを追加。

builder.Services
    .AddKernel()
    .AddOllamaChatCompletion(Environment.GetEnvironmentVariable("OLLAMA_MODEL"), new Uri("http://localhost:11434"))
    ;

AddKernel でSemantic Kernelのカーネル部分を追加しています。
内部的には CreateBuilder を呼んでそれを返す、位に短いものではあるコンビニエンス関数です。

メインビュー

Index.cshtml
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<form method="post">
    <label for="query">AIに質問 : </label>
    <input type="text" name="queryText" />
    <input type="submit" value="実行" />
</form>

<p>
@Html.Raw(Model.ResultText)
</p>

後述の IndexModelqueryText 変数を使って入力を投げ、 ResultText で返してくるようになっているRazor Pageです。
本筋ではないので、詳しい動きはRazor関連で調べてください。

メインモデル

Index.cshtml.cs
using Markdig;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.SemanticKernel.ChatCompletion;
using System.Diagnostics;

namespace Sample02.Pages;

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IChatCompletionService _chatCompletion;
    private readonly IMemoryCache _memoryCache;

    public string ResultText { get; set; } = string.Empty;

    public IndexModel(
        IChatCompletionService chatCompletion,
        IMemoryCache memoryCache,
        ILogger<IndexModel> logger
        )
    {
        _chatCompletion = chatCompletion;
        _memoryCache = memoryCache;
        _logger = logger;
    }

    public void OnGet()
    {
    }

    const string HistoryIdKey = "history_id";

    public async Task OnPost(string queryText)
    {
        var sessionId = HttpContext.Session.GetString(HistoryIdKey);
        if (string.IsNullOrEmpty(sessionId))
        {
            sessionId = HttpContext.Session.Id;
        }
        HttpContext.Session.SetString(HistoryIdKey, sessionId);

        ChatHistory history = null;

        if (!_memoryCache.TryGetValue(sessionId, out history))
        {
            history = new ChatHistory();
        }
        if (history == null)
        {
            history = new ChatHistory();
        }

        history.AddUserMessage(queryText);
        var res = await _chatCompletion.GetChatMessageContentAsync(history);
        var ret = res.Content ?? string.Empty;
        history.AddAssistantMessage(ret);

        _memoryCache.Set(sessionId, history);

        this.ResultText = Markdown.ToHtml(ret);
    }
}

DIで最初に AddOllamaChatCompletion を使って追加してある IChatCompletion をコンストラクタで取得してきています。
DI楽ですね。

後はセッション機能使ってhistory引っ張ってきて、入力と戻り値を前回と同じくUserとAssistantに設定しています。
そのおかげで、入力していくとそれに合わせて返信を行ってくれる、というものになっています。

まとめ

ASP.Net Coreに変更しただけで、やっていることは前回と何も変わっていなかったりします。
なので、今回のキモはDIで使う AddKernel だけですね。

久しぶりにRazor触って、どんなんだっけ?となったのは内緒です。

Discussion