Preserve textFormat through TeamsStreamingWriter's intermediate and final activities - #661
Preserve textFormat through TeamsStreamingWriter's intermediate and final activities#661Copilot wants to merge 5 commits into
Conversation
…inal activities Mirrors microsoft/teams.ts PR #762 for the .NET streaming writer. - StreamingActivityInput (the typing-type chunks used for informative and intermediate streaming updates) now models textFormat and gets a WithTextFormat builder method, matching MessageActivityInput. - TeamsStreamingWriter.WithTextFormat(TextFormat) sets the format applied to every subsequent intermediate chunk, and is used as the final message's default format unless the caller sets TextFormat explicitly on the activity passed to FinalizeResponseAsync. - The tracked format resets when the writer is reused for a new streamed message after finalize, matching how other per-stream state is reset. Without this, streamed extended markdown content (task lists, strikethrough, etc.) rendered as plain markdown during intermediate typing chunks and only switched to the intended format on the final message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
After fix demo: after-fix-cs.mp4 |
There was a problem hiding this comment.
Pull request overview
This PR fixes a formatting inconsistency in TeamsStreamingWriter so that TextFormat (notably Extended Markdown) is preserved across intermediate “typing” chunks and the final message, rather than only being applied at finalize time.
Changes:
- Adds
TextFormatsupport toStreamingActivityInputplus a fluentWithTextFormat()builder API. - Adds
TeamsStreamingWriter.WithTextFormat(TextFormat)to track/apply a per-stream format to informative + streaming chunks, and as the default for the final message unless explicitly overridden. - Adds unit tests covering propagation, unset behavior, final override precedence, and state reset on writer reuse.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| test/Microsoft.Teams.Apps.UnitTests/TeamsStreamingWriterTests.cs | Adds coverage for TextFormat propagation/override/reset behaviors in streaming. |
| src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs | Tracks a per-stream TextFormat and applies it to intermediate chunks and (as a default) to the final message. |
| src/Microsoft.Teams.Apps/TeamsStreamingWriter.Activity.cs | Models textFormat on StreamingActivityInput and exposes it via the builder. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// Cleared when the stream is reused for a new streamed message after | ||
| /// <see cref="FinalizeResponseAsync"/>; call this again for each new streamed message. | ||
| /// </remarks> | ||
| public TeamsStreamingWriter WithTextFormat(TextFormat textFormat) |
There was a problem hiding this comment.
I feel like having this both in activity and writer is confusing, if both are filled, which one is honored? Let's just keep it contained to streaming activity?
Replace writer-level WithTextFormat with per-chunk overloads: - Add AppendResponseAsync(MessageActivityInput) honoring Text + TextFormat - Add SendInformativeUpdateAsync(string, TextFormat) for per-informative format - Track last-streamed format for the final message's default (explicit final wins) Also update the StreamingBot sample: make Azure OpenAI optional (canned streamed fallback when unconfigured), demonstrate the per-chunk API in the extended-markdown path, and simplify to a single 'extended markdown' command. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf9dec16-4ade-483a-b60f-88d651e345f4
|
I attest that I have verified |
| .AsIChatClient()); | ||
| // Azure OpenAI is optional. When all three settings are present the default message path streams | ||
| // live model output; otherwise it falls back to a canned streamed response so the sample still runs. | ||
| string? endpoint = builder.Configuration["AzureOpenAI:Endpoint"]; |
There was a problem hiding this comment.
updated sample to not require aoai keys
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf9dec16-4ade-483a-b60f-88d651e345f4
|
I attest that I have verified |
| _sequence++; | ||
| _logger.LogDebug("Sending informative streaming update (sequence {Sequence}).", _sequence); | ||
| SendActivityResponse? response = await TrySendChunkAsync(BuildActivity(text, StreamTypes.Informative), cancellationToken).ConfigureAwait(false); | ||
| SendActivityResponse? response = await TrySendChunkAsync(BuildActivity(text, StreamTypes.Informative, textFormat), cancellationToken).ConfigureAwait(false); |
There was a problem hiding this comment.
TrySendChunkAsync doesn't seem modified
Summary
Mirrors microsoft/teams.ts#762, applied to the .NET streaming abstraction (
TeamsStreamingWriter).The .NET streamer has a different shape than the TS
HttpStream(raw string deltas are accumulated viaAppendResponseAsync, with a structuredMessageActivityInputonly supplied atFinalizeResponseAsync), so the bug shows up slightly differently here:StreamingActivityInput(thetyping-type chunk used for informative/intermediate updates) never modeledtextFormatat all, so there was no way to make intermediate streamed chunks render as anything other than the default format — only the final message (built directly from the caller-suppliedMessageActivityInput) could carry it. In practice this meant content intended to stream as Extended Markdown (task lists, strikethrough, etc.) rendered as plain markdown while streaming and only switched format once the final message landed.Fix
TextFormatonStreamingActivityInput(the typing-type chunk used for informative/intermediate updates), with aWithTextFormat()builder mirroringMessageActivityInput.TeamsStreamingWriter.WithTextFormat(TextFormat): sets the format applied to every subsequent informative/intermediate typing chunk, and used as the final message's default format unless the caller setsTextFormatexplicitly on the activity passed toFinalizeResponseAsync(explicit final value wins).FinalizeResponseAsync, consistent with how the rest of the per-stream state (accumulated text, sequence, etc.) resets.Note
Extended Markdown is available in public developer preview, which is why the corresponding
TextFormats.ExtendedMarkdownconstant remains behind theExperimentalTeamsExtendedMarkdowndiagnostic.Testing
TeamsStreamingWriterTests.cscovering: format applied to informative + streaming chunks + final message, no format sent when unset, explicit final-activity format overriding the writer's tracked format, and reset on stream reuse.Microsoft.Teams.Apps.UnitTestssuite passes (532/532).Created from a Microsoft Teams conversation.
I attest that I have verified