-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from erinnmclaughlin/api-endpoint
added signalr hub for chat + some knowledge about me
- Loading branch information
Showing
31 changed files
with
428 additions
and
260 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
File renamed without changes.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } = ""; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.