From ee31f84c19aea1047db78bc6b203cc82fd8d3ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Brand=C3=A3o?= Date: Tue, 26 Aug 2025 15:06:25 -0700 Subject: [PATCH] Added ActivityTreatment and a few test cases for entities --- .../microsoft/agents/activity/__init__.py | 3 +- .../microsoft/agents/activity/activity.py | 12 ++- .../agents/activity/activity_treatment.py | 27 +++++++ .../microsoft/agents/activity/entity.py | 19 +++++ .../microsoft/agents/activity/mention.py | 4 +- .../tests/test_activity.py | 59 +++++++++++++++ .../tests/test_entities.py | 75 +++++++++++++++++++ 7 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 libraries/microsoft-agents-activity/microsoft/agents/activity/activity_treatment.py create mode 100644 libraries/microsoft-agents-activity/tests/test_activity.py create mode 100644 libraries/microsoft-agents-activity/tests/test_entities.py diff --git a/libraries/microsoft-agents-activity/microsoft/agents/activity/__init__.py b/libraries/microsoft-agents-activity/microsoft/agents/activity/__init__.py index 7b91fff7..aeda5eb0 100644 --- a/libraries/microsoft-agents-activity/microsoft/agents/activity/__init__.py +++ b/libraries/microsoft-agents-activity/microsoft/agents/activity/__init__.py @@ -2,6 +2,7 @@ from .action_types import ActionTypes from .activity import Activity from .activity_event_names import ActivityEventNames +from .activity_treatment import ActivityTreatment, ActivityTreatmentType from .activity_types import ActivityTypes from .adaptive_card_invoke_action import AdaptiveCardInvokeAction from .adaptive_card_invoke_response import AdaptiveCardInvokeResponse @@ -24,7 +25,7 @@ from .conversation_resource_response import ConversationResourceResponse from .conversations_result import ConversationsResult from .expected_replies import ExpectedReplies -from .entity import Entity +from .entity import Entity, EntityTypes from .ai_entity import ( AIEntity, ClientCitation, diff --git a/libraries/microsoft-agents-activity/microsoft/agents/activity/activity.py b/libraries/microsoft-agents-activity/microsoft/agents/activity/activity.py index 67a359ad..c2d6b71e 100644 --- a/libraries/microsoft-agents-activity/microsoft/agents/activity/activity.py +++ b/libraries/microsoft-agents-activity/microsoft/agents/activity/activity.py @@ -3,6 +3,7 @@ from typing import Optional from pydantic import Field, SerializeAsAny from .activity_types import ActivityTypes +from .activity_treatment import ActivityTreatment from .channel_account import ChannelAccount from .conversation_account import ConversationAccount from .mention import Mention @@ -532,8 +533,15 @@ def get_mentions(self) -> list[Mention]: This method is defined on the :class:`Activity` class, but is only intended for use with a message activity, where the activity Activity.Type is set to ActivityTypes.Message. """ - _list = self.entities - return [x for x in _list if str(x.type).lower() == "mention"] + return [x for x in self.entities if str(x.type).lower() == "mention"] + + def get_activity_treatments(self) -> list[ActivityTreatment]: + """ + Resolves the activity treatments from the entities of this activity. + + :returns: The array of activity treatments; or an empty array, if none are found. + """ + return [x for x in self.entities if str(x.type).lower() == "activitytreatment"] def get_reply_conversation_reference( self, reply: ResourceResponse diff --git a/libraries/microsoft-agents-activity/microsoft/agents/activity/activity_treatment.py b/libraries/microsoft-agents-activity/microsoft/agents/activity/activity_treatment.py new file mode 100644 index 00000000..fb80c8f5 --- /dev/null +++ b/libraries/microsoft-agents-activity/microsoft/agents/activity/activity_treatment.py @@ -0,0 +1,27 @@ +from enum import Enum + +from .entity import Entity, EntityTypes +from ._type_aliases import NonEmptyString + + +class ActivityTreatmentType(str, Enum): + """Activity treatment types + + TARGETED: only the recipient should be able to see the message even if the activity + is being sent to a group of people. + """ + + TARGETED = "targeted" + + +class ActivityTreatment(Entity): + """Activity treatment information (entity type: "activitytreatment"). + + :param treatment: The treatment type + :type treatment: ~microsoft.agents.activity.ActivityTreatmentType + :param type: Type of this entity (RFC 3987 IRI) + :type type: NonEmptyString + """ + + treatment: ActivityTreatmentType = None + type: NonEmptyString = EntityTypes.ACTIVITY_TREATMENT diff --git a/libraries/microsoft-agents-activity/microsoft/agents/activity/entity.py b/libraries/microsoft-agents-activity/microsoft/agents/activity/entity.py index 08bbf242..7b276819 100644 --- a/libraries/microsoft-agents-activity/microsoft/agents/activity/entity.py +++ b/libraries/microsoft-agents-activity/microsoft/agents/activity/entity.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, Optional from pydantic import model_serializer, model_validator @@ -5,6 +7,19 @@ from pydantic.alias_generators import to_camel, to_snake from ._type_aliases import NonEmptyString +from enum import Enum + + +class EntityTypes: + + MENTION = "mention" + ACTIVITY_TREATMENT = "activitytreatment" + + # ENTITY_MAP = { + # EntityTypes.MENTION: Mention, + # EntityTypes.ACTIVITY_TREATMENT: ActivityTreatment, + # } + class Entity(AgentsModel): """Metadata object pertaining to an activity. @@ -36,3 +51,7 @@ def to_camel_for_all(self, config): new_data[to_camel(k)] = v return new_data return {k: v for k, v in self} + + # @classmethod + # def deserialize_entity(cls, entity: Entity) -> Entity: + # return EntityTypes.ENTITY_MAP[entity.type](entity.model_dump()) diff --git a/libraries/microsoft-agents-activity/microsoft/agents/activity/mention.py b/libraries/microsoft-agents-activity/microsoft/agents/activity/mention.py index d57f6d6b..b144afbf 100644 --- a/libraries/microsoft-agents-activity/microsoft/agents/activity/mention.py +++ b/libraries/microsoft-agents-activity/microsoft/agents/activity/mention.py @@ -1,5 +1,5 @@ from .channel_account import ChannelAccount -from .entity import Entity +from .entity import Entity, EntityTypes from ._type_aliases import NonEmptyString @@ -16,4 +16,4 @@ class Mention(Entity): mentioned: ChannelAccount = None text: NonEmptyString = None - type: NonEmptyString = None + type: NonEmptyString = EntityTypes.MENTION diff --git a/libraries/microsoft-agents-activity/tests/test_activity.py b/libraries/microsoft-agents-activity/tests/test_activity.py new file mode 100644 index 00000000..1d923a59 --- /dev/null +++ b/libraries/microsoft-agents-activity/tests/test_activity.py @@ -0,0 +1,59 @@ +import pytest + +from microsoft.agents.activity import ( + Activity, + ActivityTreatment, + ActivityTreatmentType, + Entity, + EntityTypes, + Mention, +) + + +class TestActivityGetEntities: + + @pytest.fixture + def activity(self): + return Activity( + type="message", + entities=[ + ActivityTreatment(treatment=ActivityTreatmentType.TARGETED), + Entity( + type=EntityTypes.ACTIVITY_TREATMENT, + treatment=ActivityTreatmentType.TARGETED, + ), + Mention(type=EntityTypes.MENTION, text="Hello"), + Entity(type=EntityTypes.MENTION), + Entity(type=EntityTypes.ACTIVITY_TREATMENT, treatment=None), + ], + ) + + def test_activity_get_mentions(self, activity): + expected = [ + Mention(type=EntityTypes.MENTION, text="Hello"), + Entity(type=EntityTypes.MENTION), + ] + ret = activity.get_mentions() + assert activity.get_mentions() == expected + assert ret[0].text == "Hello" + assert ret[0].type == EntityTypes.MENTION + assert not hasattr(ret[1], "text") + assert ret[1].type == EntityTypes.MENTION + + def test_activity_get_activity_treatments(self, activity): + expected = [ + ActivityTreatment(treatment=ActivityTreatmentType.TARGETED), + Entity( + type=EntityTypes.ACTIVITY_TREATMENT, + treatment=ActivityTreatmentType.TARGETED, + ), + Entity(type=EntityTypes.ACTIVITY_TREATMENT, treatment=None), + ] + ret = activity.get_activity_treatments() + assert ret == expected + assert ret[0].treatment == ActivityTreatmentType.TARGETED + assert ret[0].type == EntityTypes.ACTIVITY_TREATMENT + assert ret[1].treatment == ActivityTreatmentType.TARGETED + assert ret[1].type == EntityTypes.ACTIVITY_TREATMENT + assert ret[2].treatment is None + assert ret[2].type == EntityTypes.ACTIVITY_TREATMENT diff --git a/libraries/microsoft-agents-activity/tests/test_entities.py b/libraries/microsoft-agents-activity/tests/test_entities.py new file mode 100644 index 00000000..4b42225c --- /dev/null +++ b/libraries/microsoft-agents-activity/tests/test_entities.py @@ -0,0 +1,75 @@ +import pytest + +from microsoft.agents.activity import ( + Entity, + EntityTypes, + Mention, + ActivityTreatment, + ActivityTreatmentType, + ChannelAccount, +) + + +class TestSerialization: + + def test_mention_serializer(self): + initial_mention = Mention(text="Hello", mentioned=ChannelAccount(id="abc")) + initial_mention_dict = initial_mention.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) + mention = Mention.model_validate(initial_mention_dict) + + assert initial_mention_dict == { + "text": "Hello", + "mentioned": {"id": "abc"}, + "type": EntityTypes.MENTION, + } + assert mention == initial_mention + + def test_mention_serializer_as_entity(self): + initial_mention = Entity( + text="Hello", mentioned=ChannelAccount(id="abc"), type=EntityTypes.MENTION + ) + initial_mention_dict = initial_mention.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) + mention = Mention.model_validate(initial_mention_dict) + + assert initial_mention_dict == { + "text": "Hello", + "mentioned": {"id": "abc"}, + "type": EntityTypes.MENTION, + } + assert mention.type == initial_mention.type + assert mention.text == initial_mention.text + assert mention.mentioned == initial_mention.mentioned + + def test_activity_treatment_serializer(self): + initial_treatment = ActivityTreatment(treatment=ActivityTreatmentType.TARGETED) + initial_treatment_dict = initial_treatment.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) + treatment = ActivityTreatment.model_validate(initial_treatment_dict) + + assert initial_treatment_dict == { + "treatment": ActivityTreatmentType.TARGETED, + "type": EntityTypes.ACTIVITY_TREATMENT, + } + assert treatment == initial_treatment + + def test_activity_treatment_serializer_as_entity(self): + initial_treatment = Entity( + treatment=ActivityTreatmentType.TARGETED, + type=EntityTypes.ACTIVITY_TREATMENT, + ) + initial_treatment_dict = initial_treatment.model_dump( + mode="json", exclude_unset=True, by_alias=True + ) + treatment = ActivityTreatment.model_validate(initial_treatment_dict) + + assert initial_treatment_dict == { + "treatment": ActivityTreatmentType.TARGETED, + "type": EntityTypes.ACTIVITY_TREATMENT, + } + assert treatment.type == initial_treatment.type + assert treatment.treatment == initial_treatment.treatment