Skip to content

Commit

Permalink
Merge pull request #1452 from OfficeDev/v-hrajandira/AzureOpenAIBotSt…
Browse files Browse the repository at this point in the history
…reamingC#

[New Sample] Bot Streaming in C# for Microsoft Teams
  • Loading branch information
Harikrishnan-MSFT authored Nov 15, 2024
2 parents f8b47ad + f8fd02a commit a063b36
Show file tree
Hide file tree
Showing 36 changed files with 1,416 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build-complete-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ jobs:
- project_path: 'samples/bot-commands-menu/csharp/CommandsMenu/CommandsMenu.csproj'
name: 'bot-commands-menu'
version: '6.0.x'

- project_path: 'samples/bot-streaming/csharp/StreamingBot.csproj'
name: 'bot-streaming'
version: '8.0.x'

fail-fast: false
name: Build All "${{ matrix.name }}" csharp
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ The [Teams Toolkit](https://marketplace.visualstudio.com/items?itemName=TeamsDev
|27| Apps in Federated | This sample app allows users to easily view a list of group members. When a new member is added, their details are promptly displayed. ||[View][bot-feed-members#js] ![toolkit-icon](assets/toolkit-icon.png)
|28| Requirement Targeting OneWay Dependency | Microsoft M365 RT sample app in Node.js which specify one-way-dependency relationships between app capabilities (using elementRelationshipSet) and functionality using hostMustSupportFunctionalities. ||[View][RequirementTargetingOneWayDependency#nodejs] ![toolkit-icon](assets/toolkit-icon.png)
|29| Requirement Targeting Mutual Dependency | Microsoft M365 RT sample app in Node.js which specify mutual-dependency relationships between app capabilities using elementRelationshipSet. ||[View][RequirementTargetingMutualDependency#nodejs] ![toolkit-icon](assets/toolkit-icon.png)
|30| Streaming Bot |This sample showcases the conversational streaming token scenario for teams bot in personal scope.|[View][botstreaming#csharp]| | | |

#### Additional samples

Expand Down Expand Up @@ -387,6 +388,7 @@ The [Teams Toolkit](https://marketplace.visualstudio.com/items?itemName=TeamsDev
[suggestedactionsbot#csharp]:samples/bot-suggested-actions/csharp
[suggestedactionsbot#nodejs]:samples/bot-suggested-actions/nodejs
[botadaptivecardsuserspecificviews#csharp]:samples/bot-adaptivecards-user-specific-views/csharp
[botstreaming#csharp]:samples/bot-streaming/csharp
[Tagmention#csharp]:samples/bot-tag-mention/csharp
[Tagmention#nodejs]:samples/bot-tag-mention/nodejs
[CommandsMenu#csharp]:samples/bot-commands-menu/csharp
Expand Down
25 changes: 25 additions & 0 deletions samples/bot-streaming/csharp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# TeamsFx files
build
appPackage/build
env/.env.*.user
env/.env.local
appsettings.Development.json
.deployment

# User-specific files
*.user

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Notification local store
.notification.localstore.json
32 changes: 32 additions & 0 deletions samples/bot-streaming/csharp/AdapterWithErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Builder.TraceExtensions;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Logging;

namespace Microsoft.BotBuilderSamples
{
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger)
: base(auth, logger)
{
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, you should consider logging this to
// Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
// to add telemetry capture to your bot.
logger.LogError($"Exception caught : {exception.Message}");
// Uncomment below commented line for local debugging.
// await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}");
// Send a trace activity, which will be displayed in the Bot Framework Emulator
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
};
}
}
}
19 changes: 19 additions & 0 deletions samples/bot-streaming/csharp/Bots/Models/Streaming/ChannelData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Newtonsoft.Json;

namespace TeamsConversationBot.Bots.Models.Streaming
{
public class ChannelData
{
[JsonProperty(PropertyName = "streamId")]
public string StreamId { get; set; }

[JsonConverter(typeof(StringEnumConverter), typeof(CamelCaseNamingStrategy))]
[JsonProperty(PropertyName = "streamType")]
public StreamType StreamType { get; set; }

[JsonProperty(PropertyName = "streamSequence")]
public int StreamSequence { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TeamsConversationBot.Bots.Models.Streaming
{
public enum StreamType
{
Informative,
Streaming,
Final
}
}
254 changes: 254 additions & 0 deletions samples/bot-streaming/csharp/Bots/TeamsConversationBot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using AdaptiveCards.Templating;
using Azure.AI.OpenAI;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenAI.Chat;
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using TeamsConversationBot.Bots.Models.Streaming;
using Activity = Microsoft.Bot.Schema.Activity;

namespace Microsoft.BotBuilderSamples.Bots
{
public class TeamsConversationBot : TeamsActivityHandler
{
private string _appId;
private string _appPassword;
private string _appTenantId;

private string _endpoint;
private string _key;
private string _deployment;

private ChatClient _chatClient;
private readonly string adaptiveCardTemplate = Path.Combine(".", "Resources", "CardTemplate.json");

public TeamsConversationBot(IConfiguration config)
{
_appId = config["MicrosoftAppId"];
_appPassword = config["MicrosoftAppPassword"];
_appTenantId = config["MicrosoftAppTenantId"];
_endpoint = config["AzureOpenAIEndpoint"];
_key = config["AzureOpenAIKey"];
_deployment = config["AzureOpenAIDeployment"];

ApiKeyCredential credential = new ApiKeyCredential(_key);
AzureOpenAIClient azureClient = new(new Uri(_endpoint), credential);

_chatClient = azureClient.GetChatClient(_deployment);
}

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
string userInput = turnContext.Activity.Text?.Trim().ToLower();
try
{
StringBuilder contentBuilder = new();
Stopwatch stopwatch = new Stopwatch();
int streamSequence = 1; // Sream sequence should always start with 1.
int rps = 1000; // The current allowance is 1 RPS.

/*
* We can send an initial streaming message as informative while we get the response from the LLM setting the StreamType to Informative.
* This action is helpful to get the streaming sequence started and get messageId back, which we will use later as the StreamId
*/
ChannelData channelData = new ChannelData
{
StreamType = StreamType.Informative,
StreamSequence = streamSequence,
};
string streamId = await buildAndSendStreamingActivity(turnContext, cancellationToken, "Getting the information...", channelData).ConfigureAwait(false);

// Send request to chat client with suitable specifications
CollectionResult<StreamingChatCompletionUpdate> completionUpdates = _chatClient.CompleteChatStreaming(
[
new SystemChatMessage("You are an AI great at storytelling which creates compelling fantastical stories."),
new UserChatMessage (userInput),
],
new ChatCompletionOptions()
{
Temperature = (float)0.7,
FrequencyPenalty = (float)0,
PresencePenalty = (float)0,
},
cancellationToken);

stopwatch.Start(); // Starting stopwatch to chunk by RPS (elapsedMiliseconds)

foreach (StreamingChatCompletionUpdate streamingChatUpdate in completionUpdates)
{
streamSequence++; // Increment the streamSequence number per each update received for internal purposes

/*
* If the streaming has ended for some reason, build the final message seeting the ChannelSata.StreamType to Final.
* Send the message to the bot and break/continue to prevent further processing.
*/
if (streamingChatUpdate.FinishReason != null)
{
channelData = new ChannelData
{
StreamType = StreamType.Final,
StreamSequence = streamSequence,
StreamId = streamId
};
await buildAndSendStreamingActivity(turnContext, cancellationToken, contentBuilder.ToString(), channelData).ConfigureAwait(false);
break;
}

/*
* Teams Content Streaming feature needs bot developers to build chunks from the LLM responses.
* So, we accumulate what is being send and once RPS is reached request is sent.
*/

foreach (ChatMessageContentPart contentPart in streamingChatUpdate.ContentUpdate)
{
contentBuilder.Append(contentPart.Text);
}

if (contentBuilder.Length > 0 && stopwatch.ElapsedMilliseconds > rps)
{
channelData = new ChannelData
{
StreamType = StreamType.Streaming,
StreamSequence = streamSequence,
StreamId = streamId
};

stopwatch.Restart(); // Restart the stopwatch for the next chunk
await buildAndSendStreamingActivity(turnContext, cancellationToken, contentBuilder.ToString(), channelData).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
await turnContext.SendActivityAsync(ex.Message);
}
}

/// <summary>
/// Builds the activity with the corresponding data for streaming and sends it.
/// </summary>
/// <param name="turnContext">Turn context of the bot</param>
/// <param name="cancellationToken">Cancellation Token</param>
/// <param name="text">Text being streamed and to be sent as part of the activity</param>
/// <param name="channelData">ChannelData information needed for streaming purposes</param>
/// <returns></returns>
private async Task<string> buildAndSendStreamingActivity(
ITurnContext<IMessageActivity> turnContext,
CancellationToken cancellationToken,
string text,
ChannelData channelData)
{
bool isStreamFinal = channelData.StreamType.ToString().Equals(StreamType.Final.ToString());
Activity streamingActivity = new()
{
Type = isStreamFinal ? ActivityTypes.Message : ActivityTypes.Typing,
Id = channelData.StreamId,
ChannelData = channelData
};

/*
* For the moment, we need to add the streaming information in 2 places: Entities and ChannelData.
* to prevent breaking changes in the near future.
* The final placement for this information will be in Entities once the feature is available to
* the public. As per DevPreview timing, data is set in ChannelData.
*/
var streamingInfoProperties = new
{
streamId = channelData.StreamId,
streamType = channelData.StreamType.ToString(),
streamSequence = channelData.StreamSequence,
};

streamingActivity.Entities = new List<Entity>
{
new Entity("streaminfo")
{
Properties = JObject.FromObject(streamingInfoProperties)
}
};

if (!string.IsNullOrEmpty(text))
{
streamingActivity.Text = text;
}

/*
* We are sending the final streamed message as an Adaptive Card Attachment built
* using a template.
*/
if (isStreamFinal)
{
//Build the adaptive card
AdaptiveCardTemplate template = new AdaptiveCardTemplate(File.ReadAllText(adaptiveCardTemplate));
var tempData = new
{
finaltStreamText = text
};
var attachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(template.Expand(tempData)),
};

streamingActivity.Attachments = new List<Attachment>() { attachment };

//Add text to the activity
streamingActivity.Text = "This is what I've got:";
}

return await sendStreamingActivityAsync(turnContext, cancellationToken, streamingActivity).ConfigureAwait(false);
}

/// <summary>
/// Sends the activity
/// </summary>
/// <param name="turnContext">Turn context of the bot</param>
/// <param name="cancellationToken">Cancellation Token</param>
/// <param name="streamingActivity">Activity to be sent</param>
/// <returns>The messageId</returns>
/// <exception cref="Exception"></exception>
private async Task<string> sendStreamingActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken, IActivity streamingActivity)
{
try
{
ResourceResponse streamingResponse = await turnContext.SendActivityAsync(streamingActivity, cancellationToken).ConfigureAwait(false);
return streamingResponse.Id;
}
catch (Exception ex)
{
ErrorResponseException errorResponse = ex as ErrorResponseException;
string excetionTemplate = "Error while sending streaming activity: ";
await turnContext.SendActivityAsync(MessageFactory.Text(excetionTemplate + errorResponse?.Body?.Error?.Message), cancellationToken).ConfigureAwait(false);
throw new Exception(excetionTemplate + ex.Message);
}
}

//---------------------------------TeamsActivityHandler.cs--------------------------------------------
protected override async Task OnInstallationUpdateActivityAsync(ITurnContext<IInstallationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Conversation.ConversationType == "channel")
{
await turnContext.SendActivityAsync($"Welcome to Streaming demo bot is configured in {turnContext.Activity.Conversation.Name}. Unfurtonately, the streaming feature is not yet available for channels or group chats.");
}
else
{
await turnContext.SendActivityAsync("Welcome to Streaming demo bot! You can ask me a question and I'll do my best to answer it.");

}
}
}
}
Loading

0 comments on commit a063b36

Please sign in to comment.