From 6d989521d4cf423060355617efd0c41b9992099f Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:53:02 +0000 Subject: [PATCH 1/4] Preserve text_format through HttpStream's intermediate and final activities HttpStream sends intermediate typing chunks built from scratch on every flush. It already applies last-message-wins semantics for attachments, entities, suggested_actions, and channel_data via _final_activity, but never forwarded text_format to the rebuilt TypingActivityInput chunks sent during flush(), so stream.emit(MessageActivityInput(...).with_text_format('extendedmarkdown')) was silently dropped from intermediate updates (though it was already preserved on the final/timeout-fallback message since those reuse the _final_activity object directly). - Add text_format to _TypingBase (shared by TypingActivity and TypingActivityInput) with a with_text_format() builder, mirroring MessageActivityInput. - HttpStream._flush() now reads the last emitted message's text_format off self._final_activity and applies it to informative updates and the combined typing chunk. - Add unit tests covering text_format on typing activities and propagation across intermediate chunks, the final message, and the timeout fallback. - Add an extended-markdown streaming scenario to the stream example app, mirroring microsoft/teams.ts#762. Mirrors microsoft/teams.ts#762. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- examples/stream/README.md | 1 + examples/stream/src/main.py | 23 +++++ .../microsoft_teams/api/activities/typing.py | 12 ++- packages/api/tests/unit/test_typing.py | 13 +++ .../src/microsoft_teams/apps/http_stream.py | 9 ++ packages/apps/tests/test_http_stream.py | 84 +++++++++++++++++++ 6 files changed, 141 insertions(+), 1 deletion(-) diff --git a/examples/stream/README.md b/examples/stream/README.md index 99d0d7192..78548daa0 100644 --- a/examples/stream/README.md +++ b/examples/stream/README.md @@ -4,6 +4,7 @@ A test application that demonstrates streaming functionality. - Send any message for the normal single-stream demo with suggested actions. - Send `simple-card` to send a minimal Adaptive Card outside the streaming flow. +- Send `extended-markdown` to stream a release-status update where every delta sets `text_format="extendedmarkdown"` — the task list and strikethrough render as they stream, which plain markdown can't do. - Send `multi-stream` to test emitting an Adaptive Card as part of the first stream final message, finalizing with `close()`, and then reusing `ctx.stream` for another streamed response. ## Running diff --git a/examples/stream/src/main.py b/examples/stream/src/main.py index 16e305b4b..6221f9726 100644 --- a/examples/stream/src/main.py +++ b/examples/stream/src/main.py @@ -44,12 +44,26 @@ "[stream 2] The app processor will close this stream when the handler returns.", ] +EXTENDED_MARKDOWN_DELTAS = [ + "**On it — here's where your `v2.3.0` release stands:**\n\n", + "- [x] Run unit + integration tests\n", + "- [x] Build and publish packages\n", + "- [ ] ~~Manual smoke test~~ (skipped — covered by the integration suite)\n", + "- [x] Tag the release and push\n", + "- [ ] Publish release notes\n", +] + def should_run_multi_stream(text: str | None) -> bool: normalized = (text or "").lower().replace("-", " ") return "multi stream" in normalized +def should_run_extended_markdown(text: str | None) -> bool: + normalized = (text or "").lower().replace("-", " ") + return "extended markdown" in normalized or "extendedmarkdown" in normalized + + def should_send_simple_card(text: str | None) -> bool: normalized = (text or "").lower().replace("-", " ") return "simple card" in normalized @@ -101,6 +115,15 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): ctx.stream.emit(message) return + if should_run_extended_markdown(ctx.activity.text): + ctx.stream.update("Checking the release status...") + await asyncio.sleep(1) + + for delta in EXTENDED_MARKDOWN_DELTAS: + await asyncio.sleep(0.5) + ctx.stream.emit(MessageActivityInput(text=delta).with_text_format("extendedmarkdown")) + return + ctx.stream.update("Stream starting...") await asyncio.sleep(1) diff --git a/packages/api/src/microsoft_teams/api/activities/typing.py b/packages/api/src/microsoft_teams/api/activities/typing.py index 53b91f86b..5e4bd4a03 100644 --- a/packages/api/src/microsoft_teams/api/activities/typing.py +++ b/packages/api/src/microsoft_teams/api/activities/typing.py @@ -5,7 +5,7 @@ from typing import Literal, Optional, Self -from ..models import ActivityBase, ActivityInputBase, ChannelData, CustomBaseModel, StreamInfoEntity +from ..models import ActivityBase, ActivityInputBase, ChannelData, CustomBaseModel, StreamInfoEntity, TextFormat class _TypingBase(CustomBaseModel): @@ -18,6 +18,11 @@ class _TypingBase(CustomBaseModel): The text content of the message. """ + text_format: Optional[TextFormat] = None + """ + Format of the `text` field (ex. `'extendedmarkdown'`). Default: `'markdown'`. + """ + class TypingActivity(_TypingBase, ActivityBase): """Output model for received typing activities with required fields and read-only properties.""" @@ -31,6 +36,11 @@ def with_text(self, value: str) -> Self: self.text = value return self + def with_text_format(self, value: TextFormat) -> Self: + """Set the format of the `text` field.""" + self.text_format = value + return self + def add_text(self, text: str) -> Self: """Append text.""" if self.text is None: diff --git a/packages/api/tests/unit/test_typing.py b/packages/api/tests/unit/test_typing.py index db8b2a91c..7c9daa318 100644 --- a/packages/api/tests/unit/test_typing.py +++ b/packages/api/tests/unit/test_typing.py @@ -43,3 +43,16 @@ def test_should_build_with_text(self, user: Account, bot: Account, chat: Convers ) assert activity.type == "typing" assert activity.text == "testing123" + + def test_should_build_with_text_format(self, user: Account, bot: Account, chat: ConversationAccount) -> None: + """Test activity construction with text_format set via the builder.""" + activity = TypingActivityInput(id="1", from_=user, conversation=chat, recipient=bot).with_text_format( + "extendedmarkdown" + ) + assert activity.type == "typing" + assert activity.text_format == "extendedmarkdown" + + def test_text_format_defaults_to_none(self, user: Account, bot: Account, chat: ConversationAccount) -> None: + """Test that text_format is unset unless explicitly provided.""" + activity = TypingActivityInput(id="1", from_=user, conversation=chat, recipient=bot) + assert activity.text_format is None diff --git a/packages/apps/src/microsoft_teams/apps/http_stream.py b/packages/apps/src/microsoft_teams/apps/http_stream.py index afceb4875..907d87dd6 100644 --- a/packages/apps/src/microsoft_teams/apps/http_stream.py +++ b/packages/apps/src/microsoft_teams/apps/http_stream.py @@ -323,13 +323,22 @@ async def _flush(self) -> None: if self._timed_out: return + # Last emitted message wins for text_format (same as attachments/entities/etc.), + # applied to every cumulative typing chunk below so intermediate updates render + # with the same format as the eventual final message. + text_format = self._final_activity.text_format if self._final_activity else None + # Send informative updates immediately for typing_update in informative_updates: + if text_format: + typing_update.with_text_format(text_format) await self._send_activity(typing_update) # Send the combined text chunk if self._text: to_send = TypingActivityInput(text=self._text) + if text_format: + to_send.with_text_format(text_format) await self._send_activity(to_send) # If more queued, schedule another flush diff --git a/packages/apps/tests/test_http_stream.py b/packages/apps/tests/test_http_stream.py index 7e0e961b1..9a34e3184 100644 --- a/packages/apps/tests/test_http_stream.py +++ b/packages/apps/tests/test_http_stream.py @@ -600,6 +600,90 @@ async def mock_send(*args): assert len(result.activity_params.suggested_actions.actions) == 2 assert result.activity_params.suggested_actions.actions[0].title == "Option A" + @pytest.mark.asyncio + async def test_text_format_retained_on_intermediate_chunks_and_final_message( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + """Last emitted message's text_format applies to intermediate typing chunks and the + final message (matches the attachments/entities/suggested_actions last-wins behavior).""" + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + stream = HttpStream(mock_api_client, conversation_reference) + + # First message carries no text_format. + stream.emit(MessageActivityInput(text="hello ")) + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + # A later message sets text_format; last-message-wins semantics apply. + stream.emit(MessageActivityInput(text="world").with_text_format("extendedmarkdown")) + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + result = await stream.close() + + sent = mock_api_client.sent_activities + # First intermediate chunk (sent before text_format was ever emitted) has none. + assert sent[0].type == "typing" + assert sent[0].text_format is None + + # Second intermediate chunk, sent after the text_format-carrying message, retains it. + assert sent[1].type == "typing" + assert sent[1].text_format == "extendedmarkdown" + + # Final message also carries the last emitted text_format. + assert result is not None + assert result.activity_params.type == "message" + assert result.activity_params.text_format == "extendedmarkdown" + assert result.activity_params.text == "hello world" + + @pytest.mark.asyncio + async def test_final_send_timeout_retains_text_format( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + """The timeout fallback (sendFinal-equivalent) still carries the last emitted text_format + since it reuses the same buffered final activity.""" + create_calls = 0 + updates: list[dict] = [] + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + + async def mock_create(conversation_id, activity): + nonlocal create_calls + create_calls += 1 + if create_calls == 2: + raise HTTPStatusError( + "Forbidden", + request=Request("POST", "https://example.com"), + response=Response( + 403, + json={"error": {"message": "Content stream finished due to exceeded streaming time."}}, + ), + ) + return SentActivity(id="stream-1", activity_params=activity) + + async def mock_update(conversation_id, activity_id, activity): + updates.append({"id": activity_id, "text": activity.text, "text_format": activity.text_format}) + return SentActivity(id=activity_id, activity_params=activity) + + mock_api_client.conversations.create_activity = mock_create + mock_api_client.conversations.update_activity = mock_update + stream = HttpStream(mock_api_client, conversation_reference) + + stream.emit(MessageActivityInput(text="Final answer").with_text_format("extendedmarkdown")) + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + result = await stream.close() + + assert stream._timed_out is True + assert len(updates) == 1 + assert updates[0]["text"] == "Final answer" + assert updates[0]["text_format"] == "extendedmarkdown" + assert result is not None + @pytest.mark.asyncio async def test_close_waits_for_flush_to_complete(self, mock_api_client, conversation_reference): """close() must not send the final message while a flush is still mid-await.""" From d6e39038f4bf5cdd6c519eabdd63589c61d3f9fa Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Mon, 31 Aug 2026 13:03:32 -0700 Subject: [PATCH 2/4] fix(apps): source informative text_format from the update + add text_format to update() Port of microsoft/teams.ts#784 (follow-up to #762). - HttpStream._flush() no longer overwrites an informative update's own text_format with the last emitted message's format; informative chunks now keep the value set on the update itself. - StreamerProtocol.update / HttpStream.update gain an optional text_format arg so informative updates can carry a format without hand-building a typing activity. - examples/stream: refresh the extended-markdown scenario and use the new update(text, 'markdown') overload. - Tests for informative-update format independence and the update() overload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- examples/stream/README.md | 2 +- examples/stream/src/main.py | 19 +++-- .../src/microsoft_teams/apps/http_stream.py | 20 +++--- .../microsoft_teams/apps/plugins/streamer.py | 6 +- packages/apps/tests/test_http_stream.py | 72 +++++++++++++++++++ 5 files changed, 98 insertions(+), 21 deletions(-) diff --git a/examples/stream/README.md b/examples/stream/README.md index 78548daa0..e3ad3f070 100644 --- a/examples/stream/README.md +++ b/examples/stream/README.md @@ -4,7 +4,7 @@ A test application that demonstrates streaming functionality. - Send any message for the normal single-stream demo with suggested actions. - Send `simple-card` to send a minimal Adaptive Card outside the streaming flow. -- Send `extended-markdown` to stream a release-status update where every delta sets `text_format="extendedmarkdown"` — the task list and strikethrough render as they stream, which plain markdown can't do. +- Send `extended-markdown` to stream a message where every chunk sets `text_format="extendedmarkdown"` — task-list checkboxes and strikethrough render as they stream, which plain markdown can't do. The leading informative update uses the new `stream.update(text, "markdown")` overload. - Send `multi-stream` to test emitting an Adaptive Card as part of the first stream final message, finalizing with `close()`, and then reusing `ctx.stream` for another streamed response. ## Running diff --git a/examples/stream/src/main.py b/examples/stream/src/main.py index 6221f9726..eecf0ac54 100644 --- a/examples/stream/src/main.py +++ b/examples/stream/src/main.py @@ -44,13 +44,12 @@ "[stream 2] The app processor will close this stream when the handler returns.", ] -EXTENDED_MARKDOWN_DELTAS = [ - "**On it — here's where your `v2.3.0` release stands:**\n\n", - "- [x] Run unit + integration tests\n", - "- [x] Build and publish packages\n", - "- [ ] ~~Manual smoke test~~ (skipped — covered by the integration suite)\n", - "- [x] Tag the release and push\n", - "- [ ] Publish release notes\n", +EXTENDED_MARKDOWN_MESSAGES = [ + "**Extended markdown stream** — rendering features plain markdown can't:\n\n", + '- [x] Sent with `text_format="extendedmarkdown"`\n', + "- [x] Task list items render as real checkboxes\n", + "- [ ] ~~Under plain markdown these would be literal `[ ]` text~~\n", + "- [x] Strikethrough renders too\n", ] @@ -116,12 +115,12 @@ async def handle_message(ctx: ActivityContext[MessageActivity]): return if should_run_extended_markdown(ctx.activity.text): - ctx.stream.update("Checking the release status...") + ctx.stream.update("Starting the *extended* markdown stream...", "markdown") await asyncio.sleep(1) - for delta in EXTENDED_MARKDOWN_DELTAS: + for message in EXTENDED_MARKDOWN_MESSAGES: await asyncio.sleep(0.5) - ctx.stream.emit(MessageActivityInput(text=delta).with_text_format("extendedmarkdown")) + ctx.stream.emit(MessageActivityInput(text=message).with_text_format("extendedmarkdown")) return ctx.stream.update("Stream starting...") diff --git a/packages/apps/src/microsoft_teams/apps/http_stream.py b/packages/apps/src/microsoft_teams/apps/http_stream.py index 907d87dd6..fd0d21f0f 100644 --- a/packages/apps/src/microsoft_teams/apps/http_stream.py +++ b/packages/apps/src/microsoft_teams/apps/http_stream.py @@ -15,6 +15,7 @@ ConversationReference, MessageActivityInput, SentActivity, + TextFormat, TypingActivityInput, ) from microsoft_teams.common import EventEmitter @@ -147,14 +148,19 @@ def emit(self, activity: Union[MessageActivityInput, TypingActivityInput, str]) # Schedule a flush immediately when no timeout is set (first emit) self._pending = asyncio.create_task(self._flush()) - def update(self, text: str) -> None: + def update(self, text: str, text_format: Optional[TextFormat] = None) -> None: """ Send status updates before emitting (ex. "Thinking..."). Args: text: The status text to send. + text_format: Format of ``text`` (ex. ``'extendedmarkdown'``). Omit or pass + ``None`` to use the Teams default (``'markdown'``). """ - self.emit(TypingActivityInput().with_text(text).with_channel_data(ChannelData(stream_type="informative"))) + typing_update = TypingActivityInput().with_text(text).with_channel_data(ChannelData(stream_type="informative")) + if text_format: + typing_update.with_text_format(text_format) + self.emit(typing_update) def clear_text(self) -> None: """ @@ -323,15 +329,13 @@ async def _flush(self) -> None: if self._timed_out: return - # Last emitted message wins for text_format (same as attachments/entities/etc.), - # applied to every cumulative typing chunk below so intermediate updates render - # with the same format as the eventual final message. + # Streamed text chunks use last-emitted-message-wins for text_format (same as + # attachments/entities/etc.), so they render like the final message. text_format = self._final_activity.text_format if self._final_activity else None - # Send informative updates immediately + # Send informative updates immediately. Each carries its own text_format + # (_final_activity isn't set yet at this point), so keep the update's own value. for typing_update in informative_updates: - if text_format: - typing_update.with_text_format(text_format) await self._send_activity(typing_update) # Send the combined text chunk diff --git a/packages/apps/src/microsoft_teams/apps/plugins/streamer.py b/packages/apps/src/microsoft_teams/apps/plugins/streamer.py index b684b1215..8500381f7 100644 --- a/packages/apps/src/microsoft_teams/apps/plugins/streamer.py +++ b/packages/apps/src/microsoft_teams/apps/plugins/streamer.py @@ -6,7 +6,7 @@ import asyncio from typing import Awaitable, Callable, Literal, Optional, Protocol, Union -from microsoft_teams.api import MessageActivityInput, SentActivity, TypingActivityInput +from microsoft_teams.api import MessageActivityInput, SentActivity, TextFormat, TypingActivityInput StreamerEvent = Literal["chunk", "close"] @@ -91,12 +91,14 @@ def emit(self, activity: Union[MessageActivityInput, TypingActivityInput, str]) """ ... - def update(self, text: str) -> None: + def update(self, text: str, text_format: Optional[TextFormat] = None) -> None: """ Send status updates before emitting (ex. "Thinking..."). Args: text: The status text to send. + text_format: Format of ``text`` (ex. ``'extendedmarkdown'``). Omit or pass + ``None`` to use the Teams default (``'markdown'``). """ ... diff --git a/packages/apps/tests/test_http_stream.py b/packages/apps/tests/test_http_stream.py index 9a34e3184..892f6ad46 100644 --- a/packages/apps/tests/test_http_stream.py +++ b/packages/apps/tests/test_http_stream.py @@ -14,6 +14,7 @@ ApiClient, CardAction, CardActionType, + ChannelData, ConversationAccount, ConversationReference, MessageActivityInput, @@ -638,6 +639,77 @@ async def test_text_format_retained_on_intermediate_chunks_and_final_message( assert result.activity_params.text_format == "extendedmarkdown" assert result.activity_params.text == "hello world" + @pytest.mark.asyncio + async def test_informative_update_sources_its_own_text_format( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + """An informative update keeps its own text_format and is NOT overwritten by the + last emitted message's format (the previous behavior incorrectly applied + _final_activity.text_format to informative updates).""" + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + stream = HttpStream(mock_api_client, conversation_reference) + + # Informative update with its own text_format, plus a message carrying a DIFFERENT + # format in the same flush cycle. The informative chunk must keep its own value. + stream.emit( + TypingActivityInput() + .with_text("Checking the release status...") + .with_channel_data(ChannelData(stream_type="informative")) + .with_text_format("extendedmarkdown") + ) + stream.emit(MessageActivityInput(text="body").with_text_format("markdown")) + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + sent = mock_api_client.sent_activities + informative = [a for a in sent if getattr(a.channel_data, "stream_type", None) == "informative"] + assert len(informative) == 1 + assert informative[0].type == "typing" + assert informative[0].text == "Checking the release status..." + assert informative[0].text_format == "extendedmarkdown" + + @pytest.mark.asyncio + async def test_update_with_text_format_sends_informative_chunk_with_that_format( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + """update(text, text_format) sends an informative typing chunk carrying that format.""" + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + stream = HttpStream(mock_api_client, conversation_reference) + stream.update("Thinking...", "extendedmarkdown") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + sent = mock_api_client.sent_activities + assert sent[0].type == "typing" + assert sent[0].channel_data is not None + assert sent[0].channel_data.stream_type == "informative" + assert sent[0].text_format == "extendedmarkdown" + + @pytest.mark.asyncio + async def test_update_without_text_format_omits_it( + self, mock_api_client, conversation_reference, patch_loop_call_later + ): + """update(text) and update(text, None) omit text_format (Teams default: markdown).""" + loop = asyncio.get_running_loop() + patcher, scheduled = patch_loop_call_later(loop) + with patcher: + stream = HttpStream(mock_api_client, conversation_reference) + stream.update("no format") + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + stream.update("explicit none", None) + await asyncio.sleep(0) + await self._run_scheduled_flushes(scheduled) + + sent = mock_api_client.sent_activities + assert sent[0].type == "typing" + assert sent[0].text_format is None + assert sent[1].text_format is None + @pytest.mark.asyncio async def test_final_send_timeout_retains_text_format( self, mock_api_client, conversation_reference, patch_loop_call_later From 9388cdd5098d8f117497b7a3a210880a53808e99 Mon Sep 17 00:00:00 2001 From: Kavin <115390646+singhk97@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:21:31 -0700 Subject: [PATCH 3/4] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/api/src/microsoft_teams/api/activities/typing.py | 2 +- packages/apps/src/microsoft_teams/apps/http_stream.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/typing.py b/packages/api/src/microsoft_teams/api/activities/typing.py index 5e4bd4a03..9985a872d 100644 --- a/packages/api/src/microsoft_teams/api/activities/typing.py +++ b/packages/api/src/microsoft_teams/api/activities/typing.py @@ -20,7 +20,7 @@ class _TypingBase(CustomBaseModel): text_format: Optional[TextFormat] = None """ - Format of the `text` field (ex. `'extendedmarkdown'`). Default: `'markdown'`. + Format of the `text` field (ex. `'extendedmarkdown'`). Omit or leave as ``None`` to use the Teams default (``'markdown'``). """ diff --git a/packages/apps/src/microsoft_teams/apps/http_stream.py b/packages/apps/src/microsoft_teams/apps/http_stream.py index fd0d21f0f..cf52b3214 100644 --- a/packages/apps/src/microsoft_teams/apps/http_stream.py +++ b/packages/apps/src/microsoft_teams/apps/http_stream.py @@ -158,7 +158,7 @@ def update(self, text: str, text_format: Optional[TextFormat] = None) -> None: ``None`` to use the Teams default (``'markdown'``). """ typing_update = TypingActivityInput().with_text(text).with_channel_data(ChannelData(stream_type="informative")) - if text_format: + if text_format is not None: typing_update.with_text_format(text_format) self.emit(typing_update) @@ -333,8 +333,7 @@ async def _flush(self) -> None: # attachments/entities/etc.), so they render like the final message. text_format = self._final_activity.text_format if self._final_activity else None - # Send informative updates immediately. Each carries its own text_format - # (_final_activity isn't set yet at this point), so keep the update's own value. + # Send informative updates immediately. Each carries its own text_format, so keep the update's own value. for typing_update in informative_updates: await self._send_activity(typing_update) From 1a9d41bd8442c9ec251cef010def94c74bbbcbdb Mon Sep 17 00:00:00 2001 From: Kavin Singh Date: Mon, 31 Aug 2026 14:51:28 -0700 Subject: [PATCH 4/4] style: wrap text_format docstring to satisfy line length Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- packages/api/src/microsoft_teams/api/activities/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/api/src/microsoft_teams/api/activities/typing.py b/packages/api/src/microsoft_teams/api/activities/typing.py index 9985a872d..18290ac1b 100644 --- a/packages/api/src/microsoft_teams/api/activities/typing.py +++ b/packages/api/src/microsoft_teams/api/activities/typing.py @@ -20,7 +20,8 @@ class _TypingBase(CustomBaseModel): text_format: Optional[TextFormat] = None """ - Format of the `text` field (ex. `'extendedmarkdown'`). Omit or leave as ``None`` to use the Teams default (``'markdown'``). + Format of the `text` field (ex. `'extendedmarkdown'`). Omit or leave as ``None`` + to use the Teams default (``'markdown'``). """