Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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:
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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
)
Comment on lines 102 to 106

@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:
Expand All @@ -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
)

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -38,3 +39,4 @@ class ConversationParameters(AgentsModel):
activity: Activity = None
channel_data: object = None
tenant_id: NonEmptyString = None
conversation: ConversationAccount | None = None
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading