Skip to content

Commit

Permalink
Merge pull request #4 from erinnmclaughlin/api-endpoint
Browse files Browse the repository at this point in the history
added signalr hub for chat + some knowledge about me
  • Loading branch information
erinnmclaughlin authored Apr 19, 2024
2 parents d09cdf2 + 88db4c3 commit 76976c4
Show file tree
Hide file tree
Showing 31 changed files with 428 additions and 260 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ dotnet_diagnostic.SKEXP0001.severity = none

# SKEXP0010: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
dotnet_diagnostic.SKEXP0010.severity = none

# SKEXP0050: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
dotnet_diagnostic.SKEXP0050.severity = none
1 change: 1 addition & 0 deletions PersonalWebsite.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Site.Client", "src\Site\Sit
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6680D40B-9A58-4838-BCAD-56D382006878}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\main_erinnmclaughlin.yml = .github\workflows\main_erinnmclaughlin.yml
EndProjectSection
EndProject
Expand Down
32 changes: 32 additions & 0 deletions src/Site/Site.Client/Components/TerminalChat.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@rendermode InteractiveAuto
@attribute [StreamRendering]

<AntiforgeryToken />

<div class="terminal-chat">
@foreach (var message in Messages)
{
<div>
<label style="color: @GetLabelColor(message.Author)">
@message.Author
</label>
@if (message.Author == "User")
{
<p>@message.Message</p>
}
else
{
<TerminalChatContent Content="@message.Message" />
}
</div>
}

@if (State is ChatState.WaitingForAssistant)
{
<p>Thinking...</p>
}
else if (State is ChatState.WaitingForUser)
{
<TerminalInput OnUserInputReceived="RenderUserMessage" />
}
</div>
95 changes: 95 additions & 0 deletions src/Site/Site.Client/Components/TerminalChat.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using System.Diagnostics.CodeAnalysis;

namespace Site.Client.Components;

public sealed partial class TerminalChat : IAsyncDisposable
{
private HubConnection? _hubConnection;

[Inject, NotNull]
private NavigationManager? NavigationManager { get; set; }

private List<TerminalChatMessage> Messages { get; } = [];

private ChatState State { get; set; }

public async ValueTask DisposeAsync()
{
if (_hubConnection is not null)
await _hubConnection.DisposeAsync();
}

protected override void OnInitialized()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/aibba"))
.Build();

_hubConnection.On<List<TerminalChatMessage>>("ReceiveMessage", (chatMessages) =>
{
InvokeAsync(async () => await RenderReceivedMessagesAsync(chatMessages));
});

InvokeAsync(() => _hubConnection.StartAsync());
}

private async Task RenderUserMessage(string message)
{
State = ChatState.WaitingForAssistant;
Messages.Add(new TerminalChatMessage { Author = "User", Message = message });
StateHasChanged();

if (_hubConnection is not null)
await _hubConnection.SendAsync("SendMessage", message);
}

private async Task RenderReceivedMessagesAsync(List<TerminalChatMessage> messages)
{
State = ChatState.Rendering;
StateHasChanged();

for (int i = 0; i < messages.Count; i++)
{
await RenderReceivedMessageAsync(messages[i]);

if (i != messages.Count - 1)
await Task.Delay(300);
}

State = ChatState.WaitingForUser;
StateHasChanged();
}

private async Task RenderReceivedMessageAsync(TerminalChatMessage chatContent)
{
var message = new TerminalChatMessage
{
Author = chatContent.Author
};

Messages.Add(message);

foreach (var c in chatContent.Message)
{
message.Message += c;
await Task.Delay(30);
StateHasChanged();
}
}

private static string GetLabelColor(string authorName) => authorName switch
{
"Erin" => "cyan",
"Aibba" => "magenta",
_ => "default"
};

enum ChatState
{
Rendering,
WaitingForAssistant,
WaitingForUser
}
}
File renamed without changes.
19 changes: 0 additions & 19 deletions src/Site/Site.Client/Pages/Counter.razor

This file was deleted.

4 changes: 3 additions & 1 deletion src/Site/Site.Client/Site.Client.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand All @@ -11,6 +11,8 @@
<ItemGroup>
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.4" />
<PackageReference Include="Vereyon.Web.HtmlSanitizer" Version="1.8.0" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/Site/Site.Client/TerminalChatMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Site.Client;

public class TerminalChatMessage
{
public string Author { get; set; } = "";
public string Message { get; set; } = "";
}
1 change: 1 addition & 0 deletions src/Site/Site.Client/_Imports.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Site.Client
@using Site.Client.Components
11 changes: 9 additions & 2 deletions src/Site/Site/AI/AIOptions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
namespace Site.AI;
using Microsoft.SemanticKernel;

internal sealed class AIOptions
namespace Site.AI;

