Skip to content
Merged
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 @@ -20,7 +20,6 @@
from .card_image import CardImage
from .channels import Channels
from .channel_account import ChannelAccount
from ._channel_id_field_mixin import _ChannelIdFieldMixin
from .channel_id import ChannelId
from .conversation_account import ConversationAccount
from .conversation_members import ConversationMembers
Expand Down Expand Up @@ -126,7 +125,6 @@
"Channels",
"ChannelAccount",
"ChannelId",
"_ChannelIdFieldMixin",
"ConversationAccount",
"ConversationMembers",
"ConversationParameters",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
model_validator,
SerializerFunctionWrapHandler,
ModelWrapValidatorHandler,
computed_field,
ValidationError,
)

Expand All @@ -41,7 +40,6 @@
from .semantic_action import SemanticAction
from .agents_model import AgentsModel
from .role_types import RoleTypes
from ._channel_id_field_mixin import _ChannelIdFieldMixin
from .channel_id import ChannelId
from ._model_utils import pick_model, SkipNone
from ._type_aliases import NonEmptyString
Expand All @@ -53,7 +51,7 @@


# TODO: A2A Agent 2 is responding with None as id, had to mark it as optional (investigate)
class Activity(AgentsModel, _ChannelIdFieldMixin):
class Activity(AgentsModel):
"""An Activity is the basic communication type for the protocol.
Comment thread
rodrigobr-msft marked this conversation as resolved.

:param type: Contains the activity type. Possible values include:
Expand Down Expand Up @@ -155,6 +153,7 @@ class Activity(AgentsModel, _ChannelIdFieldMixin):
"""

