Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C#] feat: Implemented basic changes needed to support streaming tool calls #2214

Merged
merged 18 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
17ab55a
[repo] fix: Update CODEOWNERS (#2181)
singhk97 Nov 12, 2024
7d4b02b
[C#] bump: dotnet to v1.8.1 (#2180)
singhk97 Nov 12, 2024
650a6a3
[repo] bump: (deps): Bump the production group across 1 directory wit…
dependabot[bot] Nov 21, 2024
be7c894
[JS] bump: (deps): Bump the production group across 1 directory with …
dependabot[bot] Nov 21, 2024
e6186c1
[JS] bump: (deps-dev): Bump the development group across 1 directory …
dependabot[bot] Nov 21, 2024
524f08c
[PY] fix: content safety public preview deprecation (#2207)
lilyydu Nov 27, 2024
bfba04b
Implemented basic changes needed to support streaming tool calls
Stevenic Dec 3, 2024
cd919c0
[JS] feat: support for v2 Assistants (#2193)
corinagum Dec 3, 2024
a8cc2c2
[JS] feat: add helper functions to to get Teams channels, members, an…
yiqing-zhao Dec 3, 2024
d7446e2
[repo] bump: (deps): Bump the production group with 3 updates (#2203)
dependabot[bot] Dec 3, 2024
60209fc
Merge branch 'main' into stevenic/dotnet-streaming
Stevenic Dec 4, 2024
d8f56ae
Updated LightBot sample
Stevenic Dec 4, 2024
ad65ed8
Merge branch 'stevenic/dotnet-streaming' of https://github.com/Steven…
Stevenic Dec 4, 2024
eb63a75
DO NOT MERGE [JS] feat: custom feedback form + citation changes (#2182)
aacebo Dec 5, 2024
8e33ff8
Applied fixes from Kavin
Stevenic Dec 9, 2024
0671c79
Merge branch 'main' into stevenic/dotnet-streaming
Stevenic Dec 9, 2024
8669f7a
Added missed removal to MaxOutputTokenCount
Stevenic Dec 9, 2024
7cde4d8
Merge branch 'dotnet-dev' into stevenic/dotnet-streaming
Stevenic Dec 9, 2024
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
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

# JS

/js @aacebo @corinagum @lilyydu @singhk97
/js @aacebo @corinagum @lilyydu @singhk97 @rajan-chari

# .NET

/dotnet @aacebo @corinagum @lilyydu @singhk97
/dotnet @aacebo @corinagum @lilyydu @singhk97 @rajan-chari

# Python

/python @aacebo @corinagum @lilyydu @singhk97
/python @aacebo @corinagum @lilyydu @singhk97 @rajan-chari

# TTK

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
- name: Test
run: dotnet test Microsoft.TeamsAI.Tests/Microsoft.Teams.AI.Tests.csproj --verbosity normal --logger trx --results-directory ./TestResults --collect:"XPlat Code Coverage" --configuration Release
- name: Coverage
uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # 5.3.11
uses: danielpalme/ReportGenerator-GitHub-Action@810356ce07a94200154301fb73d878e327b2dd58 # 5.4.1
with:
reports: ${{ env.SOLUTION_DIR }}TestResults/*/coverage.cobertura.xml
targetdir: ${{ env.SOLUTION_DIR }}TestResults/coverage
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dotnet-build-test-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Test
run: dotnet test Microsoft.TeamsAI.Tests/Microsoft.Teams.AI.Tests.csproj --no-restore --verbosity normal --logger trx --results-directory ./TestResults --collect:"XPlat Code Coverage" --configuration Release
- name: Coverage
uses: danielpalme/ReportGenerator-GitHub-Action@62f9e70ab348d56eee76d446b4db903a85ab0ea8 # 5.3.11
uses: danielpalme/ReportGenerator-GitHub-Action@810356ce07a94200154301fb73d878e327b2dd58 # 5.4.1
with:
reports: ${{ env.SOLUTION_DIR }}TestResults/*/coverage.cobertura.xml
targetdir: ${{ env.SOLUTION_DIR }}TestResults/coverage
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dotnet-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
languages: csharp
- name: Setup .NET
Expand All @@ -50,6 +50,6 @@ jobs:
working-directory: dotnet/packages/Microsoft.TeamsAI/
run: dotnet build Microsoft.Teams.AI.sln --configuration Release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
category: "/language:csharp"
4 changes: 2 additions & 2 deletions .github/workflows/js-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
category: "/language:javascript"
4 changes: 2 additions & 2 deletions .github/workflows/python-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ jobs:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
category: "/language:python"
2 changes: 1 addition & 1 deletion .github/workflows/scorecards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ jobs:

# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
sarif_file: results.sarif
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Bot.Builder;
using Google.Protobuf.WellKnownTypes;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this being used? if not can you remove it

using Microsoft.Bot.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Teams.AI.AI.Models;
Expand Down Expand Up @@ -155,7 +156,6 @@ public async Task<PromptResponse> CompletePromptAsync(
)
{
// Define event handlers
bool isStreaming = false;
StreamingResponse? streamer = null;

BeforeCompletionHandler handleBeforeCompletion = new((object sender, BeforeCompletionEventArgs args) =>
Expand All @@ -168,22 +168,26 @@ public async Task<PromptResponse> CompletePromptAsync(

if (args.Streaming)
{
isStreaming = true;

// Create streamer and send initial message
streamer = new StreamingResponse(context);
memory.SetValue("temp.streamer", streamer);

if (this._enableFeedbackLoop != null)
// Attach to any existing streamer
// - see tool call note below to understand.
streamer = (StreamingResponse?)memory.GetValue("temp.streamer");
if (streamer == null)
{
streamer.EnableFeedbackLoop = this._enableFeedbackLoop;
}
// Create streamer and send initial message
streamer = new StreamingResponse(context);
memory.SetValue("temp.streamer", streamer);

streamer.EnableGeneratedByAILabel = true;
if (this._enableFeedbackLoop != null)
{
streamer.EnableFeedbackLoop = this._enableFeedbackLoop;
}

if (!string.IsNullOrEmpty(this._startStreamingMessage))
{
streamer.QueueInformativeUpdate(this._startStreamingMessage!);
streamer.EnableGeneratedByAILabel = true;

if (!string.IsNullOrEmpty(this._startStreamingMessage))
{
streamer.QueueInformativeUpdate(this._startStreamingMessage!);
}
}
}
});
Expand All @@ -195,6 +199,15 @@ public async Task<PromptResponse> CompletePromptAsync(
return;
}


// Ignore content without text
// - The chunk is likely for a Tool Call.
// - See the tool call note below to understand why we're ignoring them.
if (args.Chunk.delta?.GetContent<string>() == null)
{
return;
}

// Send chunk to client
string text = args.Chunk.delta?.GetContent<string>() ?? "";
IList<Citation>? citations = args.Chunk.delta?.Context?.Citations ?? null;
Expand Down Expand Up @@ -234,23 +247,32 @@ public async Task<PromptResponse> CompletePromptAsync(
cancellationToken
);

if (response.Status != PromptResponseStatus.Success)
{
return response;
}
else
// Handle streaming responses
if (streamer != null)
{
if (isStreaming)
// Tool call handling
// - We need to keep the streamer around during tool calls so we're just letting them return as normal
// messages minus the message content. The text content is being streamed to the client in chunks.
// - When the tool call completes we'll call back into ActionPlanner and end up re-attaching to the
// streamer. This will result in us continuing to stream the response to the client.
if (response.Message?.ActionCalls != null && response.Message.ActionCalls.Count > 0)
{
// Delete the message from the response to avoid sending it twice.
response.Message = null;
// Ensure content is empty for tool calls
response.Message.Content = "";
}
}
else
{
if (response.Status == PromptResponseStatus.Success)
{
// Delete message from response to avoid sending it twice
response.Message = null;
}

// End the stream
if (streamer != null)
{
await streamer.EndStream();
// End the stream and remove pointer from memory
// - We're not listening for the response received event because we can't await the completion of events.
await streamer.EndStream();
memory.DeleteValue("temp.streamer");
}
}

// Get input message/s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ public async Task<PromptResponse> CompletePromptAsync(ITurnContext turnContext,
};
AsyncCollectionResult<StreamingChatCompletionUpdate> streamCompletion = _openAIClient.GetChatClient(_deploymentName).CompleteChatStreamingAsync(chatMessages, chatCompletionOptions, cancellationToken);

var toolCallBuilder = new StreamingChatToolCallsBuilder();
await foreach (StreamingChatCompletionUpdate delta in streamCompletion)
{
if (delta.Role != null)
Expand All @@ -295,9 +296,19 @@ public async Task<PromptResponse> CompletePromptAsync(ITurnContext turnContext,
message.Content += delta.ContentUpdate[0].Text;
}

// TODO: Handle tool calls
// Handle tool calls
if (isToolsAugmentation && delta.ToolCallUpdates != null && delta.ToolCallUpdates.Count > 0)
{
foreach (var toolCallUpdate in delta.ToolCallUpdates)
{
toolCallBuilder.Append(toolCallUpdate);
}
}

ChatMessage currDeltaMessage = new(delta);
ChatMessage currDeltaMessage = new(delta)
{
ActionCalls = message.ActionCalls // Ensure ActionCalls are included
};
PromptChunk chunk = new()
{
delta = currDeltaMessage
Expand All @@ -311,7 +322,19 @@ public async Task<PromptResponse> CompletePromptAsync(ITurnContext turnContext,
_logger.LogTrace("CHUNK", delta);
}

Events!.OnChunkReceived(args);
Events!.OnChunkReceived(args);
}

// Add any tool calls to message
var toolCalls = toolCallBuilder.Build();
if (toolCalls.Count > 0)
{
message.ActionCalls = new List<ActionCall>();
foreach (var toolCall in toolCalls)
{
var actionCall = new ActionCall(toolCall);
message.ActionCalls.Add(actionCall);
}
}

promptResponse.Message = message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Buffers;
using System.Diagnostics;

namespace Microsoft.Teams.AI.AI.Models
{
public class SequenceBuilder<T>
{
private Segment _first;
private Segment _last;

public void Append(ReadOnlyMemory<T> data)
{
if (_first == null)
{
Debug.Assert(_last == null);
_first = new Segment(data);
_last = _first;
}
else
{
_last = _last!.Append(data);
}
}

public ReadOnlySequence<T> Build()
{
if (_first == null)
{
Debug.Assert(_last == null);
return ReadOnlySequence<T>.Empty;
}

if (_first == _last)
{
Debug.Assert(_first.Next == null);
return new ReadOnlySequence<T>(_first.Memory);
}

return new ReadOnlySequence<T>(_first, 0, _last!, _last!.Memory.Length);
}

private sealed class Segment : ReadOnlySequenceSegment<T>
{
public Segment(ReadOnlyMemory<T> items) : this(items, 0)
{
}

private Segment(ReadOnlyMemory<T> items, long runningIndex)
{
Debug.Assert(runningIndex >= 0);
Memory = items;
RunningIndex = runningIndex;
}

public Segment Append(ReadOnlyMemory<T> items)
{
long runningIndex;
checked { runningIndex = RunningIndex + Memory.Length; }
Segment segment = new(items, runningIndex);
Next = segment;
return segment;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using OpenAI.Chat;
using System.Buffers;

namespace Microsoft.Teams.AI.AI.Models
{
public class StreamingChatToolCallsBuilder
{
private readonly Dictionary<int, string> _indexToToolCallId = [];
private readonly Dictionary<int, string> _indexToFunctionName = [];
private readonly Dictionary<int, SequenceBuilder<byte>> _indexToFunctionArguments = [];

public void Append(StreamingChatToolCallUpdate toolCallUpdate)
{
// Keep track of which tool call ID belongs to this update index.
if (toolCallUpdate.ToolCallId != null)
{
_indexToToolCallId[toolCallUpdate.Index] = toolCallUpdate.ToolCallId;
}

// Keep track of which function name belongs to this update index.
if (toolCallUpdate.FunctionName != null)
{
_indexToFunctionName[toolCallUpdate.Index] = toolCallUpdate.FunctionName;
}

// Keep track of which function arguments belong to this update index,
// and accumulate the arguments as new updates arrive.
if (toolCallUpdate.FunctionArgumentsUpdate != null && !toolCallUpdate.FunctionArgumentsUpdate.ToMemory().IsEmpty)
{
if (!_indexToFunctionArguments.TryGetValue(toolCallUpdate.Index, out SequenceBuilder<byte> argumentsBuilder))
{
argumentsBuilder = new SequenceBuilder<byte>();
_indexToFunctionArguments[toolCallUpdate.Index] = argumentsBuilder;
}

argumentsBuilder.Append(toolCallUpdate.FunctionArgumentsUpdate);
}
}

public IReadOnlyList<ChatToolCall> Build()
{
List<ChatToolCall> toolCalls = [];

foreach (KeyValuePair<int, string> indexToToolCallIdPair in _indexToToolCallId)
{
ReadOnlySequence<byte> sequence = _indexToFunctionArguments[indexToToolCallIdPair.Key].Build();

ChatToolCall toolCall = ChatToolCall.CreateFunctionToolCall(
id: indexToToolCallIdPair.Value,
functionName: _indexToFunctionName[indexToToolCallIdPair.Key],
functionArguments: BinaryData.FromBytes(sequence.ToArray()));

toolCalls.Add(toolCall);
}

return toolCalls;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ private async Task SendActivity(Activity activity)
{
Properties = JObject.FromObject(new {
streamId = ((StreamingChannelData) activity.ChannelData).streamId,
streamType = ((StreamingChannelData) activity.ChannelData).StreamType,
streamType = ((StreamingChannelData) activity.ChannelData).StreamType.ToString(),
streamSequence = ((StreamingChannelData) activity.ChannelData).StreamSequence,

})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Nullable>enable</Nullable>
<PackageId>Microsoft.Teams.AI</PackageId>
<Product>Microsoft Teams AI SDK</Product>
<Version>1.8.0</Version>
<Version>1.8.1</Version>
<Authors>Microsoft</Authors>
<Company>Microsoft</Company>
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
Expand Down
Loading