diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/StreamingResponseTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/StreamingResponseTests.cs index d75139e13e..3cf1c340a6 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/StreamingResponseTests.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/StreamingResponseTests.cs @@ -220,7 +220,8 @@ void CaptureSend(Activity[] arg) StreamingResponse streamer = new(turnContext); List citations = new List(); citations.Add(new Citation(content: "test-content", title: "test", url: "https://example.com")); - streamer.QueueTextChunk("first", citations); + streamer.SetCitations(citations); + streamer.QueueTextChunk("first"); await streamer.WaitForQueue(); streamer.QueueTextChunk("second"); await streamer.WaitForQueue(); @@ -229,6 +230,10 @@ void CaptureSend(Activity[] arg) streamer.SensitivityLabel = new SensitivityUsageInfo() { Name= "Sensitivity"}; await streamer.EndStream(); Assert.Equal(2, streamer.UpdatesSent()); + if (streamer.Citations != null) + { + Assert.Single(streamer.Citations); + } } [Fact] diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Clients/LLMClient.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Clients/LLMClient.cs index 3b07e4a9da..9d5c065f79 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Clients/LLMClient.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/AI/Clients/LLMClient.cs @@ -199,6 +199,12 @@ public async Task CompletePromptAsync( return; } + IList? citations = args.Chunk.delta?.Context?.Citations ?? null; + + if (citations != null) + { + streamer.SetCitations(citations); + } // Ignore content without text // - The chunk is likely for a Tool Call. @@ -210,12 +216,10 @@ public async Task CompletePromptAsync( // Send chunk to client string text = args.Chunk.delta?.GetContent() ?? ""; - IList? citations = args.Chunk.delta?.Context?.Citations ?? null; - if (text.Length > 0) { - streamer.QueueTextChunk(text, citations); + streamer.QueueTextChunk(text); } }); diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/StreamingResponse.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/StreamingResponse.cs index 34d061d850..5d44f9675d 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/StreamingResponse.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/StreamingResponse.cs @@ -92,6 +92,40 @@ public Task WaitForQueue() return this._queueSync != null ? this._queueSync : Task.CompletedTask; } + /// + /// Sets the citations for the full message. + /// + /// Citations to be included in the message. + public void SetCitations(IList citations) + { + if (citations.Count > 0) + { + if (this.Citations == null) + { + this.Citations = new List(); + } + + int currPos = this.Citations.Count; + + foreach (Citation citation in citations) + { + string abs = CitationUtils.Snippet(citation.Content, 480); + + this.Citations.Add(new ClientCitation() + { + Position = $"{currPos}", + Appearance = new ClientCitationAppearance() + { + Name = citation.Title, + Abstract = abs + } + }); + currPos++; + } + + } + } + /// /// Queues an informative update to be sent to the client. /// @@ -131,38 +165,8 @@ public void QueueTextChunk(string text, IList? citations = null) Message += text; - if (citations != null && citations.Count > 0) - { - if (this.Citations == null) - { - this.Citations = new List(); - } - - int currPos = this.Citations.Count; - - foreach (Citation citation in citations) - { - string abs = CitationUtils.Snippet(citation.Content, 480); - - this.Citations.Add(new ClientCitation() - { - Position = $"{currPos}", - Appearance = new ClientCitationAppearance() - { - Name = citation.Title, - Abstract = abs - } - }); - currPos++; - } - - // If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc. - this.Message = this.Citations.Count == 0 ? this.Message : CitationUtils.FormatCitationsResponse(this.Message); - - // If there are citations, filter out the citations unused in content. - this.Citations = this.Citations.Count > 0 ? CitationUtils.GetUsedCitations(this.Message, this.Citations) : new List(); - - } + // If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc. + this.Message = CitationUtils.FormatCitationsResponse(this.Message); QueueNextChunk(); } @@ -320,6 +324,19 @@ private async Task SendActivity(Activity activity) } }; + if (this.Citations != null && this.Citations.Count > 0 && !this._ended) + { + // If there are citations, filter out the citations unused in content. + List? currCitations = CitationUtils.GetUsedCitations(this.Message, this.Citations); + AIEntity entity = new AIEntity(); + if (currCitations != null && currCitations.Count > 0) + { + entity.Citation = currCitations; + } + + activity.Entities.Add(entity); + } + // Add in Powered by AI feature flags if (this._ended) { @@ -331,10 +348,11 @@ private async Task SendActivity(Activity activity) // Add in Generated by AI if (this.EnableGeneratedByAILabel == true) { + List? currCitations = CitationUtils.GetUsedCitations(this.Message, this.Citations); AIEntity entity = new AIEntity(); if (this.Citations != null && this.Citations.Count > 0) { - entity.Citation = this.Citations; + entity.Citation = currCitations; } entity.UsageInfo = this.SensitivityLabel; diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/TypingTimer.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/TypingTimer.cs index 250e5b74dc..6dace6fac9 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/TypingTimer.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/TypingTimer.cs @@ -2,6 +2,7 @@ using Microsoft.Bot.Schema; using Microsoft.Bot.Builder; using Microsoft.Identity.Client; +using Microsoft.Teams.AI.Application; namespace Microsoft.Teams.AI { @@ -128,7 +129,7 @@ private async Task StopTimerWhenSendMessageActivityHandlerAs { foreach (Activity activity in activities) { - if (activity.Type == ActivityTypes.Message) + if (activity.Type == ActivityTypes.Message || activity.GetChannelData()?.StreamType != null) { await _lastSend; Dispose();