OllamaSharp 을 이용하면 ollama 에 있는 모델을 로드하여 질의할 수 있습니다.
Ollama 설치는 아래 포스팅을 확인합니다.
2024.06.15 - [코딩/Python_AI] - 로컬에서 Llama3-8B 모델 돌려보기 - Ollama
아래 코드는 Blazor 에서 OllamaSharp 을 이용해 로컬에 설치된 모델 중 하나를 선택하여 질의할 수 있게 합니다.
Markdig, Prism 을 이용해 MarkDown 처리를 하였습니다.
먼저 OllamaSharp, Markdig Nuget Package 설치합니다.
index.html 에 아래와 같이 Prism 라이브러리를 사용할 수 있게 합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OllamaChatBlazorApp</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism.min.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
window.initializePrism = function () {
Prism.plugins.autoloader.languages_path = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/components/';
}
window.highlightAll = function () {
Prism.highlightAll();
}
</script>
</body>
</html>
아래는 채팅화면입니다.
코드를 보면 알 수 있듯이 Prism 을 이용해 코드블록을 Highlight 하였습니다.
Home.razor
@page "/"
@using System.Net.Http
@using System.Text.Json
@using Microsoft.AspNetCore.Components
@using OllamaSharp
@using Markdig
@using Markdig.Syntax
@using Markdig.Renderers
@using Markdig.Renderers.Html
@inject IJSRuntime JSRuntime
<div class="chat-app">
<h1>Ollama Chat</h1>
<!-- 모델 선택 드롭다운 -->
<div class="model-selector">
<select @bind="selectedModel">
@foreach (var model in availableModels)
{
<option value="@model">@model</option>
}
</select>
</div>
<!-- 채팅 메시지 표시 영역 -->
<div class="chat-container">
@foreach (var message in chatHistory)
{
<div class="@(message.IsUser ? "user-message" : "ai-message")">
@if (message.IsUser)
{
<p>@message.Content</p>
}
else
{
@((MarkupString)RenderMarkdownWithCodeHighlighting(message.Content))
}
</div>
}
</div>
<!-- 사용자 입력 영역 -->
<div class="input-container">
<input @bind="userInput" @onkeyup="HandleKeyUp" placeholder="메시지를 입력하세요..." />
<button @onclick="SendMessage" disabled="@isSending">전송</button>
</div>
</div>
@code {
private List<ChatMessage> chatHistory = new List<ChatMessage>();
private string userInput = "";
private bool isSending = false;
private OllamaApiClient ollama;
private List<string> availableModels = new List<string>();
private string selectedModel = "";
// 컴포넌트 초기화 시 실행
protected override async Task OnInitializedAsync()
{
var uri = new Uri("http://localhost:11434");
ollama = new OllamaApiClient(uri);
// 사용 가능한 모델 목록 가져오기
var models = await ollama.ListLocalModels();
availableModels = models.Select(m => m.Name).ToList();
if (availableModels.Any())
{
selectedModel = availableModels.First();
ollama.SelectedModel = selectedModel;
}
await base.OnInitializedAsync();
}
// 컴포넌트 렌더링 후 실행
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("initializePrism");
}
else
{
await JSRuntime.InvokeVoidAsync("highlightAll");
}
}
// 키 입력 처리
private async Task HandleKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
await SendMessage();
}
}
// 메시지 전송 및 AI 응답 처리
private async Task SendMessage()
{
if (string.IsNullOrWhiteSpace(userInput) || isSending)
return;
isSending = true;
chatHistory.Add(new ChatMessage { Content = userInput, IsUser = true });
var aiMessage = new ChatMessage { Content = "", IsUser = false };
chatHistory.Add(aiMessage);
ollama.SelectedModel = selectedModel;
string currentResponse = "";
await foreach (var stream in ollama.Generate(userInput))
{
currentResponse += stream.Response;
aiMessage.Content = currentResponse;
StateHasChanged();
await JSRuntime.InvokeVoidAsync("highlightAll");
await Task.Delay(10); // UI 업데이트를 위한 짧은 지연
}
userInput = "";
isSending = false;
}
// 마크다운을 HTML로 렌더링하고 코드 하이라이팅 적용
private string RenderMarkdownWithCodeHighlighting(string markdown)
{
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Use<CustomCodeBlockRenderer>()
.Build();
var writer = new StringWriter();
var renderer = new Markdig.Renderers.HtmlRenderer(writer);
pipeline.Setup(renderer);
var document = Markdig.Parsers.MarkdownParser.Parse(markdown, pipeline);
foreach (var node in document.Descendants())
{
if (node is CodeBlock codeBlock)
{
var fencedCodeBlock = codeBlock as FencedCodeBlock;
var language = fencedCodeBlock?.Info ?? "plaintext";
codeBlock.GetAttributes().AddClass($"language-{language}");
}
}
renderer.Render(document);
writer.Flush();
return writer.ToString();
}
// 커스텀 코드 블록 렌더러
private class CustomCodeBlockRenderer : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is Markdig.Renderers.HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.Replace<CodeBlockRenderer>(new CustomHtmlCodeBlockRenderer());
}
}
}
// 커스텀 HTML 코드 블록 렌더러
private class CustomHtmlCodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
{
protected override void Write(Markdig.Renderers.HtmlRenderer renderer, CodeBlock obj)
{
var fencedCodeBlock = obj as FencedCodeBlock;
var language = fencedCodeBlock?.Info?.ToLower() ?? "plaintext";
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<pre><code class=\"language-");
renderer.WriteEscape(language);
renderer.Write("\">");
}
var code = obj.Lines.ToString();
if (renderer.EnableHtmlEscape)
{
renderer.WriteEscape(code);
}
else
{
renderer.Write(code);
}
if (renderer.EnableHtmlForBlock)
{
renderer.WriteLine("</code></pre>");
}
}
}
// 채팅 메시지 클래스
private class ChatMessage
{
public string Content { get; set; }
public bool IsUser { get; set; }
}
}
<style>
/* 스타일 정의 */
.chat-app {
display: flex;
flex-direction: column;
height: 100vh;
padding: 20px;
box-sizing: border-box;
}
h1 {
margin-top: 0;
}
.model-selector {
margin-bottom: 10px;
}
.model-selector select {
width: 100%;
padding: 5px;
}
.chat-container {
flex-grow: 1;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.user-message {
background-color: #e6f2ff;
padding: 5px;
margin: 5px 0;
border-radius: 5px;
}
.ai-message {
background-color: #f0f0f0;
padding: 5px;
margin: 5px 0;
border-radius: 5px;
}
.input-container {
display: flex;
}
input {
flex-grow: 1;
padding: 5px;
}
button {
padding: 5px 10px;
margin-left: 5px;
}
/* 코드 블록 스타일링 */
pre {
background-color: #f4f4f4;
border: 1px solid #ddd;
border-left: 3px solid #f36d33;
color: #666;
page-break-inside: avoid;
font-family: monospace;
font-size: 15px;
line-height: 1.6;
margin-bottom: 1.6em;
max-width: 100%;
overflow: auto;
padding: 1em 1.5em;
display: block;
word-wrap: break-word;
}
code {
font-family: monospace;
font-size: 0.9em;
background-color: #f4f4f4;
padding: 2px 4px;
}
/* 마크다운 스타일링 */
.ai-message :deep(h1, h2, h3, h4, h5, h6) {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.ai-message :deep(ul, ol) {
padding-left: 20px;
}
.ai-message :deep(blockquote) {
border-left: 4px solid #ccc;
margin: 0;
padding-left: 10px;
}
</style>
결과
상단 콤보에서 현재 로컬에 다운받은 Model 목록이 표시됩니다
현재 제가 설치한 모델 리스트입니다.
소스코드
https://github.com/kei-soft/OllamaBlazorApp
Blazor iFrame 내에 포함될 수 있게 하기 - Content Security Policy (CSP) (0) | 2024.10.21 |
---|---|
Blazor App 을 GitHub Action 으로 CI/CD 하기 (0) | 2024.10.16 |
Blazor FluentUI Emoji 사용하기 - Microsoft.Fast.Components.FluentUI.Emojis (0) | 2024.06.10 |
Blazor FluentUI Icon 사용하기 - Microsoft.Fast.Components.FluentUI.Icons (0) | 2024.06.10 |
Blazor FluentUI Templates 사용하기 (0) | 2024.06.10 |