From 2b7b7f8dea28909084d21a172b93b466df96984a Mon Sep 17 00:00:00 2001 From: Rajan Date: Tue, 31 Mar 2026 18:34:59 -0400 Subject: [PATCH 1/8] fix: remove unsendable activity types from ActivityParams union (#338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ActivityParams union included 10 *ActivityInput types, but the Teams service (APX) only accepts `message` and `typing` on outbound POST/PUT. The other types (messageDelete, messageUpdate, conversationUpdate, handoff, trace, commandSend, commandResult) are inbound-only event notifications — sending them produces gateway errors. Narrowing the union to MessageActivityInput, TypingActivityInput, and MessageReactionActivityInput (kept temporarily — needs its own send path via the reactions API) so the type system matches what actually works at runtime. The *ActivityInput classes themselves are preserved for model symmetry; only the union membership changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/activities/activity_params.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/activity_params.py index a566f263..66282d46 100644 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ b/packages/api/src/microsoft_teams/api/activities/activity_params.py @@ -3,38 +3,25 @@ Licensed under the MIT License. """ -# Union of all activity input types (each defined next to their respective activities) +# Union of activity input types that APX actually accepts on outbound send. +# Other *ActivityInput classes exist for model symmetry but are not sendable — +# the Teams service rejects them (messageDelete, messageUpdate, etc. +# are inbound-only event notifications, not outbound activity types). from typing import Annotated, Union from pydantic import Field -from .command import CommandResultActivityInput, CommandSendActivityInput -from .conversation import ConversationUpdateActivityInput -from .handoff import HandoffActivityInput from .message import ( MessageActivityInput, - MessageDeleteActivityInput, MessageReactionActivityInput, - MessageUpdateActivityInput, ) -from .trace import TraceActivityInput from .typing import TypingActivityInput ActivityParams = Annotated[ Union[ - # Simple activities - ConversationUpdateActivityInput, - HandoffActivityInput, - TraceActivityInput, - TypingActivityInput, - # Message activities MessageActivityInput, - MessageDeleteActivityInput, MessageReactionActivityInput, - MessageUpdateActivityInput, - # Command activities - CommandSendActivityInput, - CommandResultActivityInput, + TypingActivityInput, ], Field(discriminator="type"), ] From d84ca9bc020f50dfc92a4d01cb3840a925d2dddd Mon Sep 17 00:00:00 2001 From: Rajan Date: Tue, 31 Mar 2026 18:36:52 -0400 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20clari?= =?UTF-8?q?fy=20comment,=20drop=20APX=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite header comment to explain the union's purpose without internal terminology. Explicitly call out MessageReactionActivityInput as a temporary inclusion pending its own send path. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../microsoft_teams/api/activities/activity_params.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/activity_params.py index 66282d46..ede6d342 100644 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ b/packages/api/src/microsoft_teams/api/activities/activity_params.py @@ -3,10 +3,13 @@ Licensed under the MIT License. """ -# Union of activity input types that APX actually accepts on outbound send. -# Other *ActivityInput classes exist for model symmetry but are not sendable — -# the Teams service rejects them (messageDelete, messageUpdate, etc. -# are inbound-only event notifications, not outbound activity types). +# Union of activity input types that can be sent via the +# /v3/conversations/{id}/activities create/update endpoints. +# Other *ActivityInput classes exist for model symmetry but represent +# inbound-only event notifications (messageDelete, messageUpdate, etc.). +# NOTE: MessageReactionActivityInput is temporarily included here even +# though reactions have a dedicated /activities/{id}/reactions endpoint +# exposed via ReactionClient. from typing import Annotated, Union from pydantic import Field From 16affe2cff5118b1346bca2577ee090ce5613bab Mon Sep 17 00:00:00 2001 From: Rajan Date: Tue, 31 Mar 2026 19:10:00 -0400 Subject: [PATCH 3/8] chore: remove dead *ActivityInput classes for inbound-only types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete 7 *ActivityInput classes that were removed from the ActivityParams union in the previous commit and have zero references: - MessageDeleteActivityInput - MessageUpdateActivityInput - ConversationUpdateActivityInput - HandoffActivityInput - TraceActivityInput - CommandSendActivityInput - CommandResultActivityInput These represented inbound-only event types (server→bot) and had no valid outbound send path. The corresponding *Activity classes (used as handler type hints) are preserved. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/activities/command/__init__.py | 6 +- .../api/activities/command/command_result.py | 6 +- .../api/activities/command/command_send.py | 6 +- .../api/activities/conversation/__init__.py | 2 - .../conversation/conversation_update.py | 6 +- .../microsoft_teams/api/activities/handoff.py | 4 +- .../api/activities/message/__init__.py | 5 +- .../api/activities/message/message_delete.py | 6 +- .../api/activities/message/message_update.py | 58 +------------------ .../microsoft_teams/api/activities/trace.py | 4 +- 10 files changed, 11 insertions(+), 92 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/command/__init__.py b/packages/api/src/microsoft_teams/api/activities/command/__init__.py index 1d8eea47..2b73f0b2 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/command/__init__.py @@ -7,17 +7,15 @@ from pydantic import Field -from .command_result import CommandResultActivity, CommandResultActivityInput, CommandResultValue -from .command_send import CommandSendActivity, CommandSendActivityInput, CommandSendValue +from .command_result import CommandResultActivity, CommandResultValue +from .command_send import CommandSendActivity, CommandSendValue CommandActivity = Annotated[Union[CommandSendActivity, CommandResultActivity], Field(discriminator="type")] __all__ = [ "CommandResultValue", "CommandResultActivity", - "CommandResultActivityInput", "CommandSendValue", "CommandSendActivity", - "CommandSendActivityInput", "CommandActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_result.py b/packages/api/src/microsoft_teams/api/activities/command/command_result.py index 5d32ceeb..82760101 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_result.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_result.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandResultValue(CustomBaseModel): @@ -47,7 +47,3 @@ class CommandResultActivity(_CommandResultBase, ActivityBase): """The name of the event.""" -class CommandResultActivityInput(_CommandResultBase, ActivityInputBase): - """Input model for creating command result activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/command/command_send.py b/packages/api/src/microsoft_teams/api/activities/command/command_send.py index 0f5a17fd..f6a1f7e4 100644 --- a/packages/api/src/microsoft_teams/api/activities/command/command_send.py +++ b/packages/api/src/microsoft_teams/api/activities/command/command_send.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, CustomBaseModel +from ...models import ActivityBase, CustomBaseModel class CommandSendValue(CustomBaseModel): @@ -43,7 +43,3 @@ class CommandSendActivity(_CommandSendBase, ActivityBase): """The name of the event.""" -class CommandSendActivityInput(_CommandSendBase, ActivityInputBase): - """Input model for creating command send activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py index d6aa67be..51b90e6a 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/__init__.py @@ -7,7 +7,6 @@ ConversationChannelData, ConversationEventType, ConversationUpdateActivity, - ConversationUpdateActivityInput, ) ConversationActivity = ConversationUpdateActivity @@ -16,6 +15,5 @@ "ConversationEventType", "ConversationChannelData", "ConversationUpdateActivity", - "ConversationUpdateActivityInput", "ConversationActivity", ] diff --git a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py index c13aa338..072f02f0 100644 --- a/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py +++ b/packages/api/src/microsoft_teams/api/activities/conversation/conversation_update.py @@ -5,7 +5,7 @@ from typing import List, Literal, Optional -from ...models import Account, ActivityBase, ActivityInputBase, ChannelData, CustomBaseModel +from ...models import Account, ActivityBase, ChannelData, CustomBaseModel ConversationEventType = Literal[ "channelCreated", @@ -55,7 +55,3 @@ class ConversationUpdateActivity(_ConversationUpdateBase, ActivityBase): """Channel data with event type information.""" -class ConversationUpdateActivityInput(_ConversationUpdateBase, ActivityInputBase): - """Input model for creating conversation update activities with builder methods.""" - - pass diff --git a/packages/api/src/microsoft_teams/api/activities/handoff.py b/packages/api/src/microsoft_teams/api/activities/handoff.py index 065b538a..8a19e6bc 100644 --- a/packages/api/src/microsoft_teams/api/activities/handoff.py +++ b/packages/api/src/microsoft_teams/api/activities/handoff.py @@ -5,7 +5,7 @@ from typing import Literal -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel +from ..models import ActivityBase, CustomBaseModel class _HandoffBase(CustomBaseModel): @@ -18,5 +18,3 @@ class HandoffActivity(_HandoffBase, ActivityBase): """Output model for received handoff activities with required fields and read-only properties.""" -class HandoffActivityInput(_HandoffBase, ActivityInputBase): - """Input model for creating handoff activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/__init__.py b/packages/api/src/microsoft_teams/api/activities/message/__init__.py index a6ad3a6c..2bbfffa1 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/message/__init__.py @@ -8,12 +8,11 @@ from pydantic import Field from .message import MessageActivity, MessageActivityInput -from .message_delete import MessageDeleteActivity, MessageDeleteActivityInput, MessageDeleteChannelData +from .message_delete import MessageDeleteActivity, MessageDeleteChannelData from .message_reaction import MessageReactionActivity, MessageReactionActivityInput from .message_update import ( MessageEventType, MessageUpdateActivity, - MessageUpdateActivityInput, MessageUpdateChannelData, ) @@ -32,12 +31,10 @@ "MessageActivity", "MessageActivityInput", "MessageDeleteActivity", - "MessageDeleteActivityInput", "MessageDeleteChannelData", "MessageReactionActivity", "MessageReactionActivityInput", "MessageUpdateActivity", - "MessageUpdateActivityInput", "MessageUpdateChannelData", "MessageEventType", ] diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py index 7f7272f0..ee203ccc 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_delete.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_delete.py @@ -5,7 +5,7 @@ from typing import Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel @@ -30,7 +30,3 @@ class MessageDeleteActivity(_MessageDeleteBase, ActivityBase): channel_data: MessageDeleteChannelData # pyright: ignore [reportGeneralTypeIssues] """Channel-specific data for message delete events.""" - - -class MessageDeleteActivityInput(_MessageDeleteBase, ActivityInputBase): - """Input model for creating message delete activities with builder methods.""" diff --git a/packages/api/src/microsoft_teams/api/activities/message/message_update.py b/packages/api/src/microsoft_teams/api/activities/message/message_update.py index 78f6ab80..1bd1bb38 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message_update.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message_update.py @@ -4,9 +4,9 @@ """ from datetime import datetime -from typing import Any, Literal, Optional, Self +from typing import Any, Literal, Optional -from ...models import ActivityBase, ActivityInputBase, ChannelData +from ...models import ActivityBase, ChannelData from ...models.custom_base_model import CustomBaseModel MessageEventType = Literal["undeleteMessage", "editMessage"] @@ -56,57 +56,3 @@ class MessageUpdateActivity(_MessageUpdateBase, ActivityBase): """Channel-specific data for message update events.""" -class MessageUpdateActivityInput(_MessageUpdateBase, ActivityInputBase): - """Input model for creating message update activities with builder methods.""" - - def with_text(self, text: str) -> Self: - """ - Set the text content of the message. - - Args: - text: The text content to set - - Returns: - Self for method chaining - """ - self.text = text - return self - - def with_speak(self, speak: str) -> Self: - """ - Set the text to speak. - - Args: - speak: The text to speak - - Returns: - Self for method chaining - """ - self.speak = speak - return self - - def with_summary(self, summary: str) -> Self: - """ - Set the summary text. - - Args: - summary: The summary text to set - - Returns: - Self for method chaining - """ - self.summary = summary - return self - - def with_expiration(self, expiration: datetime) -> Self: - """ - Set the expiration time for the activity. - - Args: - expiration: The expiration datetime to set - - Returns: - Self for method chaining - """ - self.expiration = expiration - return self diff --git a/packages/api/src/microsoft_teams/api/activities/trace.py b/packages/api/src/microsoft_teams/api/activities/trace.py index ff12876a..aa144d41 100644 --- a/packages/api/src/microsoft_teams/api/activities/trace.py +++ b/packages/api/src/microsoft_teams/api/activities/trace.py @@ -5,7 +5,7 @@ from typing import Any, Literal, Optional -from ..models import ActivityBase, ActivityInputBase, CustomBaseModel +from ..models import ActivityBase, CustomBaseModel class _TraceBase(CustomBaseModel): @@ -48,5 +48,3 @@ class TraceActivity(_TraceBase, ActivityBase): """ -class TraceActivityInput(_TraceBase, ActivityInputBase): - """Input model for creating trace activities with builder methods.""" From bf62a5c2eaa7c52de801d5c6a66a02a62840754b Mon Sep 17 00:00:00 2001 From: Rajan Date: Tue, 31 Mar 2026 19:28:50 -0400 Subject: [PATCH 4/8] =?UTF-8?q?refactor:=20rename=20ActivityParams=20?= =?UTF-8?q?=E2=86=92=20SendableActivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the outbound activity union type to better communicate its purpose. ActivityParams was vague — SendableActivity makes the API self-documenting: `async def send(self, activity: SendableActivity)`. Also renames the module file activity_params.py → sendable_activity.py. 13 files changed, pure rename — no logic changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/microsoft_teams/api/activities/__init__.py | 4 ++-- .../{activity_params.py => sendable_activity.py} | 2 +- .../microsoft_teams/api/activities/sent_activity.py | 6 +++--- .../api/clients/conversation/activity.py | 12 ++++++------ .../api/clients/conversation/client.py | 12 ++++++------ .../api/clients/conversation/params.py | 4 ++-- packages/api/tests/unit/test_sent_activity.py | 8 ++++---- .../apps/src/microsoft_teams/apps/activity_sender.py | 4 ++-- packages/apps/src/microsoft_teams/apps/app.py | 4 ++-- .../apps/src/microsoft_teams/apps/app_process.py | 4 ++-- .../apps/contexts/function_context.py | 6 +++--- .../microsoft_teams/apps/routing/activity_context.py | 10 +++++----- packages/apps/tests/test_activity_context.py | 4 ++-- 13 files changed, 40 insertions(+), 40 deletions(-) rename packages/api/src/microsoft_teams/api/activities/{activity_params.py => sendable_activity.py} (96%) diff --git a/packages/api/src/microsoft_teams/api/activities/__init__.py b/packages/api/src/microsoft_teams/api/activities/__init__.py index be3f19c3..5c38aa53 100644 --- a/packages/api/src/microsoft_teams/api/activities/__init__.py +++ b/packages/api/src/microsoft_teams/api/activities/__init__.py @@ -8,7 +8,7 @@ from pydantic import Field, TypeAdapter from . import event, install_update, invoke, message -from .activity_params import ActivityParams +from .sendable_activity import SendableActivity from .command import CommandActivity, CommandResultActivity, CommandResultValue, CommandSendActivity, CommandSendValue from .conversation import ( ConversationActivity, @@ -68,7 +68,7 @@ "ConversationEventType", "InvokeActivity", "TraceActivity", - "ActivityParams", + "SendableActivity", "SentActivity", ] __all__.extend(event.__all__) diff --git a/packages/api/src/microsoft_teams/api/activities/activity_params.py b/packages/api/src/microsoft_teams/api/activities/sendable_activity.py similarity index 96% rename from packages/api/src/microsoft_teams/api/activities/activity_params.py rename to packages/api/src/microsoft_teams/api/activities/sendable_activity.py index ede6d342..167cffe5 100644 --- a/packages/api/src/microsoft_teams/api/activities/activity_params.py +++ b/packages/api/src/microsoft_teams/api/activities/sendable_activity.py @@ -20,7 +20,7 @@ ) from .typing import TypingActivityInput -ActivityParams = Annotated[ +SendableActivity = Annotated[ Union[ MessageActivityInput, MessageReactionActivityInput, diff --git a/packages/api/src/microsoft_teams/api/activities/sent_activity.py b/packages/api/src/microsoft_teams/api/activities/sent_activity.py index f28eaec6..1543e7a4 100644 --- a/packages/api/src/microsoft_teams/api/activities/sent_activity.py +++ b/packages/api/src/microsoft_teams/api/activities/sent_activity.py @@ -4,7 +4,7 @@ """ from ..models import CustomBaseModel -from . import ActivityParams +from . import SendableActivity class SentActivity(CustomBaseModel): @@ -13,10 +13,10 @@ class SentActivity(CustomBaseModel): id: str """Id of the activity.""" - activity_params: ActivityParams + activity_params: SendableActivity """Additional parameters for the activity.""" @classmethod - def merge(cls, activity_params: ActivityParams, curr_activity: "SentActivity") -> "SentActivity": + def merge(cls, activity_params: SendableActivity, curr_activity: "SentActivity") -> "SentActivity": merged_data = {**activity_params.model_dump(), **curr_activity.model_dump()} return cls(**merged_data) diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py index a1ee86ef..e6746d91 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py @@ -8,7 +8,7 @@ from microsoft_teams.common.experimental import experimental from microsoft_teams.common.http import Client -from ...activities import ActivityParams, SentActivity +from ...activities import SendableActivity, SentActivity from ...models import TeamsChannelAccount from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -38,7 +38,7 @@ def __init__( super().__init__(http_client, api_client_settings) self.service_url = service_url.rstrip("/") - async def create(self, conversation_id: str, activity: ActivityParams) -> SentActivity: + async def create(self, conversation_id: str, activity: SendableActivity) -> SentActivity: """ Create a new activity in a conversation. @@ -61,7 +61,7 @@ async def create(self, conversation_id: str, activity: ActivityParams) -> SentAc id = response.json().get("id", _PLACEHOLDER_ACTIVITY_ID) return SentActivity(id=id, activity_params=activity) - async def update(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def update(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Update an existing activity in a conversation. @@ -80,7 +80,7 @@ async def update(self, conversation_id: str, activity_id: str, activity: Activit id = response.json()["id"] return SentActivity(id=id, activity_params=activity) - async def reply(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def reply(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Reply to an activity in a conversation. @@ -128,7 +128,7 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Team return [TeamsChannelAccount.model_validate(member) for member in response.json()] @experimental("ExperimentalTeamsTargeted") - async def create_targeted(self, conversation_id: str, activity: ActivityParams) -> SentActivity: + async def create_targeted(self, conversation_id: str, activity: SendableActivity) -> SentActivity: """ Create a new targeted activity in a conversation. @@ -153,7 +153,7 @@ async def create_targeted(self, conversation_id: str, activity: ActivityParams) return SentActivity(id=id, activity_params=activity) @experimental("ExperimentalTeamsTargeted") - async def update_targeted(self, conversation_id: str, activity_id: str, activity: ActivityParams) -> SentActivity: + async def update_targeted(self, conversation_id: str, activity_id: str, activity: SendableActivity) -> SentActivity: """ Update an existing targeted activity in a conversation. diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/client.py b/packages/api/src/microsoft_teams/api/clients/conversation/client.py index 1c42a103..0422a551 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/client.py @@ -10,7 +10,7 @@ from ...models import ConversationResource from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient -from .activity import ActivityParams, ConversationActivityClient +from .activity import SendableActivity, ConversationActivityClient from .member import ConversationMemberClient from .params import CreateConversationParams @@ -26,13 +26,13 @@ def __init__(self, client: "ConversationClient", conversation_id: str) -> None: class ActivityOperations(ConversationOperations): """Operations for managing activities in a conversation.""" - async def create(self, activity: ActivityParams): + async def create(self, activity: SendableActivity): return await self._client.activities_client.create(self._conversation_id, activity) - async def update(self, activity_id: str, activity: ActivityParams): + async def update(self, activity_id: str, activity: SendableActivity): return await self._client.activities_client.update(self._conversation_id, activity_id, activity) - async def reply(self, activity_id: str, activity: ActivityParams): + async def reply(self, activity_id: str, activity: SendableActivity): return await self._client.activities_client.reply(self._conversation_id, activity_id, activity) async def delete(self, activity_id: str): @@ -41,11 +41,11 @@ async def delete(self, activity_id: str): async def get_members(self, activity_id: str): return await self._client.activities_client.get_members(self._conversation_id, activity_id) - async def create_targeted(self, activity: ActivityParams): + async def create_targeted(self, activity: SendableActivity): """Create a new targeted activity visible only to the specified recipient.""" return await self._client.activities_client.create_targeted(self._conversation_id, activity) - async def update_targeted(self, activity_id: str, activity: ActivityParams): + async def update_targeted(self, activity_id: str, activity: SendableActivity): """Update an existing targeted activity.""" return await self._client.activities_client.update_targeted(self._conversation_id, activity_id, activity) diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/params.py b/packages/api/src/microsoft_teams/api/clients/conversation/params.py index 3d35407c..5997c202 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/params.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional from ...models import Account, CustomBaseModel -from .activity import ActivityParams +from .activity import SendableActivity class CreateConversationParams(CustomBaseModel): @@ -20,7 +20,7 @@ class CreateConversationParams(CustomBaseModel): """ The tenant ID for the conversation. """ - activity: Optional[ActivityParams] = None + activity: Optional[SendableActivity] = None """ The initial activity to post in the conversation. """ diff --git a/packages/api/tests/unit/test_sent_activity.py b/packages/api/tests/unit/test_sent_activity.py index 549ed97c..b7ed7a6a 100644 --- a/packages/api/tests/unit/test_sent_activity.py +++ b/packages/api/tests/unit/test_sent_activity.py @@ -5,19 +5,19 @@ # pyright: basic import pytest -from microsoft_teams.api.activities import ActivityParams, MessageActivityInput, SentActivity +from microsoft_teams.api.activities import SendableActivity, MessageActivityInput, SentActivity @pytest.fixture -def mock_new_activity_params() -> ActivityParams: - """Create a mock ActivityParams for testing.""" +def mock_new_activity_params() -> SendableActivity: + """Create a mock SendableActivity for testing.""" return MessageActivityInput( id="updated-id", type="message", text="updated message", locale="en-US", reply_to_id="activity-3" ) @pytest.fixture -def mock_sent_activity(mock_new_activity_params: ActivityParams) -> SentActivity: +def mock_sent_activity(mock_new_activity_params: SendableActivity) -> SentActivity: """Create a mock SentActivity for testing.""" return SentActivity( id="sent-1", diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 78d02ae8..40bd8bdb 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -7,7 +7,7 @@ from typing import cast from microsoft_teams.api import ( - ActivityParams, + SendableActivity, ApiClient, ConversationReference, MessageActivityInput, @@ -36,7 +36,7 @@ def __init__(self, client: Client): """ self._client = client - async def send(self, activity: ActivityParams, ref: ConversationReference) -> SentActivity: + async def send(self, activity: SendableActivity, ref: ConversationReference) -> SentActivity: """ Send an activity to the Bot Framework. diff --git a/packages/apps/src/microsoft_teams/apps/app.py b/packages/apps/src/microsoft_teams/apps/app.py index 287ec08b..af337d52 100644 --- a/packages/apps/src/microsoft_teams/apps/app.py +++ b/packages/apps/src/microsoft_teams/apps/app.py @@ -14,7 +14,7 @@ from microsoft_teams.api import ( Account, ActivityBase, - ActivityParams, + SendableActivity, ApiClient, ClientCredentials, ConversationAccount, @@ -275,7 +275,7 @@ async def stop(self) -> None: self._events.emit("error", ErrorEvent(error, context={"method": "stop"})) raise - async def send(self, conversation_id: str, activity: str | ActivityParams | AdaptiveCard): + async def send(self, conversation_id: str, activity: str | SendableActivity | AdaptiveCard): """Send an activity proactively.""" if not self._initialized: diff --git a/packages/apps/src/microsoft_teams/apps/app_process.py b/packages/apps/src/microsoft_teams/apps/app_process.py index 9b048113..78f99ed9 100644 --- a/packages/apps/src/microsoft_teams/apps/app_process.py +++ b/packages/apps/src/microsoft_teams/apps/app_process.py @@ -8,7 +8,7 @@ from microsoft_teams.api import ( ActivityBase, - ActivityParams, + SendableActivity, ActivityTypeAdapter, ApiClient, ApiClientSettings, @@ -131,7 +131,7 @@ async def _build_context( send = activityCtx.send async def updated_send( - message: str | ActivityParams | AdaptiveCard, + message: str | SendableActivity | AdaptiveCard, conversation_ref: Optional[ConversationReference] = None, ) -> SentActivity: res = await send(message, conversation_ref) diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index 4ce90e9e..28f316b7 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -11,7 +11,7 @@ from microsoft_teams.api import ( Account, - ActivityParams, + SendableActivity, ApiClient, ConversationAccount, ConversationReference, @@ -50,7 +50,7 @@ class FunctionContext(ClientContext, Generic[T]): data: T """The function payload.""" - async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[SentActivity]: + async def send(self, activity: str | SendableActivity | AdaptiveCard) -> Optional[SentActivity]: """ Send an activity to the current conversation. @@ -81,7 +81,7 @@ async def send(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[ return await self.activity_sender.send(activity, conversation_ref) - async def _resolve_conversation_id(self, activity: str | ActivityParams | AdaptiveCard) -> Optional[str]: + async def _resolve_conversation_id(self, activity: str | SendableActivity | AdaptiveCard) -> Optional[str]: """Resolve or create a conversation ID for the current user/context. Args: diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 9f273a19..c466757f 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -11,7 +11,7 @@ from microsoft_teams.api import ( ActivityBase, - ActivityParams, + SendableActivity, ApiClient, CardAction, CardActionType, @@ -71,7 +71,7 @@ class SignInOptions: Optional[TokenPostResource], Optional[str], ], - ActivityParams, + SendableActivity, ] ] = None @@ -165,14 +165,14 @@ def app_graph(self) -> "GraphServiceClient": async def send( self, - message: str | ActivityParams | AdaptiveCard, + message: str | SendableActivity | AdaptiveCard, conversation_ref: Optional[ConversationReference] = None, ) -> SentActivity: """ Send a message to the conversation. Args: - message: The message to send, can be a string, ActivityParams, or AdaptiveCard + message: The message to send, can be a string, SendableActivity, or AdaptiveCard conversation_ref: Optional conversation reference to override the current conversation reference """ if isinstance(message, str): @@ -186,7 +186,7 @@ async def send( res = await self._activity_sender.send(activity, ref) return res - async def reply(self, input: str | ActivityParams) -> SentActivity: + async def reply(self, input: str | SendableActivity) -> SentActivity: """Send a reply to the activity.""" activity = MessageActivityInput(text=input) if isinstance(input, str) else input if isinstance(activity, MessageActivityInput): diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index 96fdfbd9..d12bc25f 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -217,8 +217,8 @@ async def test_reply_with_string(self) -> None: assert "My reply" in (sent_activity.text or "") @pytest.mark.asyncio - async def test_reply_with_activity_params(self) -> None: - """reply() with an ActivityParams instance sets reply_to_id and delegates to send.""" + async def test_reply_with_sendable_activity(self) -> None: + """reply() with a SendableActivity instance sets reply_to_id and delegates to send.""" mock_activity = MagicMock() mock_activity.type = "event" mock_activity.id = "evt-id-999" From aaefa6db9f1cd9907a2e1042349c3d0dca3fc253 Mon Sep 17 00:00:00 2001 From: Rajan Date: Tue, 31 Mar 2026 19:44:33 -0400 Subject: [PATCH 5/8] feat: add ctx.delete(activity_id) convenience method Adds delete() to ActivityContext and ActivitySender so handlers can delete activities without reaching through the raw API client: await ctx.delete(activity_id) instead of: await ctx.api.conversations.activities(conv_id).delete(activity_id) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/src/microsoft_teams/apps/activity_sender.py | 11 +++++++++++ .../microsoft_teams/apps/routing/activity_context.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 40bd8bdb..1eba81eb 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -76,6 +76,17 @@ async def send(self, activity: SendableActivity, ref: ConversationReference) -> res = await activities.create(activity) return SentActivity.merge(activity, res) + async def delete(self, activity_id: str, ref: ConversationReference) -> None: + """ + Delete an activity from a conversation. + + Args: + activity_id: The ID of the activity to delete + ref: The conversation reference + """ + api = ApiClient(service_url=ref.service_url, options=self._client) + await api.conversations.activities(ref.conversation.id).delete(activity_id) + def create_stream(self, ref: ConversationReference) -> StreamerProtocol: """ Create a new activity stream for real-time updates. diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index c466757f..a43195ea 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -186,6 +186,15 @@ async def send( res = await self._activity_sender.send(activity, ref) return res + async def delete(self, activity_id: str) -> None: + """ + Delete an activity from the conversation. + + Args: + activity_id: The ID of the activity to delete + """ + await self._activity_sender.delete(activity_id, self.conversation_ref) + async def reply(self, input: str | SendableActivity) -> SentActivity: """Send a reply to the activity.""" activity = MessageActivityInput(text=input) if isinstance(input, str) else input From 11cb1f027b98708f9ebd71a9d58e5a0247029cb4 Mon Sep 17 00:00:00 2001 From: lilydu Date: Fri, 3 Apr 2026 15:31:25 -0700 Subject: [PATCH 6/8] feat: add ActivitySender.delete() with targeted support --- .../microsoft_teams/apps/activity_sender.py | 11 +++-- packages/apps/tests/test_activity_sender.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/apps/src/microsoft_teams/apps/activity_sender.py b/packages/apps/src/microsoft_teams/apps/activity_sender.py index 1eba81eb..124fbe18 100644 --- a/packages/apps/src/microsoft_teams/apps/activity_sender.py +++ b/packages/apps/src/microsoft_teams/apps/activity_sender.py @@ -7,10 +7,10 @@ from typing import cast from microsoft_teams.api import ( - SendableActivity, ApiClient, ConversationReference, MessageActivityInput, + SendableActivity, SentActivity, ) from microsoft_teams.common import Client @@ -76,16 +76,21 @@ async def send(self, activity: SendableActivity, ref: ConversationReference) -> res = await activities.create(activity) return SentActivity.merge(activity, res) - async def delete(self, activity_id: str, ref: ConversationReference) -> None: + async def delete(self, activity_id: str, ref: ConversationReference, targeted: bool = False) -> None: """ Delete an activity from a conversation. Args: activity_id: The ID of the activity to delete ref: The conversation reference + targeted: If True, deletes a targeted (ephemeral) activity """ api = ApiClient(service_url=ref.service_url, options=self._client) - await api.conversations.activities(ref.conversation.id).delete(activity_id) + activities = api.conversations.activities(ref.conversation.id) + if targeted: + await activities.delete_targeted(activity_id) + else: + await activities.delete(activity_id) def create_stream(self, ref: ConversationReference) -> StreamerProtocol: """ diff --git a/packages/apps/tests/test_activity_sender.py b/packages/apps/tests/test_activity_sender.py index 384edbb9..ce3890f7 100644 --- a/packages/apps/tests/test_activity_sender.py +++ b/packages/apps/tests/test_activity_sender.py @@ -170,3 +170,50 @@ async def test_update_non_targeted_message_calls_update(self, sender, conversati mock_activities.update.assert_called_once_with("existing-msg-id", activity) mock_activities.update_targeted.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_calls_delete(self, sender, conversation_ref): + """Test that delete() calls the underlying delete method.""" + mock_activities = MagicMock() + mock_activities.delete = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref) + + mock_activities.delete.assert_called_once_with("msg-123") + mock_activities.delete_targeted.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_targeted_calls_delete_targeted(self, sender, conversation_ref): + """Test that delete() with targeted=True calls delete_targeted.""" + mock_activities = MagicMock() + mock_activities.delete_targeted = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref, targeted=True) + + mock_activities.delete_targeted.assert_called_once_with("msg-123") + mock_activities.delete.assert_not_called() + + @pytest.mark.asyncio + async def test_delete_uses_correct_conversation_id(self, sender, conversation_ref): + """Test that delete() uses conversation id from ref.""" + mock_activities = MagicMock() + mock_activities.delete = AsyncMock(return_value=None) + + with patch("microsoft_teams.apps.activity_sender.ApiClient") as mock_api_client: + mock_api = MagicMock() + mock_api.conversations.activities.return_value = mock_activities + mock_api_client.return_value = mock_api + + await sender.delete("msg-123", conversation_ref) + + mock_api.conversations.activities.assert_called_once_with(conversation_ref.conversation.id) From 8cae464f9945e5c313c1eb28b0364847ec36396a Mon Sep 17 00:00:00 2001 From: lilydu Date: Fri, 3 Apr 2026 15:38:37 -0700 Subject: [PATCH 7/8] feat: add ctx.delete() with proactive and targeted support --- .../apps/routing/activity_context.py | 28 +++++++- packages/apps/tests/test_activity_context.py | 72 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index a43195ea..39f4153a 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -6,12 +6,12 @@ import base64 import json import logging +import warnings from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Awaitable, Callable, Generic, Optional, TypeVar, cast from microsoft_teams.api import ( ActivityBase, - SendableActivity, ApiClient, CardAction, CardActionType, @@ -21,6 +21,7 @@ GetUserTokenParams, JsonWebToken, MessageActivityInput, + SendableActivity, SentActivity, SignOutUserParams, TokenExchangeResource, @@ -34,6 +35,7 @@ from microsoft_teams.api.models.oauth import OAuthCard from microsoft_teams.cards import AdaptiveCard from microsoft_teams.common import Storage +from microsoft_teams.common.experimental import ExperimentalWarning from microsoft_teams.common.http.client_token import Token from ..activity_sender import ActivitySender @@ -186,14 +188,34 @@ async def send( res = await self._activity_sender.send(activity, ref) return res - async def delete(self, activity_id: str) -> None: + async def delete( + self, + activity_id: str, + conversation_ref: Optional[ConversationReference] = None, + targeted: bool = False, + ) -> None: """ Delete an activity from the conversation. Args: activity_id: The ID of the activity to delete + conversation_ref: Optional conversation reference to override the current one, + enabling proactive deletion from a different conversation + targeted: If True, deletes a targeted (ephemeral) activity + + .. warning:: Preview + The ``targeted`` parameter is in preview and may change or be + removed in future versions. Diagnostic: ExperimentalTeamsTargeted """ - await self._activity_sender.delete(activity_id, self.conversation_ref) + if targeted: + warnings.warn( + "The targeted parameter of delete is in preview and may change " + "or be removed in future versions. Diagnostic: ExperimentalTeamsTargeted", + ExperimentalWarning, + stacklevel=2, + ) + ref = conversation_ref or self.conversation_ref + await self._activity_sender.delete(activity_id, ref, targeted=targeted) async def reply(self, input: str | SendableActivity) -> SentActivity: """Send a reply to the activity.""" diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index d12bc25f..a3690bf2 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -387,3 +387,75 @@ async def test_sign_out_logs_error_and_does_not_raise_on_failure(self) -> None: mock_log_error.assert_called_once() logged_message = mock_log_error.call_args[0][0] assert "Failed to sign out user" in logged_message + + +class TestActivityContextDelete: + """Tests for ActivityContext.delete().""" + + @pytest.mark.asyncio + async def test_delete_delegates_to_sender(self) -> None: + """delete() calls ActivitySender.delete with current conversation_ref.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + await ctx.delete("msg-123") + + mock_sender.delete.assert_called_once_with("msg-123", ctx.conversation_ref, targeted=False) + + @pytest.mark.asyncio + async def test_delete_with_custom_conversation_ref(self) -> None: + """delete() uses provided conversation_ref for proactive deletion.""" + from microsoft_teams.api import ConversationReference + + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + custom_ref = MagicMock(spec=ConversationReference) + await ctx.delete("msg-123", conversation_ref=custom_ref) + + mock_sender.delete.assert_called_once_with("msg-123", custom_ref, targeted=False) + + @pytest.mark.asyncio + async def test_delete_targeted_passes_flag(self) -> None: + """delete() with targeted=True passes targeted=True to sender.""" + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + await ctx.delete("msg-123", targeted=True) + + mock_sender.delete.assert_called_once_with("msg-123", ctx.conversation_ref, targeted=True) + + @pytest.mark.asyncio + async def test_delete_targeted_emits_experimental_warning(self) -> None: + """delete() with targeted=True emits ExperimentalWarning.""" + import warnings + + from microsoft_teams.common.experimental import ExperimentalWarning + + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + await ctx.delete("msg-123", targeted=True) + + experimental = [w for w in caught if issubclass(w.category, ExperimentalWarning)] + assert len(experimental) == 1 + assert "ExperimentalTeamsTargeted" in str(experimental[0].message) + + @pytest.mark.asyncio + async def test_delete_non_targeted_no_warning(self) -> None: + """delete() without targeted=True does not emit ExperimentalWarning.""" + import warnings + + from microsoft_teams.common.experimental import ExperimentalWarning + + ctx, mock_sender = _create_activity_context() + mock_sender.delete = AsyncMock(return_value=None) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + await ctx.delete("msg-123") + + experimental = [w for w in caught if issubclass(w.category, ExperimentalWarning)] + assert len(experimental) == 0 From b5d322d2b073ef615f25fbbab5cbd62a0a1b6169 Mon Sep 17 00:00:00 2001 From: lilydu Date: Fri, 3 Apr 2026 15:45:28 -0700 Subject: [PATCH 8/8] fix imports in test to top level --- packages/apps/tests/test_activity_context.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/apps/tests/test_activity_context.py b/packages/apps/tests/test_activity_context.py index a3690bf2..85a33564 100644 --- a/packages/apps/tests/test_activity_context.py +++ b/packages/apps/tests/test_activity_context.py @@ -5,12 +5,14 @@ # pyright: basic +import warnings from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from microsoft_teams.api import Account, MessageActivityInput, SentActivity +from microsoft_teams.api import Account, ConversationReference, MessageActivityInput, SentActivity from microsoft_teams.apps.routing.activity_context import ActivityContext +from microsoft_teams.common.experimental import ExperimentalWarning def _create_activity_context( @@ -405,8 +407,6 @@ async def test_delete_delegates_to_sender(self) -> None: @pytest.mark.asyncio async def test_delete_with_custom_conversation_ref(self) -> None: """delete() uses provided conversation_ref for proactive deletion.""" - from microsoft_teams.api import ConversationReference - ctx, mock_sender = _create_activity_context() mock_sender.delete = AsyncMock(return_value=None) @@ -428,10 +428,6 @@ async def test_delete_targeted_passes_flag(self) -> None: @pytest.mark.asyncio async def test_delete_targeted_emits_experimental_warning(self) -> None: """delete() with targeted=True emits ExperimentalWarning.""" - import warnings - - from microsoft_teams.common.experimental import ExperimentalWarning - ctx, mock_sender = _create_activity_context() mock_sender.delete = AsyncMock(return_value=None) @@ -446,10 +442,6 @@ async def test_delete_targeted_emits_experimental_warning(self) -> None: @pytest.mark.asyncio async def test_delete_non_targeted_no_warning(self) -> None: """delete() without targeted=True does not emit ExperimentalWarning.""" - import warnings - - from microsoft_teams.common.experimental import ExperimentalWarning - ctx, mock_sender = _create_activity_context() mock_sender.delete = AsyncMock(return_value=None)