diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/_model_utils.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/_model_utils.py index b747c8ed..342d4572 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/_model_utils.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/_model_utils.py @@ -2,10 +2,11 @@ # Licensed under the MIT License. from abc import ABC -from typing import Any, Callable +from typing import Any, Callable, TypeVar from .agents_model import AgentsModel +AgentsModelT = TypeVar("AgentsModelT", bound=AgentsModel) class ModelFieldHelper(ABC): """Base class for model field processing prior to initialization of an AgentsModel""" @@ -54,8 +55,7 @@ def pick_model_dict(**kwargs): return model_dict - -def pick_model(model_class: type[AgentsModel], **kwargs) -> AgentsModel: +def pick_model(model_class: type[AgentsModelT], **kwargs) -> AgentsModelT: """Picks model fields from the given keyword arguments. Usage: diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py index c3881d30..68eeb9bb 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/activity.py @@ -6,7 +6,7 @@ import logging from copy import copy from datetime import datetime, timezone -from typing import Optional, Any, cast, Annotated, TypeVar +from typing import Optional, Any, cast, Annotated, TypeVar, TYPE_CHECKING from typing_extensions import Self from pydantic import ( @@ -54,105 +54,7 @@ # TODO: A2A Agent 2 is responding with None as id, had to mark it as optional (investigate) class Activity(AgentsModel, _ChannelIdFieldMixin): - """An Activity is the basic communication type for the protocol. - - :param type: Contains the activity type. Possible values include: - 'message', 'contactRelationUpdate', 'conversationUpdate', 'typing', - 'endOfConversation', 'event', 'invoke', 'deleteUserData', 'messageUpdate', - 'messageDelete', 'installationUpdate', 'messageReaction', 'suggestion', - 'trace', 'handoff' - :type type: str or ~microsoft_agents.activity.ActivityTypes - :param id: Contains an ID that uniquely identifies the activity on the channel. - :type id: str - :param timestamp: Contains the date and time that the message was sent, in UTC, expressed in ISO-8601 format. - :type timestamp: datetime - :param local_timestamp: Contains the local date and time of the message expressed in ISO-8601 format. - For example, 2016-09-23T13:07:49.4714686-07:00. - :type local_timestamp: datetime - :param local_timezone: Contains the name of the local timezone of the message, expressed in IANA Time Zone database format. - For example, America/Los_Angeles. - :type local_timezone: str - :param service_url: Contains the URL that specifies the channel's service endpoint. Set by the channel. - :type service_url: str - :param channel_id: Contains an ID that uniquely identifies the channel (and possibly the sub-channel). Set by the channel. - :type channel_id: ~microsoft_agents.activity.ChannelId - :param from_property: Identifies the sender of the message. - :type from_property: ~microsoft_agents.activity.ChannelAccount - :param conversation: Identifies the conversation to which the activity belongs. - :type conversation: ~microsoft_agents.activity.ConversationAccount - :param recipient: Identifies the recipient of the message. - :type recipient: ~microsoft_agents.activity.ChannelAccount - :param text_format: Format of text fields Default:markdown. Possible values include: 'markdown', 'plain', 'xml' - :type text_format: str or ~microsoft_agents.activity.TextFormatTypes - :param attachment_layout: The layout hint for multiple attachments. Default: list. Possible values include: 'list', 'carousel' - :type attachment_layout: str or ~microsoft_agents.activity.AttachmentLayoutTypes - :param members_added: The collection of members added to the conversation. - :type members_added: list[~microsoft_agents.activity.ChannelAccount] - :param members_removed: The collection of members removed from the conversation. - :type members_removed: list[~microsoft_agents.activity.ChannelAccount] - :param reactions_added: The collection of reactions added to the conversation. - :type reactions_added: list[~microsoft_agents.activity.MessageReaction] - :param reactions_removed: The collection of reactions removed from the conversation. - :type reactions_removed: list[~microsoft_agents.activity.MessageReaction] - :param topic_name: The updated topic name of the conversation. - :type topic_name: str - :param history_disclosed: Indicates whether the prior history of the channel is disclosed. - :type history_disclosed: bool - :param locale: A locale name for the contents of the text field. The locale name is a combination of an ISO 639 two- or three-letter - culture code associated with a language and an ISO 3166 two-letter subculture code associated with a country or region. - The locale name can also correspond to a valid BCP-47 language tag. - :type locale: str - :param text: The text content of the message. - :type text: str - :param speak: The text to speak. - :type speak: str - :param input_hint: Indicates whether your agent is accepting, expecting, or ignoring user input after the message is delivered to the client. - Possible values include: 'acceptingInput', 'ignoringInput', 'expectingInput' - :type input_hint: str or ~microsoft_agents.activity.InputHints - :param summary: The text to display if the channel cannot render cards. - :type summary: str - :param suggested_actions: The suggested actions for the activity. - :type suggested_actions: ~microsoft_agents.activity.SuggestedActions - :param attachments: Attachments - :type attachments: list[~microsoft_agents.activity.Attachment] - :param entities: Represents the entities that were mentioned in the message. - :type entities: list[~microsoft_agents.activity.Entity] - :param channel_data: Contains channel-specific content. - :type channel_data: object - :param action: Indicates whether the recipient of a contactRelationUpdate was added or removed from the sender's contact list. - :type action: str - :param reply_to_id: Contains the ID of the message to which this message is a reply. - :type reply_to_id: str - :param label: A descriptive label for the activity. - :type label: str - :param value_type: The type of the activity's value object. - :type value_type: str - :param value: A value that is associated with the activity. - :type value: object - :param name: The name of the operation associated with an invoke or event activity. - :type name: str - :param relates_to: A reference to another conversation or activity. - :type relates_to: ~microsoft_agents.activity.ConversationReference - :param code: The a code for endOfConversation activities that indicates why the conversation ended. Possible values include: 'unknown', - 'completedSuccessfully', 'userCancelled', 'botTimedOut', 'botIssuedInvalidMessage', 'channelFailed' - :type code: str or ~microsoft_agents.activity.EndOfConversationCodes - :param expiration: The time at which the activity should be considered to be "expired" and should not be presented to the recipient. - :type expiration: datetime - :param importance: The importance of the activity. Possible values include: 'low', 'normal', 'high' - :type importance: str or ~microsoft_agents.activity.ActivityImportance - :param delivery_mode: A delivery hint to signal to the recipient alternate delivery paths for the activity. - The default delivery mode is "default". Possible values include: 'normal', 'notification', 'expectReplies', 'ephemeral' - :type delivery_mode: str or ~microsoft_agents.activity.DeliveryModes - :param listen_for: List of phrases and references that speech and language priming systems should listen for - :type listen_for: list[str] - :param text_highlights: The collection of text fragments to highlight when the activity contains a ReplyToId value. - :type text_highlights: list[~microsoft_agents.activity.TextHighlight] - :param semantic_action: An optional programmatic action accompanying this request - :type semantic_action: ~microsoft_agents.activity.SemanticAction - :param caller_id: A string containing an IRI identifying the caller of an agent. This field is not intended to be transmitted over the wire, - but is instead populated by agents and clients based on cryptographically verifiable data that asserts the identity of the callers (e.g. tokens). - :type caller_id: str - """ + """An Activity is the basic communication type for the protocol.""" type: NonEmptyString id: Optional[NonEmptyString] = None @@ -196,6 +98,115 @@ class Activity(AgentsModel, _ChannelIdFieldMixin): semantic_action: SemanticAction = None caller_id: NonEmptyString = None + # def __init__(self, channel_id: str | ChannelId | None = None, *args, **kwargs): + # kwargs["channel_id"] = channel_id + # super().__init__(*args, **kwargs) + + if TYPE_CHECKING: + def __init__(self, + *, + channel_id: ChannelId | str | None = None, + **kwargs) -> None: + """Initialize an Activity instance. + + :param type: Contains the activity type. Possible values include: + 'message', 'contactRelationUpdate', 'conversationUpdate', 'typing', + 'endOfConversation', 'event', 'invoke', 'deleteUserData', 'messageUpdate', + 'messageDelete', 'installationUpdate', 'messageReaction', 'suggestion', + 'trace', 'handoff' + :type type: str or ~microsoft_agents.activity.ActivityTypes + :param id: Contains an ID that uniquely identifies the activity on the channel. + :type id: str + :param timestamp: Contains the date and time that the message was sent, in UTC, expressed in ISO-8601 format. + :type timestamp: datetime + :param local_timestamp: Contains the local date and time of the message expressed in ISO-8601 format. + For example, 2016-09-23T13:07:49.4714686-07:00. + :type local_timestamp: datetime + :param local_timezone: Contains the name of the local timezone of the message, expressed in IANA Time Zone database format. + For example, America/Los_Angeles. + :type local_timezone: str + :param service_url: Contains the URL that specifies the channel's service endpoint. Set by the channel. + :type service_url: str + :param channel_id: Contains an ID that uniquely identifies the channel (and possibly the sub-channel). Set by the channel. + :type channel_id: ~microsoft_agents.activity.ChannelId + :param from_property: Identifies the sender of the message. + :type from_property: ~microsoft_agents.activity.ChannelAccount + :param conversation: Identifies the conversation to which the activity belongs. + :type conversation: ~microsoft_agents.activity.ConversationAccount + :param recipient: Identifies the recipient of the message. + :type recipient: ~microsoft_agents.activity.ChannelAccount + :param text_format: Format of text fields Default:markdown. Possible values include: 'markdown', 'plain', 'xml' + :type text_format: str or ~microsoft_agents.activity.TextFormatTypes + :param attachment_layout: The layout hint for multiple attachments. Default: list. Possible values include: 'list', 'carousel' + :type attachment_layout: str or ~microsoft_agents.activity.AttachmentLayoutTypes + :param members_added: The collection of members added to the conversation. + :type members_added: list[~microsoft_agents.activity.ChannelAccount] + :param members_removed: The collection of members removed from the conversation. + :type members_removed: list[~microsoft_agents.activity.ChannelAccount] + :param reactions_added: The collection of reactions added to the conversation. + :type reactions_added: list[~microsoft_agents.activity.MessageReaction] + :param reactions_removed: The collection of reactions removed from the conversation. + :type reactions_removed: list[~microsoft_agents.activity.MessageReaction] + :param topic_name: The updated topic name of the conversation. + :type topic_name: str + :param history_disclosed: Indicates whether the prior history of the channel is disclosed. + :type history_disclosed: bool + :param locale: A locale name for the contents of the text field. The locale name is a combination of an ISO 639 two- or three-letter + culture code associated with a language and an ISO 3166 two-letter subculture code associated with a country or region. + The locale name can also correspond to a valid BCP-47 language tag. + :type locale: str + :param text: The text content of the message. + :type text: str + :param speak: The text to speak. + :type speak: str + :param input_hint: Indicates whether your agent is accepting, expecting, or ignoring user input after the message is delivered to the client. + Possible values include: 'acceptingInput', 'ignoringInput', 'expectingInput' + :type input_hint: str or ~microsoft_agents.activity.InputHints + :param summary: The text to display if the channel cannot render cards. + :type summary: str + :param suggested_actions: The suggested actions for the activity. + :type suggested_actions: ~microsoft_agents.activity.SuggestedActions + :param attachments: Attachments + :type attachments: list[~microsoft_agents.activity.Attachment] + :param entities: Represents the entities that were mentioned in the message. + :type entities: list[~microsoft_agents.activity.Entity] + :param channel_data: Contains channel-specific content. + :type channel_data: object + :param action: Indicates whether the recipient of a contactRelationUpdate was added or removed from the sender's contact list. + :type action: str + :param reply_to_id: Contains the ID of the message to which this message is a reply. + :type reply_to_id: str + :param label: A descriptive label for the activity. + :type label: str + :param value_type: The type of the activity's value object. + :type value_type: str + :param value: A value that is associated with the activity. + :type value: object + :param name: The name of the operation associated with an invoke or event activity. + :type name: str + :param relates_to: A reference to another conversation or activity. + :type relates_to: ~microsoft_agents.activity.ConversationReference + :param code: The a code for endOfConversation activities that indicates why the conversation ended. Possible values include: 'unknown', + 'completedSuccessfully', 'userCancelled', 'botTimedOut', 'botIssuedInvalidMessage', 'channelFailed' + :type code: str or ~microsoft_agents.activity.EndOfConversationCodes + :param expiration: The time at which the activity should be considered to be "expired" and should not be presented to the recipient. + :type expiration: datetime + :param importance: The importance of the activity. Possible values include: 'low', 'normal', 'high' + :type importance: str or ~microsoft_agents.activity.ActivityImportance + :param delivery_mode: A delivery hint to signal to the recipient alternate delivery paths for the activity. + The default delivery mode is "default". Possible values include: 'normal', 'notification', 'expectReplies', 'ephemeral' + :type delivery_mode: str or ~microsoft_agents.activity.DeliveryModes + :param listen_for: List of phrases and references that speech and language priming systems should listen for + :type listen_for: list[str] + :param text_highlights: The collection of text fragments to highlight when the activity contains a ReplyToId value. + :type text_highlights: list[~microsoft_agents.activity.TextHighlight] + :param semantic_action: An optional programmatic action accompanying this request + :type semantic_action: ~microsoft_agents.activity.SemanticAction + :param caller_id: A string containing an IRI identifying the caller of an agent. This field is not intended to be transmitted over the wire, + but is instead populated by agents and clients based on cryptographically verifiable data that asserts the identity of the callers (e.g. tokens). + :type caller_id: str + """ + @model_validator(mode="wrap") @classmethod def _validate_channel_id( diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py index e8192d6c..406ebd44 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channel_id.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Optional, Any +from typing import Any from pydantic_core import CoreSchema, core_schema from pydantic import GetCoreSchemaHandler @@ -16,10 +16,10 @@ class ChannelId(str): def __init__( self, - value: Optional[str] = None, + value: str | None = None, *, - channel: Optional[str] = None, - sub_channel: Optional[str] = None, + channel: str | None = None, + sub_channel: str | None = None, ) -> None: """Initialize a ChannelId instance. @@ -39,10 +39,10 @@ def __init__( def __new__( cls, - value: Optional[str] = None, + value: str | None = None, *, - channel: Optional[str] = None, - sub_channel: Optional[str] = None, + channel: str | None = None, + sub_channel: str | None = None, ) -> ChannelId: """Create a new ChannelId instance. @@ -83,7 +83,7 @@ def channel(self) -> str: return self._channel # type: ignore[return-value] @property - def sub_channel(self) -> Optional[str]: + def sub_channel(self) -> str | None: """The sub-channel, e.g. 'work' in 'email:work'. May be None.""" return self._sub_channel @@ -93,3 +93,21 @@ def __get_pydantic_core_schema__( cls, source_type: Any, handler: GetCoreSchemaHandler ) -> CoreSchema: return core_schema.no_info_after_validator_function(cls, handler(str)) + + @staticmethod + def get_channel(s: str | ChannelId) -> str: + """Return the main channel from a ChannelId string.""" + if not s: + return s + if isinstance(s, ChannelId): + return s.channel + return ChannelId(s).channel + + @staticmethod + def get_sub_channel(s: str | ChannelId) -> str | None: + """Return the sub-channel from a ChannelId string.""" + if not s: + return None + if isinstance(s, ChannelId): + return s.sub_channel + return ChannelId(s).sub_channel \ No newline at end of file diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py index f94e7fe1..7abcc130 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py @@ -2,7 +2,8 @@ # Licensed under the MIT License. from enum import Enum -from typing_extensions import Self + +from .channel_id import ChannelId class Channels(str, Enum): @@ -70,9 +71,8 @@ class Channels(str, Enum): copilot_studio = "pva-studio" """Microsoft Copilot Studio channel.""" - # TODO: validate the need of Self annotations in the following methods @staticmethod - def supports_suggested_actions(channel_id: Self, button_cnt: int = 100) -> bool: + def supports_suggested_actions(channel_id: str | ChannelId, button_count: int = 100) -> bool: """Determine if a number of Suggested Actions are supported by a Channel. Args: @@ -83,29 +83,30 @@ def supports_suggested_actions(channel_id: Self, button_cnt: int = 100) -> bool: bool: True if the Channel supports the button_cnt total Suggested Actions, False if the Channel does not support that number of Suggested Actions. """ + channel = ChannelId.get_channel(channel_id) max_actions = { # https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies - Channels.facebook: 10, - Channels.skype: 10, + Channels.facebook.value: 10, + Channels.skype.value: 10, # https://developers.line.biz/en/reference/messaging-api/#items-object - Channels.line: 13, + Channels.line.value: 13, # https://dev.kik.com/#/docs/messaging#text-response-object - Channels.kik: 20, - Channels.telegram: 100, - Channels.emulator: 100, - Channels.direct_line: 100, - Channels.direct_line_speech: 100, - Channels.webchat: 100, + Channels.kik.value: 20, + Channels.telegram.value: 100, + Channels.emulator.value: 100, + Channels.direct_line.value: 100, + Channels.direct_line_speech.value: 100, + Channels.webchat.value: 100, } return ( - button_cnt <= max_actions[channel_id] + button_count <= max_actions[channel] if channel_id in max_actions else False ) @staticmethod - def supports_card_actions(channel_id: Self, button_cnt: int = 100) -> bool: + def supports_card_actions(channel_id: str | ChannelId, button_count: int = 100) -> bool: """Determine if a number of Card Actions are supported by a Channel. Args: @@ -117,21 +118,23 @@ def supports_card_actions(channel_id: Self, button_cnt: int = 100) -> bool: that number of Card Actions. """ + channel = ChannelId.get_channel(channel_id) + max_actions = { - Channels.facebook: 3, - Channels.skype: 3, - Channels.ms_teams: 3, - Channels.line: 99, - Channels.slack: 100, - Channels.telegram: 100, - Channels.emulator: 100, - Channels.direct_line: 100, - Channels.direct_line_speech: 100, - Channels.webchat: 100, + Channels.facebook.value: 3, + Channels.skype.value: 3, + Channels.ms_teams.value: 3, + Channels.line.value: 99, + Channels.slack.value: 100, + Channels.telegram.value: 100, + Channels.emulator.value: 100, + Channels.direct_line.value: 100, + Channels.direct_line_speech.value: 100, + Channels.webchat.value: 100, } return ( - button_cnt <= max_actions[channel_id] - if channel_id in max_actions + button_count <= max_actions[channel] + if channel in max_actions else False ) @@ -150,15 +153,14 @@ def has_message_feed(_: str) -> bool: @staticmethod def max_action_title_length( # pylint: disable=unused-argument - channel_id: Self, + channel_id: str | ChannelId, ) -> int: """Maximum length allowed for Action Titles. Args: - channel_id (str): The Channel to determine Maximum Action Title Length. + channel_id (str | ChannelId): The Channel to determine Maximum Action Title Length. Returns: int: The total number of characters allowed for an Action Title on a specific Channel. """ - return 20 diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_parameters.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_parameters.py index 747e7814..07eeb55e 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_parameters.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_parameters.py @@ -7,6 +7,7 @@ from .activity import Activity from .agents_model import AgentsModel from ._type_aliases import NonEmptyString +from .conversation_account import ConversationAccount class ConversationParameters(AgentsModel): @@ -38,3 +39,4 @@ class ConversationParameters(AgentsModel): activity: Activity = None channel_data: object = None tenant_id: NonEmptyString = None + conversation: ConversationAccount | None = None \ No newline at end of file diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_reference.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_reference.py index cde7b100..9f4e3ed4 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_reference.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/conversation_reference.py @@ -4,10 +4,11 @@ from __future__ import annotations from uuid import uuid4 as uuid -from typing import Optional, Annotated +from typing import Optional, Annotated, TYPE_CHECKING from pydantic import Field +from .channel_id import ChannelId from .channel_account import ChannelAccount from ._channel_id_field_mixin import _ChannelIdFieldMixin from .conversation_account import ConversationAccount @@ -16,30 +17,12 @@ from .activity_types import ActivityTypes from .activity_event_names import ActivityEventNames +if TYPE_CHECKING: + from .activity import Activity -class ConversationReference(AgentsModel, _ChannelIdFieldMixin): - """An object relating to a particular point in a conversation. - :param activity_id: (Optional) ID of the activity to refer to - :type activity_id: str - :param user: (Optional) User participating in this conversation - :type user: ~microsoft_agents.activity.ChannelAccount - :param agent: Agent participating in this conversation - :type agent: ~microsoft_agents.activity.ChannelAccount - :param conversation: Conversation reference - :type conversation: ~microsoft_agents.activity.ConversationAccount - :param channel_id: Channel ID - :type channel_id: ~microsoft_agents.activity.ChannelId - :param locale: A locale name for the contents of the text field. - The locale name is a combination of an ISO 639 two- or three-letter - culture code associated with a language and an ISO 3166 two-letter - subculture code associated with a country or region. - The locale name can also correspond to a valid BCP-47 language tag. - :type locale: str - :param service_url: Service endpoint where operations concerning the - referenced conversation may be performed - :type service_url: str - """ +class ConversationReference(AgentsModel, _ChannelIdFieldMixin): + """An object relating to a particular point in a conversation.""" # optionals here are due to webchat activity_id: Optional[NonEmptyString] = None @@ -49,8 +32,36 @@ class ConversationReference(AgentsModel, _ChannelIdFieldMixin): locale: Optional[NonEmptyString] = None service_url: NonEmptyString = None - def get_continuation_activity(self) -> "Activity": # type: ignore - from .activity import Activity + if TYPE_CHECKING: + def __init__(self, + *, + channel_id: ChannelId | str | None = None, + **kwargs + ) -> None: + """ + :param activity_id: (Optional) ID of the activity to refer to + :type activity_id: str + :param user: (Optional) User participating in this conversation + :type user: ~microsoft_agents.activity.ChannelAccount + :param agent: Agent participating in this conversation + :type agent: ~microsoft_agents.activity.ChannelAccount + :param conversation: Conversation reference + :type conversation: ~microsoft_agents.activity.ConversationAccount + :param channel_id: Channel ID + :type channel_id: ~microsoft_agents.activity.ChannelId + :param locale: A locale name for the contents of the text field. + The locale name is a combination of an ISO 639 two- or three-letter + culture code associated with a language and an ISO 3166 two-letter + subculture code associated with a country or region. + The locale name can also correspond to a valid BCP-47 language tag. + :type locale: str + :param service_url: Service endpoint where operations concerning the + referenced conversation may be performed + :type service_url: str + """ + ... + + def get_continuation_activity(self) -> Activity: return Activity( type=ActivityTypes.event, diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/token_response.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/token_response.py index fc4f49f9..5da5701a 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/token_response.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/token_response.py @@ -41,16 +41,18 @@ def is_exchangeable(self) -> bool: try: # Decode without verification to check the audience payload = jwt.decode(self.token, options={"verify_signature": False}) + except Exception: + return False - idtyp = payload.get("idtyp") - if idtyp == "user": - return False + idtyp = payload.get("idtyp") + if idtyp == "user": + return False - aud = payload.get("aud") - app_id = self._get_app_id_from_token_payload(payload) + aud = payload.get("aud") + app_id = self._get_app_id_from_token_payload(payload) + if app_id is not None: return isinstance(aud, str) and app_id in aud - except Exception: - return False + return False @staticmethod def _get_app_id_from_token_payload(token_payload: dict) -> Optional[str]: diff --git a/libraries/microsoft-agents-activity/microsoft_agents/activity/turn_context_protocol.py b/libraries/microsoft-agents-activity/microsoft_agents/activity/turn_context_protocol.py index e9e613f0..9e6b5fcb 100644 --- a/libraries/microsoft-agents-activity/microsoft_agents/activity/turn_context_protocol.py +++ b/libraries/microsoft-agents-activity/microsoft_agents/activity/turn_context_protocol.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Protocol, Callable, Optional, Generic, TypeVar +from typing import Protocol, Callable, Optional, TypeVar from abc import abstractmethod from microsoft_agents.activity import ( @@ -18,9 +18,9 @@ T = TypeVar("T", bound=Activity) -class TurnContextProtocol(Protocol, Generic[T]): +class TurnContextProtocol(Protocol): adapter: "ChannelAdapterProtocol" - activity: Activity | T + activity: Activity responded: bool turn_state: dict diff --git a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py index a25321b1..bf7a80c9 100644 --- a/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py +++ b/libraries/microsoft-agents-copilotstudio-client/microsoft_agents/copilotstudio/client/power_platform_environment.py @@ -330,7 +330,7 @@ def get_environment_endpoint( @staticmethod def get_endpoint_suffix(cloud: PowerPlatformCloud, cloud_base_address: str) -> str: - return { + val = { PowerPlatformCloud.LOCAL: "api.powerplatform.localhost", PowerPlatformCloud.EXP: "api.exp.powerplatform.com", PowerPlatformCloud.DEV: "api.dev.powerplatform.com", @@ -347,7 +347,10 @@ def get_endpoint_suffix(cloud: PowerPlatformCloud, cloud_base_address: str) -> s PowerPlatformCloud.EX: "api.powerplatform.eaglex.ic.gov", PowerPlatformCloud.RX: "api.powerplatform.microsoft.scloud", PowerPlatformCloud.OTHER: cloud_base_address, - }.get(cloud, ValueError(f"Invalid cloud category value: {cloud}")) + }.get(cloud) + if not val: + raise ValueError(f"Invalid cloud category value: {cloud}") + return val @staticmethod def get_id_suffix_length(cloud: PowerPlatformCloud) -> int: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py index 6100c4c1..7f1f0ea3 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/_oauth/_oauth_flow.py @@ -192,6 +192,11 @@ async def begin_flow(self, activity: Activity) -> _FlowResponse: ms_app_id=self._ms_app_id, ) + if not token_exchange_state.conversation.channel_id: + raise ValueError( + "OAuthFlow.begin_flow(): activity must have a channel_id in the conversation reference" + ) + res = await self._user_token_client.user_token._get_token_or_sign_in_resource( activity.from_property.id, self._abs_oauth_connection_name, @@ -271,7 +276,7 @@ async def _continue_from_invoke_token_exchange( self._user_id, ) - return None, _FlowErrorTag.PRECONDITION_FAILED + return TokenResponse(), _FlowErrorTag.PRECONDITION_FAILED raise async def continue_flow(self, activity: Activity) -> _FlowResponse: diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py index d4d6ee6e..33160264 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/agent_application.py @@ -10,6 +10,7 @@ from contextlib import nullcontext from copy import copy from functools import partial +from warnings import deprecated import re from typing import ( @@ -74,22 +75,22 @@ class AgentApplication(Agent, Generic[StateT]): typing: TypingIndicator _options: ApplicationOptions - _adapter: Optional[ChannelServiceAdapter] = None + _adapter: ChannelServiceAdapter | None = None _auth: Authorization - _proactive: Optional[Proactive] = None + _proactive: Proactive | None = None _internal_before_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] _internal_after_turn: list[Callable[[TurnContext, StateT], Awaitable[bool]]] _route_list: _RouteList[StateT] - _error: Optional[Callable[[TurnContext, Exception], Awaitable[None]]] = None - _turn_state_factory: Optional[Callable[[TurnContext], StateT]] = None + _error: Callable[[TurnContext, Exception], Awaitable[None]] | None = None + _turn_state_factory: Callable[[], StateT] | None = None _connection_manager: Connections def __init__( self, - options: Optional[ApplicationOptions] = None, + options: ApplicationOptions | None = None, *, - connection_manager: Optional[Connections] = None, - authorization: Optional[Authorization] = None, + connection_manager: Connections | None = None, + authorization: Authorization | None = None, **kwargs, ) -> None: """ @@ -131,6 +132,8 @@ def __init__( raise ApplicationError(""" The `ApplicationOptions.storage` property is required and was not configured. """) + + self._storage = self._options.storage if options.long_running_messages and ( not options.adapter or not options.bot_app_id @@ -150,13 +153,13 @@ def __init__( self._turn_state_factory = ( options.turn_state_factory or kwargs.get("turn_state_factory", None) - or partial(TurnState.with_storage, self._options.storage) + or partial(StateT.with_storage, self._storage) ) if options.proactive: proactive_opts = copy(options.proactive) if not proactive_opts.storage: - proactive_opts.storage = self._options.storage + proactive_opts.storage = self._storage self._proactive = Proactive(self, proactive_opts) # TODO: decide how to initialize the Authorization (params vs options vs kwargs) @@ -186,7 +189,7 @@ def __init__( if key not in ["storage", "connection_manager", "handlers"] } self._auth = Authorization( - storage=self._options.storage, + storage=self._storage, connection_manager=connection_manager, auth_handlers=options.authorization_handlers, **auth_options, @@ -378,7 +381,7 @@ def __selector(context: TurnContext): def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( - f"Registering activity handler for route handler {func.__name__} with type: {activity_type} with auth handlers: {auth_handlers}" + f"Registering activity handler for route handler {func.__qualname__} with type: {activity_type} with auth handlers: {auth_handlers}" ) self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func @@ -432,7 +435,7 @@ def __selector(context: TurnContext): def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( - f"Registering message handler for route handler {func.__name__} with select: {select} with auth handlers: {auth_handlers}" + f"Registering message handler for route handler {func.__qualname__} with select: {select} with auth handlers: {auth_handlers}" ) self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func @@ -488,7 +491,7 @@ def __selector(context: TurnContext): def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( - f"Registering conversation update handler for route handler {func.__name__} with type: {update_type} with auth handlers: {auth_handlers}" + f"Registering conversation update handler for route handler {func.__qualname__} with type: {update_type} with auth handlers: {auth_handlers}" ) self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func @@ -540,7 +543,7 @@ def __selector(context: TurnContext): def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( - f"Registering message reaction handler for route handler {func.__name__} with type: {reaction_type} with auth handlers: {auth_handlers}" + f"Registering message reaction handler for route handler {func.__qualname__} with type: {reaction_type} with auth handlers: {auth_handlers}" ) self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func @@ -605,7 +608,7 @@ def __selector(context: TurnContext): def __call(func: RouteHandler[StateT]) -> RouteHandler[StateT]: logger.debug( - f"Registering message update handler for route handler {func.__name__} with type: {update_type} with auth handlers: {auth_handlers}" + f"Registering message update handler for route handler {func.__qualname__} with type: {update_type} with auth handlers: {auth_handlers}" ) self.add_route(__selector, func, auth_handlers=auth_handlers, **kwargs) return func @@ -687,7 +690,7 @@ async def __handler(context: TurnContext, state: StateT): ) logger.debug( - f"Registering handoff handler for route handler {func.__name__} with auth handlers: {auth_handlers}" + f"Registering handoff handler for route handler {func.__qualname__} with auth handlers: {auth_handlers}" ) self.add_route(__selector, __handler, auth_handlers=auth_handlers, **kwargs) @@ -793,7 +796,7 @@ async def on_error(context: TurnContext, err: Exception): return func - def turn_state_factory(self, func: Callable[[TurnContext], Awaitable[StateT]]): + def turn_state_factory(self, func: Callable[[], StateT]): """ Custom Turn State Factory """ @@ -872,6 +875,7 @@ def _remove_mentions(self, context: TurnContext): ): context.activity.text = context.remove_recipient_mention(context.activity) + @deprecated("Use `load_configuration_from_env` instead.") @staticmethod def parse_env_vars_configuration(vars: dict[str, Any]) -> dict: """ @@ -908,12 +912,12 @@ async def _initialize_state(self, context: TurnContext) -> StateT: turn_state = self._turn_state_factory() else: logger.debug("Using default turn state factory") - turn_state = TurnState.with_storage(self._options.storage) + turn_state = TurnState.with_storage(self._storage) turn_state = cast(StateT, turn_state) logger.debug("Loading turn state from storage") - await turn_state.load(context, self._options.storage) + await turn_state.load(context, self._storage) turn_state.temp.input = context.activity.text return turn_state diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py index 283168f2..02e17749 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/app_options.py @@ -20,7 +20,6 @@ from .state.turn_state import TurnState from .proactive.proactive_options import ProactiveOptions - @dataclass class ApplicationOptions: adapter: Optional[ChannelServiceAdapter] = None diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py index 542b74cb..5c9709ee 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_authorization_handler.py @@ -55,10 +55,14 @@ def __init__( self._storage = storage self._connection_manager = connection_manager - + if auth_handler: self._handler = auth_handler else: + if not auth_handler_settings: + raise ValueError( + "auth_handler_settings must be provided if auth_handler is not." + ) self._handler = AuthHandler._from_settings(auth_handler_settings) self._id = auth_handler_id or self._handler.name diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py index 3dd21bf1..166af8d3 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/_user_authorization.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from typing import Optional +from typing import Optional, cast from microsoft_agents.activity import ( Activity, @@ -36,6 +36,7 @@ _FlowStorageClient, _FlowStateTag, ) +from microsoft_agents.hosting.core.authorization import ClaimsIdentity from .._sign_in_response import _SignInResponse from ._authorization_handler import _AuthorizationHandler from ..telemetry import spans @@ -65,9 +66,13 @@ async def _load_flow( context and the specified auth handler. :rtype: tuple[OAuthFlow, FlowStorageClient] """ - user_token_client: UserTokenClient = context.turn_state.get( - context.adapter.USER_TOKEN_CLIENT_KEY - ) + user_token_client: UserTokenClient | None = cast(UserTokenClient | None, + context.turn_state.get( + context.adapter.USER_TOKEN_CLIENT_KEY + )) + + if not user_token_client: + raise ValueError("UserTokenClient is required in turn state") if ( not context.activity.channel_id @@ -79,14 +84,17 @@ async def _load_flow( channel_id = context.activity.channel_id user_id = context.activity.from_property.id - ms_app_id = context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY).claims[ - "aud" - ] + identity = cast(ClaimsIdentity | None, context.turn_state.get(context.adapter.AGENT_IDENTITY_KEY)) + + if not identity: + raise ValueError("ClaimsIdentity is required in turn state") + + ms_app_id = identity.claims["aud"] # try to load existing state flow_storage_client = _FlowStorageClient(channel_id, user_id, self._storage) logger.info("Loading OAuth flow state from storage") - flow_state: _FlowState = await flow_storage_client.read(self._id) + flow_state: _FlowState | None = await flow_storage_client.read(self._id) if not flow_state: logger.info("No existing flow state found, creating new flow state") flow_state = _FlowState( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py index 548edaab..27d77485 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/agentic_user_authorization.py @@ -75,8 +75,19 @@ async def get_agentic_instance_token(self, context: TurnContext) -> TokenRespons ) agentic_instance_id = context.activity.get_agentic_instance_id() assert agentic_instance_id + + tenant_id = context.activity.get_agentic_tenant_id() + if not tenant_id: + logger.error( + "Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Tenant ID: %s", + tenant_id, + ) + raise ValueError( + f"Unable to retrieve agentic instance token: missing agentic tenant Id. Agentic Tenant ID: {tenant_id}" + ) + instance_token, _ = await connection.get_agentic_instance_token( - context.activity.get_agentic_tenant_id(), agentic_instance_id + tenant_id, agentic_instance_id ) return ( TokenResponse(token=instance_token) if instance_token else TokenResponse() @@ -130,9 +141,19 @@ async def get_agentic_user_token( raise ValueError( f"Unable to retrieve agentic user token: missing agentic User Id or agentic instance Id. agentic_user_id: {agentic_user_id}, Agentic Instance ID: {agentic_instance_id}" ) + + tenant_id = context.activity.get_agentic_tenant_id() + if not tenant_id: + logger.error( + "Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Tenant ID: %s", + tenant_id, + ) + raise ValueError( + f"Unable to retrieve agentic user token: missing agentic tenant Id. Agentic Tenant ID: {tenant_id}" + ) token = await connection.get_agentic_user_token( - context.activity.get_agentic_tenant_id(), + tenant_id, agentic_instance_id, agentic_user_id, scopes, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py index 61f838f7..93606924 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py @@ -196,7 +196,7 @@ async def _handle_obo( scopes=scopes, user_assertion=input_token_response.token, ) - return TokenResponse(token=token) if token else None + return TokenResponse(token=token) def _create_token_response(self, context: TurnContext) -> TokenResponse: """ diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py index 51cc75bc..132253a8 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py @@ -282,8 +282,9 @@ async def _start_or_continue_sign_in( elif sign_in_response.tag in [_FlowStateTag.BEGIN, _FlowStateTag.CONTINUE]: # Handling special case for Teams SSO, ConsentRequired + channel = context.activity.channel_id.channel if context.activity.channel_id else None if not ( - context.activity.channel_id.channel == Channels.ms_teams + channel == Channels.ms_teams and sign_in_state.continuation_activity and context.activity.type == ActivityTypes.invoke and context.activity.name diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py index aadc876a..3c205401 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/proactive/conversation.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, cast from microsoft_agents.activity import ConversationReference from microsoft_agents.hosting.core.authorization import ClaimsIdentity @@ -67,9 +67,12 @@ def from_turn_context(cls, context: "TurnContext") -> "Conversation": """ from microsoft_agents.hosting.core.channel_adapter import ChannelAdapter - identity: Optional[ClaimsIdentity] = context.turn_state.get( - ChannelAdapter.AGENT_IDENTITY_KEY - ) + identity: Optional[ClaimsIdentity] = cast( + ClaimsIdentity | None, + context.turn_state.get( + ChannelAdapter.AGENT_IDENTITY_KEY + )) + reference = context.activity.get_conversation_reference() return cls(identity or {}, reference) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py index 4466b98b..b23bc54d 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/streaming/streaming_response.py @@ -4,7 +4,9 @@ import uuid import asyncio import logging -from typing import Optional, Callable, Literal, cast +from typing import Optional, Callable, Literal, cast, TYPE_CHECKING +if TYPE_CHECKING: + from microsoft_agents.hosting.core.turn_context import TurnContext from microsoft_agents.activity import ( Activity, @@ -39,7 +41,7 @@ class StreamingResponse: Once `end_stream()` is called, the stream is considered ended and no further updates can be sent. """ - def __init__(self, context: "TurnContext"): + def __init__(self, context: TurnContext): """ Creates a new StreamingResponse instance. @@ -267,7 +269,7 @@ async def wait_for_queue(self) -> None: if self._queue_sync: await self._queue_sync - def _set_defaults(self, context: "TurnContext"): + def _set_defaults(self, context: TurnContext): channel = ( context.activity.channel_id.channel if context.activity.channel_id else None diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py index 7d00fc3a..1be540ad 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_adapter.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from __future__ import annotations + from abc import ABC, abstractmethod from collections.abc import Callable from typing import Awaitable @@ -8,6 +10,7 @@ from microsoft_agents.activity import ChannelAdapterProtocol from microsoft_agents.activity import ( Activity, + ActivityTypes, ConversationAccount, ConversationReference, ConversationParameters, @@ -15,7 +18,7 @@ ) from .turn_context import TurnContext -from .middleware_set import MiddlewareSet +from .middleware_set import MiddlewareSet, Middleware class ChannelAdapter(ABC, ChannelAdapterProtocol): @@ -78,7 +81,7 @@ async def delete_activity( """ raise NotImplementedError() - def use(self, middleware): + def use(self, middleware: Middleware) -> ChannelAdapter: """ Registers a middleware handler with the adapter. @@ -179,7 +182,6 @@ async def create_conversation( If the conversation is established with the specified users, the ID of the activity will contain the ID of the new conversation. """ - from microsoft_agents.activity import ActivityTypes # If credentials are not provided, we can't create a conversation if not conversation_parameters: @@ -202,11 +204,11 @@ async def create_conversation( # Create a conversation update activity conversation_update = Activity( - type=ActivityTypes.CONVERSATION_UPDATE, + type=ActivityTypes.conversation_update, channel_id=channel_id, service_url=service_url, conversation=conversation_parameters.conversation, - recipient=conversation_parameters.bot, + recipient=conversation_parameters.agent, from_property=conversation_parameters.members[0], members_added=conversation_parameters.members, ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 752ae078..0365c882 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -130,13 +130,12 @@ async def update_activity(self, context: TurnContext, activity: Activity): :type activity: :class:`microsoft_agents.activity.Activity` :return: Resource response for the updated activity. :rtype: :class:`microsoft_agents.activity.ResourceResponse` - :raises TypeError: If context or activity are None/invalid. + :raises TypeError: If activity ID is None. + :raises RuntimeError: If unable to extract ConnectorClient from turn context. """ - if not context: - raise TypeError("Expected TurnContext but got None instead") - if activity is None: - raise TypeError("Expected Activity but got None instead") + if activity.id is None: + raise TypeError("Activity ID is required to update an activity.") with spans.AdapterUpdateActivity(activity): @@ -145,7 +144,7 @@ async def update_activity(self, context: TurnContext, activity: Activity): context.turn_state.get(self._AGENT_CONNECTOR_CLIENT_KEY), ) if not connector_client: - raise Error("Unable to extract ConnectorClient from turn context.") + raise RuntimeError("Unable to extract ConnectorClient from turn context.") return await connector_client.conversations.update_activity( activity.conversation.id, activity.id, activity @@ -161,13 +160,12 @@ async def delete_activity( :type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext` :param reference: Reference to the conversation and activity to delete. :type reference: :class:`microsoft_agents.activity.ConversationReference` - :raises TypeError: If context or reference are None/invalid. + :raises TypeError: If reference.activity_id is None. + :raises RuntimeError: If unable to extract ConnectorClient from turn context. """ - if not context: - raise TypeError("Expected TurnContext but got None instead") - if not reference: - raise TypeError("Expected ConversationReference but got None instead") + if not reference.activity_id: + raise TypeError("Activity ID is required to delete an activity.") with spans.AdapterDeleteActivity(context.activity): @@ -176,7 +174,7 @@ async def delete_activity( context.turn_state.get(self._AGENT_CONNECTOR_CLIENT_KEY), ) if not connector_client: - raise Error("Unable to extract ConnectorClient from turn context.") + raise RuntimeError("Unable to extract ConnectorClient from turn context.") await connector_client.conversations.delete_activity( reference.conversation.id, reference.activity_id @@ -272,7 +270,7 @@ async def create_conversation( # pylint: disable=arguments-differ claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url # Create the connector client to use for outbound requests. - connector_client: ConnectorClient = ( + connector_client: ConnectorClientBase = ( await self._channel_service_client_factory.create_connector_client( None, claims_identity, service_url, audience ) @@ -300,7 +298,7 @@ async def create_conversation( # pylint: disable=arguments-differ context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity ) @@ -329,7 +327,7 @@ async def process_proactive( activity=continuation_activity, ) - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity ) @@ -337,7 +335,7 @@ async def process_proactive( context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client # Create the connector client to use for outbound requests. - connector_client: ConnectorClient = ( + connector_client = ( await self._channel_service_client_factory.create_connector_client( context, claims_identity, continuation_activity.service_url, audience ) @@ -368,7 +366,7 @@ async def process_activity( claims_identity: ClaimsIdentity, activity: Activity, callback: Callable[[TurnContext], Awaitable], - ): + ) -> InvokeResponse | None: """ Creates a turn context and runs the middleware pipeline for an incoming activity. @@ -415,7 +413,7 @@ async def process_activity( ) # Create a UserTokenClient instance for the OAuth flow. - user_token_client: UserTokenClient = ( + user_token_client = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity, use_anonymous_auth_callback ) @@ -423,7 +421,7 @@ async def process_activity( context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client # Create the connector client to use for outbound requests. - connector_client: Optional[ConnectorClient] = None + connector_client: ConnectorClientBase | None = None if self._resolve_if_connector_client_is_needed(activity): connector_client = ( await self._channel_service_client_factory.create_connector_client( @@ -503,7 +501,7 @@ def _create_create_activity( def _create_turn_context( self, claims_identity: ClaimsIdentity, - oauth_scope: str, + oauth_scope: str | None, callback: Callable[[TurnContext], Awaitable], activity: Optional[Activity] = None, ) -> TurnContext: @@ -518,7 +516,7 @@ def _create_turn_context( return context - def _process_turn_results(self, context: TurnContext) -> Optional[InvokeResponse]: + def _process_turn_results(self, context: TurnContext) -> InvokeResponse | None: """Process the results of a turn and return the appropriate response. :param context: The turn context @@ -541,11 +539,11 @@ def _process_turn_results(self, context: TurnContext) -> Optional[InvokeResponse if context.activity.type == ActivityTypes.invoke: with spans.AdapterSendActivities([context.activity]): - activity_invoke_response: Activity = context.turn_state.get( + activity_invoke_response: Activity | None = cast(Activity | None, context.turn_state.get( self.INVOKE_RESPONSE_KEY - ) + )) if not activity_invoke_response: - return InvokeResponse(status=HTTPStatus.OK) + return InvokeResponse(status=HTTPStatus.NOT_IMPLEMENTED) return InvokeResponse.model_validate(activity_invoke_response.value) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py index a55001cc..fc1e1775 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_factory_protocol.py @@ -10,4 +10,4 @@ class ChannelFactoryProtocol(Protocol): def create_channel(self, token_access: AccessTokenProviderBase) -> ChannelProtocol: - pass + raise NotImplementedError("create_channel must be implemented by subclasses") diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py index a2990357..9227a117 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_host_protocol.py @@ -8,6 +8,11 @@ class ChannelHostProtocol(Protocol): + + host_endpoint: str + host_app_id: str + channels: dict[str, ChannelInfoProtocol] + def __init__( self, host_endpoint: str, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py index a5855af0..c045b3d0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channel_protocol.py @@ -16,7 +16,7 @@ async def post_activity( conversation_id: str, activity: Activity, *, - response_body_type: type[AgentsModel] = None, + response_body_type: type[AgentsModel] | None = None, **kwargs, ) -> InvokeResponse: raise NotImplementedError() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py index 03547f23..8d6b32b9 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/client/channels_configuration.py @@ -39,4 +39,6 @@ class ChannelsConfiguration(Protocol): @staticmethod def CHANNEL_HOST_CONFIGURATION() -> ChannelHostConfiguration: - pass + raise NotImplementedError( + "ChannelsConfiguration.CHANNEL_HOST_CONFIGURATION() must be implemented" + ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py index 545c5177..18bbde3b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/connector_client_base.py @@ -23,3 +23,8 @@ def attachments(self) -> AttachmentsBase: @abstractmethod def conversations(self) -> ConversationsBase: pass + + @abstractmethod + async def close(self) -> None: + """Close the client and release any resources.""" + raise NotImplementedError("Subclasses must implement the close method.") \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py index 7c398a58..7de00219 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/user_token_client_base.py @@ -9,6 +9,7 @@ class UserTokenClientBase(Protocol): + @property @abstractmethod def agent_sign_in(self) -> AgentSignInBase: @@ -18,3 +19,9 @@ def agent_sign_in(self) -> AgentSignInBase: @abstractmethod def user_token(self) -> UserTokenBase: pass + + + @abstractmethod + async def close(self) -> None: + """Close the client and release any resources.""" + raise NotImplementedError("Subclasses must implement the close method.") \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/http/_http_adapter_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/http/_http_adapter_base.py index 4ea5438b..0ddfec57 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/http/_http_adapter_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/http/_http_adapter_base.py @@ -5,6 +5,7 @@ from abc import ABC from traceback import format_exc +from http import HTTPStatus from microsoft_agents.activity import Activity, DeliveryModes from microsoft_agents.hosting.core.authorization import ClaimsIdentity, Connections @@ -58,6 +59,9 @@ async def on_turn_error(context: TurnContext, error: Exception): self.on_turn_error = on_turn_error + if not connection_manager: + raise ValueError("HttpAdapterBase.__init__: connection_manager can't be None") + channel_service_client_factory = ( channel_service_client_factory or RestChannelServiceClientFactory(connection_manager) @@ -128,8 +132,10 @@ async def process_request( ): with spans.AdapterWriteResponse(activity): # Invoke and ExpectReplies cannot be performed async + invoke_response_status = invoke_response.status if invoke_response else None return HttpResponseFactory.json( - invoke_response.body, invoke_response.status + invoke_response.body if invoke_response else None, + invoke_response_status or HTTPStatus.NOT_IMPLEMENTED, ) return HttpResponseFactory.accepted() diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/message_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/message_factory.py index 91a55730..77a1f8b3 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/message_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/message_factory.py @@ -88,9 +88,9 @@ def suggested_actions( :param input_hint: :return: """ - actions = SuggestedActions(actions=actions) + suggested_actions = SuggestedActions(actions=actions) message = Activity( - type=ActivityTypes.message, input_hint=input_hint, suggested_actions=actions + type=ActivityTypes.message, input_hint=input_hint, suggested_actions=suggested_actions ) if text: message.text = text @@ -122,16 +122,14 @@ def attachment( :param input_hint: :return: """ - return attachment_activity( - AttachmentLayoutTypes.list, [attachment], text, speak, input_hint - ) + return MessageFactory.list([attachment], text, speak, input_hint) @staticmethod def list( attachments: list[Attachment], text: str | None = None, speak: str | None = None, - input_hint: InputHints | str = None, + input_hint: InputHints | str | None = None, ) -> Activity: """ Returns a message that will display a set of attachments in list form. @@ -154,6 +152,10 @@ def list( :param input_hint: :return: """ + if not input_hint: + return attachment_activity( + AttachmentLayoutTypes.list, attachments, text, speak + ) return attachment_activity( AttachmentLayoutTypes.list, attachments, text, speak, input_hint ) @@ -163,7 +165,7 @@ def carousel( attachments: list[Attachment], text: str | None = None, speak: str | None = None, - input_hint: InputHints | str = None, + input_hint: InputHints | str | None = None, ) -> Activity: """ Returns a message that will display a set of attachments using a carousel layout. @@ -186,6 +188,10 @@ def carousel( :param input_hint: :return: """ + if not input_hint: + return attachment_activity( + AttachmentLayoutTypes.carousel, attachments, text, speak + ) return attachment_activity( AttachmentLayoutTypes.carousel, attachments, text, speak, input_hint ) @@ -218,6 +224,4 @@ def content_url( attachment = Attachment(content_type=content_type, content_url=url) if name: attachment.name = name - return attachment_activity( - AttachmentLayoutTypes.list, [attachment], text, speak, input_hint - ) + return MessageFactory.attachment(attachment, text, speak, input_hint) \ No newline at end of file diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index e1347b78..af25d044 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -70,17 +70,21 @@ async def _get_agentic_token(self, context: TurnContext, service_url: str) -> st agent_instance_id = context.activity.get_agentic_instance_id() if not agent_instance_id: raise ValueError("Agent instance ID is required for agentic identity role") + + tenant_id = context.activity.get_agentic_tenant_id() + if not tenant_id: + raise ValueError("Tenant ID is required for agentic identity role") if context.activity.recipient.role == RoleTypes.agentic_identity: token, _ = await connection.get_agentic_instance_token( - context.activity.get_agentic_tenant_id(), agent_instance_id + tenant_id, agent_instance_id ) else: agentic_user = context.activity.get_agentic_user() if not agentic_user: raise ValueError("Agentic user is required for agentic user role") token = await connection.get_agentic_user_token( - context.activity.get_agentic_tenant_id(), + tenant_id, agent_instance_id, agentic_user, [AuthenticationConstants.APX_PRODUCTION_SCOPE], @@ -163,7 +167,7 @@ async def create_user_token_client( if not context or not claims_identity: raise ValueError("context and claims_identity are required") - scopes = claims_identity.get_token_scope() if claims_identity else None + scopes = claims_identity.get_token_scope() if claims_identity else [] with spans.AdapterCreateUserTokenClient( token_service_endpoint=self._token_service_endpoint, 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 3bd9a7cd..24b087b7 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,21 +3,23 @@ from __future__ import annotations import re -from typing import Optional +from typing import Optional, Awaitable, Any -from copy import copy, deepcopy +from copy import deepcopy from collections.abc import Callable from datetime import datetime, timezone -from microsoft_agents.activity import TurnContextProtocol + from microsoft_agents.activity import ( Activity, ActivityTypes, ConversationReference, + DeliveryModes, InputHints, Mention, ResourceResponse, - DeliveryModes, + 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 @@ -40,32 +42,35 @@ def __init__( :param adapter_or_context: :param request: """ + _activity: Activity | None = None if isinstance(adapter_or_context, TurnContext): adapter_or_context.copy_to(self) self._identity = adapter_or_context.identity + _activity = self._activity else: self.adapter = adapter_or_context - self._activity = request # exception thrown if None further down + _activity = request self.responses: list[Activity] = [] self._services: dict = {} - self._on_send_activities: Callable[ - ["TurnContext", list[Activity], Callable], list[ResourceResponse] - ] = [] - self._on_update_activity: Callable[ - ["TurnContext", Activity, Callable], ResourceResponse - ] = [] - self._on_delete_activity: Callable[ - ["TurnContext", ConversationReference, Callable], None - ] = [] + self._on_send_activities: list[Callable[ + [TurnContext, list[Activity], Callable], list[ResourceResponse] + ]] = [] + self._on_update_activity: list[Callable[ + [TurnContext, Activity, Callable], ResourceResponse + ]] = [] + self._on_delete_activity: list[Callable[ + [TurnContext, ConversationReference, Callable], None + ]] = [] self._responded: bool = False self._identity = identity if self.adapter is None: raise TypeError("TurnContext must be instantiated with an adapter.") - if self._activity is None: + if _activity is None: raise TypeError( "TurnContext must be instantiated with a request parameter of type Activity." ) + self._activity = _activity self._turn_state = {} @@ -319,9 +324,14 @@ def on_delete_activity(self, handler) -> "TurnContext": self._on_delete_activity.append(handler) return self - async def _emit(self, plugins, arg, logic): - handlers = copy(plugins) - + async def _emit( + self, + plugins: list[Callable], + arg: Any, + logic: Awaitable[Any], + ) -> Any: + handlers = list(plugins) + async def emit_next(i: int): context = self try: @@ -346,13 +356,14 @@ async def send_trace_activity( value_type: str | None = None, label: str | None = None, ) -> ResourceResponse: - trace_activity = Activity( + trace_activity = pick_model( + Activity, type=ActivityTypes.trace, timestamp=datetime.now(timezone.utc), name=name, value=value, - value_type=value_type, - label=label, + value_type=SkipNone(value_type), + label=SkipNone(label), ) return await self.send_activity(trace_activity)