From 060562428eae941f1c64e6d82114136f2191a95a Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 15 Jul 2025 13:32:31 -0700 Subject: [PATCH 1/8] port test case from botframework for turn-context --- .../tests/test_turn_context.py | 420 ++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py diff --git a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py new file mode 100644 index 00000000..8ae8cac2 --- /dev/null +++ b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py @@ -0,0 +1,420 @@ +import pytest +from typing import Callable, List + +from microsoft.agents.core.models import ( + Activity, + ActivityTypes, + ChannelAccount, + ConversationAccount, + Entity, + Mention, + ResourceResponse, +) +from microsoft.agents.builder import ChannelAdapter, MessageFactory, TurnContext + +activity_data = { + "type": "message", + "id": "1234", + "text":"test", + "from_property": ChannelAccount(id="user", name="User Name"), + "recipient": ChannelAccount(id="bot", name="Bot Name"), + "conversation": ConversationAccount(id="convo", name="Convo Name"), + "channel_id":"UnitTest", + "locale":"en-uS", + "service_url":"https://example.org", +} + +ACTIVITY = Activity(**activity_data) + + +class MockSimpleAdapter(ChannelAdapter): + async def send_activities(self, context, activities) -> List[ResourceResponse]: + responses = [] + assert context is not None + assert activities is not None + assert isinstance(activities, list) + assert activities + for idx, activity in enumerate(activities): # pylint: disable=unused-variable + assert isinstance(activity, Activity) + assert activity.type == "message" or activity.type == ActivityTypes.trace + responses.append(ResourceResponse(id="5678")) + return responses + + async def update_activity(self, context, activity): + assert context is not None + assert activity is not None + assert activity.id is not None + return ResourceResponse(id=activity.id) + + async def delete_activity(self, context, reference): + assert context is not None + assert reference is not None + assert reference.activity_id == ACTIVITY.id + + +class TestTurnContext: + def test_should_create_context_with_request_and_adapter(self): + TurnContext(MockSimpleAdapter(), ACTIVITY) + + def test_should_not_create_context_without_request(self): + try: + TurnContext(MockSimpleAdapter(), None) + except TypeError: + pass + except Exception as error: + raise error + + def test_should_not_create_context_without_adapter(self): + try: + TurnContext(None, ACTIVITY) + except TypeError: + pass + except Exception as error: + raise error + + def test_should_create_context_with_older_context(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + TurnContext(context) + + def test_copy_to_should_copy_all_references(self): + # pylint: disable=protected-access + old_adapter = MockSimpleAdapter() + old_activity = Activity(id="2", type="message", text="test copy") + old_context = TurnContext(old_adapter, old_activity) + old_context.responded = True + + async def send_activities_handler(context, activities, next_handler): + assert context is not None + assert activities is not None + assert next_handler is not None + await next_handler + + async def delete_activity_handler(context, reference, next_handler): + assert context is not None + assert reference is not None + assert next_handler is not None + await next_handler + + async def update_activity_handler(context, activity, next_handler): + assert context is not None + assert activity is not None + assert next_handler is not None + await next_handler + + old_context.on_send_activities(send_activities_handler) + old_context.on_delete_activity(delete_activity_handler) + old_context.on_update_activity(update_activity_handler) + + adapter = MockSimpleAdapter() + new_context = TurnContext(adapter, ACTIVITY) + assert not new_context._on_send_activities # pylint: disable=protected-access + assert not new_context._on_update_activity # pylint: disable=protected-access + assert not new_context._on_delete_activity # pylint: disable=protected-access + + old_context.copy_to(new_context) + + assert new_context.adapter == old_adapter + assert new_context.activity == old_activity + assert new_context.responded is True + assert ( + len(new_context._on_send_activities) == 1 + ) # pylint: disable=protected-access + assert ( + len(new_context._on_update_activity) == 1 + ) # pylint: disable=protected-access + assert ( + len(new_context._on_delete_activity) == 1 + ) # pylint: disable=protected-access + + def test_responded_should_be_automatically_set_to_false(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + assert context.responded is False + + def test_should_be_able_to_set_responded_to_true(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + assert context.responded is False + context.responded = True + assert context.responded + + def test_should_not_be_able_to_set_responded_to_false(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + try: + context.responded = False + except ValueError: + pass + except Exception as error: + raise error + + @pytest.mark.asyncio + async def test_should_call_on_delete_activity_handlers_before_deletion(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called = False + + async def delete_handler(context, reference, next_handler_coroutine): + nonlocal called + called = True + assert reference is not None + assert context is not None + assert reference.activity_id == "1234" + await next_handler_coroutine() + + context.on_delete_activity(delete_handler) + await context.delete_activity(ACTIVITY.id) + assert called is True + + @pytest.mark.asyncio + async def test_should_call_multiple_on_delete_activity_handlers_in_order(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called_first = False + called_second = False + + async def first_delete_handler(context, reference, next_handler_coroutine): + nonlocal called_first, called_second + assert ( + called_first is False + ), "called_first should not be True before first_delete_handler is called." + called_first = True + assert ( + called_second is False + ), "Second on_delete_activity handler was called before first." + assert reference is not None + assert context is not None + assert reference.activity_id == "1234" + await next_handler_coroutine() + + async def second_delete_handler(context, reference, next_handler_coroutine): + nonlocal called_first, called_second + assert called_first + assert ( + called_second is False + ), "called_second was set to True before second handler was called." + called_second = True + assert reference is not None + assert context is not None + assert reference.activity_id == "1234" + await next_handler_coroutine() + + context.on_delete_activity(first_delete_handler) + context.on_delete_activity(second_delete_handler) + await context.delete_activity(ACTIVITY.id) + assert called_first is True + assert called_second is True + + @pytest.mark.asyncio + async def test_should_call_send_on_activities_handler_before_send(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called = False + + async def send_handler(context, activities, next_handler_coroutine): + nonlocal called + called = True + assert activities is not None + assert context is not None + assert not activities[0].id + await next_handler_coroutine() + + context.on_send_activities(send_handler) + await context.send_activity(ACTIVITY) + assert called is True + + @pytest.mark.asyncio + async def test_should_call_on_update_activity_handler_before_update(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called = False + + async def update_handler(context, activity, next_handler_coroutine): + nonlocal called + called = True + assert activity is not None + assert context is not None + assert activity.id == "1234" + await next_handler_coroutine() + + context.on_update_activity(update_handler) + await context.update_activity(ACTIVITY) + assert called is True + + @pytest.mark.asyncio + async def test_update_activity_should_apply_conversation_reference(self): + activity_id = "activity ID" + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called = False + + async def update_handler(context, activity, next_handler_coroutine): + nonlocal called + called = True + assert context is not None + assert activity.id == activity_id + assert activity.conversation.id == ACTIVITY.conversation.id + await next_handler_coroutine() + + context.on_update_activity(update_handler) + new_activity = MessageFactory.text("test text") + new_activity.id = activity_id + update_result = await context.update_activity(new_activity) + assert called is True + assert update_result.id == activity_id + + def test_get_conversation_reference_should_return_valid_reference(self): + reference = ACTIVITY.get_conversation_reference() + + assert reference.activity_id == ACTIVITY.id + assert reference.user == ACTIVITY.from_property + assert reference.agent == ACTIVITY.recipient + assert reference.conversation == ACTIVITY.conversation + assert reference.channel_id == ACTIVITY.channel_id + assert reference.locale == ACTIVITY.locale + assert reference.service_url == ACTIVITY.service_url + + def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false( + self, + ): + reference = ACTIVITY.get_conversation_reference() + reply = TurnContext.apply_conversation_reference( + Activity(type="message", text="reply"), reference + ) + + assert reply.recipient == ACTIVITY.from_property + assert reply.from_property == ACTIVITY.recipient + assert reply.conversation == ACTIVITY.conversation + assert reply.locale == ACTIVITY.locale + assert reply.service_url == ACTIVITY.service_url + assert reply.channel_id == ACTIVITY.channel_id + + def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply( + self, + ): + reference = ACTIVITY.get_conversation_reference() + reply = TurnContext.apply_conversation_reference( + Activity(type="message", text="reply"), reference, True + ) + + assert reply.recipient == ACTIVITY.recipient + assert reply.from_property == ACTIVITY.from_property + assert reply.conversation == ACTIVITY.conversation + assert reply.locale == ACTIVITY.locale + assert reply.service_url == ACTIVITY.service_url + assert reply.channel_id == ACTIVITY.channel_id + + @pytest.mark.asyncio + async def test_should_get_conversation_reference_using_get_reply_conversation_reference( + self, + ): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + reply = await context.send_activity("test") + + assert reply is not None + assert reply.id, "reply has an id" + + reference = TurnContext.get_reply_conversation_reference( + context.activity, reply + ) + + assert reference.activity_id, "reference has an activity id" + assert ( + reference.activity_id == reply.id + ), "reference id matches outgoing reply id" + + def test_should_remove_at_mention_from_activity(self): + activity = Activity( + type="message", + text="TestOAuth619 test activity", + recipient=ChannelAccount(id="TestOAuth619"), + entities=[ + Entity( + type="mention", + text="TestOAuth619", + mentioned=ChannelAccount(name="Bot", id="TestOAuth619"), + ) + ], + ) + + text = TurnContext.remove_recipient_mention(activity) + + assert text == " test activity" + assert activity.text == " test activity" + + def test_should_remove_at_mention_with_regex_characters(self): + activity = Activity( + type="message", + text="Test (*.[]$%#^&?) test activity", + recipient=ChannelAccount(id="Test (*.[]$%#^&?)"), + entities=[ + Entity( + type="mention", + text="Test (*.[]$%#^&?)", + mentioned=ChannelAccount(name="Bot", id="Test (*.[]$%#^&?)"), + ) + ], + ) + + text = TurnContext.remove_recipient_mention(activity) + + assert text == " test activity" + assert activity.text == " test activity" + + def test_should_remove_custom_mention_from_activity(self): + activity = Activity( + text="Hallo", + text_format="plain", + type="message", + timestamp="2025-03-11T14:16:47.0093935Z", + id="1741702606984", + channel_id="msteams", + service_url="https://smba.trafficmanager.net/emea/REDACTED/", + from_property=ChannelAccount( + id="29:1J-K4xVh-sLpdwQ-R5GkOZ_TB0W3ec_37p710aH8qe8bITA0zxdgIGc9l-MdDdkdE_jasSfNOeWXyyL1nsrHtBQ", + name="", + aad_object_id="REDACTED", + ), + conversation=ConversationAccount( + is_group=True, + conversation_type="groupChat", + tenant_id="REDACTED", + id="19:Ql86tXNM2lTBXNKJdqKdwIF9ltGZwpvluLvnJdA0tmg1@thread.v2", + ), + recipient=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", name="Azure AI Agent" + ), + entities=[ + Entity( + type="mention", + mentioned=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", + name="Custom Agent", + ), + ) + ], + channel_data={"tenant": {"id": "REDACTED"}, "productContext": "COPILOT"}, + ) + + text = TurnContext.remove_mention_text(activity, activity.recipient.id) + + assert text == "Hallo" + assert activity.text == "Hallo" + + @pytest.mark.asyncio + async def test_should_send_a_trace_activity(self): + context = TurnContext(MockSimpleAdapter(), ACTIVITY) + called = False + + # pylint: disable=unused-argument + async def aux_func( + ctx: TurnContext, activities: List[Activity], next: Callable + ): + nonlocal called + called = True + assert isinstance(activities, list), "activities not array." + assert len(activities) == 1, "invalid count of activities." + assert activities[0].type == ActivityTypes.trace, "type wrong." + assert activities[0].name == "name-text", "name wrong." + assert activities[0].value == "value-text", "value worng." + assert activities[0].value_type == "valueType-text", "valeuType wrong." + assert activities[0].label == "label-text", "label wrong." + return [] + + context.on_send_activities(aux_func) + await context.send_trace_activity( + "name-text", "value-text", "valueType-text", "label-text" + ) + assert called From dd38170e3f472ebaffb88a93e0d4c8d17943fb08 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 15 Jul 2025 13:36:01 -0700 Subject: [PATCH 2/8] fix model to derive from instead of --- .../microsoft/agents/core/models/mention.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py index c96d8d29..d57f6d6b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/mention.py @@ -1,9 +1,9 @@ from .channel_account import ChannelAccount -from .agents_model import AgentsModel +from .entity import Entity from ._type_aliases import NonEmptyString -class Mention(AgentsModel): +class Mention(Entity): """Mention information (entity type: "mention"). :param mentioned: The mentioned user From d95ab64c738774c8daa7e11b10e68baad433cf83 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 15 Jul 2025 13:42:45 -0700 Subject: [PATCH 3/8] format tests with black --- .../tests/test_turn_context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py index 8ae8cac2..f6f48406 100644 --- a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py +++ b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py @@ -13,15 +13,15 @@ from microsoft.agents.builder import ChannelAdapter, MessageFactory, TurnContext activity_data = { - "type": "message", + "type": "message", "id": "1234", - "text":"test", + "text": "test", "from_property": ChannelAccount(id="user", name="User Name"), "recipient": ChannelAccount(id="bot", name="Bot Name"), "conversation": ConversationAccount(id="convo", name="Convo Name"), - "channel_id":"UnitTest", - "locale":"en-uS", - "service_url":"https://example.org", + "channel_id": "UnitTest", + "locale": "en-uS", + "service_url": "https://example.org", } ACTIVITY = Activity(**activity_data) From 9d0c307fb6770b3c1c35208c85cc085d96cb1adc Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 15 Jul 2025 13:46:25 -0700 Subject: [PATCH 4/8] fix branches --- .../tests/test_turn_context.py | 420 ------------------ 1 file changed, 420 deletions(-) delete mode 100644 libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py diff --git a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py deleted file mode 100644 index 8ae8cac2..00000000 --- a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py +++ /dev/null @@ -1,420 +0,0 @@ -import pytest -from typing import Callable, List - -from microsoft.agents.core.models import ( - Activity, - ActivityTypes, - ChannelAccount, - ConversationAccount, - Entity, - Mention, - ResourceResponse, -) -from microsoft.agents.builder import ChannelAdapter, MessageFactory, TurnContext - -activity_data = { - "type": "message", - "id": "1234", - "text":"test", - "from_property": ChannelAccount(id="user", name="User Name"), - "recipient": ChannelAccount(id="bot", name="Bot Name"), - "conversation": ConversationAccount(id="convo", name="Convo Name"), - "channel_id":"UnitTest", - "locale":"en-uS", - "service_url":"https://example.org", -} - -ACTIVITY = Activity(**activity_data) - - -class MockSimpleAdapter(ChannelAdapter): - async def send_activities(self, context, activities) -> List[ResourceResponse]: - responses = [] - assert context is not None - assert activities is not None - assert isinstance(activities, list) - assert activities - for idx, activity in enumerate(activities): # pylint: disable=unused-variable - assert isinstance(activity, Activity) - assert activity.type == "message" or activity.type == ActivityTypes.trace - responses.append(ResourceResponse(id="5678")) - return responses - - async def update_activity(self, context, activity): - assert context is not None - assert activity is not None - assert activity.id is not None - return ResourceResponse(id=activity.id) - - async def delete_activity(self, context, reference): - assert context is not None - assert reference is not None - assert reference.activity_id == ACTIVITY.id - - -class TestTurnContext: - def test_should_create_context_with_request_and_adapter(self): - TurnContext(MockSimpleAdapter(), ACTIVITY) - - def test_should_not_create_context_without_request(self): - try: - TurnContext(MockSimpleAdapter(), None) - except TypeError: - pass - except Exception as error: - raise error - - def test_should_not_create_context_without_adapter(self): - try: - TurnContext(None, ACTIVITY) - except TypeError: - pass - except Exception as error: - raise error - - def test_should_create_context_with_older_context(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - TurnContext(context) - - def test_copy_to_should_copy_all_references(self): - # pylint: disable=protected-access - old_adapter = MockSimpleAdapter() - old_activity = Activity(id="2", type="message", text="test copy") - old_context = TurnContext(old_adapter, old_activity) - old_context.responded = True - - async def send_activities_handler(context, activities, next_handler): - assert context is not None - assert activities is not None - assert next_handler is not None - await next_handler - - async def delete_activity_handler(context, reference, next_handler): - assert context is not None - assert reference is not None - assert next_handler is not None - await next_handler - - async def update_activity_handler(context, activity, next_handler): - assert context is not None - assert activity is not None - assert next_handler is not None - await next_handler - - old_context.on_send_activities(send_activities_handler) - old_context.on_delete_activity(delete_activity_handler) - old_context.on_update_activity(update_activity_handler) - - adapter = MockSimpleAdapter() - new_context = TurnContext(adapter, ACTIVITY) - assert not new_context._on_send_activities # pylint: disable=protected-access - assert not new_context._on_update_activity # pylint: disable=protected-access - assert not new_context._on_delete_activity # pylint: disable=protected-access - - old_context.copy_to(new_context) - - assert new_context.adapter == old_adapter - assert new_context.activity == old_activity - assert new_context.responded is True - assert ( - len(new_context._on_send_activities) == 1 - ) # pylint: disable=protected-access - assert ( - len(new_context._on_update_activity) == 1 - ) # pylint: disable=protected-access - assert ( - len(new_context._on_delete_activity) == 1 - ) # pylint: disable=protected-access - - def test_responded_should_be_automatically_set_to_false(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - assert context.responded is False - - def test_should_be_able_to_set_responded_to_true(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - assert context.responded is False - context.responded = True - assert context.responded - - def test_should_not_be_able_to_set_responded_to_false(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - try: - context.responded = False - except ValueError: - pass - except Exception as error: - raise error - - @pytest.mark.asyncio - async def test_should_call_on_delete_activity_handlers_before_deletion(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called = False - - async def delete_handler(context, reference, next_handler_coroutine): - nonlocal called - called = True - assert reference is not None - assert context is not None - assert reference.activity_id == "1234" - await next_handler_coroutine() - - context.on_delete_activity(delete_handler) - await context.delete_activity(ACTIVITY.id) - assert called is True - - @pytest.mark.asyncio - async def test_should_call_multiple_on_delete_activity_handlers_in_order(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called_first = False - called_second = False - - async def first_delete_handler(context, reference, next_handler_coroutine): - nonlocal called_first, called_second - assert ( - called_first is False - ), "called_first should not be True before first_delete_handler is called." - called_first = True - assert ( - called_second is False - ), "Second on_delete_activity handler was called before first." - assert reference is not None - assert context is not None - assert reference.activity_id == "1234" - await next_handler_coroutine() - - async def second_delete_handler(context, reference, next_handler_coroutine): - nonlocal called_first, called_second - assert called_first - assert ( - called_second is False - ), "called_second was set to True before second handler was called." - called_second = True - assert reference is not None - assert context is not None - assert reference.activity_id == "1234" - await next_handler_coroutine() - - context.on_delete_activity(first_delete_handler) - context.on_delete_activity(second_delete_handler) - await context.delete_activity(ACTIVITY.id) - assert called_first is True - assert called_second is True - - @pytest.mark.asyncio - async def test_should_call_send_on_activities_handler_before_send(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called = False - - async def send_handler(context, activities, next_handler_coroutine): - nonlocal called - called = True - assert activities is not None - assert context is not None - assert not activities[0].id - await next_handler_coroutine() - - context.on_send_activities(send_handler) - await context.send_activity(ACTIVITY) - assert called is True - - @pytest.mark.asyncio - async def test_should_call_on_update_activity_handler_before_update(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called = False - - async def update_handler(context, activity, next_handler_coroutine): - nonlocal called - called = True - assert activity is not None - assert context is not None - assert activity.id == "1234" - await next_handler_coroutine() - - context.on_update_activity(update_handler) - await context.update_activity(ACTIVITY) - assert called is True - - @pytest.mark.asyncio - async def test_update_activity_should_apply_conversation_reference(self): - activity_id = "activity ID" - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called = False - - async def update_handler(context, activity, next_handler_coroutine): - nonlocal called - called = True - assert context is not None - assert activity.id == activity_id - assert activity.conversation.id == ACTIVITY.conversation.id - await next_handler_coroutine() - - context.on_update_activity(update_handler) - new_activity = MessageFactory.text("test text") - new_activity.id = activity_id - update_result = await context.update_activity(new_activity) - assert called is True - assert update_result.id == activity_id - - def test_get_conversation_reference_should_return_valid_reference(self): - reference = ACTIVITY.get_conversation_reference() - - assert reference.activity_id == ACTIVITY.id - assert reference.user == ACTIVITY.from_property - assert reference.agent == ACTIVITY.recipient - assert reference.conversation == ACTIVITY.conversation - assert reference.channel_id == ACTIVITY.channel_id - assert reference.locale == ACTIVITY.locale - assert reference.service_url == ACTIVITY.service_url - - def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false( - self, - ): - reference = ACTIVITY.get_conversation_reference() - reply = TurnContext.apply_conversation_reference( - Activity(type="message", text="reply"), reference - ) - - assert reply.recipient == ACTIVITY.from_property - assert reply.from_property == ACTIVITY.recipient - assert reply.conversation == ACTIVITY.conversation - assert reply.locale == ACTIVITY.locale - assert reply.service_url == ACTIVITY.service_url - assert reply.channel_id == ACTIVITY.channel_id - - def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply( - self, - ): - reference = ACTIVITY.get_conversation_reference() - reply = TurnContext.apply_conversation_reference( - Activity(type="message", text="reply"), reference, True - ) - - assert reply.recipient == ACTIVITY.recipient - assert reply.from_property == ACTIVITY.from_property - assert reply.conversation == ACTIVITY.conversation - assert reply.locale == ACTIVITY.locale - assert reply.service_url == ACTIVITY.service_url - assert reply.channel_id == ACTIVITY.channel_id - - @pytest.mark.asyncio - async def test_should_get_conversation_reference_using_get_reply_conversation_reference( - self, - ): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - reply = await context.send_activity("test") - - assert reply is not None - assert reply.id, "reply has an id" - - reference = TurnContext.get_reply_conversation_reference( - context.activity, reply - ) - - assert reference.activity_id, "reference has an activity id" - assert ( - reference.activity_id == reply.id - ), "reference id matches outgoing reply id" - - def test_should_remove_at_mention_from_activity(self): - activity = Activity( - type="message", - text="TestOAuth619 test activity", - recipient=ChannelAccount(id="TestOAuth619"), - entities=[ - Entity( - type="mention", - text="TestOAuth619", - mentioned=ChannelAccount(name="Bot", id="TestOAuth619"), - ) - ], - ) - - text = TurnContext.remove_recipient_mention(activity) - - assert text == " test activity" - assert activity.text == " test activity" - - def test_should_remove_at_mention_with_regex_characters(self): - activity = Activity( - type="message", - text="Test (*.[]$%#^&?) test activity", - recipient=ChannelAccount(id="Test (*.[]$%#^&?)"), - entities=[ - Entity( - type="mention", - text="Test (*.[]$%#^&?)", - mentioned=ChannelAccount(name="Bot", id="Test (*.[]$%#^&?)"), - ) - ], - ) - - text = TurnContext.remove_recipient_mention(activity) - - assert text == " test activity" - assert activity.text == " test activity" - - def test_should_remove_custom_mention_from_activity(self): - activity = Activity( - text="Hallo", - text_format="plain", - type="message", - timestamp="2025-03-11T14:16:47.0093935Z", - id="1741702606984", - channel_id="msteams", - service_url="https://smba.trafficmanager.net/emea/REDACTED/", - from_property=ChannelAccount( - id="29:1J-K4xVh-sLpdwQ-R5GkOZ_TB0W3ec_37p710aH8qe8bITA0zxdgIGc9l-MdDdkdE_jasSfNOeWXyyL1nsrHtBQ", - name="", - aad_object_id="REDACTED", - ), - conversation=ConversationAccount( - is_group=True, - conversation_type="groupChat", - tenant_id="REDACTED", - id="19:Ql86tXNM2lTBXNKJdqKdwIF9ltGZwpvluLvnJdA0tmg1@thread.v2", - ), - recipient=ChannelAccount( - id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", name="Azure AI Agent" - ), - entities=[ - Entity( - type="mention", - mentioned=ChannelAccount( - id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", - name="Custom Agent", - ), - ) - ], - channel_data={"tenant": {"id": "REDACTED"}, "productContext": "COPILOT"}, - ) - - text = TurnContext.remove_mention_text(activity, activity.recipient.id) - - assert text == "Hallo" - assert activity.text == "Hallo" - - @pytest.mark.asyncio - async def test_should_send_a_trace_activity(self): - context = TurnContext(MockSimpleAdapter(), ACTIVITY) - called = False - - # pylint: disable=unused-argument - async def aux_func( - ctx: TurnContext, activities: List[Activity], next: Callable - ): - nonlocal called - called = True - assert isinstance(activities, list), "activities not array." - assert len(activities) == 1, "invalid count of activities." - assert activities[0].type == ActivityTypes.trace, "type wrong." - assert activities[0].name == "name-text", "name wrong." - assert activities[0].value == "value-text", "value worng." - assert activities[0].value_type == "valueType-text", "valeuType wrong." - assert activities[0].label == "label-text", "label wrong." - return [] - - context.on_send_activities(aux_func) - await context.send_trace_activity( - "name-text", "value-text", "valueType-text", "label-text" - ) - assert called From ddee434da6f988f81f2811d401f97e21ec5585ab Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 16 Jul 2025 10:24:13 -0700 Subject: [PATCH 5/8] change entity type to mention subtype --- .../microsoft/agents/builder/turn_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py index d2d49402..3f097592 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py @@ -378,7 +378,7 @@ def remove_mention_text(activity: Activity, identifier: str) -> str: """ mentions = TurnContext.get_mentions(activity) for mention in mentions: - if mention.additional_properties["mentioned"]["id"] == identifier: + if mention.additional_properties["id"] == identifier: mention_name_match = re.match( r"(.*?)<\/at>", re.escape(mention.additional_properties["text"]), From 491501f324613a4bdc0381577616749edcd37102 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 16 Jul 2025 10:45:33 -0700 Subject: [PATCH 6/8] Revert turn_context to original --- .../microsoft/agents/builder/turn_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py index 3f097592..f65508a6 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py @@ -378,7 +378,7 @@ def remove_mention_text(activity: Activity, identifier: str) -> str: """ mentions = TurnContext.get_mentions(activity) for mention in mentions: - if mention.additional_properties["id"] == identifier: + if mention.additional_properties["mention"]["id"] == identifier: mention_name_match = re.match( r"(.*?)<\/at>", re.escape(mention.additional_properties["text"]), From 201fd9101a9366de9fce6c19457c89af3093e697 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 17 Jul 2025 10:36:37 -0700 Subject: [PATCH 7/8] set default value for text in case that doesn't exist in a mention --- .../microsoft/agents/builder/turn_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py index f65508a6..f6f2f6c4 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/turn_context.py @@ -378,10 +378,10 @@ def remove_mention_text(activity: Activity, identifier: str) -> str: """ mentions = TurnContext.get_mentions(activity) for mention in mentions: - if mention.additional_properties["mention"]["id"] == identifier: + if mention.additional_properties["mentioned"]["id"] == identifier: mention_name_match = re.match( r"(.*?)<\/at>", - re.escape(mention.additional_properties["text"]), + re.escape(mention.additional_properties.get("text", "")), re.IGNORECASE, ) if mention_name_match: From 73da620353a6efe0433da6ca1e3209b800057559 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 17 Jul 2025 10:37:02 -0700 Subject: [PATCH 8/8] all test_turn_context tests passing --- .../tests/test_turn_context.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py index f6f48406..cd70d958 100644 --- a/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py +++ b/libraries/Builder/microsoft-agents-builder/tests/test_turn_context.py @@ -321,10 +321,11 @@ def test_should_remove_at_mention_from_activity(self): text="TestOAuth619 test activity", recipient=ChannelAccount(id="TestOAuth619"), entities=[ + # Mentions are likely entities due to serialization Entity( type="mention", text="TestOAuth619", - mentioned=ChannelAccount(name="Bot", id="TestOAuth619"), + mentioned={"name": "Bot", "id": "TestOAuth619"}, ) ], ) @@ -340,10 +341,11 @@ def test_should_remove_at_mention_with_regex_characters(self): text="Test (*.[]$%#^&?) test activity", recipient=ChannelAccount(id="Test (*.[]$%#^&?)"), entities=[ + # mentions are likely entities due to serialization Entity( type="mention", text="Test (*.[]$%#^&?)", - mentioned=ChannelAccount(name="Bot", id="Test (*.[]$%#^&?)"), + mentioned={"name": "Bot", "id": "Test (*.[]$%#^&?)"}, ) ], ) @@ -377,12 +379,13 @@ def test_should_remove_custom_mention_from_activity(self): id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", name="Azure AI Agent" ), entities=[ + # mentions are likely entities due to serialization Entity( type="mention", - mentioned=ChannelAccount( - id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", - name="Custom Agent", - ), + mentioned={ + "id": "28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", + "name": "Custom Agent", + }, ) ], channel_data={"tenant": {"id": "REDACTED"}, "productContext": "COPILOT"},