Skip to content

Commit

Permalink
Add AI streaming samples (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
zackliu authored Sep 4, 2024
1 parent 316e023 commit 1bc834b
Show file tree
Hide file tree
Showing 17 changed files with 767 additions and 0 deletions.
14 changes: 14 additions & 0 deletions samples/AIStreaming/AIStreaming.csproj
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>
25 changes: 25 additions & 0 deletions samples/AIStreaming/AIStreaming.sln
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
24 changes: 24 additions & 0 deletions samples/AIStreaming/GroupAccessor.cs
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);
}
}
}
41 changes: 41 additions & 0 deletions samples/AIStreaming/GroupHistoryStore.cs
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}";
}
}
}
74 changes: 74 additions & 0 deletions samples/AIStreaming/Hubs/GroupChatHub.cs
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);
}
}
}
}
32 changes: 32 additions & 0 deletions samples/AIStreaming/OpenAIExtensions.cs
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));
});
}
}
}
20 changes: 20 additions & 0 deletions samples/AIStreaming/OpenAIOptions.cs
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; }
}
}
21 changes: 21 additions & 0 deletions samples/AIStreaming/Program.cs
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();
28 changes: 28 additions & 0 deletions samples/AIStreaming/Properties/launchSettings.json
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"
}
}
}
}
Loading

0 comments on commit 1bc834b

Please sign in to comment.