type: NonEmptyString
channel_id: Optional[ChannelId] = None
id: Optional[NonEmptyString] = None
timestamp: datetime = None
local_timestamp: datetime = None
Expand Down Expand Up @@ -211,10 +210,6 @@ def _validate_channel_id(
# run Pydantic's standard validation first
activity = handler(data)

# needed to assign to a computed field
# needed because we override the mixin validator
activity._set_validated_channel_id(data)

# sync sub_channel with productInfo entity
product_info = activity.get_product_info_entity()
if product_info and activity.channel_id:
Expand Down Expand Up @@ -280,9 +275,6 @@ def _serialize_sub_channel_data(
if not serialized["entities"]: # after removal above, list may be empty
del serialized["entities"]

# necessary due to computed_field serialization
self._remove_serialized_unset_channel_id(serialized)

return serialized

def apply_conversation_reference(
Expand Down Expand Up @@ -662,8 +654,8 @@ def get_conversation_reference(
agent=copy(self.recipient),
conversation=copy(self.conversation),
channel_id=(
self.channel_id.split(":", 1)[0]
if force_base_channel and self.channel_id is not None
ChannelId.get_channel(self.channel_id)
if force_base_channel
else self.channel_id
),
locale=self.locale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,28 @@
class ChannelId(str):
"""A ChannelId represents a channel and optional sub-channel in the format 'channel:sub_channel'."""

_channel: str
_sub_channel: str | None

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.
"""Accept the public constructor signature after __new__ initializes the instance.

:param value: The full channel ID string in the format 'channel:sub_channel'. Must be provided if channel is not provided.
:param channel: The main channel string. Must be provided if value is not provided.
:param sub_channel: The sub-channel string.
:raises ValueError: If the input parameters are invalid. value and channel cannot both be provided.
ChannelId subclasses str, an immutable type, so the string value and derived
channel parts must be assigned in __new__ when the instance is created.
"""
super().__init__()
if not channel:
split = self.strip().split(":", 1)
self._channel = split[0].strip()
self._sub_channel = split[1].strip() if len(split) == 2 else None
else:
self._channel = channel
self._sub_channel = sub_channel

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 All @@ -52,38 +45,57 @@ def __new__(
:return: A new ChannelId instance.
:raises ValueError: If the input parameters are invalid. value and channel cannot both be provided.
"""
if isinstance(value, cls) and channel is None and sub_channel is None:
return value

value, channel, sub_channel = cls._normalize(value, channel, sub_channel)

instance = str.__new__(cls, value)
instance._channel = channel
instance._sub_channel = sub_channel
return instance

@staticmethod
def _normalize(
value: Optional[str],
channel: Optional[str],
sub_channel: Optional[str],
) -> tuple[str, str, Optional[str]]:
"""Normalize constructor arguments into string, channel, and sub-channel."""
if isinstance(value, str):
if channel or sub_channel:
raise ValueError(str(activity_errors.ChannelIdValueConflict))

value = value.strip()
if value:
return str.__new__(cls, value)
raise TypeError(str(activity_errors.ChannelIdValueMustBeNonEmpty))
else:
if (
not isinstance(channel, str)
or len(channel.strip()) == 0
or ":" in channel
):
raise TypeError(
"channel must be a non empty string, and must not contain the ':' character"
)
if sub_channel is not None and (not isinstance(sub_channel, str)):
raise TypeError("sub_channel must be a string if provided")
channel = channel.strip()
sub_channel = sub_channel.strip() if sub_channel else None
if sub_channel:
return str.__new__(cls, f"{channel}:{sub_channel}")
return str.__new__(cls, channel)
if not value:
raise TypeError(str(activity_errors.ChannelIdValueMustBeNonEmpty))

split = value.split(":", 1)
channel = split[0].strip()
if not channel:
raise ValueError(str(activity_errors.ChannelIdValueMustBeNonEmpty))
sub_channel = (split[1].strip() or None) if len(split) == 2 else None
return value, channel, sub_channel
Comment thread
Copilot marked this conversation as resolved.

if not isinstance(channel, str) or len(channel.strip()) == 0 or ":" in channel:
raise TypeError(
"channel must be a non empty string, and must not contain the ':' character"
)
if sub_channel is not None and (not isinstance(sub_channel, str)):
raise TypeError("sub_channel must be a string if provided")
channel = channel.strip()
sub_channel = sub_channel.strip() if sub_channel else None
if sub_channel:
return f"{channel}:{sub_channel}", channel, sub_channel
return channel, channel, None

@property
def channel(self) -> str:
"""The main channel, e.g. 'email' in 'email:work'."""
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 +105,25 @@ 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_sub_channel(channel_id: str | ChannelId | None) -> str | None:
"""Return the sub-channel from a ChannelId or string."""
if not channel_id or not channel_id.strip():
return None
if isinstance(channel_id, ChannelId):
return channel_id.sub_channel
value = channel_id.strip()
sub = value.split(":", 1)[1].strip() if ":" in value else None
return sub or None

@staticmethod
def get_channel(channel_id: str | ChannelId | None) -> str | None:
"""Return the Bot Framework channel without an optional sub-channel."""
if not channel_id or not channel_id.strip():
return channel_id
if isinstance(channel_id, ChannelId):
return channel_id.channel
Comment thread
rodrigobr-msft marked this conversation as resolved.
channel_id = channel_id.strip()
parsed = channel_id.split(":", 1)[0].strip() if ":" in channel_id else None
return parsed or channel_id
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
from pydantic import Field

from .channel_account import ChannelAccount
from ._channel_id_field_mixin import _ChannelIdFieldMixin
from .channel_id import ChannelId
from .conversation_account import ConversationAccount
from .agents_model import AgentsModel
from ._type_aliases import NonEmptyString
from .activity_types import ActivityTypes
from .activity_event_names import ActivityEventNames


class ConversationReference(AgentsModel, _ChannelIdFieldMixin):
class ConversationReference(AgentsModel):
"""An object relating to a particular point in a conversation.
Comment thread
rodrigobr-msft marked this conversation as resolved.

:param activity_id: (Optional) ID of the activity to refer to
Expand Down Expand Up @@ -46,6 +46,7 @@ class ConversationReference(AgentsModel, _ChannelIdFieldMixin):
user: Optional[ChannelAccount] = None
agent: Annotated[ChannelAccount, Field(alias="bot")] = None
conversation: ConversationAccount
channel_id: Optional[ChannelId] = None
locale: Optional[NonEmptyString] = None
service_url: NonEmptyString = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ActivityTypes,
CallerIdConstants,
Channels,
ChannelId,
ConversationAccount,
ConversationReference,
ConversationResourceResponse,
Expand Down Expand Up @@ -488,7 +489,7 @@ def _create_create_activity(
# Create a conversation update activity to represent the result.
activity = Activity.create_event_activity()
activity.name = ActivityEventNames.create_conversation
activity.channel_id = channel_id
activity.channel_id = ChannelId(channel_id)
activity.service_url = service_url
activity.id = create_conversation_result.activity_id or str(uuid4())
activity.conversation = ConversationAccount(
Expand Down
Loading
Loading