diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/__init__.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/__init__.py
index 85286180d..7645a28e9 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/__init__.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/__init__.py
@@ -6,6 +6,7 @@
from .activity import Activity
from .activity_event_names import ActivityEventNames
from .activity_types import ActivityTypes
+from .adaptive_card_card import AdaptiveCardCard
from .adaptive_card_invoke_action import AdaptiveCardInvokeAction
from .adaptive_card_invoke_response import AdaptiveCardInvokeResponse
from .adaptive_card_invoke_value import AdaptiveCardInvokeValue
@@ -16,6 +17,7 @@
from .attachment_view import AttachmentView
from .audio_card import AudioCard
from .basic_card import BasicCard
+from .card import Card
from .card_action import CardAction
from .card_image import CardImage
from .channels import Channels
@@ -27,6 +29,7 @@
from .conversation_reference import ConversationReference
from .conversation_resource_response import ConversationResourceResponse
from .conversations_result import ConversationsResult
+from .content_types import ContentTypes
from .expected_replies import ExpectedReplies
from .entity import (
Entity,
@@ -96,7 +99,6 @@
from .token_exchange_resource import TokenExchangeResource
from .token_post_resource import TokenPostResource
-from .delivery_modes import DeliveryModes
from .caller_id_constants import CallerIdConstants
from .conversation_update_types import ConversationUpdateTypes
@@ -111,6 +113,7 @@
"Activity",
"ActionTypes",
"ActivityEventNames",
+ "AdaptiveCardCard",
"AdaptiveCardInvokeAction",
"AdaptiveCardInvokeResponse",
"AdaptiveCardInvokeValue",
@@ -121,6 +124,7 @@
"AttachmentView",
"AudioCard",
"BasicCard",
+ "Card",
"CardAction",
"CardImage",
"Channels",
@@ -132,6 +136,7 @@
"ConversationReference",
"ConversationResourceResponse",
"ConversationsResult",
+ "ContentTypes",
"ExpectedReplies",
"Entity",
"AIEntity",
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py
index 8e0b7080b..224c43fb5 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py
@@ -4,6 +4,7 @@
from __future__ import annotations
import logging
+import re
from copy import copy
from datetime import datetime, timezone
from typing import Optional, Any, cast, Annotated, TypeVar
@@ -30,6 +31,8 @@
Entity,
EntityTypes,
Mention,
+ ActivityTreatment,
+ ActivityTreatmentTypes,
AIEntity,
ClientCitation,
ProductInfo,
@@ -429,6 +432,377 @@ def as_typing_activity(self):
"""
return self if self.__is_activity(ActivityTypes.typing) else None
+ def with_text(self, text: str) -> Self:
+ """
+ Sets the text content of the activity.
+
+ :param text: The text content of the message.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.text = text
+ return self
+
+ def with_speak(self, speak: str) -> Self:
+ """
+ Sets the text to speak for the activity.
+
+ :param speak: The text to speak.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.speak = speak
+ return self
+
+ def with_input_hint(self, input_hint: str) -> Self:
+ """
+ Sets the input hint for the activity.
+
+ :param input_hint: Indicates whether the agent is accepting, expecting, or ignoring user input.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.input_hint = input_hint
+ return self
+
+ def with_summary(self, summary: str) -> Self:
+ """
+ Sets the summary of the activity.
+
+ :param summary: The text to display if the channel cannot render cards.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.summary = summary
+ return self
+
+ def with_locale(self, locale: str) -> Self:
+ """
+ Sets the locale of the activity.
+
+ :param locale: A locale name for the contents of the text field.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.locale = locale
+ return self
+
+ def with_text_format(self, text_format: str) -> Self:
+ """
+ Sets the text format of the activity.
+
+ :param text_format: Format of the text fields. Possible values include: 'markdown', 'plain', 'xml'.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.text_format = text_format
+ return self
+
+ def with_attachment_layout(self, attachment_layout: str) -> Self:
+ """
+ Sets the attachment layout hint for the activity.
+
+ :param attachment_layout: The layout hint for multiple attachments. Possible values include: 'list', 'carousel'.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.attachment_layout = attachment_layout
+ return self
+
+ def with_delivery_mode(self, delivery_mode: str) -> Self:
+ """
+ Sets the delivery mode of the activity.
+
+ :param delivery_mode: The delivery mode for the activity.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.delivery_mode = delivery_mode
+ return self
+
+ def with_name(self, name: str) -> Self:
+ """
+ Sets the name of the activity.
+
+ :param name: The name of the operation associated with an invoke or event activity.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.name = name
+ return self
+
+ def with_value(self, value: object, value_type: str | None = None) -> Self:
+ """
+ Sets the value of the activity, and optionally its value type.
+
+ :param value: A value that is associated with the activity.
+ :param value_type: The type of the activity's value object. Only set when provided.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.value = value
+ if value_type is not None:
+ self.value_type = value_type
+ return self
+
+ def with_suggested_actions(self, suggested_actions: SuggestedActions) -> Self:
+ """
+ Sets the suggested actions for the activity.
+
+ :param suggested_actions: The suggested actions for the activity.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.suggested_actions = suggested_actions
+ return self
+
+ def add_text(self, text: str) -> Self:
+ """
+ Appends text to the existing text content of the activity.
+
+ :param text: The text to append to the activity's text.
+ :returns: This activity, to allow for method chaining.
+ """
+ self.text = (self.text or "") + text
+ return self
+
+ def add_attachment(self, *attachments: Attachment) -> Self:
+ """
+ Adds one or more attachments to the activity.
+
+ :param attachments: The attachments to add to the activity.
+ :returns: This activity, to allow for method chaining.
+ """
+ if not attachments:
+ return self
+
+ self.attachments = self.attachments or []
+ self.attachments.extend(attachments)
+ return self
+
+ def add_entity(self, *entities: Entity) -> Self:
+ """
+ Adds one or more entities to the activity.
+
+ :param entities: The entities to add to the activity.
+ :returns: This activity, to allow for method chaining.
+ """
+ if not entities:
+ return self
+
+ self.entities = self.entities or []
+ self.entities.extend(entities)
+ return self
+
+ def add_mention(
+ self,
+ account: ChannelAccount,
+ text: NonEmptyString | None = None,
+ add_text: bool = True,
+ ) -> Self:
+ """
+ Adds a mention of the given account to the activity.
+
+ :param account: The account to mention.
+ :param text: The text of the mention. Defaults to the account's name.
+ :param add_text: Whether to prepend the mention markup to the activity's text.
+ :returns: This activity, to allow for method chaining.
+ """
+ mention_text = text if text is not None else (account.name if account else None)
+ if mention_text is None:
+ logger.warning("Adding a mention with no text or account name.")
+
+ markup = f"{mention_text}"
+
+ if add_text:
+ self.text = markup if not self.text else f"{markup} {self.text}"
+
+ return self.add_entity(Mention(mentioned=account, text=markup))
+
+ def get_account_mention(self, account_id: NonEmptyString) -> Optional[Mention]:
+ """
+ Resolves the mention for the given account id, if any.
+
+ :param account_id: The id of the account to find a mention for.
+ :returns: The matching mention; or None, if none is found.
+ """
+ if not self.entities or account_id is None:
+ return None
+
+ for mention in self.get_mentions():
+ if mention.mentioned and mention.mentioned.id == account_id:
+ return mention
+
+ return None
+
+ def is_recipient_mentioned(self) -> bool:
+ """
+ Indicates whether the recipient of this activity was mentioned.
+
+ :returns: True if the recipient was mentioned; otherwise, False.
+ """
+ return (
+ self.recipient is not None
+ and self.recipient.id is not None
+ and self.get_account_mention(self.recipient.id) is not None
+ )
+
+ def remove_recipient_mention(self) -> str:
+ """
+ Removes the recipient's mention text from the activity's text.
+
+ :returns: The updated activity text.
+
+ .. remarks::
+ This method is defined on the :class:`microsoft_agents.activity.Activity` class, but is only intended for
+ use with a message activity, where the activity Activity.Type is set to ActivityTypes.Message.
+ """
+ return self.remove_mention_text(self.recipient.id if self.recipient else None)
+
+ def remove_mention_text(self, identifier: NonEmptyString | None) -> str:
+ """
+ Removes the mention text for the given account id from the activity's text.
+
+ For example, given the message `echoAgent Hi Agent`, this removes
+ `echoAgent`, leaving `Hi Agent`.
+
+ :param identifier: The id of the account whose mention text should be removed.
+ :returns: The updated activity text.
+
+ .. remarks::
+ The format of a mention entity is dependent on the channel, but in all cases
+ the Mention.text is expected to contain the exact text for the user as it
+ appears in Activity.text.
+ """
+ if not identifier:
+ return self.text
+
+ for mention in self.get_mentions():
+ if not mention.mentioned or mention.mentioned.id != identifier:
+ continue
+
+ if mention.text is None:
+ pattern = f"{re.escape(mention.mentioned.name)}"
+ else:
+ pattern = re.escape(mention.text)
+
+ self.text = re.sub(
+ pattern, "", self.text or "", flags=re.IGNORECASE
+ ).strip()
+
+ return self.text
+
+ def is_targeted_activity(self) -> bool:
+ """
+ Indicates whether this activity is targeted.
+
+ :returns: True if this activity carries a targeted treatment; otherwise, False.
+ """
+ if not self.entities:
+ return False
+
+ for entity in self.entities:
+ if (
+ entity.type == EntityTypes.ACTIVITY_TREATMENT
+ and isinstance(entity, ActivityTreatment)
+ and entity.treatment == ActivityTreatmentTypes.TARGETED
+ ):
+ return True
+
+ return False
+
+ def make_targeted_activity(self, user: ChannelAccount | None = None) -> Self:
+ """
+ Marks this activity as targeted, setting the recipient if provided.
+
+ :param user: The account to target. Defaults to the current recipient.
+ :returns: This activity, to allow for method chaining.
+ :raises ValueError: If both the activity's recipient and the user argument are None.
+ """
+ if self.is_targeted_activity():
+ return self
+
+ if self.recipient is None and user is None:
+ raise ValueError(str(activity_errors.InvalidTargetedActivityRecipient))
+
+ self.entities = self.entities or []
+ self.entities.append(
+ ActivityTreatment(treatment=ActivityTreatmentTypes.TARGETED)
+ )
+
+ self.recipient = user if user is not None else self.recipient
+
+ return self
+
+ def is_message(self) -> bool:
+ """
+ Indicates whether this activity is a message activity.
+
+ :return: True if this activity is a message activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.message)
+
+ def is_event(self) -> bool:
+ """
+ Indicates whether this activity is an event activity.
+
+ :return: True if this activity is an event activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.event)
+
+ def is_invoke(self) -> bool:
+ """
+ Indicates whether this activity is an invoke activity.
+
+ :return: True if this activity is an invoke activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.invoke)
+
+ def is_typing(self) -> bool:
+ """
+ Indicates whether this activity is a typing activity.
+
+ :return: True if this activity is a typing activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.typing)
+
+ def is_conversation_update(self) -> bool:
+ """
+ Indicates whether this activity is a conversation update activity.
+
+ :return: True if this activity is a conversation update activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.conversation_update)
+
+ def is_end_of_conversation(self) -> bool:
+ """
+ Indicates whether this activity is an end of conversation activity.
+
+ :return: True if this activity is an end of conversation activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.end_of_conversation)
+
+ def is_handoff(self) -> bool:
+ """
+ Indicates whether this activity is a handoff activity.
+
+ :return: True if this activity is a handoff activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.handoff)
+
+ def is_trace(self) -> bool:
+ """
+ Indicates whether this activity is a trace activity.
+
+ :return: True if this activity is a trace activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.trace)
+
+ def is_command(self) -> bool:
+ """
+ Indicates whether this activity is a command activity.
+
+ :return: True if this activity is a command activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.command)
+
+ def is_command_result(self) -> bool:
+ """
+ Indicates whether this activity is a command result activity.
+
+ :return: True if this activity is a command result activity; otherwise, False.
+ """
+ return self.__is_activity(ActivityTypes.command_result)
+
@staticmethod
def create_contact_relation_update_activity():
"""
@@ -492,7 +866,9 @@ def create_message_activity():
"""
return Activity(type=ActivityTypes.message)
- def create_reply(self, text: str | None = None, locale: str | None = None):
+ def create_reply(
+ self, text: str | None = None, locale: str | None = None
+ ) -> Activity:
"""
Creates a new message activity as a response to this activity.
@@ -621,7 +997,7 @@ def create_trace_activity(
)
@staticmethod
- def create_typing_activity() -> "Activity":
+ def create_typing_activity() -> Activity:
"""
Creates an instance of the :class:`microsoft_agents.activity.Activity` class as a TypingActivity object.
@@ -713,8 +1089,8 @@ def get_mentions(self) -> list[Mention]:
:returns: The array of mentions; or an empty array, if none are found.
.. remarks::
- This method is defined on the :class:`microsoft_agents.activity.Activity` class, but is only intended for use with a message activity,
- where the activity Activity.Type is set to ActivityTypes.Message.
+ This method is defined on the :class:`microsoft_agents.activity.Activity` class, but is only intended for use with
+ a message activity, where the activity Activity.Type is set to ActivityTypes.Message.
"""
if not self.entities:
return []
@@ -728,7 +1104,8 @@ def get_reply_conversation_reference(
self, reply: ResourceResponse
) -> ConversationReference:
"""
- Create a ConversationReference based on this Activity's Conversation info and the ResourceResponse from sending an activity.
+ Create a ConversationReference based on this Activity's Conversation info and the ResourceResponse from sending an
+ activity.
:param reply: ResourceResponse returned from send_activity.
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/adaptive_card_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/adaptive_card_card.py
new file mode 100644
index 000000000..c5f9bd2b3
--- /dev/null
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/adaptive_card_card.py
@@ -0,0 +1,35 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import json
+
+from .attachment import Attachment
+from .card import Card
+from .content_types import ContentTypes
+from ._type_aliases import NonEmptyString
+
+
+class AdaptiveCardCard(Card):
+ """A card that carries a raw Adaptive Card JSON payload.
+
+ The JSON is stored verbatim in :attr:`content` and unpacked into the attachment as nested
+ JSON when the activity is serialized.
+
+ :param content: The Adaptive Card content as a JSON string.
+ :type content: str
+ """
+
+ content: NonEmptyString = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ The stored JSON string is parsed so that it is embedded as nested JSON in the attachment.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(
+ content_type=ContentTypes.adaptive_card,
+ content=json.loads(self.content),
+ )
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/animation_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/animation_card.py
index 558ceb5c2..cc2868068 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/animation_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/animation_card.py
@@ -1,14 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
from .thumbnail_url import ThumbnailUrl
+from .attachment import Attachment
+from .card import Card
+from .content_types import ContentTypes
from .media_url import MediaUrl
from .card_action import CardAction
-from .agents_model import AgentsModel
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
-class AnimationCard(AgentsModel):
+class AnimationCard(Card):
"""An animation card (Ex: gif or short video clip).
:param title: Title of this card
@@ -55,3 +60,56 @@ class AnimationCard(AgentsModel):
aspect: NonEmptyString = None
duration: NonEmptyString = None
value: object = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.animation_card, content=self)
+
+ @overload
+ def add_media(self, media: MediaUrl) -> "AnimationCard": ...
+
+ @overload
+ def add_media(
+ self, *, url: NonEmptyString, profile: NonEmptyString | None = None
+ ) -> "AnimationCard": ...
+
+ def add_media(
+ self,
+ media: MediaUrl | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ profile: NonEmptyString | None = None,
+ ) -> "AnimationCard":
+ """
+ Adds a media URL and returns this card.
+
+ :param media: The media URL to add.
+ :param url: The URL of the media, used when no media instance is provided.
+ :param profile: The profile of the media built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if media is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a MediaUrl instance or the url parameter."
+ )
+ media = pick_model(MediaUrl, url=url, profile=SkipNone(profile))
+
+ self.media = self.media or []
+ self.media.append(media)
+ return self
+
+ def add_button(self, button: CardAction) -> "AnimationCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/audio_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/audio_card.py
index dc7db79e5..4bceb534b 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/audio_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/audio_card.py
@@ -1,14 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
-from .agents_model import AgentsModel
+from typing import overload
+
+from .attachment import Attachment
+from .card import Card
+from .content_types import ContentTypes
from .thumbnail_url import ThumbnailUrl
from .media_url import MediaUrl
from .card_action import CardAction
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
-class AudioCard(AgentsModel):
+class AudioCard(Card):
"""Audio card.
:param title: Title of this card
@@ -55,3 +60,56 @@ class AudioCard(AgentsModel):
aspect: NonEmptyString = None
duration: NonEmptyString = None
value: object = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.audio_card, content=self)
+
+ @overload
+ def add_media(self, media: MediaUrl) -> "AudioCard": ...
+
+ @overload
+ def add_media(
+ self, *, url: NonEmptyString, profile: NonEmptyString | None = None
+ ) -> "AudioCard": ...
+
+ def add_media(
+ self,
+ media: MediaUrl | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ profile: NonEmptyString | None = None,
+ ) -> "AudioCard":
+ """
+ Adds a media URL and returns this card.
+
+ :param media: The media URL to add.
+ :param url: The URL of the media, used when no media instance is provided.
+ :param profile: The profile of the media built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if media is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a MediaUrl instance or the url parameter."
+ )
+ media = pick_model(MediaUrl, url=url, profile=SkipNone(profile))
+
+ self.media = self.media or []
+ self.media.append(media)
+ return self
+
+ def add_button(self, button: CardAction) -> "AudioCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/basic_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/basic_card.py
index b89cffd66..24565fa80 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/basic_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/basic_card.py
@@ -1,15 +1,29 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
+from typing_extensions import deprecated
+
+from .action_types import ActionTypes
from .agents_model import AgentsModel
from .card_image import CardImage
from .card_action import CardAction
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
+@deprecated(
+ "BasicCard is not an Activity Protocol card type (it has no content type) "
+ "and will be removed in a future release."
+)
class BasicCard(AgentsModel):
"""A basic card.
+ .. deprecated::
+ BasicCard is not an Activity Protocol card type (it has no content type)
+ and will be removed in a future release.
+
:param title: Title of the card
:type title: str
:param subtitle: Subtitle of the card
@@ -31,3 +45,93 @@ class BasicCard(AgentsModel):
images: list[CardImage] = None
buttons: list[CardAction] = None
tap: CardAction = None
+
+ @overload
+ def add_image(self, image: CardImage) -> "BasicCard": ...
+
+ @overload
+ def add_image(
+ self, *, url: NonEmptyString, alt: NonEmptyString | None = None
+ ) -> "BasicCard": ...
+
+ def add_image(
+ self,
+ image: CardImage | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ alt: NonEmptyString | None = None,
+ ) -> "BasicCard":
+ """
+ Adds an image and returns this card.
+
+ :param image: The image to add.
+ :param url: The URL of the image, used when no image instance is provided.
+ :param alt: The alternate text for the image built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if image is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a CardImage instance or the url parameter."
+ )
+ image = pick_model(CardImage, url=url, alt=SkipNone(alt))
+
+ self.images = self.images or []
+ self.images.append(image)
+ return self
+
+ @overload
+ def add_button(self, button: CardAction) -> "BasicCard": ...
+
+ @overload
+ def add_button(
+ self,
+ *,
+ title: NonEmptyString,
+ type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "BasicCard": ...
+
+ def add_button(
+ self,
+ button: CardAction | None = None,
+ *,
+ title: NonEmptyString | None = None,
+ type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "BasicCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :param title: The title of the button, used when no button instance is provided.
+ :param type: The action type of the button built from a title.
+ :param value: The value of the button built from a title. Defaults to the title.
+ :returns: This card, to allow for method chaining.
+ """
+ if button is None:
+ if title is None:
+ raise ValueError(
+ "Either provide a CardAction instance or the title parameter."
+ )
+ button = CardAction(
+ type=type, title=title, value=value if value is not None else title
+ )
+
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
+
+ def add_buttons(self, *buttons: CardAction) -> "BasicCard":
+ """
+ Adds one or more buttons and returns this card.
+
+ :param buttons: The buttons to add.
+ :returns: This card, to allow for method chaining.
+ """
+ if not buttons:
+ return self
+
+ self.buttons = self.buttons or []
+ self.buttons.extend(buttons)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/card.py
new file mode 100644
index 000000000..26c36d821
--- /dev/null
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/card.py
@@ -0,0 +1,36 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from abc import ABC, abstractmethod
+
+from .activity import Activity
+from .agents_model import AgentsModel
+from .attachment import Attachment
+from .activity_types import ActivityTypes
+
+
+class Card(AgentsModel, ABC):
+ """Base class for rich cards that can be sent to a user as an Attachment.
+
+ Each concrete card implements :meth:`to_attachment` to wrap itself in an attachment using its
+ own content type. :meth:`to_message` builds on that to produce a ready-to-send message activity.
+ """
+
+ @abstractmethod
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ raise NotImplementedError(
+ "to_attachment must be implemented by concrete Card subclasses."
+ )
+
+ def to_message(self) -> Activity:
+ """
+ Creates a new message activity that includes this card as an attachment.
+
+ :returns: An Activity representing a message activity with the card attached.
+ """
+ return Activity(type=ActivityTypes.message, attachments=[self.to_attachment()])
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/content_types.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/content_types.py
new file mode 100644
index 000000000..2599c36f4
--- /dev/null
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/content_types.py
@@ -0,0 +1,16 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+
+class ContentTypes:
+ """Well-known content types for card attachments."""
+
+ adaptive_card = "application/vnd.microsoft.card.adaptive"
+ animation_card = "application/vnd.microsoft.card.animation"
+ audio_card = "application/vnd.microsoft.card.audio"
+ hero_card = "application/vnd.microsoft.card.hero"
+ receipt_card = "application/vnd.microsoft.card.receipt"
+ oauth_card = "application/vnd.microsoft.card.oauth"
+ signin_card = "application/vnd.microsoft.card.signin"
+ thumbnail_card = "application/vnd.microsoft.card.thumbnail"
+ video_card = "application/vnd.microsoft.card.video"
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py
index 455f7bc4a..0d4b7181d 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/errors/error_resources.py
@@ -47,6 +47,12 @@ class ActivityErrorResources:
-64005,
)
+ InvalidTargetedActivityRecipient = ErrorMessage(
+ "Cannot mark activity as targeted because both the Activity.recipient "
+ "and `user` argument are None. At least one must be provided.",
+ -64006,
+ )
+
def __init__(self):
"""Initialize ActivityErrorResources."""
pass
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/hero_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/hero_card.py
index 810dc000a..eda7fe761 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/hero_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/hero_card.py
@@ -1,13 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
+from .action_types import ActionTypes
+from .attachment import Attachment
+from .card import Card
from .card_action import CardAction
from .card_image import CardImage
-from .agents_model import AgentsModel
+from .content_types import ContentTypes
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
-class HeroCard(AgentsModel):
+class HeroCard(Card):
"""A Hero card (card with a single, large image).
:param title: Title of the card
@@ -31,3 +37,101 @@ class HeroCard(AgentsModel):
images: list[CardImage] = None
buttons: list[CardAction] = None
tap: CardAction = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.hero_card, content=self)
+
+ @overload
+ def add_image(self, image: CardImage) -> "HeroCard": ...
+
+ @overload
+ def add_image(
+ self, *, url: NonEmptyString, alt: NonEmptyString | None = None
+ ) -> "HeroCard": ...
+
+ def add_image(
+ self,
+ image: CardImage | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ alt: NonEmptyString | None = None,
+ ) -> "HeroCard":
+ """
+ Adds an image and returns this card.
+
+ :param image: The image to add.
+ :param url: The URL of the image, used when no image instance is provided.
+ :param alt: The alternate text for the image built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if image is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a CardImage instance or the url parameter."
+ )
+ image = pick_model(CardImage, url=url, alt=SkipNone(alt))
+
+ self.images = self.images or []
+ self.images.append(image)
+ return self
+
+ @overload
+ def add_button(self, button: CardAction) -> "HeroCard": ...
+
+ @overload
+ def add_button(
+ self,
+ *,
+ title: NonEmptyString,
+ type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "HeroCard": ...
+
+ def add_button(
+ self,
+ button: CardAction | None = None,
+ *,
+ title: NonEmptyString | None = None,
+ type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "HeroCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :param title: The title of the button, used when no button instance is provided.
+ :param type: The action type of the button built from a title.
+ :param value: The value of the button built from a title. Defaults to the title.
+ :returns: This card, to allow for method chaining.
+ """
+ if button is None:
+ if title is None:
+ raise ValueError(
+ "Either provide a CardAction instance or the title parameter."
+ )
+ button = CardAction(
+ type=type, title=title, value=value if value is not None else title
+ )
+
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
+
+ def add_buttons(self, *buttons: CardAction) -> "HeroCard":
+ """
+ Adds one or more buttons and returns this card.
+
+ :param buttons: The buttons to add.
+ :returns: This card, to allow for method chaining.
+ """
+ if not buttons:
+ return self
+
+ self.buttons = self.buttons or []
+ self.buttons.extend(buttons)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/media_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/media_card.py
index 025c9f0ac..284982878 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/media_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/media_card.py
@@ -1,16 +1,31 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
+from typing_extensions import deprecated
+
from .thumbnail_url import ThumbnailUrl
from .media_url import MediaUrl
from .card_action import CardAction
from .agents_model import AgentsModel
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
+@deprecated(
+ "MediaCard is a structural base without an Activity Protocol content type. "
+ "Use AnimationCard, AudioCard, or VideoCard instead. "
+ "Will be removed in a future release."
+)
class MediaCard(AgentsModel):
"""Media card.
+ .. deprecated::
+ MediaCard is a structural base without an Activity Protocol content type.
+ Use AnimationCard, AudioCard, or VideoCard instead. Will be removed in a
+ future release.
+
:param title: Title of this card
:type title: str
:param subtitle: Subtitle of this card
@@ -55,3 +70,48 @@ class MediaCard(AgentsModel):
aspect: NonEmptyString = None
duration: NonEmptyString = None
value: object = None
+
+ @overload
+ def add_media(self, media: MediaUrl) -> "MediaCard": ...
+
+ @overload
+ def add_media(
+ self, *, url: NonEmptyString, profile: NonEmptyString | None = None
+ ) -> "MediaCard": ...
+
+ def add_media(
+ self,
+ media: MediaUrl | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ profile: NonEmptyString | None = None,
+ ) -> "MediaCard":
+ """
+ Adds a media URL and returns this card.
+
+ :param media: The media URL to add.
+ :param url: The URL of the media, used when no media instance is provided.
+ :param profile: The profile of the media built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if media is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a MediaUrl instance or the url parameter."
+ )
+ media = pick_model(MediaUrl, url=url, profile=SkipNone(profile))
+
+ self.media = self.media or []
+ self.media.append(media)
+ return self
+
+ def add_button(self, button: CardAction) -> "MediaCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/oauth_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/oauth_card.py
index a59ecf843..7930ca971 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/oauth_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/oauth_card.py
@@ -2,14 +2,16 @@
# Licensed under the MIT License.
from typing import Optional
+from .attachment import Attachment
+from .card import Card
from .card_action import CardAction
-from .agents_model import AgentsModel
+from .content_types import ContentTypes
from .token_exchange_resource import TokenExchangeResource
from .token_post_resource import TokenPostResource
from ._type_aliases import NonEmptyString
-class OAuthCard(AgentsModel):
+class OAuthCard(Card):
"""A card representing a request to perform a sign in via OAuth.
:param text: Text for signin request
@@ -25,3 +27,11 @@ class OAuthCard(AgentsModel):
buttons: list[CardAction] = None
token_exchange_resource: Optional[TokenExchangeResource] = None
token_post_resource: TokenPostResource = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.oauth_card, content=self)
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/receipt_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/receipt_card.py
index 6a80cd42f..3af2bd8f1 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/receipt_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/receipt_card.py
@@ -1,14 +1,16 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from .attachment import Attachment
+from .card import Card
+from .content_types import ContentTypes
from .fact import Fact
from .receipt_item import ReceiptItem
from .card_action import CardAction
-from .agents_model import AgentsModel
from ._type_aliases import NonEmptyString
-class ReceiptCard(AgentsModel):
+class ReceiptCard(Card):
"""A receipt card.
:param title: Title of the card
@@ -37,3 +39,44 @@ class ReceiptCard(AgentsModel):
tax: NonEmptyString = None
vat: NonEmptyString = None
buttons: list[CardAction] = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.receipt_card, content=self)
+
+ def add_fact(self, fact: Fact) -> "ReceiptCard":
+ """
+ Adds a fact and returns this card.
+
+ :param fact: The fact to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.facts = self.facts or []
+ self.facts.append(fact)
+ return self
+
+ def add_item(self, item: ReceiptItem) -> "ReceiptCard":
+ """
+ Adds a receipt item and returns this card.
+
+ :param item: The receipt item to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.items = self.items or []
+ self.items.append(item)
+ return self
+
+ def add_button(self, button: CardAction) -> "ReceiptCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/signin_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/signin_card.py
index 31242699b..3f46eb496 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/signin_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/signin_card.py
@@ -1,12 +1,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from .attachment import Attachment
+from .card import Card
from .card_action import CardAction
-from .agents_model import AgentsModel
+from .content_types import ContentTypes
from ._type_aliases import NonEmptyString
-class SigninCard(AgentsModel):
+class SigninCard(Card):
"""A card representing a request to sign in.
:param text: Text for signin request
@@ -17,3 +19,11 @@ class SigninCard(AgentsModel):
text: str = None
buttons: list[CardAction] = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.signin_card, content=self)
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/suggested_actions.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/suggested_actions.py
index f82ca3ec4..ec4dacc8d 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/suggested_actions.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/suggested_actions.py
@@ -20,4 +20,43 @@ class SuggestedActions(AgentsModel):
"""
to: list[NonEmptyString] = Field(default_factory=list)
- actions: list[CardAction]
+ actions: list[CardAction] = None
+
+ def add_action(self, action: CardAction) -> "SuggestedActions":
+ """
+ Adds a single action to the actions and returns this instance.
+
+ :param action: The action to add.
+ :returns: This instance, to allow for method chaining.
+ """
+ self.actions = self.actions or []
+ self.actions.append(action)
+ return self
+
+ def add_actions(self, *actions: CardAction) -> "SuggestedActions":
+ """
+ Adds one or more actions to the actions and returns this instance.
+
+ :param actions: The actions to add.
+ :returns: This instance, to allow for method chaining.
+ """
+ if not actions:
+ return self
+
+ self.actions = self.actions or []
+ self.actions.extend(actions)
+ return self
+
+ def add_recipients(self, *recipients: NonEmptyString) -> "SuggestedActions":
+ """
+ Adds one or more recipient ids to the recipients and returns this instance.
+
+ :param recipients: The recipient ids to add.
+ :returns: This instance, to allow for method chaining.
+ """
+ if not recipients:
+ return self
+
+ self.to = self.to or []
+ self.to.extend(recipients)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/thumbnail_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/thumbnail_card.py
index 028446f91..92a994d8a 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/thumbnail_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/thumbnail_card.py
@@ -1,13 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
+from .action_types import ActionTypes
+from .attachment import Attachment
+from .card import Card
from .card_image import CardImage
from .card_action import CardAction
-from .agents_model import AgentsModel
+from .content_types import ContentTypes
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
-class ThumbnailCard(AgentsModel):
+class ThumbnailCard(Card):
"""A thumbnail card (card with a single, small thumbnail image).
:param title: Title of the card
@@ -31,3 +37,101 @@ class ThumbnailCard(AgentsModel):
images: list[CardImage] = None
buttons: list[CardAction] = None
tap: CardAction = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.thumbnail_card, content=self)
+
+ @overload
+ def add_image(self, image: CardImage) -> "ThumbnailCard": ...
+
+ @overload
+ def add_image(
+ self, *, url: NonEmptyString, alt: NonEmptyString | None = None
+ ) -> "ThumbnailCard": ...
+
+ def add_image(
+ self,
+ image: CardImage | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ alt: NonEmptyString | None = None,
+ ) -> "ThumbnailCard":
+ """
+ Adds an image and returns this card.
+
+ :param image: The image to add.
+ :param url: The URL of the image, used when no image instance is provided.
+ :param alt: The alternate text for the image built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if image is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a CardImage instance or the url parameter."
+ )
+ image = pick_model(CardImage, url=url, alt=SkipNone(alt))
+
+ self.images = self.images or []
+ self.images.append(image)
+ return self
+
+ @overload
+ def add_button(self, button: CardAction) -> "ThumbnailCard": ...
+
+ @overload
+ def add_button(
+ self,
+ *,
+ title: NonEmptyString,
+ card_type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "ThumbnailCard": ...
+
+ def add_button(
+ self,
+ button: CardAction | None = None,
+ *,
+ title: NonEmptyString | None = None,
+ card_type: NonEmptyString = ActionTypes.im_back,
+ value: object | None = None,
+ ) -> "ThumbnailCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :param title: The title of the button, used when no button instance is provided.
+ :param card_type: The action type of the button built from a title.
+ :param value: The value of the button built from a title. Defaults to the title.
+ :returns: This card, to allow for method chaining.
+ """
+ if button is None:
+ if title is None:
+ raise ValueError(
+ "Either provide a CardAction instance or the title parameter."
+ )
+ button = CardAction(
+ type=card_type, title=title, value=value if value is not None else title
+ )
+
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
+
+ def add_buttons(self, *buttons: CardAction) -> "ThumbnailCard":
+ """
+ Adds one or more buttons and returns this card.
+
+ :param buttons: The buttons to add.
+ :returns: This card, to allow for method chaining.
+ """
+ if not buttons:
+ return self
+
+ self.buttons = self.buttons or []
+ self.buttons.extend(buttons)
+ return self
diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/video_card.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/video_card.py
index 5a1fd3923..a5f73316c 100644
--- a/libraries/microsoft-agents-activity/microsoft_agents/activity/video_card.py
+++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/video_card.py
@@ -1,14 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+from typing import overload
+
+from .attachment import Attachment
+from .card import Card
+from .content_types import ContentTypes
from .thumbnail_url import ThumbnailUrl
from .media_url import MediaUrl
from .card_action import CardAction
-from .agents_model import AgentsModel
+from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
-class VideoCard(AgentsModel):
+class VideoCard(Card):
"""Video card.
:param title: Title of this card
@@ -55,3 +60,56 @@ class VideoCard(AgentsModel):
aspect: NonEmptyString = None
duration: NonEmptyString = None
value: object = None
+
+ def to_attachment(self) -> Attachment:
+ """
+ Creates a new Attachment that wraps this card.
+
+ :returns: The generated attachment.
+ """
+ return Attachment(content_type=ContentTypes.video_card, content=self)
+
+ @overload
+ def add_media(self, media: MediaUrl) -> "VideoCard": ...
+
+ @overload
+ def add_media(
+ self, *, url: NonEmptyString, profile: NonEmptyString | None = None
+ ) -> "VideoCard": ...
+
+ def add_media(
+ self,
+ media: MediaUrl | None = None,
+ *,
+ url: NonEmptyString | None = None,
+ profile: NonEmptyString | None = None,
+ ) -> "VideoCard":
+ """
+ Adds a media URL and returns this card.
+
+ :param media: The media URL to add.
+ :param url: The URL of the media, used when no media instance is provided.
+ :param profile: The profile of the media built from a URL.
+ :returns: This card, to allow for method chaining.
+ """
+ if media is None:
+ if url is None:
+ raise ValueError(
+ "Either provide a MediaUrl instance or the url parameter."
+ )
+ media = pick_model(MediaUrl, url=url, profile=SkipNone(profile))
+
+ self.media = self.media or []
+ self.media.append(media)
+ return self
+
+ def add_button(self, button: CardAction) -> "VideoCard":
+ """
+ Adds a button and returns this card.
+
+ :param button: The button to add.
+ :returns: This card, to allow for method chaining.
+ """
+ self.buttons = self.buttons or []
+ self.buttons.append(button)
+ return self
diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/card_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/card_factory.py
index 5ec369940..86434d315 100644
--- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/card_factory.py
+++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/card_factory.py
@@ -5,6 +5,7 @@
AnimationCard,
Attachment,
AudioCard,
+ ContentTypes,
HeroCard,
OAuthCard,
ReceiptCard,
@@ -14,21 +15,7 @@
)
-class ContentTypes:
- adaptive_card = "application/vnd.microsoft.card.adaptive"
- animation_card = "application/vnd.microsoft.card.animation"
- audio_card = "application/vnd.microsoft.card.audio"
- hero_card = "application/vnd.microsoft.card.hero"
- receipt_card = "application/vnd.microsoft.card.receipt"
- oauth_card = "application/vnd.microsoft.card.oauth"
- signin_card = "application/vnd.microsoft.card.signin"
- thumbnail_card = "application/vnd.microsoft.card.thumbnail"
- video_card = "application/vnd.microsoft.card.video"
-
-
class CardFactory:
- content_types = ContentTypes
-
@staticmethod
def adaptive_card(card: dict) -> Attachment:
"""
@@ -44,9 +31,7 @@ def adaptive_card(card: dict) -> Attachment:
"attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.adaptive_card, content=card
- )
+ return Attachment(content_type=ContentTypes.adaptive_card, content=card)
@staticmethod
def animation_card(card: AnimationCard) -> Attachment:
@@ -62,9 +47,7 @@ def animation_card(card: AnimationCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.animation_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def audio_card(card: AudioCard) -> Attachment:
@@ -79,9 +62,7 @@ def audio_card(card: AudioCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.audio_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def hero_card(card: HeroCard) -> Attachment:
@@ -98,9 +79,7 @@ def hero_card(card: HeroCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.hero_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def oauth_card(card: OAuthCard) -> Attachment:
@@ -116,9 +95,7 @@ def oauth_card(card: OAuthCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.oauth_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def receipt_card(card: ReceiptCard) -> Attachment:
@@ -133,9 +110,7 @@ def receipt_card(card: ReceiptCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.receipt_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def signin_card(card: SigninCard) -> Attachment:
@@ -151,9 +126,7 @@ def signin_card(card: SigninCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.signin_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def thumbnail_card(card: ThumbnailCard) -> Attachment:
@@ -171,9 +144,7 @@ def thumbnail_card(card: ThumbnailCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.thumbnail_card, content=card
- )
+ return card.to_attachment()
@staticmethod
def video_card(card: VideoCard) -> Attachment:
@@ -188,6 +159,7 @@ def video_card(card: VideoCard) -> Attachment:
"unable to prepare attachment."
)
- return Attachment(
- content_type=CardFactory.content_types.video_card, content=card
- )
+ return card.to_attachment()
+
+ # Deprecated alias; use microsoft_agents.activity.ContentTypes instead.
+ content_types: type[ContentTypes] = ContentTypes
diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py
index 74ebac636..f0d596aa3 100644
--- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py
+++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/turn_context.py
@@ -3,7 +3,6 @@
from __future__ import annotations
-import re
from typing import Optional, Awaitable, TypeVar, Protocol
from copy import deepcopy
@@ -20,7 +19,6 @@
TurnContextProtocol,
)
from microsoft_agents.activity._model_utils import pick_model, SkipNone
-from microsoft_agents.activity.entity.entity_types import EntityTypes
from microsoft_agents.hosting.core.authorization.claims_identity import ClaimsIdentity
import microsoft_agents.hosting.core.telemetry.turn_context.spans as spans
@@ -410,33 +408,31 @@ def get_reply_conversation_reference(
@staticmethod
def remove_recipient_mention(activity: Activity) -> str:
- return TurnContext.remove_mention_text(activity, activity.recipient.id)
+ """
+ Removes the recipient's mention text from the activity's text.
+
+ :param activity: The activity to remove the recipient mention from.
+ :return: The updated activity text.
+ """
+ return activity.remove_recipient_mention()
@staticmethod
def remove_mention_text(activity: Activity, identifier: str) -> str:
"""
- Remove a mention matching the given account identifier from activity.text.
+ Removes the mention text for the given account id from the activity's text.
+
+ :param activity: The activity to remove the mention text from.
+ :param identifier: The id of the account whose mention text should be removed.
+ :return: The updated activity text.
"""
- mentions = TurnContext.get_mentions(activity)
- for mention in mentions:
- if mention.mentioned and mention.mentioned.id == identifier:
- mention_name_match = re.match(
- r"(.*?)<\/at>",
- re.escape(mention.text or ""),
- re.IGNORECASE,
- )
- if mention_name_match:
- activity.text = re.sub(
- mention_name_match.groups()[1], "", activity.text
- )
- activity.text = re.sub(r"<\/at>", "", activity.text)
- return activity.text
+ return activity.remove_mention_text(identifier)
@staticmethod
def get_mentions(activity: Activity) -> list[Mention]:
- """Get all mentions from the activity.
+ """
+ Returns all the mentions in the activity.
- :param activity: The activity to get mentions from.
- :return: A list of Mention objects.
+ :param activity: the activity to get mentions from
+ :return: A list of Mention objects representing all mentions in the activity.
"""
return activity.get_mentions()
diff --git a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py
index 8fd172844..283b0b43a 100644
--- a/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py
+++ b/libraries/microsoft-agents-hosting-dialogs/microsoft_agents/hosting/dialogs/prompts/oauth_prompt.py
@@ -12,6 +12,7 @@
ActivityTypes,
ActionTypes,
CardAction,
+ ContentTypes,
InputHints,
SigninCard,
SignInConstants,
@@ -37,7 +38,6 @@
_FlowStateTag,
_FlowResponse,
)
-from opentelemetry import context
from ..dialog import Dialog
from ..dialog_context import DialogContext
@@ -284,7 +284,7 @@ async def _send_oauth_card(
if OAuthPrompt._channel_suppports_oauth_card(context.activity.channel_id or ""):
if not any(
- att.content_type == CardFactory.content_types.oauth_card
+ att.content_type == ContentTypes.oauth_card
for att in prompt.attachments
):
card_action_type = ActionTypes.signin
@@ -343,7 +343,7 @@ async def _send_oauth_card(
)
else:
if not any(
- att.content_type == CardFactory.content_types.signin_card
+ att.content_type == ContentTypes.signin_card
for att in prompt.attachments
):
if not hasattr(context.adapter, "get_oauth_sign_in_link"):
diff --git a/tests/activity/test_activity_builders.py b/tests/activity/test_activity_builders.py
new file mode 100644
index 000000000..11a5cdbd2
--- /dev/null
+++ b/tests/activity/test_activity_builders.py
@@ -0,0 +1,226 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from microsoft_agents.activity.card_action import CardAction
+import pytest
+
+from microsoft_agents.activity import (
+ Activity,
+ AttachmentLayoutTypes,
+ Attachment,
+ ChannelAccount,
+ DeliveryModes,
+ Entity,
+ InputHints,
+ Mention,
+ SuggestedActions,
+ TextFormatTypes,
+)
+
+
+class TestActivityFluentBuilders:
+ def test_fluent_setters_chain_and_set_properties(self):
+ activity = (
+ Activity.create_message_activity()
+ .with_text("hello")
+ .with_speak("hello there")
+ .with_input_hint(InputHints.expecting_input)
+ .with_summary("a summary")
+ .with_locale("en-US")
+ .with_text_format(TextFormatTypes.markdown)
+ .with_attachment_layout(AttachmentLayoutTypes.carousel)
+ .with_delivery_mode(DeliveryModes.normal)
+ .with_name("theName")
+ .with_value("theValue", "theValueType")
+ )
+
+ assert activity.text == "hello"
+ assert activity.speak == "hello there"
+ assert activity.input_hint == InputHints.expecting_input
+ assert activity.summary == "a summary"
+ assert activity.locale == "en-US"
+ assert activity.text_format == TextFormatTypes.markdown
+ assert activity.attachment_layout == AttachmentLayoutTypes.carousel
+ assert activity.delivery_mode == DeliveryModes.normal
+ assert activity.name == "theName"
+ assert activity.value == "theValue"
+ assert activity.value_type == "theValueType"
+
+ def test_with_value_without_value_type(self):
+ activity = Activity.create_message_activity().with_value("theValue")
+
+ assert activity.value == "theValue"
+ assert activity.value_type is None
+
+ def test_with_suggested_actions(self):
+ actions = SuggestedActions(
+ to=["u1"],
+ actions=[CardAction(type="imBack", title="Click Me", value="clicked")],
+ )
+ activity = Activity.create_message_activity().with_suggested_actions(actions)
+
+ assert activity.suggested_actions is actions
+
+ def test_add_text_appends(self):
+ activity = Activity.create_message_activity().with_text("foo")
+ activity.add_text("bar")
+
+ assert activity.text == "foobar"
+
+ def test_add_text_appends_when_text_is_none(self):
+ activity = Activity.create_message_activity()
+ activity.add_text("bar")
+
+ assert activity.text == "bar"
+
+ def test_add_attachment_adds_attachments(self):
+ activity = Activity.create_message_activity().add_attachment(
+ Attachment(content_type="a"), Attachment(content_type="b")
+ )
+
+ assert len(activity.attachments) == 2
+ assert activity.attachments[0].content_type == "a"
+ assert activity.attachments[1].content_type == "b"
+
+ def test_add_entity_adds_entities(self):
+ activity = Activity.create_message_activity().add_entity(Entity(type="myType"))
+
+ assert len(activity.entities) == 1
+ assert activity.entities[0].type == "myType"
+
+
+class TestSuggestedActionsBuilders:
+ def test_suggested_actions_fluent_adders(self):
+ suggested = (
+ SuggestedActions()
+ .add_recipients("r1", "r2")
+ .add_action(CardAction(type="imBack", title="a"))
+ .add_actions(
+ CardAction(type="imBack", title="b"),
+ CardAction(type="imBack", title="c"),
+ )
+ )
+
+ assert suggested.to == ["r1", "r2"]
+ assert len(suggested.actions) == 3
+ assert suggested.actions[0].title == "a"
+ assert suggested.actions[2].title == "c"
+
+
+class TestActivityMentions:
+ def test_add_mention_adds_entity_and_text(self):
+ account = ChannelAccount(id="u1", name="User One")
+ activity = (
+ Activity.create_message_activity().with_text("hi").add_mention(account)
+ )
+
+ assert activity.text == "User One hi"
+ mention = activity.entities[0]
+ assert isinstance(mention, Mention)
+ assert mention.mentioned.id == "u1"
+ assert mention.text == "User One"
+
+ def test_add_mention_can_skip_text(self):
+ account = ChannelAccount(id="u1", name="User One")
+ activity = (
+ Activity.create_message_activity()
+ .with_text("hi")
+ .add_mention(account, text="Custom", add_text=False)
+ )
+
+ assert activity.text == "hi"
+ mention = activity.entities[0]
+ assert isinstance(mention, Mention)
+ assert mention.text == "Custom"
+
+ def test_get_account_mention_returns_match(self):
+ account = ChannelAccount(id="u1", name="User One")
+ activity = Activity.create_message_activity().add_mention(account)
+
+ mention = activity.get_account_mention("u1")
+ assert mention is not None
+ assert mention.mentioned.id == "u1"
+
+ assert activity.get_account_mention("other") is None
+ assert activity.get_account_mention(None) is None
+
+ def test_is_recipient_mentioned(self):
+ recipient = ChannelAccount(id="bot", name="Bot")
+ activity = Activity.create_message_activity()
+ activity.recipient = recipient
+
+ assert activity.is_recipient_mentioned() is False
+
+ activity.add_mention(recipient)
+ assert activity.is_recipient_mentioned() is True
+
+ def test_remove_recipient_mention(self):
+ recipient = ChannelAccount(id="bot", name="Bot")
+ activity = Activity.create_message_activity()
+ activity.recipient = recipient
+ activity.with_text("Hi Agent").add_mention(recipient)
+
+ assert activity.text == "Bot Hi Agent"
+
+ result = activity.remove_recipient_mention()
+ assert result == "Hi Agent"
+ assert activity.text == "Hi Agent"
+
+
+class TestActivityTargeting:
+ def test_make_targeted_activity_sets_treatment(self):
+ recipient = ChannelAccount(id="bot", name="Bot")
+ activity = Activity.create_message_activity()
+ activity.recipient = recipient
+
+ assert activity.is_targeted_activity() is False
+
+ activity.make_targeted_activity()
+ assert activity.is_targeted_activity() is True
+
+ def test_make_targeted_activity_uses_user_argument(self):
+ user = ChannelAccount(id="u1", name="User One")
+ activity = Activity.create_message_activity().make_targeted_activity(user)
+
+ assert activity.is_targeted_activity() is True
+ assert activity.recipient.id == "u1"
+
+ def test_make_targeted_activity_is_idempotent(self):
+ recipient = ChannelAccount(id="bot", name="Bot")
+ activity = Activity.create_message_activity()
+ activity.recipient = recipient
+ activity.make_targeted_activity()
+ activity.make_targeted_activity()
+
+ treatments = [e for e in activity.entities if e.type == "activityTreatment"]
+ assert len(treatments) == 1
+
+ def test_make_targeted_activity_raises_without_recipient_or_user(self):
+ activity = Activity.create_message_activity()
+
+ with pytest.raises(ValueError):
+ activity.make_targeted_activity()
+
+
+class TestActivityTypePredicates:
+ def test_is_type_helpers(self):
+ assert Activity.create_message_activity().is_message() is True
+ assert Activity.create_typing_activity().is_typing() is True
+ assert Activity.create_event_activity().is_event() is True
+ assert Activity.create_invoke_activity().is_invoke() is True
+ assert (
+ Activity.create_conversation_update_activity().is_conversation_update()
+ is True
+ )
+ assert (
+ Activity.create_end_of_conversation_activity().is_end_of_conversation()
+ is True
+ )
+ assert Activity.create_handoff_activity().is_handoff() is True
+ assert Activity.create_message_activity().is_invoke() is False
+
+ def test_is_trace_command_predicates(self):
+ assert Activity(type="trace").is_trace() is True
+ assert Activity(type="command").is_command() is True
+ assert Activity(type="commandResult").is_command_result() is True
+ assert Activity(type="message").is_command() is False
diff --git a/tests/activity/test_card_builders.py b/tests/activity/test_card_builders.py
new file mode 100644
index 000000000..69b93522a
--- /dev/null
+++ b/tests/activity/test_card_builders.py
@@ -0,0 +1,142 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import pytest
+from microsoft_agents.activity import (
+ ActionTypes,
+ AdaptiveCardCard,
+ AnimationCard,
+ AudioCard,
+ BasicCard,
+ CardAction,
+ CardImage,
+ ContentTypes,
+ Fact,
+ HeroCard,
+ MediaCard,
+ MediaUrl,
+ ReceiptCard,
+ ReceiptItem,
+ ThumbnailCard,
+ VideoCard,
+)
+
+
+class TestHeroCardBuilders:
+ def test_hero_card_fluent_builders(self):
+ card = (
+ HeroCard(
+ title="t",
+ subtitle="s",
+ text="txt",
+ tap=CardAction(type=ActionTypes.open_url, title="tap"),
+ )
+ .add_image(url="https://img", alt="alt")
+ .add_image(CardImage(url="https://img2"))
+ .add_button(title="Yes", value="yes")
+ .add_button(CardAction(type=ActionTypes.post_back, title="No"))
+ .add_buttons(
+ CardAction(type=ActionTypes.im_back, title="A"),
+ CardAction(type=ActionTypes.im_back, title="B"),
+ )
+ )
+
+ assert card.title == "t"
+ assert card.subtitle == "s"
+ assert card.text == "txt"
+ assert card.tap.type == ActionTypes.open_url
+ assert len(card.images) == 2
+ assert card.images[0].url == "https://img"
+ assert card.images[0].alt == "alt"
+ assert len(card.buttons) == 4
+ assert card.buttons[0].title == "Yes"
+ assert card.buttons[0].value == "yes"
+
+
+class TestThumbnailAndBasicCardBuilders:
+ @pytest.mark.filterwarnings("ignore::DeprecationWarning")
+ def test_thumbnail_and_basic_card_builders(self):
+ thumb = ThumbnailCard(title="t").add_image(url="u").add_button(title="b")
+ assert thumb.title == "t"
+ assert len(thumb.images) == 1
+ assert len(thumb.buttons) == 1
+
+ basic = (
+ BasicCard(text="x")
+ .add_image(CardImage(url="u"))
+ .add_button(CardAction(type=ActionTypes.im_back, title="b"))
+ )
+ assert basic.text == "x"
+ assert len(basic.images) == 1
+ assert len(basic.buttons) == 1
+
+
+class TestMediaCardBuilders:
+ @pytest.mark.filterwarnings("ignore::DeprecationWarning")
+ def test_media_card_builders_add_media_and_buttons(self):
+ animation = (
+ AnimationCard(title="a")
+ .add_media(url="https://m")
+ .add_button(CardAction(type=ActionTypes.im_back, title="b"))
+ )
+ assert animation.title == "a"
+ assert len(animation.media) == 1
+ assert animation.media[0].url == "https://m"
+ assert len(animation.buttons) == 1
+
+ audio = AudioCard().add_media(MediaUrl(url="https://a"))
+ assert len(audio.media) == 1
+
+ video = VideoCard().add_media(url="https://v", profile="profile")
+ assert video.media[0].profile == "profile"
+
+ media = MediaCard(text="m").add_media(url="https://x")
+ assert media.text == "m"
+ assert len(media.media) == 1
+
+
+class TestReceiptCardBuilders:
+ def test_receipt_card_builders(self):
+ receipt = (
+ ReceiptCard(
+ title="r",
+ total="$10",
+ tax="$1",
+ tap=CardAction(type=ActionTypes.open_url, title="tap"),
+ )
+ .add_fact(Fact(key="key", value="value"))
+ .add_item(ReceiptItem(title="item"))
+ .add_button(CardAction(type=ActionTypes.im_back, title="b"))
+ )
+
+ assert receipt.title == "r"
+ assert receipt.total == "$10"
+ assert receipt.tax == "$1"
+ assert receipt.tap.type == ActionTypes.open_url
+ assert len(receipt.facts) == 1
+ assert len(receipt.items) == 1
+ assert len(receipt.buttons) == 1
+
+
+class TestAdaptiveCardCardBuilders:
+ def test_adaptive_card_card_from_json_to_attachment(self):
+ json_content = '{"type":"AdaptiveCard","version":"1.4"}'
+ card = AdaptiveCardCard(content=json_content)
+
+ assert card.content == json_content
+
+ attachment = card.to_attachment()
+ assert attachment.content_type == ContentTypes.adaptive_card
+ assert attachment.content == {"type": "AdaptiveCard", "version": "1.4"}
+
+ def test_adaptive_card_card_serializes_content_as_nested_json(self):
+ json_content = '{"type":"AdaptiveCard","version":"1.4"}'
+ activity = AdaptiveCardCard(content=json_content).to_message()
+
+ data = activity.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ # Content is unpacked into nested JSON (an object), not an escaped string.
+ assert data["attachments"][0]["content"] == {
+ "type": "AdaptiveCard",
+ "version": "1.4",
+ }
+ assert data["attachments"][0]["contentType"] == ContentTypes.adaptive_card
diff --git a/tests/hosting_core/test_turn_context.py b/tests/hosting_core/test_turn_context.py
index 6f2a34d59..23433c7ed 100644
--- a/tests/hosting_core/test_turn_context.py
+++ b/tests/hosting_core/test_turn_context.py
@@ -455,8 +455,8 @@ def test_should_remove_at_mention_from_activity(self):
text = TurnContext.remove_recipient_mention(activity)
- assert text == " test activity"
- assert activity.text == " test activity"
+ assert text == "test activity"
+ assert activity.text == "test activity"
def test_should_remove_at_mention_with_regex_characters(self):
activity = Activity(
@@ -475,8 +475,8 @@ def test_should_remove_at_mention_with_regex_characters(self):
text = TurnContext.remove_recipient_mention(activity)
- assert text == " test activity"
- assert activity.text == " test activity"
+ assert text == "test activity"
+ assert activity.text == "test activity"
def test_should_remove_custom_mention_from_activity(self):
activity = Activity(
diff --git a/tests/hosting_dialogs/test_choice_prompt.py b/tests/hosting_dialogs/test_choice_prompt.py
index 990ab6860..3182217e5 100644
--- a/tests/hosting_dialogs/test_choice_prompt.py
+++ b/tests/hosting_dialogs/test_choice_prompt.py
@@ -26,7 +26,7 @@
PromptOptions,
PromptValidatorContext,
)
-from microsoft_agents.activity import Activity, ActivityTypes
+from microsoft_agents.activity import Activity, ActivityTypes, ContentTypes
from tests.hosting_dialogs.helpers import DialogTestAdapter
_color_choices: List[Choice] = [
@@ -833,10 +833,7 @@ def assert_expected_activity(
activity: Activity, description
): # pylint: disable=unused-argument
assert len(activity.attachments) == 1
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.hero_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.hero_card
assert activity.attachments[0].content.text == "Please choose a size."
return True
@@ -892,14 +889,8 @@ def assert_expected_activity(
activity: Activity, description
): # pylint: disable=unused-argument
assert len(activity.attachments) == 2
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.adaptive_card
- )
- assert (
- activity.attachments[1].content_type
- == CardFactory.content_types.hero_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.adaptive_card
+ assert activity.attachments[1].content_type == ContentTypes.hero_card
return True
convo_state = ConversationState(MemoryStorage())
diff --git a/tests/hosting_dialogs/test_oauth_prompt.py b/tests/hosting_dialogs/test_oauth_prompt.py
index 331abe6a5..4297019f7 100644
--- a/tests/hosting_dialogs/test_oauth_prompt.py
+++ b/tests/hosting_dialogs/test_oauth_prompt.py
@@ -8,12 +8,12 @@
ActivityTypes,
ChannelAccount,
ConversationAccount,
+ ContentTypes,
InputHints,
SignInConstants,
TokenResponse,
)
from microsoft_agents.hosting.core import (
- CardFactory,
ConversationState,
MemoryStorage,
TurnContext,
@@ -76,10 +76,7 @@ async def callback_handler(turn_context: TurnContext):
async def inspector(activity: Activity, description: str = None):
assert len(activity.attachments) == 1
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.oauth_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.oauth_card
adapter.add_user_token(
connection_name, activity.channel_id, activity.recipient.id, token
@@ -133,10 +130,7 @@ async def exec_test(turn_context: TurnContext):
def inspector(activity: Activity, description: str = None):
assert len(activity.attachments) == 1
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.oauth_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.oauth_card
adapter.add_user_token(
connection_name,
activity.channel_id,
@@ -184,10 +178,7 @@ async def exec_test(turn_context: TurnContext):
def inspector(activity: Activity, description: str = None):
assert len(activity.attachments) == 1
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.oauth_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.oauth_card
step1 = await adapter.send(magic_code)
await step1.assert_reply(inspector)
@@ -264,10 +255,7 @@ async def exec_test(turn_context: TurnContext):
def inspector(activity: Activity, description: str = None):
assert len(activity.attachments) == 1
- assert (
- activity.attachments[0].content_type
- == CardFactory.content_types.oauth_card
- )
+ assert activity.attachments[0].content_type == ContentTypes.oauth_card
adapter.add_user_token(
connection_name,
activity.channel_id,
@@ -345,10 +333,7 @@ async def exec_test(turn_context: TurnContext):
def inspector(activity_: Activity, description: str = None):
assert len(activity_.attachments) == 1
- assert (
- activity_.attachments[0].content_type
- == CardFactory.content_types.oauth_card
- )
+ assert activity_.attachments[0].content_type == ContentTypes.oauth_card
adapter.add_user_token(
connection_name,
activity_.channel_id,