-
Notifications
You must be signed in to change notification settings - Fork 389
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
767 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Azure.AI.OpenAI" Version="2.0.0-beta.2" /> | ||
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.27.0" /> | ||
</ItemGroup> | ||
|
||
</Project> |
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,25 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 17 | ||
VisualStudioVersion = 17.10.35201.131 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIStreaming", "AIStreaming.csproj", "{6DC96F1B-8F65-471C-9676-0DD3E0A8A5BB}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{6DC96F1B-8F65-471C-9676-0DD3E0A8A5BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{6DC96F1B-8F65-471C-9676-0DD3E0A8A5BB}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{6DC96F1B-8F65-471C-9676-0DD3E0A8A5BB}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{6DC96F1B-8F65-471C-9676-0DD3E0A8A5BB}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {7D19F711-78E2-46B8-B90B-33CF6F10724A} | ||
EndGlobalSection | ||
EndGlobal |
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,24 @@ | ||
using System.Collections.Concurrent; | ||
|
||
namespace AIStreaming | ||
{ | ||
public class GroupAccessor | ||
{ | ||
private readonly ConcurrentDictionary<string, string> _store = new(); | ||
|
||
public void Join(string connectionId, string groupName) | ||
{ | ||
_store.AddOrUpdate(connectionId, groupName, (key, value) => groupName); | ||
} | ||
|
||
public void Leave(string connectionId) | ||
{ | ||
_store.TryRemove(connectionId, out _); | ||
} | ||
|
||
public bool TryGetGroup(string connectionId, out string? group) | ||
{ | ||
return _store.TryGetValue(connectionId, out group); | ||
} | ||
} | ||
} |
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,41 @@ | ||
using OpenAI.Chat; | ||
using System.Collections.Concurrent; | ||
|
||
namespace AIStreaming | ||
{ | ||
public class GroupHistoryStore | ||
{ | ||
private readonly ConcurrentDictionary<string, IList<ChatMessage>> _store = new(); | ||
|
||
public IReadOnlyList<ChatMessage> GetOrAddGroupHistory(string groupName, string userName, string message) | ||
{ | ||
var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); | ||
chatMessages.Add(new UserChatMessage(GenerateUserChatMessage(userName, message))); | ||
return chatMessages.AsReadOnly(); | ||
} | ||
|
||
public void UpdateGroupHistoryForAssistant(string groupName, string message) | ||
{ | ||
var chatMessages = _store.GetOrAdd(groupName, _ => InitiateChatMessages()); | ||
chatMessages.Add(new AssistantChatMessage(message)); | ||
} | ||
|
||
private IList<ChatMessage> InitiateChatMessages() | ||
{ | ||
var messages = new List<ChatMessage> | ||
{ | ||
new SystemChatMessage("You are a friendly and knowledgeable assistant participating in a group discussion." + | ||
" Your role is to provide helpful, accurate, and concise information when addressed." + | ||
" Maintain a respectful tone, ensure your responses are clear and relevant to the group's ongoing conversation, and assist in facilitating productive discussions." + | ||
" Messages from users will be in the format 'UserName: chat messages'." + | ||
" Pay attention to the 'UserName' to understand who is speaking and tailor your responses accordingly."), | ||
}; | ||
return messages; | ||
} | ||
|
||
private string GenerateUserChatMessage(string userName, string message) | ||
{ | ||
return $"{userName}: {message}"; | ||
} | ||
} | ||
} |
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,74 @@ | ||
using Microsoft.AspNetCore.SignalR; | ||
using Microsoft.Extensions.Options; | ||
using OpenAI; | ||
using System.Text; | ||
|
||
namespace AIStreaming.Hubs | ||
{ | ||
public class GroupChatHub : Hub | ||
{ | ||
private readonly GroupAccessor _groupAccessor; | ||
private readonly GroupHistoryStore _history; | ||
private readonly OpenAIClient _openAI; | ||
private readonly OpenAIOptions _options; | ||
|
||
public GroupChatHub(GroupAccessor groupAccessor, GroupHistoryStore history, OpenAIClient openAI, IOptions<OpenAIOptions> options) | ||
{ | ||
_groupAccessor = groupAccessor ?? throw new ArgumentNullException(nameof(groupAccessor)); | ||
_history = history ?? throw new ArgumentNullException(nameof(history)); | ||
_openAI = openAI ?? throw new ArgumentNullException(nameof(openAI)); | ||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options)); | ||
} | ||
|
||
public async Task JoinGroup(string groupName) | ||
{ | ||
await Groups.AddToGroupAsync(Context.ConnectionId, groupName); | ||
_groupAccessor.Join(Context.ConnectionId, groupName); | ||
} | ||
|
||
public override Task OnDisconnectedAsync(Exception? exception) | ||
{ | ||
_groupAccessor.Leave(Context.ConnectionId); | ||
return Task.CompletedTask; | ||
} | ||
|
||
public async Task Chat(string userName, string message) | ||
{ | ||
if (!_groupAccessor.TryGetGroup(Context.ConnectionId, out var groupName)) | ||
{ | ||
throw new InvalidOperationException("Not in a group."); | ||
} | ||
|
||
if (message.StartsWith("@gpt")) | ||
{ | ||
var id = Guid.NewGuid().ToString(); | ||
var actualMessage = message.Substring(4).Trim(); | ||
var messagesIncludeHistory = _history.GetOrAddGroupHistory(groupName, userName, actualMessage); | ||
await Clients.OthersInGroup(groupName).SendAsync("NewMessage", userName, message); | ||
|
||
var chatClient = _openAI.GetChatClient(_options.Model); | ||
var totalCompletion = new StringBuilder(); | ||
var lastSentTokenLength = 0; | ||
await foreach (var completion in chatClient.CompleteChatStreamingAsync(messagesIncludeHistory)) | ||
{ | ||
foreach (var content in completion.ContentUpdate) | ||
{ | ||
totalCompletion.Append(content); | ||
if (totalCompletion.Length - lastSentTokenLength > 20) | ||
{ | ||
await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); | ||
lastSentTokenLength = totalCompletion.Length; | ||
} | ||
} | ||
} | ||
_history.UpdateGroupHistoryForAssistant(groupName, totalCompletion.ToString()); | ||
await Clients.Group(groupName).SendAsync("newMessageWithId", "ChatGPT", id, totalCompletion.ToString()); | ||
} | ||
else | ||
{ | ||
_history.GetOrAddGroupHistory(groupName, userName, message); | ||
await Clients.OthersInGroup(groupName).SendAsync("NewMessage", userName, message); | ||
} | ||
} | ||
} | ||
} |
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 @@ | ||
using Azure.AI.OpenAI; | ||
using Microsoft.Extensions.Options; | ||
using OpenAI; | ||
using System.ClientModel; | ||
|
||
namespace AIStreaming | ||
{ | ||
public static class OpenAIExtensions | ||
{ | ||
public static IServiceCollection AddAzureOpenAI(this IServiceCollection services, IConfiguration configuration) | ||
{ | ||
return services | ||
.Configure<OpenAIOptions>(configuration.GetSection("OpenAI")) | ||
.AddSingleton<OpenAIClient>(provider => | ||
{ | ||
var options = provider.GetRequiredService<IOptions<OpenAIOptions>>().Value; | ||
return new AzureOpenAIClient(new Uri(options.Endpoint), new ApiKeyCredential(options.Key)); | ||
}); | ||
} | ||
|
||
public static IServiceCollection AddOpenAI(this IServiceCollection services, IConfiguration configuration) | ||
{ | ||
return services | ||
.Configure<OpenAIOptions>(configuration.GetSection("OpenAI")) | ||
.AddSingleton<OpenAIClient>(provider => | ||
{ | ||
var options = provider.GetRequiredService<IOptions<OpenAIOptions>>().Value; | ||
return new OpenAIClient(new ApiKeyCredential(options.Key)); | ||
}); | ||
} | ||
} | ||
} |
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,20 @@ | ||
namespace AIStreaming | ||
{ | ||
public class OpenAIOptions | ||
{ | ||
/// <summary> | ||
/// The endpoint of Azure OpenAI service. Only available for Azure OpenAI. | ||
/// </summary> | ||
public string? Endpoint { get; set; } | ||
|
||
/// <summary> | ||
/// The key of OpenAI service. | ||
/// </summary> | ||
public string? Key { get; set; } | ||
|
||
/// <summary> | ||
/// The model to use. | ||
/// </summary> | ||
public string? Model { 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using AIStreaming; | ||
using AIStreaming.Hubs; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
// Add services to the container. | ||
builder.Services.AddSignalR().AddAzureSignalR(); | ||
builder.Services.AddSingleton<GroupAccessor>() | ||
.AddSingleton<GroupHistoryStore>() | ||
.AddAzureOpenAI(builder.Configuration); | ||
|
||
var app = builder.Build(); | ||
|
||
app.UseHttpsRedirection(); | ||
app.UseDefaultFiles(); | ||
app.UseStaticFiles(); | ||
|
||
app.UseRouting(); | ||
|
||
app.MapHub<GroupChatHub>("/groupChat"); | ||
app.Run(); |
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,28 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/launchsettings.json", | ||
"iisSettings": { | ||
"windowsAuthentication": false, | ||
"anonymousAuthentication": true, | ||
"iisExpress": { | ||
"applicationUrl": "http://localhost:54484", | ||
"sslPort": 44368 | ||
} | ||
}, | ||
"profiles": { | ||
"AIStreaming": { | ||
"commandName": "Project", | ||
"launchBrowser": true, | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
}, | ||
"applicationUrl": "http://localhost:5000/" | ||
}, | ||
"IIS Express": { | ||
"commandName": "IISExpress", | ||
"launchBrowser": true, | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.