From a3d8e938bc1a3d36e72ae25f0d891b26d66118bc Mon Sep 17 00:00:00 2001 From: Alex Acebo Date: Wed, 22 Jan 2025 10:21:31 -0800 Subject: [PATCH] add and test messages invoke module --- .../ai/teams/ai/citations/__init__.py | 12 +++- python/packages/ai/teams/app.py | 10 +++ python/packages/ai/teams/messages/__init__.py | 8 +++ python/packages/ai/teams/messages/messages.py | 67 +++++++++++++++++++ python/packages/ai/tests/test_messages.py | 47 +++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 python/packages/ai/teams/messages/__init__.py create mode 100644 python/packages/ai/teams/messages/messages.py create mode 100644 python/packages/ai/tests/test_messages.py diff --git a/python/packages/ai/teams/ai/citations/__init__.py b/python/packages/ai/teams/ai/citations/__init__.py index c5ef00835..829667849 100644 --- a/python/packages/ai/teams/ai/citations/__init__.py +++ b/python/packages/ai/teams/ai/citations/__init__.py @@ -6,9 +6,19 @@ from .citations import ( AIEntity, Appearance, + AppearanceImage, ClientCitation, + ClientCitationIconName, Pattern, SensitivityUsageInfo, ) -__all__ = ["ClientCitation", "Appearance", "SensitivityUsageInfo", "Pattern", "AIEntity"] +__all__ = [ + "ClientCitation", + "ClientCitationIconName", + "Appearance", + "AppearanceImage", + "SensitivityUsageInfo", + "Pattern", + "AIEntity", +] diff --git a/python/packages/ai/teams/app.py b/python/packages/ai/teams/app.py index a79d4a587..ebe314340 100644 --- a/python/packages/ai/teams/app.py +++ b/python/packages/ai/teams/app.py @@ -41,6 +41,7 @@ from .feedback_loop_data import FeedbackLoopData from .meetings.meetings import Meetings from .message_extensions.message_extensions import MessageExtensions +from .messages.messages import Messages from .route import Route, RouteHandler from .state import TurnState from .task_modules import TaskModules @@ -79,6 +80,7 @@ class Application(Bot, Generic[StateT]): _message_extensions: MessageExtensions[StateT] _task_modules: TaskModules[StateT] _meetings: Meetings[StateT] + _messages: Messages[StateT] def __init__(self, options: ApplicationOptions = ApplicationOptions()) -> None: """ @@ -96,6 +98,7 @@ def __init__(self, options: ApplicationOptions = ApplicationOptions()) -> None: self._routes, options.task_modules.task_data_filter ) self._meetings = Meetings[StateT](self._routes) + self._messages = Messages[StateT](self._routes) if options.long_running_messages and (not options.adapter or not options.bot_app_id): raise ApplicationError(""" @@ -189,6 +192,13 @@ def meetings(self) -> Meetings[StateT]: """ return self._meetings + @property + def messages(self) -> Messages[StateT]: + """ + Access the application's messages functionalities. + """ + return self._messages + def activity( self, type: ActivityType ) -> Callable[[RouteHandler[StateT]], RouteHandler[StateT]]: diff --git a/python/packages/ai/teams/messages/__init__.py b/python/packages/ai/teams/messages/__init__.py new file mode 100644 index 000000000..fb2e61063 --- /dev/null +++ b/python/packages/ai/teams/messages/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from .messages import Messages + +__all__ = ["Messages"] diff --git a/python/packages/ai/teams/messages/messages.py b/python/packages/ai/teams/messages/messages.py new file mode 100644 index 000000000..4bdc9513a --- /dev/null +++ b/python/packages/ai/teams/messages/messages.py @@ -0,0 +1,67 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from __future__ import annotations + +from typing import Awaitable, Callable, Generic, List, TypeVar + +from botbuilder.core import TurnContext +from botbuilder.core.serializer_helper import serializer_helper +from botbuilder.schema import ActivityTypes + +from teams.route import Route +from teams.state import TurnState + +FETCH_INVOKE_NAME = "message/fetchTask" + +StateT = TypeVar("StateT", bound=TurnState) + + +class Messages(Generic[StateT]): + _routes: List[Route[StateT]] = [] + + def __init__(self, routes: List[Route[StateT]]) -> None: + self._routes = routes + + def fetch(self) -> Callable[ + [Callable[[TurnContext, StateT, dict], Awaitable[None]]], + Callable[[TurnContext, StateT, dict], Awaitable[None]], + ]: + """ + Adds a route for handling the message fetch task activity. + This method can be used as either a decorator or a method. + + ```python + # Use this method as a decorator + @app.messages.fetch() + async def fetch(context: TurnContext, state: TurnState, data: dict): + print(f"Execute with data: {data}") + return True + + # Pass a function to this method + app.messages.fetch()(fetch) + ``` + """ + + # Create route selector for the handler + def __selector__(context: TurnContext) -> bool: + return ( + context.activity.type == ActivityTypes.invoke + and context.activity.name == FETCH_INVOKE_NAME + ) + + def __call__( + func: Callable[[TurnContext, StateT, dict], Awaitable[None]], + ) -> Callable[[TurnContext, StateT, dict], Awaitable[None]]: + async def __handler__(context: TurnContext, state: StateT): + if not context.activity.value: + return False + await func(context, state, context.activity.value) + return True + + self._routes.append(Route[StateT](__selector__, __handler__)) + return func + + return __call__ diff --git a/python/packages/ai/tests/test_messages.py b/python/packages/ai/tests/test_messages.py new file mode 100644 index 000000000..bd714ad7e --- /dev/null +++ b/python/packages/ai/tests/test_messages.py @@ -0,0 +1,47 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from unittest import IsolatedAsyncioTestCase, mock + +import pytest +from botbuilder.core import TurnContext +from botbuilder.schema import Activity, ChannelAccount, ConversationAccount + +from teams import Application +from tests.utils import SimpleAdapter + + +class TestMessages(IsolatedAsyncioTestCase): + application: Application + + @pytest.fixture(autouse=True) + def before_each(self): + self.application = Application() + yield + + @pytest.mark.asyncio + async def test_fetch(self): + handler = mock.AsyncMock() # mock handler + self.application.messages.fetch()(handler) + self.assertEqual(len(self.application._routes), 1) + + # The handler will be tirggered + await self.application.on_turn( + TurnContext( + SimpleAdapter(), + Activity( + id="1234", + type="invoke", + name="message/fetchTask", + value={"hello": "world"}, + from_property=ChannelAccount(id="user", name="Task Modules Test User"), + recipient=ChannelAccount(id="bot", name="Task Modules Test Bot"), + conversation=ConversationAccount(id="convo", name="Task Modules Test Convo"), + channel_id="UnitTest", + locale="en-uS", + service_url="https://example.org", + ), + ) + )