public sealed class AIOptions
{
public string ApiKey { get; set; } = string.Empty;
public bool Enabled { get; set; }
public string TextCompletionModel { get; set; } = string.Empty;
public string TextEmbeddingModel { get; set; } = string.Empty;

public Kernel BuildDefaultKernel() => Kernel.CreateBuilder()
.AddOpenAIChatCompletion(TextCompletionModel, ApiKey)
.AddOpenAITextEmbeddingGeneration(TextEmbeddingModel, ApiKey)
.Build();
}
75 changes: 75 additions & 0 deletions src/Site/Site/AI/Aibba.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Site.Client;

namespace Site.AI;

public sealed class Aibba
{
private readonly Kernel _kernel;
private readonly ChatHistory _messages = [];
private readonly OpenAIPromptExecutionSettings _promptExecutionSettings = new();
private readonly Queue<TerminalChatMessage> _queue = [];

public Aibba(IOptions<AIOptions> options, AibbaKnowledge knowledge, AibbaNavigationPlugin navigationPlugin)
{
_kernel = options.Value.BuildDefaultKernel();
navigationPlugin.ApplyToKernel(_kernel);
knowledge.ApplyToKernel(_kernel);

_promptExecutionSettings.ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions;

InitializeChatHistory();
}

public Task TriggerResponse(string message, CancellationToken cancellationToken = default)
{
_messages.AddMessage(AuthorRole.User, message);
return TriggerResponse(cancellationToken);
}

public IEnumerable<TerminalChatMessage> GetNextMessages()
{
while (_queue.TryDequeue(out var message))
{
yield return message;
}
}

private void InitializeChatHistory()
{
_messages.AddSystemMessage("""
You are an AI assistant named Aibba and you are running on Erin McLaughlin's personal website.
Erin is the software engineer that programmed you.
Your job is to talk to users about her. Please note that you can recall information about Erin from your memory.
""");

AddErinMessage("Hi! Welcome to my website. I'm a software engineer with a passion for building context-driven systems.");
AddErinMessage("You can check out my work on [GitHub](https://github.com/erinnmclaughlin), or ask my AI friend Aibba about me!");
AddErinMessage("Aibba is a large language model I've integrated into my website to answer questions you might have about me.");
AddErinMessage("Alright - I gotta go! Aibba, can you take it from here?");

AddAibbaMessage("Sure thing, Erin! Feel free to ask me if there's anything else you'd like to know about Erin!");
}

private void AddAibbaMessage(string message)
{
_messages.AddMessage(AuthorRole.Assistant, message);
_queue.Enqueue(new TerminalChatMessage { Author = "Aibba", Message = message });
}

private void AddErinMessage(string message)
{
_messages.AddMessage(AuthorRole.System, message);
_queue.Enqueue(new TerminalChatMessage { Author = "Erin", Message = message });
}

private async Task TriggerResponse(CancellationToken cancellationToken)
{
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
var chatMessageContent = await chatService.GetChatMessageContentAsync(_messages, _promptExecutionSettings, _kernel, cancellationToken);
AddAibbaMessage(chatMessageContent.Content ?? string.Empty);
}
}
69 changes: 69 additions & 0 deletions src/Site/Site/AI/AibbaHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.SignalR;
using System.Diagnostics.CodeAnalysis;

namespace Site.AI;

public sealed class AibbaHub(ILogger<AibbaHub> logger) : Hub
{
private readonly Dictionary<string, Aibba> _aibbas = [];
private readonly ILogger _logger = logger;

public override async Task OnConnectedAsync()
{
_logger.LogInformation("User connected: {ConnectionId}", Context.ConnectionId);

if (TryGetAibbaInstance(out var aibba))
await SendNewMessagesAsync(aibba);
}

public override Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("User disconnected: {ConnectionId}", Context.ConnectionId);
_aibbas.Remove(Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}

public async Task SendMessage(string message)
{
_logger.LogInformation("User says {message}", message);

if (TryGetAibbaInstance(out var aibba))
{
await aibba.TriggerResponse(message, Context.ConnectionAborted);
await SendNewMessagesAsync(aibba);
}
}

private bool TryGetAibbaInstance([NotNullWhen(true)] out Aibba? aibba)
{
aibba = _aibbas.GetValueOrDefault(Context.ConnectionId);

if (aibba is null)
{
aibba = Context.GetHttpContext()?.RequestServices.GetRequiredService<Aibba>();

if (aibba is null)
{
_logger.LogError("Failed to create Aibba instance for connection {ConnectionId}.", Context.ConnectionId);
return false;
}

_aibbas.Add(Context.ConnectionId, aibba);
}

return true;
}

private async Task SendNewMessagesAsync(Aibba aibba)
{
var messages = aibba.GetNextMessages().ToList();

if (messages.Count != 0)
{
foreach (var message in messages)
_logger.LogInformation("Sending message {message} from {author}", message.Message, message.Author);

await Clients.Caller.SendAsync("ReceiveMessage", messages, Context.ConnectionAborted);
}
}
}
Loading

0 comments on commit 76976c4

Please sign in to comment.