Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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