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 16 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/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@4081bf99e2866ebe428fc0477b69eb4fcda7220a # v4.4.0
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
4 changes: 2 additions & 2 deletions .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 All @@ -55,6 +55,6 @@ jobs:
name: testresults-dotnet-${{ matrix.dotnet-version }}
path: ${{ env.SOLUTION_DIR }}TestResults
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
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@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
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@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
category: "/language:csharp"
2 changes: 1 addition & 1 deletion .github/workflows/js-build-test-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ jobs:
- name: Lint
run: yarn lint
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
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@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
category: "/language:javascript"
2 changes: 1 addition & 1 deletion .github/workflows/python-build-test-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ jobs:
run: |
python scripts/lint.py
- name: Harden Runner
uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
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@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/analyze@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
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@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
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 @@ -21,6 +21,7 @@
using Microsoft.Teams.AI.Application;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Options;

[assembly: InternalsVisibleTo("Microsoft.Teams.AI.Tests")]
#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
Expand Down Expand Up @@ -223,6 +224,7 @@ public async Task<PromptResponse> CompletePromptAsync(ITurnContext turnContext,

if (isO1Model)
{
chatCompletionOptions.MaxOutputTokenCount = completion.MaxTokens;
chatCompletionOptions.Temperature = 1;
chatCompletionOptions.TopP = 1;
chatCompletionOptions.PresencePenalty = 0;
Expand Down Expand Up @@ -282,6 +284,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 +298,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 +324,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;
}
}
}
}
Loading