diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..5e5c8b15 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/roul/devcontainer-features/mise-python:1": { + "version": "3.9" + } + } +} \ No newline at end of file diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/activity_handler.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/activity_handler.py index af53ba10..9ca5b5c9 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/activity_handler.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/activity_handler.py @@ -84,8 +84,9 @@ async def on_turn( if invoke_response and not turn_context.turn_state.get( ActivityTypes.invoke_response ): + response = invoke_response.model_dump(by_alias=True, exclude_none=True) await turn_context.send_activity( - Activity(value=invoke_response, type=ActivityTypes.invoke_response) + Activity(value=response, type=ActivityTypes.invoke_response) ) elif turn_context.activity.type == ActivityTypes.end_of_conversation: await self.on_end_of_conversation_activity(turn_context) diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/basic_oauth_flow.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/basic_oauth_flow.py index e8f795ee..1d87060d 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/basic_oauth_flow.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/basic_oauth_flow.py @@ -18,7 +18,7 @@ TurnContextProtocol as TurnContext, ) from microsoft.agents.storage import StoreItem -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel from .message_factory import MessageFactory from .card_factory import CardFactory diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py index 18150b5a..803c12d9 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/channel_service_adapter.py @@ -39,7 +39,6 @@ class ChannelServiceAdapter(ChannelAdapter, ABC): _AGENT_CONNECTOR_CLIENT_KEY = "ConnectorClient" - _INVOKE_RESPONSE_KEY = "ChannelServiceAdapter.InvokeResponse" def __init__(self, channel_service_client_factory: ChannelServiceClientFactoryBase): super().__init__() @@ -87,18 +86,15 @@ async def send_activities( response = await connector_client.conversations.reply_to_activity( activity.conversation.id, activity.reply_to_id, - activity.model_dump(by_alias=True, exclude_unset=True), + activity, ) else: response = ( await connector_client.conversations.send_to_conversation( activity.conversation.id, - activity.model_dump(by_alias=True, exclude_unset=True), + activity, ) ) - # TODO: The connector client is not casting the response but returning the JSON, need to fix it appropiatly - if not isinstance(response, ResourceResponse): - response = ResourceResponse.model_validate(response) response = response or ResourceResponse(id=activity.id or "") responses.append(response) @@ -455,12 +451,12 @@ def _process_turn_results(self, context: TurnContext) -> InvokeResponse: # Handle Invoke scenarios where the agent will return a specific body and return code. if context.activity.type == ActivityTypes.invoke: activity_invoke_response: Activity = context.turn_state.get( - self._INVOKE_RESPONSE_KEY + self.INVOKE_RESPONSE_KEY ) if not activity_invoke_response: return InvokeResponse(status=HTTPStatus.NOT_IMPLEMENTED) - return activity_invoke_response.value + return InvokeResponse.model_validate(activity_invoke_response.value) # No body to return return None diff --git a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py index c89d90cd..40d66d77 100644 --- a/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py +++ b/libraries/Builder/microsoft-agents-builder/microsoft/agents/builder/rest_channel_service_client_factory.py @@ -6,11 +6,12 @@ ClaimsIdentity, Connections, ) +from microsoft.agents.authorization import AccessTokenProviderBase from microsoft.agents.connector import ( ConnectorClientBase, - ConnectorClient, UserTokenClient, ) +from microsoft.agents.connector.teams import TeamsConnectorClient from .channel_service_client_factory_base import ChannelServiceClientFactoryBase @@ -46,17 +47,19 @@ async def create_connector_client( "RestChannelServiceClientFactory.create_connector_client: audience can't be None or Empty" ) - token_provider = ( + token_provider: AccessTokenProviderBase = ( self._connections.get_token_provider(claims_identity, service_url) if not use_anonymous else self._ANONYMOUS_TOKEN_PROVIDER ) - return ConnectorClient( + token = await token_provider.get_access_token( + audience, scopes or [f"{audience}/.default"] + ) + + return TeamsConnectorClient( endpoint=service_url, - credential_token_provider=token_provider, - credential_resource_url=audience, - credential_scopes=scopes, + token=token, ) async def create_user_token_client( diff --git a/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/__init__.py b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/__init__.py new file mode 100644 index 00000000..c6446c9c --- /dev/null +++ b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/__init__.py @@ -0,0 +1,4 @@ +from .connector_client import ConnectorClient +from .user_token_client import UserTokenClient + +__all__ = ["ConnectorClient", "UserTokenClient"] diff --git a/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/connector_client.py b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/connector_client.py new file mode 100644 index 00000000..e7262ac4 --- /dev/null +++ b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/connector_client.py @@ -0,0 +1,504 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Connector Client for Microsoft Agents.""" + +import logging +from typing import Any, Optional +import json +from aiohttp import ClientSession +from io import BytesIO + +from microsoft.agents.core.models import ( + Activity, + ChannelAccount, + ConversationParameters, + ConversationResourceResponse, + ResourceResponse, + ConversationsResult, + PagedMembersResult, +) +from microsoft.agents.authorization import ( + AccessTokenProviderBase, +) +from microsoft.agents.connector import ConnectorClientBase +from ..attachments_base import AttachmentsBase +from ..conversations_base import ConversationsBase + + +logger = logging.getLogger("microsoft.agents.connector.client") + + +class AttachmentInfo: + """Information about an attachment.""" + + def __init__(self, **kwargs): + self.name = kwargs.get("name") + self.type = kwargs.get("type") + self.views = kwargs.get("views") + + +class AttachmentData: + """Data for an attachment.""" + + def __init__(self, **kwargs): + self.name = kwargs.get("name") + self.original_base64 = kwargs.get("originalBase64") + self.type = kwargs.get("type") + self.thumbnail_base64 = kwargs.get("thumbnailBase64") + + +def normalize_outgoing_activity(data: Any) -> Any: + """ + Normalizes an outgoing activity object for wire transmission. + + :param data: The activity object to normalize. + :return: The normalized activity object. + """ + # This is a placeholder for any transformations needed + # Similar to the normalizeOutgoingActivity function in TypeScript + return data + + +class AttachmentsOperations(AttachmentsBase): + + def __init__(self, client: ClientSession): + self.client = client + + async def get_attachment_info(self, attachment_id: str) -> AttachmentInfo: + """ + Retrieves attachment information by attachment ID. + + :param attachment_id: The ID of the attachment. + :return: The attachment information. + """ + if attachment_id is None: + raise ValueError("attachmentId is required") + + url = f"v3/attachments/{attachment_id}" + + async with self.client.get(url) as response: + if response.status >= 400: + logger.error(f"Error getting attachment info: {response.status}") + response.raise_for_status() + + data = await response.json() + return AttachmentInfo(**data) + + async def get_attachment(self, attachment_id: str, view_id: str) -> BytesIO: + """ + Retrieves an attachment by attachment ID and view ID. + + :param attachment_id: The ID of the attachment. + :param view_id: The ID of the view. + :return: The attachment as a readable stream. + """ + if attachment_id is None: + raise ValueError("attachmentId is required") + if view_id is None: + raise ValueError("viewId is required") + + url = f"v3/attachments/{attachment_id}/views/{view_id}" + + async with self.client.get(url) as response: + if response.status >= 400: + logger.error(f"Error getting attachment: {response.status}") + response.raise_for_status() + + data = await response.read() + return BytesIO(data) + + +class ConversationsOperations(ConversationsBase): + + def __init__(self, client: ClientSession): + self.client = client + + async def get_conversations( + self, continuation_token: Optional[str] = None + ) -> ConversationsResult: + """ + Retrieves a list of conversations. + + :param continuation_token: The continuation token for pagination. + :return: A list of conversations. + """ + params = ( + {"continuationToken": continuation_token} if continuation_token else None + ) + + async with self.client.get("v3/conversations", params=params) as response: + if response.status >= 400: + logger.error(f"Error getting conversations: {response.status}") + response.raise_for_status() + + data = await response.json() + return ConversationsResult.model_validate(data) + + async def create_conversation( + self, body: ConversationParameters + ) -> ConversationResourceResponse: + """ + Creates a new conversation. + + :param body: The conversation parameters. + :return: The conversation resource response. + """ + + async with self.client.post( + "v3/conversations", + json=body.model_dump(by_alias=True, exclude_unset=True), + ) as response: + if response.status >= 400: + logger.error(f"Error creating conversation: {response.status}") + response.raise_for_status() + + data = await response.json() + return ConversationResourceResponse.model_validate(data) + + async def reply_to_activity( + self, conversation_id: str, activity_id: str, body: Activity + ) -> ResourceResponse: + """ + Replies to an activity in a conversation. + + :param conversation_id: The ID of the conversation. + :param activity_id: The ID of the activity. + :param body: The activity object. + :return: The resource response. + """ + if not conversation_id or not activity_id: + raise ValueError("conversationId and activityId are required") + + url = f"v3/conversations/{conversation_id}/activities/{activity_id}" + + async with self.client.post( + url, + json=body.model_dump(by_alias=True, exclude_unset=True), + ) as response: + if response.status >= 400: + logger.error(f"Error replying to activity: {response.status}") + response.raise_for_status() + + data = await response.json() + logger.info( + f"Reply to conversation/activity: {data.get('id')}, {activity_id}" + ) + return ResourceResponse.model_validate(data) + + async def send_to_conversation( + self, conversation_id: str, body: Activity + ) -> ResourceResponse: + """ + Sends an activity to a conversation. + + :param conversation_id: The ID of the conversation. + :param body: The activity object. + :return: The resource response. + """ + if not conversation_id: + raise ValueError("conversationId is required") + + url = f"v3/conversations/{conversation_id}/activities" + + async with self.client.post( + url, + json=body.model_dump(by_alias=True, exclude_unset=True), + ) as response: + if response.status >= 400: + logger.error(f"Error sending to conversation: {response.status}") + response.raise_for_status() + + data = await response.json() + return ResourceResponse.model_validate(data) + + async def update_activity( + self, conversation_id: str, activity_id: str, body: Activity + ) -> ResourceResponse: + """ + Updates an activity in a conversation. + + :param conversation_id: The ID of the conversation. + :param activity_id: The ID of the activity. + :param body: The activity object. + :return: The resource response. + """ + if not conversation_id or not activity_id: + raise ValueError("conversationId and activityId are required") + + url = f"v3/conversations/{conversation_id}/activities/{activity_id}" + + async with self.client.put( + url, + json=body.model_dump(by_alias=True, exclude_unset=True), + ) as response: + if response.status >= 400: + logger.error(f"Error updating activity: {response.status}") + response.raise_for_status() + + data = await response.json() + return ResourceResponse.model_validate(data) + + async def delete_activity(self, conversation_id: str, activity_id: str) -> None: + """ + Deletes an activity from a conversation. + + :param conversation_id: The ID of the conversation. + :param activity_id: The ID of the activity. + """ + if not conversation_id or not activity_id: + raise ValueError("conversationId and activityId are required") + + url = f"v3/conversations/{conversation_id}/activities/{activity_id}" + + async with self.client.delete(url) as response: + if response.status >= 400: + logger.error(f"Error deleting activity: {response.status}") + response.raise_for_status() + + async def upload_attachment( + self, conversation_id: str, body: AttachmentData + ) -> ResourceResponse: + """ + Uploads an attachment to a conversation. + + :param conversation_id: The ID of the conversation. + :param body: The attachment data. + :return: The resource response. + """ + if conversation_id is None: + raise ValueError("conversationId is required") + + url = f"v3/conversations/{conversation_id}/attachments" + + # Convert the AttachmentData to a dictionary + attachment_dict = { + "name": body.name, + "originalBase64": body.original_base64, + "type": body.type, + "thumbnailBase64": body.thumbnail_base64, + } + + async with self.client.post(url, json=attachment_dict) as response: + if response.status >= 400: + logger.error(f"Error uploading attachment: {response.status}") + response.raise_for_status() + + data = await response.json() + return ResourceResponse.model_validate(data) + + async def get_conversation_members( + self, conversation_id: str + ) -> list[ChannelAccount]: + """ + Gets the members of a conversation. + + :param conversation_id: The ID of the conversation. + :return: A list of members. + """ + if not conversation_id: + raise ValueError("conversationId is required") + + url = f"v3/conversations/{conversation_id}/members" + + async with self.client.get(url) as response: + if response.status >= 400: + logger.error(f"Error getting conversation members: {response.status}") + response.raise_for_status() + + data = await response.json() + return [ChannelAccount.model_validate(member) for member in data] + + async def get_conversation_member( + self, conversation_id: str, member_id: str + ) -> ChannelAccount: + """ + Gets a member of a conversation. + + :param conversation_id: The ID of the conversation. + :param member_id: The ID of the member. + :return: The member. + """ + if not conversation_id or not member_id: + raise ValueError("conversationId and memberId are required") + + url = f"v3/conversations/{conversation_id}/members/{member_id}" + + async with self.client.get(url) as response: + if response.status >= 400: + logger.error(f"Error getting conversation member: {response.status}") + response.raise_for_status() + + data = await response.json() + return ChannelAccount.model_validate(data) + + async def delete_conversation_member( + self, conversation_id: str, member_id: str + ) -> None: + """ + Deletes a member from a conversation. + + :param conversation_id: The ID of the conversation. + :param member_id: The ID of the member. + """ + if not conversation_id or not member_id: + raise ValueError("conversationId and memberId are required") + + url = f"v3/conversations/{conversation_id}/members/{member_id}" + + async with self.client.delete(url) as response: + if response.status >= 400 and response.status != 204: + logger.error(f"Error deleting conversation member: {response.status}") + response.raise_for_status() + + async def get_activity_members( + self, conversation_id: str, activity_id: str + ) -> list[ChannelAccount]: + """ + Gets the members who were involved in an activity. + + :param conversation_id: The ID of the conversation. + :param activity_id: The ID of the activity. + :return: A list of members. + """ + if not conversation_id or not activity_id: + raise ValueError("conversationId and activityId are required") + + url = f"v3/conversations/{conversation_id}/activities/{activity_id}/members" + + async with self.client.get(url) as response: + if response.status >= 400: + logger.error(f"Error getting activity members: {response.status}") + response.raise_for_status() + + data = await response.json() + return [ChannelAccount.model_validate(member) for member in data] + + async def get_conversation_paged_members( + self, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + """ + Gets the members of a conversation with pagination. + + :param conversation_id: The ID of the conversation. + :param page_size: The page size. + :param continuation_token: The continuation token for pagination. + :return: A paged list of members. + """ + if not conversation_id: + raise ValueError("conversationId is required") + + params = {} + if page_size is not None: + params["pageSize"] = str(page_size) + if continuation_token is not None: + params["continuationToken"] = continuation_token + + url = f"v3/conversations/{conversation_id}/pagedmembers" + + async with self.client.get(url, params=params) as response: + if response.status >= 400: + logger.error( + f"Error getting conversation paged members: {response.status}" + ) + response.raise_for_status() + + data = await response.json() + return PagedMembersResult.model_validate(data) + + async def send_conversation_history( + self, conversation_id: str, body: Any + ) -> ResourceResponse: + """ + Sends conversation history to a conversation. + + :param conversation_id: The ID of the conversation. + :param body: The conversation history. + :return: The resource response. + """ + if not conversation_id: + raise ValueError("conversationId is required") + + url = f"v3/conversations/{conversation_id}/activities/history" + + async with self.client.post(url, json=body) as response: + if ( + response.status >= 400 + and response.status != 201 + and response.status != 202 + ): + logger.error(f"Error sending conversation history: {response.status}") + response.raise_for_status() + + data = await response.json() + return ResourceResponse.model_validate(data) + + +class ConnectorClient(ConnectorClientBase): + """ + ConnectorClient is a client for interacting with the Microsoft M365 Agents SDK Connector API. + """ + + def __init__(self, endpoint: str, token: str, *, session: ClientSession = None): + """ + Initialize a new instance of ConnectorClient. + + :param session: The aiohttp ClientSession to use for HTTP requests. + """ + if not endpoint.endswith("/"): + endpoint += "/" + + # Configure headers with JSON acceptance + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + # Create session with the base URL + session = session or ClientSession( + base_url=endpoint, + headers=headers, + ) + + if len(token) > 1: + session.headers.update({"Authorization": f"Bearer {token}"}) + + self.client = session + self._attachments = AttachmentsOperations( + self.client + ) # Will implement if needed + self._conversations = ConversationsOperations( + self.client + ) # Will implement if needed + + @property + def base_uri(self) -> str: + """ + Gets the base URI for the client. + + :return: The base URI. + """ + return str(self.client._base_url) + + @property + def attachments(self) -> AttachmentsBase: + """ + Gets the attachments operations. + + :return: The attachments operations. + """ + return self._attachments + + @property + def conversations(self) -> ConversationsBase: + """ + Gets the conversations operations. + + :return: The conversations operations. + """ + return self._conversations + + async def close(self) -> None: + """Close the HTTP session.""" + if self.client: + await self.client.close() diff --git a/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/user_token_client.py b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/user_token_client.py new file mode 100644 index 00000000..b07f1ec6 --- /dev/null +++ b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/client/user_token_client.py @@ -0,0 +1,304 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""User Token Client for Microsoft Agents.""" + +import logging +from typing import Optional +from aiohttp import ClientSession + +from microsoft.agents.connector import UserTokenClientBase +from ..user_token_base import UserTokenBase +from ..agent_sign_in_base import AgentSignInBase + + +logger = logging.getLogger("microsoft.agents.connector.client.user_token_client") + + +class AgentSignIn(AgentSignInBase): + """Implementation of agent sign-in operations.""" + + def __init__(self, client: ClientSession): + self.client = client + + async def get_sign_in_url( + self, + state: str, + code_challenge: Optional[str] = None, + emulator_url: Optional[str] = None, + final_redirect: Optional[str] = None, + ) -> str: + """ + Get sign-in URL. + + :param state: State parameter for OAuth flow. + :param code_challenge: Code challenge for PKCE. + :param emulator_url: Emulator URL if used. + :param final_redirect: Final redirect URL. + :return: The sign-in URL. + """ + params = {"state": state} + if code_challenge: + params["codeChallenge"] = code_challenge + if emulator_url: + params["emulatorUrl"] = emulator_url + if final_redirect: + params["finalRedirect"] = final_redirect + + async with self.client.get( + "api/agentsignin/getSignInUrl", params=params + ) as response: + if response.status >= 400: + logger.error(f"Error getting sign-in URL: {response.status}") + response.raise_for_status() + + return await response.text() + + async def get_sign_in_resource( + self, + state: str, + code_challenge: Optional[str] = None, + emulator_url: Optional[str] = None, + final_redirect: Optional[str] = None, + ) -> dict: + """ + Get sign-in resource. + + :param state: State parameter for OAuth flow. + :param code_challenge: Code challenge for PKCE. + :param emulator_url: Emulator URL if used. + :param final_redirect: Final redirect URL. + :return: The sign-in resource. + """ + params = {"state": state} + if code_challenge: + params["codeChallenge"] = code_challenge + if emulator_url: + params["emulatorUrl"] = emulator_url + if final_redirect: + params["finalRedirect"] = final_redirect + + async with self.client.get( + "api/agentsignin/getSignInResource", params=params + ) as response: + if response.status >= 400: + logger.error(f"Error getting sign-in resource: {response.status}") + response.raise_for_status() + + data = await response.json() + return data + + +class UserToken(UserTokenBase): + """Implementation of user token operations.""" + + def __init__(self, client: ClientSession): + self.client = client + + async def get_token( + self, + user_id: str, + connection_name: str, + channel_id: Optional[str] = None, + code: Optional[str] = None, + ) -> dict: + """ + Gets a token for a user and connection. + + :param user_id: ID of the user. + :param connection_name: Name of the connection to use. + :param channel_id: ID of the channel. + :param code: Optional authorization code. + :return: A token response. + """ + params = {"userId": user_id, "connectionName": connection_name} + + if channel_id: + params["channelId"] = channel_id + if code: + params["code"] = code + + async with self.client.get("api/usertoken/GetToken", params=params) as response: + if response.status >= 400 and response.status != 404: + logger.error(f"Error getting token: {response.status}") + response.raise_for_status() + + data = await response.json() + return data + + async def get_aad_tokens( + self, + user_id: str, + connection_name: str, + channel_id: Optional[str] = None, + body: Optional[dict] = None, + ) -> dict: + """ + Gets Azure Active Directory tokens for a user and connection. + + :param user_id: ID of the user. + :param connection_name: Name of the connection to use. + :param channel_id: ID of the channel. + :param body: An optional dictionary containing resource URLs. + :return: A dictionary of tokens. + """ + params = {"userId": user_id, "connectionName": connection_name} + + if channel_id: + params["channelId"] = channel_id + + async with self.client.post( + "api/usertoken/GetAadTokens", params=params, json=body + ) as response: + if response.status >= 400: + logger.error(f"Error getting AAD tokens: {response.status}") + response.raise_for_status() + + data = await response.json() + return data + + async def sign_out( + self, + user_id: str, + connection_name: Optional[str] = None, + channel_id: Optional[str] = None, + ) -> None: + """ + Signs the user out from the specified connection. + + :param user_id: ID of the user. + :param connection_name: Name of the connection to use. + :param channel_id: ID of the channel. + """ + params = {"userId": user_id} + + if connection_name: + params["connectionName"] = connection_name + if channel_id: + params["channelId"] = channel_id + + async with self.client.delete( + "api/usertoken/SignOut", params=params + ) as response: + if response.status >= 400 and response.status != 204: + logger.error(f"Error signing out: {response.status}") + response.raise_for_status() + + async def get_token_status( + self, + user_id: str, + channel_id: Optional[str] = None, + include: Optional[str] = None, + ) -> list: + """ + Gets token status for the user. + + :param user_id: ID of the user. + :param channel_id: ID of the channel. + :param include: Optional filter. + :return: A list of token status objects. + """ + params = {"userId": user_id} + + if channel_id: + params["channelId"] = channel_id + if include: + params["include"] = include + + async with self.client.get( + "api/usertoken/GetTokenStatus", params=params + ) as response: + if response.status >= 400: + logger.error(f"Error getting token status: {response.status}") + response.raise_for_status() + + data = await response.json() + return data + + async def exchange_token( + self, + user_id: str, + connection_name: str, + channel_id: str, + body: Optional[dict] = None, + ) -> dict: + """ + Exchanges a token. + + :param user_id: ID of the user. + :param connection_name: Name of the connection to use. + :param channel_id: ID of the channel. + :param body: An optional token exchange request body. + :return: A token response. + """ + params = { + "userId": user_id, + "connectionName": connection_name, + "channelId": channel_id, + } + + async with self.client.post( + "api/usertoken/ExchangeToken", params=params, json=body + ) as response: + if response.status >= 400 and response.status != 404: + logger.error(f"Error exchanging token: {response.status}") + response.raise_for_status() + + data = await response.json() + return data + + +class UserTokenClient(UserTokenClientBase): + """ + UserTokenClient is a client for interacting with the Microsoft M365 Agents SDK User Token API. + """ + + def __init__(self, endpoint: str, token: str, *, session: ClientSession = None): + """ + Initialize a new instance of UserTokenClient. + + :param endpoint: The endpoint URL for the token service. + :param token: The authentication token to use. + :param session: The aiohttp ClientSession to use for HTTP requests. + """ + if not endpoint.endswith("/"): + endpoint += "/" + + # Configure headers with JSON acceptance + headers = {"Accept": "application/json", "Content-Type": "application/json"} + + # Create session with the base URL + session = session or ClientSession( + base_url=endpoint, + headers=headers, + ) + + if len(token) > 1: + session.headers.update({"Authorization": f"Bearer {token}"}) + + self.client = session + self._agent_sign_in = AgentSignIn(self.client) + self._user_token = UserToken(self.client) + + @property + def agent_sign_in(self) -> AgentSignInBase: + """ + Gets the agent sign-in operations. + + :return: The agent sign-in operations. + """ + return self._agent_sign_in + + @property + def user_token(self) -> UserTokenBase: + """ + Gets the user token operations. + + :return: The user token operations. + """ + return self._user_token + + async def close(self) -> None: + """Close the HTTP session.""" + if self.client: + await self.client.close() diff --git a/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/__init__.py b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/__init__.py new file mode 100644 index 00000000..08d665b9 --- /dev/null +++ b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/__init__.py @@ -0,0 +1,5 @@ +from .teams_connector_client import TeamsConnectorClient + +__all__ = [ + "TeamsConnectorClient", +] diff --git a/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/teams_connector_client.py b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/teams_connector_client.py new file mode 100644 index 00000000..80d80908 --- /dev/null +++ b/libraries/Client/microsoft-agents-connector/microsoft/agents/connector/teams/teams_connector_client.py @@ -0,0 +1,333 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Teams Connector Client for Microsoft Agents.""" + +from typing import Any +from aiohttp import ClientSession + +from microsoft.agents.core.models import Activity, ResourceResponse +from microsoft.agents.authorization import ( + AccessTokenProviderBase, + AgentAuthConfiguration, +) +from microsoft.agents.connector.client import ConnectorClient + +from microsoft.agents.core.models import ConversationParameters +from microsoft.agents.core.models.teams import ( + TeamsChannelAccount, + TeamsPagedMembersResult, + TeamDetails, + TeamsMember, + MeetingInfo, + MeetingNotification, + MeetingNotificationResponse, + TeamsBatchOperationResponse, + BatchOperationStateResponse, + BatchFailedEntriesResponse, + CancelOperationResponse, + ChannelInfo, +) + + +class TeamsConnectorClient(ConnectorClient): + """Teams Connector Client for interacting with Teams-specific APIs.""" + + @classmethod + async def create_client_with_auth_async( + cls, + base_url: str, + auth_config: AgentAuthConfiguration, + auth_provider: AccessTokenProviderBase, + scope: str, + ) -> "TeamsConnectorClient": + """ + Creates a new instance of TeamsConnectorClient with authentication. + + :param base_url: The base URL for the API. + :param auth_config: The authentication configuration. + :param auth_provider: The authentication provider. + :param scope: The scope for the authentication token. + :return: A new instance of TeamsConnectorClient. + """ + session = ClientSession( + base_url=base_url, headers={"Accept": "application/json"} + ) + + token = await auth_provider.get_access_token(auth_config, scope) + if len(token) > 1: + session.headers.update({"Authorization": f"Bearer {token}"}) + + return cls(session) + + async def get_conversation_member( + self, conversation_id: str, user_id: str + ) -> TeamsChannelAccount: + """ + Get a member from a conversation. + + :param conversation_id: The ID of the conversation to get the member from. + :param user_id: The ID of the user to get. + :return: The Teams channel account for the user. + """ + if not conversation_id: + raise ValueError("conversation_id is required") + if not user_id: + raise ValueError("user_id is required") + + async with self.client.get( + f"v3/conversations/{conversation_id}/members/{user_id}", + headers={"Content-Type": "application/json"}, + ) as response: + response.raise_for_status() + + json_response = await response.json() + return TeamsChannelAccount.model_validate(json_response) + + async def get_conversation_paged_member( + self, conversation_id: str, page_size: int, continuation_token: str + ) -> TeamsPagedMembersResult: + """ + Get paged members from a conversation. + + :param conversation_id: The ID of the conversation to get the members from. + :param page_size: The number of members to return per page. + :param continuation_token: The continuation token for paging. + :return: The paged members result. + """ + async with self.client.get( + f"v3/conversations/{conversation_id}/pagedMembers", + params={ + "pageSize": page_size, + "continuationToken": continuation_token or "", + }, + ) as response: + response.raise_for_status() + return TeamsPagedMembersResult.model_validate(await response.json()) + + async def fetch_channel_list(self, team_id: str) -> list[ChannelInfo]: + """ + Fetch the list of channels in a team. + + :param team_id: The ID of the team to fetch channels from. + :return: The list of channels. + """ + async with self.client.get(f"v3/teams/{team_id}/conversations") as response: + response.raise_for_status() + json_response = await response.json() + channels_data = json_response.get("conversations", []) + return [ChannelInfo.model_validate(channel) for channel in channels_data] + + async def fetch_team_details(self, team_id: str) -> TeamDetails: + """ + Fetch the details of a team. + + :param team_id: The ID of the team to fetch details for. + :return: The team details. + """ + async with self.client.get(f"v3/teams/{team_id}") as response: + response.raise_for_status() + return TeamDetails.model_validate(await response.json()) + + async def fetch_meeting_participant( + self, meeting_id: str, participant_id: str, tenant_id: str + ) -> Any: + """ + Fetch a participant from a meeting. + + :param meeting_id: The ID of the meeting to fetch the participant from. + :param participant_id: The ID of the participant to fetch. + :param tenant_id: The ID of the tenant. + :return: The participant information. + """ + async with self.client.get( + f"v1/meetings/{meeting_id}/participants/{participant_id}", + params={"tenantId": tenant_id}, + ) as response: + response.raise_for_status() + return await response.json() + + async def fetch_meeting_info(self, meeting_id: str) -> MeetingInfo: + """ + Fetch information about a meeting. + + :param meeting_id: The ID of the meeting to fetch information for. + :return: The meeting information. + """ + async with self.client.get(f"v1/meetings/{meeting_id}") as response: + response.raise_for_status() + return MeetingInfo.model_validate(await response.json()) + + async def create_conversation( + self, conversation_parameters: ConversationParameters + ) -> ResourceResponse: + """ + Creates a new conversation. + + :param conversation_parameters: Parameters to create the conversation. + :return: A resource response with the conversation ID. + """ + async with self.client.post( + "v3/conversations", + json=conversation_parameters.model_dump(by_alias=True, exclude_unset=True), + headers={"Content-Type": "application/json"}, + ) as response: + response.raise_for_status() + return ResourceResponse.model_validate(await response.json()) + + async def send_meeting_notification( + self, meeting_id: str, notification: MeetingNotification + ) -> MeetingNotificationResponse: + """ + Send a notification to a meeting. + + :param meeting_id: The ID of the meeting to send the notification to. + :param notification: The notification to send. + :return: The notification response. + """ + async with self.client.post( + f"v1/meetings/{meeting_id}/notification", + json=notification.model_dump(by_alias=True, exclude_unset=True), + ) as response: + response.raise_for_status() + return MeetingNotificationResponse.model_validate(await response.json()) + + async def send_message_to_list_of_users( + self, activity: Activity, tenant_id: str, members: list[TeamsMember] + ) -> TeamsBatchOperationResponse: + """ + Send a message to a list of users. + + :param activity: The activity to send. + :param tenant_id: The ID of the tenant. + :param members: The list of members to send to. + :return: The batch operation response. + """ + content = { + "activity": activity.model_dump(by_alias=True, exclude_unset=True), + "members": [ + member.model_dump(by_alias=True, exclude_unset=True) + for member in members + ], + "tenantId": tenant_id, + } + + async with self.client.post( + "v3/batch/conversation/users", json=content + ) as response: + response.raise_for_status() + return TeamsBatchOperationResponse.model_validate(await response.json()) + + async def send_message_to_all_users_in_tenant( + self, activity: Activity, tenant_id: str + ) -> TeamsBatchOperationResponse: + """ + Send a message to all users in a tenant. + + :param activity: The activity to send. + :param tenant_id: The ID of the tenant. + :return: The batch operation response. + """ + content = { + "activity": activity.model_dump(by_alias=True, exclude_unset=True), + "tenantId": tenant_id, + } + + async with self.client.post( + "v3/batch/conversation/tenant", json=content + ) as response: + response.raise_for_status() + return TeamsBatchOperationResponse.model_validate(await response.json()) + + async def send_message_to_all_users_in_team( + self, activity: Activity, tenant_id: str, team_id: str + ) -> TeamsBatchOperationResponse: + """ + Send a message to all users in a team. + + :param activity: The activity to send. + :param tenant_id: The ID of the tenant. + :param team_id: The ID of the team. + :return: The batch operation response. + """ + content = { + "activity": activity.model_dump(by_alias=True, exclude_unset=True), + "tenantId": tenant_id, + "teamId": team_id, + } + + async with self.client.post( + "v3/batch/conversation/team", json=content + ) as response: + response.raise_for_status() + return TeamsBatchOperationResponse.model_validate(await response.json()) + + async def send_message_to_list_of_channels( + self, activity: Activity, tenant_id: str, members: list[TeamsMember] + ) -> TeamsBatchOperationResponse: + """ + Send a message to a list of channels. + + :param activity: The activity to send. + :param tenant_id: The ID of the tenant. + :param members: The list of members to send to. + :return: The batch operation response. + """ + content = { + "activity": activity.model_dump(by_alias=True, exclude_unset=True), + "tenantId": tenant_id, + "members": [ + member.model_dump(by_alias=True, exclude_unset=True) + for member in members + ], + } + + async with self.client.post( + "v3/batch/conversation/channels", json=content + ) as response: + response.raise_for_status() + return TeamsBatchOperationResponse.model_validate(await response.json()) + + async def get_operation_state( + self, operation_id: str + ) -> BatchOperationStateResponse: + """ + Get the state of a batch operation. + + :param operation_id: The ID of the operation to get the state for. + :return: The operation state. + """ + async with self.client.get(f"v3/batch/conversation/{operation_id}") as response: + response.raise_for_status() + return BatchOperationStateResponse.model_validate(await response.json()) + + async def get_failed_entries(self, operation_id: str) -> BatchFailedEntriesResponse: + """ + Get failed entries from a batch operation. + + :param operation_id: The ID of the operation to get failed entries for. + :return: The failed entries. + """ + async with self.client.get( + f"v3/batch/conversation/failedentries/{operation_id}" + ) as response: + response.raise_for_status() + return BatchFailedEntriesResponse.model_validate(await response.json()) + + async def cancel_operation(self, operation_id: str) -> CancelOperationResponse: + """ + Cancel a batch operation. + + :param operation_id: The ID of the operation to cancel. + :return: The cancel operation response. + """ + async with self.client.delete( + f"v3/batch/conversation/{operation_id}" + ) as response: + response.raise_for_status() + return CancelOperationResponse.model_validate(await response.json()) + + async def close(self) -> None: + """Close the HTTP session.""" + if self.client: + await self.client.close() diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py index 7c4d1743..8e441ce9 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/channel_account.py @@ -1,3 +1,6 @@ +from typing import Any + +from pydantic import ConfigDict from .agents_model import AgentsModel from ._type_aliases import NonEmptyString @@ -17,8 +20,14 @@ class ChannelAccount(AgentsModel): :type role: str or ~microsoft.agents.protocols.models.RoleTypes """ + model_config = ConfigDict(extra="allow") + id: NonEmptyString name: str = None aad_object_id: NonEmptyString = None role: NonEmptyString = None - properties: object = None + + @property + def properties(self) -> dict[str, Any]: + """Returns the set of properties that are not None.""" + return self.model_extra diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py index c710fdfe..14885d10 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_account.py @@ -1,3 +1,4 @@ +from typing import Optional from .agents_model import AgentsModel from ._type_aliases import NonEmptyString @@ -33,5 +34,5 @@ class ConversationAccount(AgentsModel): name: NonEmptyString = None aad_object_id: NonEmptyString = None role: NonEmptyString = None - tenant_id: NonEmptyString = None + tenant_id: Optional[NonEmptyString] = None properties: object = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py index 1017c706..18136e4d 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/conversation_reference.py @@ -37,7 +37,7 @@ class ConversationReference(AgentsModel): # optionals here are due to webchat activity_id: Optional[NonEmptyString] = None - user: ChannelAccount = None + user: Optional[ChannelAccount] = None agent: ChannelAccount = Field(None, alias="bot") conversation: ConversationAccount channel_id: NonEmptyString diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py index b6bcbae6..35deb08b 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/invoke_response.py @@ -1,11 +1,7 @@ from .agents_model import AgentsModel -from typing import Generic, TypeVar, Optional -AgentModelT = TypeVar("T", bound=AgentsModel) - - -class InvokeResponse(AgentsModel, Generic[AgentModelT]): +class InvokeResponse(AgentsModel): """ Tuple class containing an HTTP Status Code and a JSON serializable object. The HTTP Status code is, in the invoke activity scenario, what will @@ -17,7 +13,7 @@ class InvokeResponse(AgentsModel, Generic[AgentModelT]): """ status: int = None - body: Optional[AgentModelT] = None + body: object = None def is_successful_status_code(self) -> bool: """ diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/__init__.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/__init__.py new file mode 100644 index 00000000..b14d4076 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/__init__.py @@ -0,0 +1,198 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .app_based_link_query import AppBasedLinkQuery +from .batch_operation_response import BatchOperationResponse +from .batch_operation_state_response import BatchOperationStateResponse +from .batch_failed_entries_response import BatchFailedEntriesResponse +from .cancel_operation_response import CancelOperationResponse +from .channel_info import ChannelInfo +from .conversation_list import ConversationList +from .file_consent_card import FileConsentCard +from .file_consent_card_response import FileConsentCardResponse +from .file_download_info import FileDownloadInfo +from .file_info_card import FileInfoCard +from .file_upload_info import FileUploadInfo +from .meeting_details import MeetingDetails +from .meeting_info import MeetingInfo +from .meeting_start_event_details import MeetingStartEventDetails +from .meeting_end_event_details import MeetingEndEventDetails +from .message_actions_payload import MessageActionsPayload +from .message_actions_payload_app import MessageActionsPayloadApp +from .message_actions_payload_attachment import MessageActionsPayloadAttachment +from .message_actions_payload_body import MessageActionsPayloadBody +from .message_actions_payload_conversation import MessageActionsPayloadConversation +from .message_actions_payload_from import MessageActionsPayloadFrom +from .message_actions_payload_mention import MessageActionsPayloadMention +from .message_actions_payload_reaction import MessageActionsPayloadReaction +from .message_actions_payload_user import MessageActionsPayloadUser +from .messaging_extension_action import MessagingExtensionAction +from .messaging_extension_action_response import MessagingExtensionActionResponse +from .messaging_extension_attachment import MessagingExtensionAttachment +from .messaging_extension_parameter import MessagingExtensionParameter +from .messaging_extension_query import MessagingExtensionQuery +from .messaging_extension_query_options import MessagingExtensionQueryOptions +from .messaging_extension_response import MessagingExtensionResponse +from .messaging_extension_result import MessagingExtensionResult +from .messaging_extension_suggested_action import MessagingExtensionSuggestedAction +from .notification_info import NotificationInfo +from .o365_connector_card import O365ConnectorCard +from .o365_connector_card_action_base import O365ConnectorCardActionBase +from .o365_connector_card_action_card import O365ConnectorCardActionCard +from .o365_connector_card_action_query import O365ConnectorCardActionQuery +from .o365_connector_card_date_input import O365ConnectorCardDateInput +from .o365_connector_card_fact import O365ConnectorCardFact +from .o365_connector_card_http_post import O365ConnectorCardHttpPOST +from .o365_connector_card_image import O365ConnectorCardImage +from .o365_connector_card_input_base import O365ConnectorCardInputBase +from .o365_connector_card_multichoice_input import O365ConnectorCardMultichoiceInput +from .o365_connector_card_multichoice_input_choice import ( + O365ConnectorCardMultichoiceInputChoice, +) +from .o365_connector_card_open_uri import O365ConnectorCardOpenUri +from .o365_connector_card_open_uri_target import O365ConnectorCardOpenUriTarget +from .o365_connector_card_section import O365ConnectorCardSection +from .o365_connector_card_text_input import O365ConnectorCardTextInput +from .o365_connector_card_view_action import O365ConnectorCardViewAction +from .signin_state_verification_query import SigninStateVerificationQuery +from .task_module_continue_response import TaskModuleContinueResponse +from .task_module_message_response import TaskModuleMessageResponse +from .task_module_request import TaskModuleRequest +from .task_module_request_context import TaskModuleRequestContext +from .task_module_response import TaskModuleResponse +from .task_module_response_base import TaskModuleResponseBase +from .task_module_task_info import TaskModuleTaskInfo +from .team_details import TeamDetails +from .team_info import TeamInfo +from .teams_channel_account import TeamsChannelAccount +from .teams_channel_data_settings import TeamsChannelDataSettings +from .teams_channel_data import TeamsChannelData +from .teams_member import TeamsMember +from .teams_paged_members_result import TeamsPagedMembersResult +from .tenant_info import TenantInfo +from .teams_meeting_info import TeamsMeetingInfo +from .teams_meeting_participant import TeamsMeetingParticipant +from .meeting_participant_info import MeetingParticipantInfo +from .cache_info import CacheInfo +from .tab_context import TabContext +from .tab_entity_context import TabEntityContext +from .tab_request import TabRequest +from .tab_response_card import TabResponseCard +from .tab_response_cards import TabResponseCards +from .tab_response_payload import TabResponsePayload +from .tab_response import TabResponse +from .tab_submit import TabSubmit +from .tab_submit_data import TabSubmitData +from .tab_suggested_actions import TabSuggestedActions +from .task_module_card_response import TaskModuleCardResponse +from .user_meeting_details import UserMeetingDetails +from .teams_meeting_member import TeamsMeetingMember +from .meeting_participants_event_details import MeetingParticipantsEventDetails +from .read_receipt_info import ReadReceiptInfo +from .bot_config_auth import BotConfigAuth +from .config_auth_response import ConfigAuthResponse +from .config_response import ConfigResponse +from .config_task_response import ConfigTaskResponse +from .meeting_notification_base import MeetingNotificationBase +from .meeting_notification import MeetingNotification +from .meeting_notification_response import MeetingNotificationResponse +from .on_behalf_of import OnBehalfOf +from .teams_batch_operation_response import TeamsBatchOperationResponse + +__all__ = [ + "AppBasedLinkQuery", + "BatchOperationResponse", + "BatchOperationStateResponse", + "BatchFailedEntriesResponse", + "CancelOperationResponse", + "ChannelInfo", + "ConversationList", + "FileConsentCard", + "FileConsentCardResponse", + "FileDownloadInfo", + "FileInfoCard", + "FileUploadInfo", + "MeetingDetails", + "MeetingInfo", + "MeetingStartEventDetails", + "MeetingEndEventDetails", + "MessageActionsPayload", + "MessageActionsPayloadApp", + "MessageActionsPayloadAttachment", + "MessageActionsPayloadBody", + "MessageActionsPayloadConversation", + "MessageActionsPayloadFrom", + "MessageActionsPayloadMention", + "MessageActionsPayloadReaction", + "MessageActionsPayloadUser", + "MessagingExtensionAction", + "MessagingExtensionActionResponse", + "MessagingExtensionAttachment", + "MessagingExtensionParameter", + "MessagingExtensionQuery", + "MessagingExtensionQueryOptions", + "MessagingExtensionResponse", + "MessagingExtensionResult", + "MessagingExtensionSuggestedAction", + "NotificationInfo", + "O365ConnectorCard", + "O365ConnectorCardActionBase", + "O365ConnectorCardActionCard", + "O365ConnectorCardActionQuery", + "O365ConnectorCardDateInput", + "O365ConnectorCardFact", + "O365ConnectorCardHttpPOST", + "O365ConnectorCardImage", + "O365ConnectorCardInputBase", + "O365ConnectorCardMultichoiceInput", + "O365ConnectorCardMultichoiceInputChoice", + "O365ConnectorCardOpenUri", + "O365ConnectorCardOpenUriTarget", + "O365ConnectorCardSection", + "O365ConnectorCardTextInput", + "O365ConnectorCardViewAction", + "SigninStateVerificationQuery", + "TaskModuleContinueResponse", + "TaskModuleMessageResponse", + "TaskModuleRequest", + "TaskModuleRequestContext", + "TaskModuleResponse", + "TaskModuleResponseBase", + "TaskModuleTaskInfo", + "TeamDetails", + "TeamInfo", + "TeamsChannelAccount", + "TeamsChannelDataSettings", + "TeamsChannelData", + "TeamsPagedMembersResult", + "TenantInfo", + "TeamsMember", + "TeamsMeetingInfo", + "TeamsMeetingParticipant", + "MeetingParticipantInfo", + "CacheInfo", + "TabContext", + "TabEntityContext", + "TabRequest", + "TabResponseCard", + "TabResponseCards", + "TabResponsePayload", + "TabResponse", + "TabSubmit", + "TabSubmitData", + "TabSuggestedActions", + "TaskModuleCardResponse", + "UserMeetingDetails", + "TeamsMeetingMember", + "MeetingParticipantsEventDetails", + "ReadReceiptInfo", + "BotConfigAuth", + "ConfigAuthResponse", + "ConfigResponse", + "ConfigTaskResponse", + "MeetingNotificationBase", + "MeetingNotification", + "MeetingNotificationResponse", + "OnBehalfOf", + "TeamsBatchOperationResponse", +] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/app_based_link_query.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/app_based_link_query.py new file mode 100644 index 00000000..703cadc2 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/app_based_link_query.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class AppBasedLinkQuery(AgentsModel): + """Invoke request body type for app-based link query. + + :param url: Url queried by user + :type url: Optional[str] + :param state: The magic code for OAuth Flow + :type state: Optional[str] + """ + + url: Optional[str] + state: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entries_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entries_response.py new file mode 100644 index 00000000..e3a90250 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entries_response.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .batch_failed_entry import BatchFailedEntry + + +class BatchFailedEntriesResponse(AgentsModel): + """ + :param operation_id: Unique identifier of the batch operation. + :type operation_id: str + """ + + continuation_token: str = None + failed_entries_responses: list[BatchFailedEntry] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entry.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entry.py new file mode 100644 index 00000000..cc54e86c --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_failed_entry.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class BatchFailedEntry(AgentsModel): + """ + :param id: Unique identifier of the entry in the batch operation. + :type id: str + :param error: Error message associated with the entry. + :type error: str + """ + + id: str = None + error: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_response.py new file mode 100644 index 00000000..2df77b0e --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_response.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class BatchOperationResponse(AgentsModel): + """ + :param operation_id: Unique identifier of the batch operation. + :type operation_id: str + """ + + operation_id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_state_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_state_response.py new file mode 100644 index 00000000..ddb0fff1 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/batch_operation_state_response.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from datetime import datetime +from typing import Optional +from ..agents_model import AgentsModel + + +class BatchOperationStateResponse(AgentsModel): + """ + :param state: The state of the batch operation. + :type state: str + :param status_map: A map of status codes to their counts. + :type status_map: dict[int, int] + :param retry_after: The time after which the operation can be retried. + :type retry_after: datetime + :param total_entries_count: The total number of entries in the batch operation. + :type total_entries_count: int + """ + + state: str = None + status_map: dict[int, int] = None + retry_after: Optional[datetime] = None + total_entries_count: int = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/bot_config_auth.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/bot_config_auth.py new file mode 100644 index 00000000..915ad535 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/bot_config_auth.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from ..suggested_actions import SuggestedActions + + +class BotConfigAuth(AgentsModel): + """Specifies bot config auth, including type and suggestedActions. + + :param type: The type of bot config auth. + :type type: str + :param suggested_actions: The suggested actions of bot config auth. + :type suggested_actions: SuggestedActions + """ + + type: str = "auth" + suggested_actions: SuggestedActions = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cache_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cache_info.py new file mode 100644 index 00000000..af6ec7b4 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cache_info.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class CacheInfo(AgentsModel): + """A cache info object which notifies Teams how long an object should be cached for. + + :param cache_type: Type of Cache Info + :type cache_type: Optional[str] + :param cache_duration: Duration of the Cached Info. + :type cache_duration: Optional[int] + """ + + cache_type: Optional[str] + cache_duration: Optional[int] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cancel_operation_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cancel_operation_response.py new file mode 100644 index 00000000..db35f3e1 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/cancel_operation_response.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .batch_operation_response import BatchOperationResponse + + +class CancelOperationResponse(BatchOperationResponse): + """ + :param operation_id: Unique identifier of the batch operation. + :type operation_id: str + :param body_as_text: The body of the request as text. + :type body_as_text: str + :param parsed_body: The parsed body of the request. + :type parsed_body: BatchOperationResponse + """ + + operation_id: str = None + body_as_text: str = None + parsed_body: BatchOperationResponse = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/channel_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/channel_info.py new file mode 100644 index 00000000..3bd5c4ee --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/channel_info.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class ChannelInfo(AgentsModel): + """A channel info object which describes the channel. + + :param id: Unique identifier representing a channel + :type id: Optional[str] + :param name: Name of the channel + :type name: Optional[str] + :param type: The channel type + :type type: Optional[str] + """ + + id: Optional[str] = None + name: Optional[str] = None + type: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_auth_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_auth_response.py new file mode 100644 index 00000000..318e37e1 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_auth_response.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field + +from .config_response import ConfigResponse +from .bot_config_auth import BotConfigAuth + + +class ConfigAuthResponse(ConfigResponse): + """Response for configuration authentication. + + :param suggested_actions: Suggested actions for the configuration authentication. + :type suggested_actions: object + """ + + config: BotConfigAuth = Field(default_factory=lambda: BotConfigAuth()) diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response.py new file mode 100644 index 00000000..61cc2f8d --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .config_response_base import ConfigResponseBase +from .cache_info import CacheInfo + + +class ConfigResponse(ConfigResponseBase): + """Envelope for Config Response Payload. + + :param config: The response to the config message. Possible values: 'auth', 'task' + :type config: object + :param cache_info: Response cache info + :type cache_info: CacheInfo + """ + + config: object = None + cache_info: CacheInfo = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response_base.py new file mode 100644 index 00000000..9e9235da --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_response_base.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class ConfigResponseBase(AgentsModel): + """Specifies Invoke response base, including response type. + + :param response_type: Response type for invoke request + :type response_type: str + """ + + response_type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_task_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_task_response.py new file mode 100644 index 00000000..9cdc9b48 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/config_task_response.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field + +from .config_response import ConfigResponse +from .task_module_response_base import TaskModuleResponseBase + + +class ConfigTaskResponse(ConfigResponse): + """Envelope for Config Task Response. + + This class uses TaskModuleResponseBase as the type for the config parameter. + """ + + config: TaskModuleResponseBase = Field( + default_factory=lambda: TaskModuleResponseBase() + ) diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/conversation_list.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/conversation_list.py new file mode 100644 index 00000000..52ddadf5 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/conversation_list.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List + +from .channel_info import ChannelInfo + + +class ConversationList(AgentsModel): + """List of channels under a team. + + :param conversations: List of ChannelInfo objects. + :type conversations: List[ChannelInfo] + """ + + conversations: List[ChannelInfo] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card.py new file mode 100644 index 00000000..dc5d9513 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Any, Optional + + +class FileConsentCard(AgentsModel): + """File consent card attachment. + + :param description: File description. + :type description: str + :param size_in_bytes: Size of the file to be uploaded in Bytes. + :type size_in_bytes: Optional[int] + :param accept_context: Context sent back to the Bot if user consented to upload. + :type accept_context: Any + :param decline_context: Context sent back to the Bot if user declined. + :type decline_context: Any + """ + + description: str + size_in_bytes: Optional[int] + accept_context: Any + decline_context: Any diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card_response.py new file mode 100644 index 00000000..2a1f1804 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_consent_card_response.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Any, Optional + + +class FileConsentCardResponse(AgentsModel): + """Represents the value of the invoke activity sent when the user acts on a file consent card. + + :param action: The action the user took. Possible values include: 'accept', 'decline' + :type action: Optional[str] + :param context: The context associated with the action. + :type context: Optional[Any] + :param upload_info: If the user accepted the file, contains information about the file to be uploaded. + :type upload_info: Optional[Any] + """ + + action: Optional[str] + context: Optional[Any] + upload_info: Optional[Any] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_download_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_download_info.py new file mode 100644 index 00000000..609ab0ae --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_download_info.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class FileDownloadInfo(AgentsModel): + """File download info attachment. + + :param download_url: File download url. + :type download_url: str + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: Optional[str] + """ + + download_url: str + unique_id: str + file_type: str + etag: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_info_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_info_card.py new file mode 100644 index 00000000..c858b1e8 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_info_card.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class FileInfoCard(AgentsModel): + """File info card. + + :param unique_id: Unique Id for the file. + :type unique_id: str + :param file_type: Type of file. + :type file_type: str + :param etag: ETag for the file. + :type etag: Optional[str] + """ + + unique_id: str + file_type: str + etag: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_upload_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_upload_info.py new file mode 100644 index 00000000..cc0faa58 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/file_upload_info.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class FileUploadInfo(AgentsModel): + """Information about the file to be uploaded. + + :param name: Name of the file. + :type name: str + :param upload_url: URL to an upload session that the bot can use to set the file contents. + :type upload_url: str + :param content_url: URL to file. + :type content_url: str + :param unique_id: ID that uniquely identifies the file. + :type unique_id: str + :param file_type: Type of the file. + :type file_type: str + """ + + name: str + upload_url: str + content_url: str + unique_id: str + file_type: str diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details.py new file mode 100644 index 00000000..92153f8b --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MeetingDetails(AgentsModel): + """Specific details of a Teams meeting. + + :param ms_graph_resource_id: The MsGraphResourceId, used specifically for MS Graph API calls. + :type ms_graph_resource_id: str + :param scheduled_start_time: The meeting's scheduled start time, in UTC. + :type scheduled_start_time: str + :param scheduled_end_time: The meeting's scheduled end time, in UTC. + :type scheduled_end_time: str + :param type: The meeting's type. + :type type: str + """ + + ms_graph_resource_id: str = None + scheduled_start_time: str = None + scheduled_end_time: str = None + type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details_base.py new file mode 100644 index 00000000..c66fc430 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_details_base.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel + + +class MeetingDetailsBase(AgentsModel): + """Specific details of a Teams meeting. + + :param id: The meeting's Id, encoded as a BASE64 string. + :type id: str + :param join_url: The URL used to join the meeting. + :type join_url: str + :param title: The title of the meeting. + :type title: str + """ + + id: str = Field(None, alias="uniqueId") + join_url: str = None + title: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_end_event_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_end_event_details.py new file mode 100644 index 00000000..740c3646 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_end_event_details.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class MeetingEndEventDetails(AgentsModel): + """Specific details of a Teams meeting end event. + + :param end_time: Timestamp for meeting end, in UTC. + :type end_time: str + """ + + end_time: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_event_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_event_details.py new file mode 100644 index 00000000..fd604a68 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_event_details.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class MeetingEventDetails(AgentsModel): + """Base class for Teams meeting start and end events. + + :param meeting_type: The meeting's type. + :type meeting_type: str + """ + + meeting_type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_info.py new file mode 100644 index 00000000..c820353b --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_info.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .meeting_details import MeetingDetails +from .teams_channel_account import TeamsChannelAccount +from ..conversation_account import ConversationAccount + + +class MeetingInfo(AgentsModel): + """General information about a Teams meeting. + + :param details: The specific details of a Teams meeting. + :type details: MeetingDetails + :param conversation: The Conversation Account for the meeting. + :type conversation: ConversationAccount + :param organizer: The meeting's organizer details. + :type organizer: TeamsChannelAccount + """ + + details: MeetingDetails = None + conversation: ConversationAccount = None + organizer: TeamsChannelAccount = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification.py new file mode 100644 index 00000000..49b6d34b --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .meeting_notification_base import MeetingNotificationBase +from .targeted_meeting_notification_value import TargetedMeetingNotificationValue + + +class MeetingNotification(MeetingNotificationBase): + """Specifies Bot meeting notification including meeting notification value. + + :param value: Teams Bot meeting notification value. + :type value: TargetedMeetingNotificationValue + """ + + value: TargetedMeetingNotificationValue = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_base.py new file mode 100644 index 00000000..f3667020 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_base.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MeetingNotificationBase(AgentsModel): + """Specifies Bot meeting notification base including channel data and type. + + :param type: Type of Bot meeting notification. + :type type: str + """ + + type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_channel_data.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_channel_data.py new file mode 100644 index 00000000..b7aaee69 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_channel_data.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .on_behalf_of import OnBehalfOf + + +class MeetingNotificationChannelData(AgentsModel): + """Specify Teams Bot meeting notification channel data. + + :param on_behalf_of_list: The Teams Bot meeting notification's OnBehalfOf list. + :type on_behalf_of_list: list[OnBehalfOf] + """ + + on_behalf_of_list: List[OnBehalfOf] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_recipient_failure_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_recipient_failure_info.py new file mode 100644 index 00000000..13edea56 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_recipient_failure_info.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MeetingNotificationRecipientFailureInfo(AgentsModel): + """Information regarding failure to notify a recipient of a meeting notification. + + :param recipient_mri: The MRI for a recipient meeting notification failure. + :type recipient_mri: str + :param error_code: The error code for a meeting notification. + :type error_code: str + :param failure_reason: The reason why a participant meeting notification failed. + :type failure_reason: str + """ + + recipient_mri: str = None + error_code: str = None + failure_reason: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_response.py new file mode 100644 index 00000000..d08dd55e --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_notification_response.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .meeting_notification_recipient_failure_info import ( + MeetingNotificationRecipientFailureInfo, +) + + +class MeetingNotificationResponse(AgentsModel): + """Specifies Bot meeting notification response. + + Contains list of MeetingNotificationRecipientFailureInfo. + + :param recipients_failure_info: The list of MeetingNotificationRecipientFailureInfo. + :type recipients_failure_info: list[MeetingNotificationRecipientFailureInfo] + """ + + recipients_failure_info: List[MeetingNotificationRecipientFailureInfo] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participant_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participant_info.py new file mode 100644 index 00000000..edc00a73 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participant_info.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class MeetingParticipantInfo(AgentsModel): + """Information about a meeting participant. + + :param role: The role of the participant in the meeting. + :type role: str + :param in_meeting: Indicates whether the participant is currently in the meeting. + :type in_meeting: bool + """ + + role: str = None + in_meeting: bool = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participants_event_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participants_event_details.py new file mode 100644 index 00000000..6d55f14f --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_participants_event_details.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .teams_meeting_member import TeamsMeetingMember + + +class MeetingParticipantsEventDetails(AgentsModel): + """Data about the meeting participants. + + :param members: The members involved in the meeting event. + :type members: list[TeamsMeetingMember] + """ + + members: List[TeamsMeetingMember] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_stage_surface.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_stage_surface.py new file mode 100644 index 00000000..0cf08790 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_stage_surface.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum + +from .surface import Surface, SurfaceType + + +class ContentType(int, Enum): + UNKNOWN = 0 + TASK = 1 + + +class MeetingStageSurface(Surface): + """Specifies meeting stage surface. + + :param content_type: The content type of this MeetingStageSurface. + :type content_type: ContentType + :param content: The content of this MeetingStageSurface. + :type content: Any + """ + + type: SurfaceType = SurfaceType.MEETING_STAGE + content_type: ContentType = ContentType.TASK + content: object = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_start_event_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_start_event_details.py new file mode 100644 index 00000000..825fefcd --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_start_event_details.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class MeetingStartEventDetails(AgentsModel): + """Specific details of a Teams meeting start event. + + :param start_time: Timestamp for meeting start, in UTC. + :type start_time: str + """ + + start_time: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_tab_icon_surface.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_tab_icon_surface.py new file mode 100644 index 00000000..4aba73ba --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/meeting_tab_icon_surface.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .surface import Surface, SurfaceType + + +class MeetingTabIconSurface(Surface): + """Specifies meeting tab icon surface. + + :param tab_entity_id: The tab entity Id of this MeetingTabIconSurface. + :type tab_entity_id: str + """ + + type: SurfaceType = SurfaceType.MEETING_TAB_ICON + tab_entity_id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload.py new file mode 100644 index 00000000..e0a07db5 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Annotated, List + +from .message_actions_payload_from import MessageActionsPayloadFrom +from .message_actions_payload_body import MessageActionsPayloadBody +from .message_actions_payload_attachment import MessageActionsPayloadAttachment +from .message_actions_payload_mention import MessageActionsPayloadMention +from .message_actions_payload_reaction import MessageActionsPayloadReaction + + +class MessageActionsPayload(AgentsModel): + """Represents the individual message within a chat or channel where a message action is taken. + + :param id: Unique id of the message. + :type id: str + :param reply_to_id: Id of the parent/root message of the thread. + :type reply_to_id: str + :param message_type: Type of message - automatically set to message. + :type message_type: str + :param created_date_time: Timestamp of when the message was created. + :type created_date_time: str + :param last_modified_date_time: Timestamp of when the message was edited or updated. + :type last_modified_date_time: str + :param deleted: Indicates whether a message has been soft deleted. + :type deleted: bool + :param subject: Subject line of the message. + :type subject: str + :param summary: Summary text of the message that could be used for notifications. + :type summary: str + :param importance: The importance of the message. Possible values include: 'normal', 'high', 'urgent' + :type importance: Annotated[str, Field(pattern=r"^(normal|high|urgent)$")] + :param locale: Locale of the message set by the client. + :type locale: str + :param link_to_message: Link back to the message. + :type link_to_message: str + :param from_property: Sender of the message. + :type from_property: MessageActionsPayloadFrom + :param body: Plaintext/HTML representation of the content of the message. + :type body: MessageActionsPayloadBody + :param attachment_layout: How the attachment(s) are displayed in the message. + :type attachment_layout: str + :param attachments: Attachments in the message - card, image, file, etc. + :type attachments: List[MessageActionsPayloadAttachment] + :param mentions: List of entities mentioned in the message. + :type mentions: List[MessageActionsPayloadMention] + :param reactions: Reactions for the message. + :type reactions: List[MessageActionsPayloadReaction] + """ + + id: str = None + reply_to_id: str = None + message_type: str = None + created_date_time: str = None + last_modified_date_time: str = None + deleted: bool = None + subject: str = None + summary: str = None + importance: Annotated[str, Field(pattern=r"^(normal|high|urgent)$")] = None + locale: str = None + link_to_message: str = None + from_property: MessageActionsPayloadFrom = None + body: MessageActionsPayloadBody = None + attachment_layout: str = None + attachments: List[MessageActionsPayloadAttachment] = None + mentions: List[MessageActionsPayloadMention] = None + reactions: List[MessageActionsPayloadReaction] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_app.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_app.py new file mode 100644 index 00000000..6768a01b --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_app.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Annotated, Optional + + +class MessageActionsPayloadApp(AgentsModel): + """Represents an application entity. + + :param application_identity_type: The type of application. Possible values include: 'aadApplication', 'bot', 'tenantBot', 'office365Connector', 'webhook' + :type application_identity_type: Optional[str] + :param id: The id of the application. + :type id: Optional[str] + :param display_name: The plaintext display name of the application. + :type display_name: Optional[str] + """ + + application_identity_type: Optional[ + Annotated[ + str, + Field( + pattern=r"^(aadApplication|bot|tenantBot|office365Connector|webhook)$" + ), + ] + ] + id: Optional[str] + display_name: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_attachment.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_attachment.py new file mode 100644 index 00000000..7b87d72e --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_attachment.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Any, Optional + + +class MessageActionsPayloadAttachment(AgentsModel): + """Represents the attachment in a message. + + :param id: The id of the attachment. + :type id: Optional[str] + :param content_type: The type of the attachment. + :type content_type: Optional[str] + :param content_url: The url of the attachment, in case of an external link. + :type content_url: Optional[str] + :param content: The content of the attachment, in case of a code snippet, email, or file. + :type content: Optional[Any] + :param name: The plaintext display name of the attachment. + :type name: Optional[str] + :param thumbnail_url: The url of a thumbnail image that might be embedded in the attachment, in case of a card. + :type thumbnail_url: Optional[str] + """ + + id: Optional[str] + content_type: Optional[str] + content_url: Optional[str] + content: Optional[Any] + name: Optional[str] + thumbnail_url: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_body.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_body.py new file mode 100644 index 00000000..4760d7a4 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_body.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MessageActionsPayloadBody(AgentsModel): + """Plaintext/HTML representation of the content of the message. + + :param content_type: Type of the content. Possible values include: 'html', 'text' + :type content_type: str + :param content: The content of the body. + :type content: str + """ + + content_type: str = None + content: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_conversation.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_conversation.py new file mode 100644 index 00000000..97992772 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_conversation.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MessageActionsPayloadConversation(AgentsModel): + """Represents a team or channel entity. + + :param conversation_identity_type: The type of conversation, whether a team or channel. + :type conversation_identity_type: str + :param id: The id of the team or channel. + :type id: str + :param display_name: The plaintext display name of the team or channel entity. + :type display_name: str + """ + + conversation_identity_type: str = None + id: str = None + display_name: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_from.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_from.py new file mode 100644 index 00000000..cd4f033f --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_from.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + +from .message_actions_payload_user import MessageActionsPayloadUser +from .message_actions_payload_app import MessageActionsPayloadApp +from .message_actions_payload_conversation import MessageActionsPayloadConversation + + +class MessageActionsPayloadFrom(AgentsModel): + """Represents a user, application, or conversation type that either sent or was referenced in a message. + + :param user: Represents details of the user. + :type user: Optional["MessageActionsPayloadUser"] + :param application: Represents details of the app. + :type application: Optional["MessageActionsPayloadApp"] + :param conversation: Represents details of the conversation. + :type conversation: Optional["MessageActionsPayloadConversation"] + """ + + user: Optional[MessageActionsPayloadUser] + application: Optional[MessageActionsPayloadApp] + conversation: Optional[MessageActionsPayloadConversation] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_mention.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_mention.py new file mode 100644 index 00000000..1c72c1da --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_mention.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + +from .message_actions_payload_from import MessageActionsPayloadFrom + + +class MessageActionsPayloadMention(AgentsModel): + """Represents the entity that was mentioned in the message. + + :param id: The id of the mentioned entity. + :type id: Optional[int] + :param mention_text: The plaintext display name of the mentioned entity. + :type mention_text: Optional[str] + :param mentioned: Provides more details on the mentioned entity. + :type mentioned: Optional["MessageActionsPayloadFrom"] + """ + + id: Optional[int] + mention_text: Optional[str] + mentioned: Optional[MessageActionsPayloadFrom] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_reaction.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_reaction.py new file mode 100644 index 00000000..d24a0020 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_reaction.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + +from .message_actions_payload_from import MessageActionsPayloadFrom + + +class MessageActionsPayloadReaction(AgentsModel): + """Represents the reaction of a user to a message. + + :param reaction_type: The type of reaction given to the message. Possible values include: 'like', 'heart', 'laugh', 'surprised', 'sad', 'angry' + :type reaction_type: Optional[str] + :param created_date_time: Timestamp of when the user reacted to the message. + :type created_date_time: Optional[str] + :param user: The user with which the reaction is associated. + :type user: Optional["MessageActionsPayloadFrom"] + """ + + reaction_type: Optional[str] + created_date_time: Optional[str] + user: Optional[MessageActionsPayloadFrom] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_user.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_user.py new file mode 100644 index 00000000..17c5f1e6 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/message_actions_payload_user.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Annotated, Optional + + +class MessageActionsPayloadUser(AgentsModel): + """Represents a user entity. + + :param user_identity_type: The identity type of the user. Possible values include: 'aadUser', 'onPremiseAadUser', 'anonymousGuest', 'federatedUser' + :type user_identity_type: Optional[str] + :param id: The id of the user. + :type id: Optional[str] + :param display_name: The plaintext display name of the user. + :type display_name: Optional[str] + """ + + user_identity_type: Optional[ + Annotated[ + str, + Field(pattern=r"^(aadUser|onPremiseAadUser|anonymousGuest|federatedUser)$"), + ] + ] + id: Optional[str] + display_name: Optional[str] diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action.py new file mode 100644 index 00000000..2e47c7d3 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Any, List + +from .task_module_request_context import TaskModuleRequestContext +from .message_actions_payload import MessageActionsPayload +from ..activity import Activity + + +class MessagingExtensionAction(AgentsModel): + """Messaging extension action. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: TaskModuleRequestContext + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param command_context: The context from which the command originates. Possible values include: 'message', 'compose', 'commandbox' + :type command_context: str + :param bot_message_preview_action: Bot message preview action taken by user. Possible values include: 'edit', 'send' + :type bot_message_preview_action: str + :param bot_activity_preview: List of bot activity previews. + :type bot_activity_preview: List[Activity] + :param message_payload: Message content sent as part of the command request. + :type message_payload: MessageActionsPayload + """ + + data: object = None + context: TaskModuleRequestContext = None + command_id: str = None + command_context: str = None + bot_message_preview_action: str = None + bot_activity_preview: List[Activity] = None + message_payload: MessageActionsPayload = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action_response.py new file mode 100644 index 00000000..ec6ffe12 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_action_response.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .task_module_response_base import TaskModuleResponseBase +from .messaging_extension_result import MessagingExtensionResult +from .cache_info import CacheInfo + + +class MessagingExtensionActionResponse(AgentsModel): + """Response of messaging extension action. + + :param task: The JSON for the Adaptive card to appear in the task module. + :type task: "TaskModuleResponseBase" + :param compose_extension: The compose extension result. + :type compose_extension: "MessagingExtensionResult" + :param cache_info: CacheInfo for this MessagingExtensionActionResponse. + :type cache_info: "CacheInfo" + """ + + task: TaskModuleResponseBase = None + compose_extension: MessagingExtensionResult = None + cache_info: CacheInfo = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_attachment.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_attachment.py new file mode 100644 index 00000000..649dc838 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_attachment.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + +from ..attachment import Attachment + + +class MessagingExtensionAttachment(AgentsModel): + """Messaging extension attachment. + + :param content_type: mimetype/Contenttype for the file + :type content_type: str + :param content_url: Content Url + :type content_url: str + :param content: Embedded content + :type content: object + :param name: (OPTIONAL) The name of the attachment + :type name: str + :param thumbnail_url: (OPTIONAL) Thumbnail associated with attachment + :type thumbnail_url: str + :param preview: Preview attachment + :type preview: "Attachment" + """ + + content_type: str + content_url: str + content: object = None + name: Optional[str] = None + thumbnail_url: Optional[str] = None + preview: Attachment = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_parameter.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_parameter.py new file mode 100644 index 00000000..2513ff18 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_parameter.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MessagingExtensionParameter(AgentsModel): + """Messaging extension query parameters. + + :param name: Name of the parameter + :type name: str + :param value: Value of the parameter + :type value: Any + """ + + name: str = None + value: object = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query.py new file mode 100644 index 00000000..c784ec9c --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List, Optional + +from .messaging_extension_parameter import MessagingExtensionParameter +from .messaging_extension_query_options import MessagingExtensionQueryOptions + + +class MessagingExtensionQuery(AgentsModel): + """Messaging extension query. + + :param command_id: Id of the command assigned by Bot + :type command_id: str + :param parameters: Parameters for the query + :type parameters: List["MessagingExtensionParameter"] + :param query_options: Query options for the extension + :type query_options: Optional["MessagingExtensionQueryOptions"] + :param state: State parameter passed back to the bot after authentication/configuration flow + :type state: str + """ + + command_id: str = None + parameters: List[MessagingExtensionParameter] = None + query_options: Optional[MessagingExtensionQueryOptions] = None + state: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query_options.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query_options.py new file mode 100644 index 00000000..07dc5239 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_query_options.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class MessagingExtensionQueryOptions(AgentsModel): + """Messaging extension query options. + + :param skip: Number of entities to skip + :type skip: int + :param count: Number of entities to fetch + :type count: int + """ + + skip: int = None + count: int = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_response.py new file mode 100644 index 00000000..86e1464e --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_response.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + +from .messaging_extension_result import MessagingExtensionResult +from .cache_info import CacheInfo + + +class MessagingExtensionResponse(AgentsModel): + """Messaging extension response. + + :param compose_extension: The compose extension result. + :type compose_extension: Optional["MessagingExtensionResult"] + :param cache_info: CacheInfo for this MessagingExtensionResponse. + :type cache_info: Optional["CacheInfo"] + """ + + compose_extension: Optional[MessagingExtensionResult] = None + cache_info: Optional[CacheInfo] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_result.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_result.py new file mode 100644 index 00000000..24f4d856 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_result.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List, Optional + +from .messaging_extension_attachment import MessagingExtensionAttachment +from .messaging_extension_suggested_action import MessagingExtensionSuggestedAction +from ..activity import Activity + + +class MessagingExtensionResult(AgentsModel): + """Messaging extension result. + + :param attachment_layout: Hint for how to deal with multiple attachments. + :type attachment_layout: str + :param type: The type of the result. Possible values include: 'result', 'auth', 'config', 'message', 'botMessagePreview' + :type type: str + :param attachments: (Only when type is result) Attachments + :type attachments: List["MessagingExtensionAttachment"] + :param suggested_actions: Suggested actions for the result. + :type suggested_actions: Optional["MessagingExtensionSuggestedAction"] + :param text: (Only when type is message) Text + :type text: Optional[str] + :param activity_preview: (Only when type is botMessagePreview) Message activity to preview + :type activity_preview: Optional["Activity"] + """ + + attachment_layout: str = None + type: str = None + attachments: List[MessagingExtensionAttachment] = None + suggested_actions: Optional[MessagingExtensionSuggestedAction] = None + text: Optional[str] = None + activity_preview: Optional["Activity"] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_suggested_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_suggested_action.py new file mode 100644 index 00000000..6829cccc --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/messaging_extension_suggested_action.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List + +from ..card_action import CardAction + + +class MessagingExtensionSuggestedAction(AgentsModel): + """Messaging extension suggested actions. + + :param actions: List of suggested actions. + :type actions: List["CardAction"] + """ + + actions: List[CardAction] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/notification_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/notification_info.py new file mode 100644 index 00000000..5d8767de --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/notification_info.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class NotificationInfo(AgentsModel): + """Specifies if a notification is to be sent for the mentions. + + :param alert: True if notification is to be sent to the user, false otherwise. + :type alert: bool + :param alert_in_meeting: True if notification is to be sent in a meeting context. + :type alert_in_meeting: Optional[bool] + :param external_resource_url: URL for external resources related to the notification. + :type external_resource_url: Optional[str] + """ + + alert: bool = None + alert_in_meeting: Optional[bool] = None + external_resource_url: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card.py new file mode 100644 index 00000000..afa2ffed --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List, Optional +from .o365_connector_card_section import O365ConnectorCardSection +from .o365_connector_card_action_base import O365ConnectorCardActionBase + + +class O365ConnectorCard(AgentsModel): + """O365 connector card. + + :param title: Title of the item + :type title: str + :param text: Text for the card + :type text: Optional[str] + :param summary: Summary for the card + :type summary: Optional[str] + :param theme_color: Theme color for the card + :type theme_color: Optional[str] + :param sections: Set of sections for the current card + :type sections: Optional[List["O365ConnectorCardSection"]] + :param potential_action: Set of actions for the current card + :type potential_action: Optional[List["O365ConnectorCardActionBase"]] + """ + + title: str = None + text: Optional[str] = None + summary: Optional[str] = None + theme_color: Optional[str] = None + sections: Optional[List[O365ConnectorCardSection]] = None + potential_action: Optional[List[O365ConnectorCardActionBase]] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_base.py new file mode 100644 index 00000000..afc52718 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_base.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel + + +class O365ConnectorCardActionBase(AgentsModel): + """O365 connector card action base. + + :param type: Type of the action. Possible values include: 'ViewAction', 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + """ + + type: str = Field(None, alias="@type") + name: str = None + id: str = Field(None, alias="@id") diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_card.py new file mode 100644 index 00000000..a72528aa --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_card.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import List +from .o365_connector_card_input_base import O365ConnectorCardInputBase +from .o365_connector_card_action_base import O365ConnectorCardActionBase + + +class O365ConnectorCardActionCard(AgentsModel): + """O365 connector card ActionCard action. + + :param type: Type of the action. Possible values include: 'ViewAction', 'OpenUri', 'HttpPOST', 'ActionCard' + :type type: str + :param name: Name of the action that will be used as button title + :type name: str + :param id: Action Id + :type id: str + :param inputs: Set of inputs contained in this ActionCard + :type inputs: List["O365ConnectorCardInputBase"] + :param actions: Set of actions contained in this ActionCard + :type actions: List["O365ConnectorCardActionBase"] + """ + + type: str = Field(None, alias="@type") + name: str = None + id: str = Field(None, alias="@id") + inputs: List[O365ConnectorCardInputBase] = None + actions: List[O365ConnectorCardActionBase] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_query.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_query.py new file mode 100644 index 00000000..f2029de7 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_action_query.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class O365ConnectorCardActionQuery(AgentsModel): + """O365 connector card action query. + + :param body: Body of the action query. + :type body: str + """ + + body: str = None + action_id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_date_input.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_date_input.py new file mode 100644 index 00000000..a1161558 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_date_input.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel + + +class O365ConnectorCardDateInput(AgentsModel): + """O365 connector card date input. + + :param type: Input type name. Possible values include: 'textInput', 'dateInput', 'multichoiceInput' + :type type: str + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default value is false. + :type is_required: bool + :param title: Input title that will be shown as the placeholder + :type title: str + :param value: Default value for this input field + :type value: str + :param include_time: Include time input field. Default value is false (date only). + :type include_time: bool + """ + + type: str = Field(None, alias="@type") + id: str = None + is_required: bool = None + title: str = None + value: str = None + include_time: bool = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_fact.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_fact.py new file mode 100644 index 00000000..46941c6f --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_fact.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class O365ConnectorCardFact(AgentsModel): + """O365 connector card fact. + + :param name: Display name of the fact + :type name: str + :param value: Display value for the fact + :type value: str + """ + + name: str = None + value: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_http_post.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_http_post.py new file mode 100644 index 00000000..10c4a693 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_http_post.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Optional + + +class O365ConnectorCardHttpPOST(AgentsModel): + """O365 connector card HttpPOST action. + + :param type: Type of the action. Default is 'HttpPOST'. + :type type: str + :param name: Name of the HttpPOST action. + :type name: str + :param id: Id of the HttpPOST action. + :type id: str + :param body: Content of the HttpPOST action. + :type body: Optional[str] + """ + + type: str = Field(None, alias="@type") + name: str = None + id: str = Field(None, alias="@id") + body: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_image.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_image.py new file mode 100644 index 00000000..7f2bd6a6 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_image.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class O365ConnectorCardImage(AgentsModel): + """O365 connector card image. + + :param image: URL for the image. + :type image: str + :param title: Title of the image. + :type title: Optional[str] + """ + + image: str = None + title: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_input_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_input_base.py new file mode 100644 index 00000000..f7b210f5 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_input_base.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Optional + + +class O365ConnectorCardInputBase(AgentsModel): + """Base class for O365 connector card inputs. + + :param type: Input type name. Possible values include: 'textInput', 'dateInput', 'multichoiceInput' + :type type: str + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default value is false. + :type is_required: Optional[bool] + :param title: Input title that will be shown as the placeholder + :type title: Optional[str] + :param value: Default value for this input field + :type value: Optional[str] + """ + + type: str = Field(None, alias="@type") + id: str = None + is_required: Optional[bool] = None + title: Optional[str] = None + value: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input.py new file mode 100644 index 00000000..efb41ead --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import List, Optional +from .o365_connector_card_multichoice_input_choice import ( + O365ConnectorCardMultichoiceInputChoice, +) + + +class O365ConnectorCardMultichoiceInput(AgentsModel): + """O365 connector card multichoice input. + + :param type: Input type name. Default is 'multichoiceInput'. + :type type: str + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default value is false. + :type is_required: Optional[bool] + :param title: Input title that will be shown as the placeholder + :type title: Optional[str] + :param value: Default value for this input field + :type value: Optional[str] + :param choices: Set of choices for this input field. + :type choices: List["O365ConnectorCardMultichoiceInputChoice"] + :param style: Choice style. Possible values include: 'compact', 'expanded' + :type style: Optional[str] + :param is_multi_select: Define if this input field allows multiple selections. Default value is false. + :type is_multi_select: Optional[bool] + """ + + type: str = Field(None, alias="@type") + id: str = None + is_required: Optional[bool] = None + title: Optional[str] = None + value: Optional[str] = None + choices: List[O365ConnectorCardMultichoiceInputChoice] = None + style: Optional[str] = None + is_multi_select: Optional[bool] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input_choice.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input_choice.py new file mode 100644 index 00000000..5200bd5f --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_multichoice_input_choice.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class O365ConnectorCardMultichoiceInputChoice(AgentsModel): + """O365 connector card multichoice input choice. + + :param display: Display text for the choice. + :type display: str + :param value: Value for the choice. + :type value: str + """ + + display: str = None + value: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri.py new file mode 100644 index 00000000..7d466a01 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import List +from .o365_connector_card_open_uri_target import O365ConnectorCardOpenUriTarget + + +class O365ConnectorCardOpenUri(AgentsModel): + """O365 connector card OpenUri action. + + :param type: Type of the action. Default is 'OpenUri'. + :type type: str + :param name: Name of the OpenUri action. + :type name: str + :param id: Id of the OpenUri action. + :type id: str + :param targets: List of targets for the OpenUri action. + :type targets: List["O365ConnectorCardOpenUriTarget"] + """ + + type: str = Field(None, alias="@type") + name: str = None + id: str = Field(None, alias="@id") + targets: List[O365ConnectorCardOpenUriTarget] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri_target.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri_target.py new file mode 100644 index 00000000..78545d37 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_open_uri_target.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class O365ConnectorCardOpenUriTarget(AgentsModel): + """O365 connector card OpenUri target. + + :param os: Target operating system. Possible values include: 'default', 'iOS', 'android', 'windows' + :type os: str + :param uri: Target URI. + :type uri: str + """ + + os: str = None + uri: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_section.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_section.py new file mode 100644 index 00000000..6bb0d700 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_section.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List, Optional +from .o365_connector_card_fact import O365ConnectorCardFact +from .o365_connector_card_image import O365ConnectorCardImage +from .o365_connector_card_action_base import O365ConnectorCardActionBase + + +class O365ConnectorCardSection(AgentsModel): + """O365 connector card section. + + :param title: Title of the section. + :type title: Optional[str] + :param text: Text for the section. + :type text: Optional[str] + :param activity_title: Activity title. + :type activity_title: Optional[str] + :param activity_subtitle: Activity subtitle. + :type activity_subtitle: Optional[str] + :param activity_image: Activity image URL. + :type activity_image: Optional[str] + :param activity_text: Activity text. + :type activity_text: Optional[str] + :param facts: List of facts for the section. + :type facts: Optional[List["O365ConnectorCardFact"]] + :param images: List of images for the section. + :type images: Optional[List["O365ConnectorCardImage"]] + :param potential_action: List of actions for the section. + :type potential_action: Optional[List["O365ConnectorCardActionBase"]] + """ + + title: Optional[str] = None + text: Optional[str] = None + activity_title: Optional[str] = None + activity_subtitle: Optional[str] = None + activity_image: Optional[str] = None + activity_text: Optional[str] = None + facts: Optional[List[O365ConnectorCardFact]] = None + images: Optional[List[O365ConnectorCardImage]] = None + potential_action: Optional[List[O365ConnectorCardActionBase]] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_text_input.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_text_input.py new file mode 100644 index 00000000..613790fa --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_text_input.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Optional + + +class O365ConnectorCardTextInput(AgentsModel): + """O365 connector card text input. + + :param type: Input type name. Default is 'textInput'. + :type type: str + :param id: Input Id. It must be unique per entire O365 connector card. + :type id: str + :param is_required: Define if this input is a required field. Default value is false. + :type is_required: Optional[bool] + :param title: Input title that will be shown as the placeholder + :type title: Optional[str] + :param value: Default value for this input field + :type value: Optional[str] + :param is_multiline: Define if this input field allows multiple lines of text. Default value is false. + :type is_multiline: Optional[bool] + """ + + type: str = Field(None, alias="@type") + id: str = None + is_required: Optional[bool] = None + title: Optional[str] = None + value: Optional[str] = None + is_multiline: Optional[bool] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_view_action.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_view_action.py new file mode 100644 index 00000000..484fd036 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/o365_connector_card_view_action.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Optional + + +class O365ConnectorCardViewAction(AgentsModel): + """O365 connector card ViewAction action. + + :param type: Type of the action. Default is 'ViewAction'. + :type type: str + :param name: Name of the ViewAction action. + :type name: str + :param id: Id of the ViewAction action. + :type id: str + :param target: Target URL for the ViewAction action. + :type target: Optional[str] + """ + + type: str = Field(None, alias="@type") + name: str = None + id: str = Field(None, alias="@id") + target: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/on_behalf_of.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/on_behalf_of.py new file mode 100644 index 00000000..3026890e --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/on_behalf_of.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class OnBehalfOf(AgentsModel): + """Specifies the OnBehalfOf entity for meeting notifications. + + :param item_id: The item id of the OnBehalfOf entity. + :type item_id: str + :param mention_type: The mention type. Default is "person". + :type mention_type: str + :param display_name: The display name of the OnBehalfOf entity. + :type display_name: str + :param mri: The MRI of the OnBehalfOf entity. + :type mri: str + """ + + item_id: str = None + mention_type: str = None + display_name: str = None + mri: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/read_receipt_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/read_receipt_info.py new file mode 100644 index 00000000..144cd880 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/read_receipt_info.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class ReadReceiptInfo(AgentsModel): + """General information about a read receipt. + + :param last_read_message_id: The id of the last read message. + :type last_read_message_id: str + """ + + last_read_message_id: str = None + + @staticmethod + def is_message_read(compare_message_id: str, last_read_message_id: str) -> bool: + """ + Helper method useful for determining if a message has been read. + This method converts the strings to integers. If the compare_message_id is + less than or equal to the last_read_message_id, then the message has been read. + + :param compare_message_id: The id of the message to compare. + :param last_read_message_id: The id of the last message read by the user. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + if not compare_message_id or not last_read_message_id: + return False + + try: + compare_message_id_long = int(compare_message_id) + last_read_message_id_long = int(last_read_message_id) + except ValueError: + return False + + return compare_message_id_long <= last_read_message_id_long + + def is_message_read_instance(self, compare_message_id: str) -> bool: + """ + Helper method useful for determining if a message has been read. + If the compare_message_id is less than or equal to the last_read_message_id, + then the message has been read. + + :param compare_message_id: The id of the message to compare. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + return self.is_message_read(compare_message_id, self.last_read_message_id) diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/signin_state_verification_query.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/signin_state_verification_query.py new file mode 100644 index 00000000..4d545745 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/signin_state_verification_query.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class SigninStateVerificationQuery(AgentsModel): + """Represents the state verification query for sign-in. + + :param state: The state value used for verification. + :type state: str + """ + + state: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/surface.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/surface.py new file mode 100644 index 00000000..4bc5c600 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/surface.py @@ -0,0 +1,18 @@ +from ..agents_model import AgentsModel +from enum import Enum + + +class SurfaceType(int, Enum): + UNKNOWN = 0 + MEETING_STAGE = 1 + MEETING_TAB_ICON = 2 + + +class Surface(AgentsModel): + """Specifies where the notification will be rendered in the meeting UX. + + :param type: The value indicating where the notification will be rendered in the meeting UX. + :type type: SurfaceType + """ + + type: SurfaceType diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_context.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_context.py new file mode 100644 index 00000000..25af9039 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_context.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TabContext(AgentsModel): + """Current tab request context, i.e., the current theme. + + :param theme: Gets or sets the current user's theme. + :type theme: str + """ + + theme: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_entity_context.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_entity_context.py new file mode 100644 index 00000000..256c21fd --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_entity_context.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TabEntityContext(AgentsModel): + """ + Current TabRequest entity context, or 'tabEntityId'. + + :param tab_entity_id: Gets or sets the entity id of the tab. + :type tab_entity_id: str + """ + + tab_entity_id: str diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_request.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_request.py new file mode 100644 index 00000000..4eac9316 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_request.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .tab_entity_context import TabEntityContext +from .tab_context import TabContext + + +class TabRequest(AgentsModel): + """Invoke ('tab/fetch') request value payload. + + :param tab_entity_context: Gets or sets current tab entity request context. + :type tab_entity_context: TabEntityContext + :param context: Gets or sets current tab entity request context. + :type context: TabContext + :param state: Gets or sets state, which is the magic code for OAuth Flow. + :type state: str + """ + + tab_entity_context: TabEntityContext = None + context: TabContext = None + state: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response.py new file mode 100644 index 00000000..e205d350 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .tab_response_payload import TabResponsePayload + + +class TabResponse(AgentsModel): + """Envelope for Card Tab Response Payload. + + :param tab: Possible values include: 'continue', 'auth' or 'silentAuth' + :type type: TabResponsePayload + """ + + tab: TabResponsePayload = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_card.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_card.py new file mode 100644 index 00000000..378597a9 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_card.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TabResponseCard(AgentsModel): + """Envelope for cards for a Tab request. + + :param card: Gets or sets adaptive card for this card tab response. + :type card: object + """ + + card: object = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_cards.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_cards.py new file mode 100644 index 00000000..e5afbbf1 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_cards.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License.from ..agents_model import AgentsModel + +from ..agents_model import AgentsModel +from typing import List + +from .tab_response_card import TabResponseCard + + +class TabResponseCards(AgentsModel): + """Envelope for cards for a TabResponse. + + :param cards: Gets or sets adaptive card for this card tab response. + :type cards: list[TabResponseCard] + """ + + cards: List[TabResponseCard] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_payload.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_payload.py new file mode 100644 index 00000000..b05ad508 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_response_payload.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .tab_response_cards import TabResponseCards +from .tab_suggested_actions import TabSuggestedActions + + +class TabResponsePayload(AgentsModel): + """Initializes a new instance of the TabResponsePayload class. + + :param type: Gets or sets choice of action options when responding to the + tab/fetch message. Possible values include: 'continue', 'auth' or 'silentAuth' + :type type: str + :param value: Gets or sets the TabResponseCards when responding to + tab/fetch activity with type of 'continue'. + :type value: TabResponseCards + :param suggested_actions: Gets or sets the Suggested Actions for this card tab. + :type suggested_actions: TabSuggestedActions + """ + + type: str = None + value: TabResponseCards = None + suggested_actions: TabSuggestedActions = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit.py new file mode 100644 index 00000000..aad507ce --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .tab_entity_context import TabEntityContext +from .tab_context import TabContext +from .tab_submit_data import TabSubmitData + + +class TabSubmit(AgentsModel): + """Initializes a new instance of the TabSubmit class. + + :param tab_entity_context: Gets or sets current tab entity request context. + :type tab_entity_context: TabEntityContext + :param context: Gets or sets current user context, i.e., the current theme. + :type context: TabContext + :param data: User input data. Free payload containing properties of key-value pairs. + :type data: TabSubmitData + """ + + tab_entity_context: TabEntityContext = None + context: TabContext = None + data: TabSubmitData = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit_data.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit_data.py new file mode 100644 index 00000000..ac727fca --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_submit_data.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TabSubmitData(AgentsModel): + """Invoke ('tab/submit') request value payload data. + + :param type: Currently, 'tab/submit'. + :type type: str + :param properties: Gets or sets properties that are not otherwise defined by the TabSubmit + type but that might appear in the serialized REST JSON object. + :type properties: object + """ + + type: str = None + properties: object = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_suggested_actions.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_suggested_actions.py new file mode 100644 index 00000000..aa285a44 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tab_suggested_actions.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List + +from ..card_action import CardAction + + +class TabSuggestedActions(AgentsModel): + """Tab SuggestedActions (Only when type is 'auth' or 'silentAuth'). + + :param actions: Gets or sets adaptive card for this card tab response. + :type actions: list[CardAction] + """ + + actions: List[CardAction] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification.py new file mode 100644 index 00000000..5e6029cb --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .meeting_notification import MeetingNotification +from .targeted_meeting_notification_value import TargetedMeetingNotificationValue +from .meeting_notification_channel_data import MeetingNotificationChannelData + + +class TargetedMeetingNotification(MeetingNotification): + """Specifies Teams targeted meeting notification. + + :param value: The value of the TargetedMeetingNotification. + :type value: TargetedMeetingNotificationValue + :param channel_data: Teams Bot meeting notification channel data. + :type channel_data: MeetingNotificationChannelData + """ + + value: TargetedMeetingNotificationValue = None + channel_data: MeetingNotificationChannelData = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification_value.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification_value.py new file mode 100644 index 00000000..0ba96d61 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/targeted_meeting_notification_value.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .surface import Surface + + +class TargetedMeetingNotificationValue(AgentsModel): + """Specifies the value for targeted meeting notifications. + + :param recipients: List of recipient MRIs for the notification. + :type recipients: List[str] + :param message: The message content of the notification. + :type message: str + """ + + recipients: List[str] = None + surfaces: List[Surface] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_card_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_card_response.py new file mode 100644 index 00000000..98126ebb --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_card_response.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .tab_response import TabResponse + + +class TaskModuleCardResponse(AgentsModel): + """Tab Response to 'task/submit' from a tab. + + :param value: The JSON for the Adaptive cards to appear in the tab. + :type value: TabResponse + """ + + value: TabResponse = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_continue_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_continue_response.py new file mode 100644 index 00000000..76874ef6 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_continue_response.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional +from .task_module_task_info import TaskModuleTaskInfo + + +class TaskModuleContinueResponse(AgentsModel): + """Response to continue a task module. + + :param type: The type of response. Default is 'continue'. + :type type: str + :param value: The task module task info. + :type value: Optional["TaskModuleTaskInfo"] + """ + + type: str = None + value: Optional[TaskModuleTaskInfo] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_message_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_message_response.py new file mode 100644 index 00000000..6169ced6 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_message_response.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class TaskModuleMessageResponse(AgentsModel): + """Response to display a message in a task module. + + :param type: The type of response. Default is 'message'. + :type type: str + :param value: The message to display. + :type value: Optional[str] + """ + + type: str = None + value: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request.py new file mode 100644 index 00000000..6d9f37df --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import Field +from ..agents_model import AgentsModel +from typing import Any, Optional + +from .task_module_request_context import TaskModuleRequestContext +from .tab_entity_context import TabEntityContext + + +class TaskModuleRequest(AgentsModel): + """Task module invoke request value payload. + + :param data: User input data. Free payload with key-value pairs. + :type data: object + :param context: Current user context, i.e., the current theme + :type context: Optional[Any] + :param tab_entity_context: Gets or sets current tab request context. + :type tab_entity_context: Optional[TabEntityContext] + """ + + data: Optional[Any] + context: Optional[TaskModuleRequestContext] + tab_entity_context: Optional[TabEntityContext] = Field(None, alias="tabContext") diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request_context.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request_context.py new file mode 100644 index 00000000..d3cde510 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_request_context.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import Optional + + +class TaskModuleRequestContext(AgentsModel): + """Context for a task module request. + + :param theme: The current user's theme. + :type theme: Optional[str] + """ + + theme: Optional[str] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response.py new file mode 100644 index 00000000..1c621d20 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .task_module_response_base import TaskModuleResponseBase +from .cache_info import CacheInfo + + +class TaskModuleResponse(TaskModuleResponseBase): + """Envelope for Task Module Response. + + :param task: The JSON for the Adaptive card to appear in the task module. + :task task: ~botframework.connector.teams.models.TaskModuleResponseBase + :param cache_info: CacheInfo for this TaskModuleResponse. + :task cache_info: ~botframework.connector.teams.models.CacheInfo + """ + + task: TaskModuleResponseBase = None + cache_info: CacheInfo = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response_base.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response_base.py new file mode 100644 index 00000000..9e0c29e6 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_response_base.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from pydantic import ConfigDict +from ..agents_model import AgentsModel + + +class TaskModuleResponseBase(AgentsModel): + """Base class for task module responses. + + :param type: The type of response. + :type type: str + """ + + model_config = ConfigDict(extra="allow") + + type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_task_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_task_info.py new file mode 100644 index 00000000..fef8144b --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/task_module_task_info.py @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from ..attachment import Attachment + + +class TaskModuleTaskInfo(AgentsModel): + """Information about a task module task. + + :param title: The title of the task module. + :type title: str + :param height: The height of the task module. + :type height: int + :param width: The width of the task module. + :type width: int + :param url: The URL of the task module. + :type url: str + :param card: The adaptive card for the task module. + :type card: object + """ + + title: str = None + height: object = None + width: object = None + url: str = None + card: Attachment = None + fallback_url: str = None + completion_bot_id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_details.py new file mode 100644 index 00000000..f4810a73 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_details.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TeamDetails(AgentsModel): + """Details related to a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + :param aad_group_id: Azure Active Directory (AAD) Group Id for the team. + :type aad_group_id: str + :param channel_count: The count of channels in the team. + :type channel_count: int + :param member_count: The count of members in the team. + :type member_count: int + :param type: The team type + :type type: str + """ + + id: str = None + name: str = None + aad_group_id: str = None + channel_count: int = None + member_count: int = None + type: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_info.py new file mode 100644 index 00000000..4e41a000 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/team_info.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TeamInfo(AgentsModel): + """Describes a team. + + :param id: Unique identifier representing a team + :type id: str + :param name: Name of team. + :type name: str + :param aad_group_id: Azure AD Teams group ID. + :type aad_group_id: str + """ + + id: str = None + name: str = None + aad_group_id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_batch_operation_response.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_batch_operation_response.py new file mode 100644 index 00000000..54b673c1 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_batch_operation_response.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .batch_operation_response import BatchOperationResponse + + +class TeamsBatchOperationResponse(BatchOperationResponse): + """ + :param operation_id: Unique identifier of the batch operation. + :type operation_id: str + :param body_as_text: The body of the request as text. + :type body_as_text: str + :param parsed_body: The parsed body of the request. + :type parsed_body: BatchOperationResponse + """ + + operation_id: str = None + body_as_text: str = None + parsed_body: BatchOperationResponse = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_account.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_account.py new file mode 100644 index 00000000..b2294998 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_account.py @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Any +from pydantic import ConfigDict +from ..agents_model import AgentsModel + + +class TeamsChannelAccount(AgentsModel): + """Teams channel account detailing user Azure Active Directory details. + + :param id: Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456) + :type id: str + :param name: Display friendly name + :type name: str + :param given_name: Given name part of the user name. + :type given_name: str + :param surname: Surname part of the user name. + :type surname: str + :param email: Email Id of the user. + :type email: str + :param user_principal_name: Unique user principal name. + :type user_principal_name: str + :param tenant_id: Tenant Id of the user. + :type tenant_id: str + :param user_role: User Role of the user. + :type user_role: str + """ + + model_config = ConfigDict(extra="allow") + + id: str = None + name: str = None + given_name: str = None + surname: str = None + email: str = None + user_principal_name: str = None + tenant_id: str = None + user_role: str = None + + @property + def properties(self) -> dict[str, Any]: + """Returns the set of properties that are not None.""" + return self.model_extra diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data.py new file mode 100644 index 00000000..14fb5cdc --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .channel_info import ChannelInfo +from .team_info import TeamInfo +from .notification_info import NotificationInfo +from .tenant_info import TenantInfo +from .teams_meeting_info import TeamsMeetingInfo +from .teams_channel_data_settings import TeamsChannelDataSettings +from .on_behalf_of import OnBehalfOf + + +class TeamsChannelData(AgentsModel): + """Channel data specific to messages received in Microsoft Teams. + + :param channel: Information about the channel in which the message was sent + :type channel: ChannelInfo + :param event_type: Type of event. + :type event_type: str + :param team: Information about the team in which the message was sent + :type team: TeamInfo + :param notification: Notification settings for the message + :type notification: NotificationInfo + :param tenant: Information about the tenant in which the message was sent + :type tenant: TenantInfo + :param meeting: Information about the meeting in which the message was sent + :type meeting: TeamsMeetingInfo + :param settings: Information about the settings in which the message was sent + :type settings: TeamsChannelDataSettings + :param on_behalf_of: The OnBehalfOf list for user attribution + :type on_behalf_of: List[OnBehalfOf] + """ + + channel: ChannelInfo = None + event_type: str = None + team: TeamInfo = None + notification: NotificationInfo = None + tenant: TenantInfo = None + meeting: TeamsMeetingInfo = None + settings: TeamsChannelDataSettings = None + on_behalf_of: List[OnBehalfOf] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data_settings.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data_settings.py new file mode 100644 index 00000000..8bc140da --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_channel_data_settings.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .channel_info import ChannelInfo + + +class TeamsChannelDataSettings(AgentsModel): + """Represents the settings information for a Teams channel data. + + :param selected_channel: Information about the selected Teams channel. + :type selected_channel: ChannelInfo + """ + + selected_channel: ChannelInfo = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_info.py new file mode 100644 index 00000000..4f408e9c --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_info.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TeamsMeetingInfo(AgentsModel): + """Describes a Teams Meeting. + + :param id: Unique identifier representing a meeting + :type id: str + """ + + id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_member.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_member.py new file mode 100644 index 00000000..0b258cbe --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_member.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from .teams_channel_account import TeamsChannelAccount +from .user_meeting_details import UserMeetingDetails + + +class TeamsMeetingMember(AgentsModel): + """Data about the meeting participants. + + :param user: The channel user data. + :type user: TeamsChannelAccount + :param meeting: The user meeting details. + :type meeting: UserMeetingDetails + """ + + user: TeamsChannelAccount = None + meeting: UserMeetingDetails = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_participant.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_participant.py new file mode 100644 index 00000000..2793fdae --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_meeting_participant.py @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + +from .teams_channel_account import TeamsChannelAccount +from .meeting_participant_info import MeetingParticipantInfo +from ..conversation_account import ConversationAccount + + +class TeamsMeetingParticipant(AgentsModel): + """Teams participant channel account detailing user Azure Active Directory and meeting participant details. + + :param user: Teams Channel Account information for this meeting participant + :type user: TeamsChannelAccount + :param meeting: Information specific to this participant in the specific meeting. + :type meeting: MeetingParticipantInfo + :param conversation: Conversation Account for the meeting. + :type conversation: ConversationAccount + """ + + user: TeamsChannelAccount = None + meeting: MeetingParticipantInfo = None + conversation: ConversationAccount = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_member.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_member.py new file mode 100644 index 00000000..61c673b0 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_member.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TeamsMember(AgentsModel): + """Describes a member. + + :param id: Unique identifier representing a member (user or channel). + :type id: str + """ + + id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_paged_members_result.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_paged_members_result.py new file mode 100644 index 00000000..b523376a --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/teams_paged_members_result.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel +from typing import List +from .teams_channel_account import TeamsChannelAccount + + +class TeamsPagedMembersResult(AgentsModel): + """Page of members for Teams. + + :param continuation_token: Paging token + :type continuation_token: str + :param members: The Teams Channel Accounts. + :type members: list[TeamsChannelAccount] + """ + + continuation_token: str = None + members: List[TeamsChannelAccount] = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tenant_info.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tenant_info.py new file mode 100644 index 00000000..0b6c7403 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/tenant_info.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class TenantInfo(AgentsModel): + """Describes a tenant. + + :param id: Unique identifier representing a tenant + :type id: str + """ + + id: str = None diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/user_meeting_details.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/user_meeting_details.py new file mode 100644 index 00000000..e62469c7 --- /dev/null +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/teams/user_meeting_details.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from ..agents_model import AgentsModel + + +class UserMeetingDetails(AgentsModel): + """Specific details of a user in a Teams meeting. + + :param role: Role of the participant in the current meeting. + :type role: str + :param in_meeting: True, if the participant is in the meeting. + :type in_meeting: bool + """ + + role: str = None + in_meeting: bool = None diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py index 697555c9..b1cdf73b 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/__init__.py @@ -1,11 +1,15 @@ from .agent_http_adapter import AgentHttpAdapter from .channel_service_route_table import channel_service_route_table from .cloud_adapter import CloudAdapter -from .jwt_authorization_middleware import jwt_authorization_middleware +from .jwt_authorization_middleware import ( + jwt_authorization_middleware, + jwt_authorization_decorator, +) __all__ = [ "AgentHttpAdapter", "CloudAdapter", "jwt_authorization_middleware", + "jwt_authorization_decorator", "channel_service_route_table", ] diff --git a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py index c6efced5..e82f3914 100644 --- a/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py +++ b/libraries/Hosting/microsoft-agents-hosting-aiohttp/microsoft/agents/hosting/aiohttp/jwt_authorization_middleware.py @@ -1,3 +1,4 @@ +import functools from aiohttp.web import Request, middleware, json_response from microsoft.agents.authorization import AgentAuthConfiguration, JwtTokenValidator @@ -26,3 +27,31 @@ async def jwt_authorization_middleware(request: Request, handler): ) return await handler(request) + + +def jwt_authorization_decorator(func): + @functools.wraps(func) + async def wrapper(request): + auth_config: AgentAuthConfiguration = request.app["agent_configuration"] + token_validator = JwtTokenValidator(auth_config) + auth_header = request.headers.get("Authorization") + if auth_header: + # Extract the token from the Authorization header + token = auth_header.split(" ")[1] + try: + claims = token_validator.validate_token(token) + request["claims_identity"] = claims + except ValueError as e: + return json_response({"error": str(e)}, status=401) + else: + if not auth_config.CLIENT_ID: + # TODO: Refine anonymous strategy + request["claims_identity"] = token_validator.get_anonymous_claims() + else: + return json_response( + {"error": "Authorization header not found"}, status=401 + ) + + return await func(request) + + return wrapper diff --git a/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/__init__.py b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/__init__.py new file mode 100644 index 00000000..7dc95ffd --- /dev/null +++ b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/__init__.py @@ -0,0 +1,4 @@ +from .teams_activity_handler import TeamsActivityHandler +from .teams_info import TeamsInfo + +__all__ = ["TeamsActivityHandler", "TeamsInfo"] diff --git a/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_activity_handler.py b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_activity_handler.py new file mode 100644 index 00000000..4d82d071 --- /dev/null +++ b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_activity_handler.py @@ -0,0 +1,912 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from http import HTTPStatus +from typing import Any, List + +from microsoft.agents.builder import ActivityHandler, TurnContext +from microsoft.agents.core.models import ( + InvokeResponse, + ChannelAccount, +) + +from microsoft.agents.core.models.teams import ( + AppBasedLinkQuery, + TeamInfo, + ChannelInfo, + ConfigResponse, + FileConsentCardResponse, + MeetingEndEventDetails, + MeetingParticipantsEventDetails, + MeetingStartEventDetails, + MessagingExtensionAction, + MessagingExtensionActionResponse, + MessagingExtensionQuery, + MessagingExtensionResponse, + O365ConnectorCardActionQuery, + ReadReceiptInfo, + SigninStateVerificationQuery, + TabRequest, + TabResponse, + TabSubmit, + TaskModuleRequest, + TaskModuleResponse, + TeamsChannelAccount, + TeamsChannelData, +) + +from .teams_info import TeamsInfo + + +class TeamsActivityHandler(ActivityHandler): + """ + The TeamsActivityHandler is derived from the ActivityHandler class and adds support for + Microsoft Teams-specific functionality. + """ + + async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: + """ + Handles invoke activities. + + :param turn_context: The context object for the turn. + :return: An InvokeResponse. + """ + + try: + if ( + not turn_context.activity.name + and turn_context.activity.channel_id == "msteams" + ): + return await self.on_teams_card_action_invoke(turn_context) + else: + name = turn_context.activity.name + value = turn_context.activity.value + + if name == "config/fetch": + return self._create_invoke_response( + await self.on_teams_config_fetch(turn_context, value) + ) + elif name == "config/submit": + return self._create_invoke_response( + await self.on_teams_config_submit(turn_context, value) + ) + elif name == "fileConsent/invoke": + return self._create_invoke_response( + await self.on_teams_file_consent(turn_context, value) + ) + elif name == "actionableMessage/executeAction": + await self.on_teams_o365_connector_card_action(turn_context, value) + return self._create_invoke_response() + elif name == "composeExtension/queryLink": + return self._create_invoke_response( + await self.on_teams_app_based_link_query(turn_context, value) + ) + elif name == "composeExtension/anonymousQueryLink": + return self._create_invoke_response( + await self.on_teams_anonymous_app_based_link_query( + turn_context, value + ) + ) + elif name == "composeExtension/query": + query = MessagingExtensionQuery.model_validate(value) + return self._create_invoke_response( + await self.on_teams_messaging_extension_query( + turn_context, query + ) + ) + elif name == "composeExtension/selectItem": + return self._create_invoke_response( + await self.on_teams_messaging_extension_select_item( + turn_context, value + ) + ) + elif name == "composeExtension/submitAction": + return self._create_invoke_response( + await self.on_teams_messaging_extension_submit_action_dispatch( + turn_context, value + ) + ) + elif name == "composeExtension/fetchTask": + return self._create_invoke_response( + await self.on_teams_messaging_extension_fetch_task( + turn_context, value + ) + ) + elif name == "composeExtension/querySettingUrl": + return self._create_invoke_response( + await self.on_teams_messaging_extension_configuration_query_setting_url( + turn_context, value + ) + ) + elif name == "composeExtension/setting": + await self.on_teams_messaging_extension_configuration_setting( + turn_context, value + ) + return self._create_invoke_response() + elif name == "composeExtension/onCardButtonClicked": + await self.on_teams_messaging_extension_card_button_clicked( + turn_context, value + ) + return self._create_invoke_response() + elif name == "task/fetch": + task_module_request = TaskModuleRequest.model_validate(value) + return self._create_invoke_response( + await self.on_teams_task_module_fetch( + turn_context, task_module_request + ) + ) + elif name == "task/submit": + task_module_request = TaskModuleRequest.model_validate(value) + return self._create_invoke_response( + await self.on_teams_task_module_submit( + turn_context, task_module_request + ) + ) + elif name == "tab/fetch": + return self._create_invoke_response( + await self.on_teams_tab_fetch(turn_context, value) + ) + elif name == "tab/submit": + return self._create_invoke_response( + await self.on_teams_tab_submit(turn_context, value) + ) + else: + return await super().on_invoke_activity(turn_context) + except Exception as err: + if str(err) == "NotImplemented": + return InvokeResponse(status=int(HTTPStatus.NOT_IMPLEMENTED)) + elif str(err) == "BadRequest": + return InvokeResponse(status=int(HTTPStatus.BAD_REQUEST)) + raise + + async def on_teams_card_action_invoke( + self, turn_context: TurnContext + ) -> InvokeResponse: + """ + Handles card action invoke. + + :param turn_context: The context object for the turn. + :return: An InvokeResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_config_fetch( + self, turn_context: TurnContext, config_data: Any + ) -> ConfigResponse: + """ + Handles config fetch. + + :param turn_context: The context object for the turn. + :param config_data: The config data. + :return: A ConfigResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_config_submit( + self, turn_context: TurnContext, config_data: Any + ) -> ConfigResponse: + """ + Handles config submit. + + :param turn_context: The context object for the turn. + :param config_data: The config data. + :return: A ConfigResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_file_consent( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ) -> None: + """ + Handles file consent. + + :param turn_context: The context object for the turn. + :param file_consent_card_response: The file consent card response. + :return: None + """ + if file_consent_card_response.action == "accept": + return await self.on_teams_file_consent_accept( + turn_context, file_consent_card_response + ) + elif file_consent_card_response.action == "decline": + return await self.on_teams_file_consent_decline( + turn_context, file_consent_card_response + ) + else: + raise ValueError("BadRequest") + + async def on_teams_file_consent_accept( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ) -> None: + """ + Handles file consent accept. + + :param turn_context: The context object for the turn. + :param file_consent_card_response: The file consent card response. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_file_consent_decline( + self, + turn_context: TurnContext, + file_consent_card_response: FileConsentCardResponse, + ) -> None: + """ + Handles file consent decline. + + :param turn_context: The context object for the turn. + :param file_consent_card_response: The file consent card response. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_o365_connector_card_action( + self, turn_context: TurnContext, query: O365ConnectorCardActionQuery + ) -> None: + """ + Handles O365 connector card action. + + :param turn_context: The context object for the turn. + :param query: The O365 connector card action query. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_signin_verify_state( + self, turn_context: TurnContext, query: SigninStateVerificationQuery + ) -> None: + """ + Handles sign-in verify state. + + :param turn_context: The context object for the turn. + :param query: The sign-in state verification query. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_signin_token_exchange( + self, turn_context: TurnContext, query: SigninStateVerificationQuery + ) -> None: + """ + Handles sign-in token exchange. + + :param turn_context: The context object for the turn. + :param query: The sign-in state verification query. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_app_based_link_query( + self, turn_context: TurnContext, query: AppBasedLinkQuery + ) -> MessagingExtensionResponse: + """ + Handles app-based link query. + + :param turn_context: The context object for the turn. + :param query: The app-based link query. + :return: A MessagingExtensionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_anonymous_app_based_link_query( + self, turn_context: TurnContext, query: AppBasedLinkQuery + ) -> MessagingExtensionResponse: + """ + Handles anonymous app-based link query. + + :param turn_context: The context object for the turn. + :param query: The app-based link query. + :return: A MessagingExtensionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_query( + self, turn_context: TurnContext, query: MessagingExtensionQuery + ) -> MessagingExtensionResponse: + """ + Handles messaging extension query. + + :param turn_context: The context object for the turn. + :param query: The messaging extension query. + :return: A MessagingExtensionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_select_item( + self, turn_context: TurnContext, query: Any + ) -> MessagingExtensionResponse: + """ + Handles messaging extension select item. + + :param turn_context: The context object for the turn. + :param query: The query. + :return: A MessagingExtensionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_submit_action_dispatch( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + """ + Handles messaging extension submit action dispatch. + + :param turn_context: The context object for the turn. + :param action: The messaging extension action. + :return: A MessagingExtensionActionResponse. + """ + if action.bot_message_preview_action: + if action.bot_message_preview_action == "edit": + return await self.on_teams_messaging_extension_message_preview_edit( + turn_context, action + ) + elif action.bot_message_preview_action == "send": + return await self.on_teams_messaging_extension_message_preview_send( + turn_context, action + ) + else: + raise ValueError("BadRequest") + else: + return await self.on_teams_messaging_extension_submit_action( + turn_context, action + ) + + async def on_teams_messaging_extension_submit_action( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + """ + Handles messaging extension submit action. + + :param turn_context: The context object for the turn. + :param action: The messaging extension action. + :return: A MessagingExtensionActionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_message_preview_edit( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + """ + Handles messaging extension message preview edit. + + :param turn_context: The context object for the turn. + :param action: The messaging extension action. + :return: A MessagingExtensionActionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_message_preview_send( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + """ + Handles messaging extension message preview send. + + :param turn_context: The context object for the turn. + :param action: The messaging extension action. + :return: A MessagingExtensionActionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_fetch_task( + self, turn_context: TurnContext, action: MessagingExtensionAction + ) -> MessagingExtensionActionResponse: + """ + Handles messaging extension fetch task. + + :param turn_context: The context object for the turn. + :param action: The messaging extension action. + :return: A MessagingExtensionActionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_configuration_query_setting_url( + self, turn_context: TurnContext, query: MessagingExtensionQuery + ) -> MessagingExtensionResponse: + """ + Handles messaging extension configuration query setting URL. + + :param turn_context: The context object for the turn. + :param query: The messaging extension query. + :return: A MessagingExtensionResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_configuration_setting( + self, turn_context: TurnContext, settings: Any + ) -> None: + """ + Handles messaging extension configuration setting. + + :param turn_context: The context object for the turn. + :param settings: The settings. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_messaging_extension_card_button_clicked( + self, turn_context: TurnContext, card_data: Any + ) -> None: + """ + Handles messaging extension card button clicked. + + :param turn_context: The context object for the turn. + :param card_data: The card data. + :return: None + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_task_module_fetch( + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ) -> TaskModuleResponse: + """ + Handles task module fetch. + + :param turn_context: The context object for the turn. + :param task_module_request: The task module request. + :return: A TaskModuleResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_task_module_submit( + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ) -> TaskModuleResponse: + """ + Handles task module submit. + + :param turn_context: The context object for the turn. + :param task_module_request: The task module request. + :return: A TaskModuleResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_tab_fetch( + self, turn_context: TurnContext, tab_request: TabRequest + ) -> TabResponse: + """ + Handles tab fetch. + + :param turn_context: The context object for the turn. + :param tab_request: The tab request. + :return: A TabResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_teams_tab_submit( + self, turn_context: TurnContext, tab_submit: TabSubmit + ) -> TabResponse: + """ + Handles tab submit. + + :param turn_context: The context object for the turn. + :param tab_submit: The tab submit. + :return: A TabResponse. + """ + raise NotImplementedError("NotImplemented") + + async def on_conversation_update_activity(self, turn_context: TurnContext): + """ + Dispatches conversation update activity. + + :param turn_context: The context object for the turn. + :return: None + """ + if turn_context.activity.channel_id == "msteams": + channel_data = ( + TeamsChannelData.model_validate(turn_context.activity.channel_data) + if turn_context.activity.channel_data + else None + ) + + if ( + turn_context.activity.members_added + and len(turn_context.activity.members_added) > 0 + ): + return await self.on_teams_members_added_dispatch( + turn_context.activity.members_added, + channel_data.team if channel_data else None, + turn_context, + ) + + if ( + turn_context.activity.members_removed + and len(turn_context.activity.members_removed) > 0 + ): + return await self.on_teams_members_removed(turn_context) + + if not channel_data or not channel_data.event_type: + return await super().on_conversation_update_activity(turn_context) + + event_type = channel_data.event_type + + if event_type == "channelCreated": + return await self.on_teams_channel_created(turn_context) + elif event_type == "channelDeleted": + return await self.on_teams_channel_deleted(turn_context) + elif event_type == "channelRenamed": + return await self.on_teams_channel_renamed(turn_context) + elif event_type == "teamArchived": + return await self.on_teams_team_archived(turn_context) + elif event_type == "teamDeleted": + return await self.on_teams_team_deleted(turn_context) + elif event_type == "teamHardDeleted": + return await self.on_teams_team_hard_deleted(turn_context) + elif event_type == "channelRestored": + return await self.on_teams_channel_restored(turn_context) + elif event_type == "teamRenamed": + return await self.on_teams_team_renamed(turn_context) + elif event_type == "teamRestored": + return await self.on_teams_team_restored(turn_context) + elif event_type == "teamUnarchived": + return await self.on_teams_team_unarchived(turn_context) + + return await super().on_conversation_update_activity(turn_context) + + async def on_message_update_activity(self, turn_context: TurnContext): + """ + Dispatches message update activity. + + :param turn_context: The context object for the turn. + :return: None + """ + if turn_context.activity.channel_id == "msteams": + channel_data = channel_data = ( + TeamsChannelData.model_validate(turn_context.activity.channel_data) + if turn_context.activity.channel_data + else None + ) + + event_type = channel_data.event_type if channel_data else None + + if event_type == "undeleteMessage": + return await self.on_teams_message_undelete(turn_context) + elif event_type == "editMessage": + return await self.on_teams_message_edit(turn_context) + + return await super().on_message_update_activity(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext) -> None: + """ + Dispatches message delete activity. + + :param turn_context: The context object for the turn. + :return: None + """ + if turn_context.activity.channel_id == "msteams": + channel_data = channel_data = ( + TeamsChannelData.model_validate(turn_context.activity.channel_data) + if turn_context.activity.channel_data + else None + ) + + event_type = channel_data.event_type if channel_data else None + + if event_type == "softDeleteMessage": + return await self.on_teams_message_soft_delete(turn_context) + + return await super().on_message_delete_activity(turn_context) + + async def on_teams_message_undelete(self, turn_context: TurnContext) -> None: + """ + Handles Teams message undelete. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_message_edit(self, turn_context: TurnContext) -> None: + """ + Handles Teams message edit. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_message_soft_delete(self, turn_context: TurnContext) -> None: + """ + Handles Teams message soft delete. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_members_added_dispatch( + self, + members_added: List[ChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ) -> None: + """ + Dispatches processing of Teams members added to the conversation. + Processes the members_added collection to get full member information when possible. + + :param members_added: The list of members being added to the conversation. + :param team_info: The team info object. + :param turn_context: The context object for the turn. + :return: None + """ + teams_members_added = [] + + for member in members_added: + # If the member has properties or is the agent/bot being added to the conversation + if len(member.properties) or ( + turn_context.activity.recipient + and turn_context.activity.recipient.id == member.id + ): + + # Convert the ChannelAccount to TeamsChannelAccount + # TODO: Converter between these two classes + teams_member = TeamsChannelAccount.model_validate( + member.model_dump(by_alias=True, exclude_unset=True) + ) + teams_members_added.append(teams_member) + else: + # Try to get the full member details from Teams + try: + teams_member = await TeamsInfo.get_member( + turn_context.activity, member.id + ) + teams_members_added.append(teams_member) + except Exception as err: + # Handle case where conversation is not found + if "ConversationNotFound" in str(err): + teams_channel_account = TeamsChannelAccount( + id=member.id, + name=member.name, + aad_object_id=getattr(member, "aad_object_id", None), + role=getattr(member, "role", None), + ) + teams_members_added.append(teams_channel_account) + else: + # Propagate any other errors + raise + + await self.on_teams_members_added(teams_members_added, team_info, turn_context) + + async def on_teams_members_added( + self, + teams_members_added: List[TeamsChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ) -> None: + """ + Handles Teams members added. + + :param turn_context: The context object for the turn. + :return: None + """ + await self.on_members_added_activity(teams_members_added, turn_context) + + async def on_teams_members_removed_dispatch( + self, + members_removed: List[ChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ) -> None: + """ + Dispatches processing of Teams members removed from the conversation. + """ + teams_members_removed = [] + for member in members_removed: + teams_members_removed.append( + TeamsChannelAccount.model_validate( + member.model_dump(by_alias=True, exclude_unset=True) + ) + ) + return await self.on_teams_members_removed( + teams_members_removed, team_info, turn_context + ) + + async def on_teams_members_removed( + self, + teams_members_removed: List[TeamsChannelAccount], + team_info: TeamInfo, + turn_context: TurnContext, + ) -> None: + """ + Handles Teams members removed. + + :param turn_context: The context object for the turn. + :return: None + """ + await self.on_members_removed_activity(teams_members_removed, turn_context) + + async def on_teams_channel_created( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams channel created. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_channel_deleted( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams channel deleted. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_channel_renamed( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams channel renamed. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_archived( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team archived. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_deleted( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team deleted. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_hard_deleted( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team hard deleted. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_channel_restored( + self, channel_info: ChannelInfo, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams channel restored. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_renamed( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team renamed. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_restored( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team restored. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_team_unarchived( + self, team_info: TeamInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams team unarchived. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_event_activity(self, turn_context: TurnContext) -> None: + """ + Dispatches event activity. + + :param turn_context: The context object for the turn. + :return: None + """ + if turn_context.activity.channel_id == "msteams": + if turn_context.activity.name == "application/vnd.microsoft.readReceipt": + return await self.on_teams_read_receipt(turn_context) + elif turn_context.activity.name == "application/vnd.microsoft.meetingStart": + return await self.on_teams_meeting_start(turn_context) + elif turn_context.activity.name == "application/vnd.microsoft.meetingEnd": + return await self.on_teams_meeting_end(turn_context) + elif ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantJoin" + ): + return await self.on_teams_meeting_participants_join(turn_context) + elif ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantLeave" + ): + return await self.on_teams_meeting_participants_leave(turn_context) + + return await super().on_event_activity(turn_context) + + async def on_teams_meeting_start( + self, meeting: MeetingStartEventDetails, turn_context: TurnContext + ) -> None: + """ + Handles Teams meeting start. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_meeting_end( + self, meeting: MeetingEndEventDetails, turn_context: TurnContext + ) -> None: + """ + Handles Teams meeting end. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_read_receipt( + self, read_receipt: ReadReceiptInfo, turn_context: TurnContext + ) -> None: + """ + Handles Teams read receipt. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_meeting_participants_join( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ) -> None: + """ + Handles Teams meeting participants join. + + :param turn_context: The context object for the turn. + :return: None + """ + return + + async def on_teams_meeting_participants_leave( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ) -> None: + """ + Handles Teams meeting participants leave. + + :param turn_context: The context object for the turn. + :return: None + """ + return diff --git a/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_info.py b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_info.py new file mode 100644 index 00000000..8c446396 --- /dev/null +++ b/libraries/Hosting/microsoft-agents-hosting-teams/microsoft/agents/hosting/teams/teams_info.py @@ -0,0 +1,653 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Teams information utilities for Microsoft Agents.""" + +from typing import Optional, Tuple, Dict, Any, List + +from microsoft.agents.core.models import Activity, Channels, ConversationParameters + +from microsoft.agents.core.models.teams import ( + TeamsChannelAccount, + TeamsMeetingParticipant, + MeetingInfo, + TeamDetails, + TeamsPagedMembersResult, + MeetingNotification, + MeetingNotificationResponse, + TeamsMember, + BatchOperationStateResponse, + BatchFailedEntriesResponse, + CancelOperationResponse, + TeamsBatchOperationResponse, + ChannelInfo, +) +from microsoft.agents.connector.teams import TeamsConnectorClient +from microsoft.agents.builder import ChannelServiceAdapter, TurnContext + + +class TeamsInfo: + """Teams information utilities for interacting with Teams-specific data.""" + + @staticmethod + async def get_meeting_participant( + context: TurnContext, + meeting_id: Optional[str] = None, + participant_id: Optional[str] = None, + tenant_id: Optional[str] = None, + ) -> TeamsMeetingParticipant: + """ + Gets the meeting participant information. + + Args: + context: The turn context. + meeting_id: The meeting ID. If not provided, it will be extracted from the activity. + participant_id: The participant ID. If not provided, it will be extracted from the activity. + tenant_id: The tenant ID. If not provided, it will be extracted from the activity. + + Returns: + The meeting participant information. + + Raises: + ValueError: If required parameters are missing. + """ + if not context: + raise ValueError("context is required.") + + activity = context.activity + teams_channel_data: dict = activity.channel_data + + if meeting_id is None: + meeting_id = teams_channel_data.get("meeting", {}).get("id", None) + + if not meeting_id: + raise ValueError("meeting_id is required.") + + if participant_id is None: + participant_id = getattr(activity.from_property, "aad_object_id", None) + + if not participant_id: + raise ValueError("participant_id is required.") + + if tenant_id is None: + tenant_id = teams_channel_data.get("tenant", {}).get("id", None) + + rest_client = TeamsInfo._get_rest_client(context) + result = await rest_client.fetch_meeting_participant( + meeting_id, participant_id, tenant_id + ) + return result + + @staticmethod + async def get_meeting_info( + context: TurnContext, meeting_id: Optional[str] = None + ) -> MeetingInfo: + """ + Gets the meeting information. + + Args: + context: The turn context. + meeting_id: The meeting ID. If not provided, it will be extracted from the activity. + + Returns: + The meeting information. + + Raises: + ValueError: If required parameters are missing. + """ + if not meeting_id: + teams_channel_data: dict = context.activity.channel_data + meeting_id = teams_channel_data.get("meeting", {}).get("id", None) + + if not meeting_id: + raise ValueError("meeting_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + result = await rest_client.fetch_meeting_info(meeting_id) + return result + + @staticmethod + async def get_team_details( + context: TurnContext, team_id: Optional[str] = None + ) -> TeamDetails: + """ + Gets the team details. + + Args: + context: The turn context. + team_id: The team ID. If not provided, it will be extracted from the activity. + + Returns: + The team details. + + Raises: + ValueError: If required parameters are missing. + """ + if not team_id: + teams_channel_data: dict = context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id", None) + + if not team_id: + raise ValueError("team_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + result = await rest_client.fetch_team_details(team_id) + return result + + @staticmethod + async def send_message_to_teams_channel( + context: TurnContext, + activity: Activity, + teams_channel_id: str, + app_id: Optional[str] = None, + ) -> Tuple[Dict[str, Any], str]: + """ + Sends a message to a Teams channel. + + Args: + context: The turn context. + activity: The activity to send. + teams_channel_id: The Teams channel ID. + app_id: The application ID. + + Returns: + A tuple containing the conversation reference and new activity ID. + + Raises: + ValueError: If required parameters are missing. + """ + if not context: + raise ValueError("TurnContext cannot be None") + + if not activity: + raise ValueError("Activity cannot be None") + + if not teams_channel_id: + raise ValueError("The teams_channel_id cannot be None or empty") + + convo_params = ConversationParameters( + is_group=True, + channel_data={ + "channel": { + "id": teams_channel_id, + }, + }, + activity=activity, + agent=context.activity.recipient, + ) + + conversation_reference = None + new_activity_id = None + + if app_id and isinstance(context.adapter, ChannelServiceAdapter): + + async def _conversation_callback( + turn_context: TurnContext, + ) -> None: + """ + Callback for create_conversation. + + Args: + turn_context: The turn context. + conversation_reference: The conversation reference to update. + new_activity_id: The new activity ID to update. + """ + nonlocal conversation_reference, new_activity_id + conversation_reference = ( + turn_context.activity.get_conversation_reference() + ) + new_activity_id = turn_context.activity.id + + await context.adapter.create_conversation( + app_id, + Channels.ms_teams, + context.activity.service_url, + "https://api.botframework.com", + convo_params, + _conversation_callback, + ) + else: + connector_client = TeamsInfo._get_rest_client(context) + conversation_resource_response = ( + await connector_client.conversations.create_conversation(convo_params) + ) + conversation_reference = context.activity.get_conversation_reference() + conversation_reference.conversation.id = conversation_resource_response.id + new_activity_id = conversation_resource_response.activity_id + + return conversation_reference, new_activity_id + + @staticmethod + async def get_team_channels( + context: TurnContext, team_id: Optional[str] = None + ) -> List[ChannelInfo]: + """ + Gets the channels of a team. + + Args: + context: The turn context. + team_id: The team ID. If not provided, it will be extracted from the activity. + + Returns: + The list of channels. + + Raises: + ValueError: If required parameters are missing. + """ + if not team_id: + teams_channel_data: dict = context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id", None) + + if not team_id: + raise ValueError("team_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.fetch_channel_list(team_id) + + @staticmethod + async def get_paged_members( + context: TurnContext, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> TeamsPagedMembersResult: + """ + Gets the paged members of a team or conversation. + + Args: + context: The turn context. + page_size: The page size. + continuation_token: The continuation token. + + Returns: + The paged members result. + + Raises: + ValueError: If required parameters are missing. + """ + teams_channel_data: dict = context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id", None) + + if team_id: + return await TeamsInfo.get_paged_team_members( + context, team_id, page_size, continuation_token + ) + else: + conversation_id = ( + context.activity.conversation.id + if context.activity.conversation + else None + ) + if not conversation_id: + raise ValueError("conversation_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.get_conversation_paged_member( + conversation_id, page_size, continuation_token + ) + + @staticmethod + async def get_member(context: TurnContext, user_id: str) -> TeamsChannelAccount: + """ + Gets a member of a team or conversation. + + Args: + context: The turn context. + user_id: The user ID. + + Returns: + The member information. + + Raises: + ValueError: If required parameters are missing. + """ + teams_channel_data: dict = context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id", None) + + if team_id: + return await TeamsInfo.get_team_member(context, team_id, user_id) + else: + conversation_id = ( + context.activity.conversation.id + if context.activity.conversation + else None + ) + if not conversation_id: + raise ValueError("conversation_id is required.") + + return await TeamsInfo._get_member_internal( + context, conversation_id, user_id + ) + + @staticmethod + async def get_paged_team_members( + context: TurnContext, + team_id: Optional[str] = None, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> TeamsPagedMembersResult: + """ + Gets the paged members of a team. + + Args: + context: The turn context. + team_id: The team ID. If not provided, it will be extracted from the activity. + page_size: The page size. + continuation_token: The continuation token. + + Returns: + The paged members result. + + Raises: + ValueError: If required parameters are missing. + """ + if not team_id: + teams_channel_data: dict = context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id", None) + + if not team_id: + raise ValueError("team_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + paged_results = await rest_client.get_conversation_paged_member( + team_id, page_size, continuation_token + ) + + # Fetch all pages if there are more + while paged_results.continuation_token: + next_results = await rest_client.get_conversation_paged_member( + team_id, page_size, paged_results.continuation_token + ) + paged_results.members.extend(next_results.members) + paged_results.continuation_token = next_results.continuation_token + + return paged_results + + @staticmethod + async def get_team_member( + context: TurnContext, team_id: str, user_id: str + ) -> TeamsChannelAccount: + """ + Gets a member of a team. + + Args: + context: The turn context. + team_id: The team ID. + user_id: The user ID. + + Returns: + The member information. + + Raises: + ValueError: If required parameters are missing. + """ + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.get_conversation_member(team_id, user_id) + + @staticmethod + async def send_meeting_notification( + context: TurnContext, + notification: MeetingNotification, + meeting_id: Optional[str] = None, + ) -> MeetingNotificationResponse: + """ + Sends a meeting notification. + + Args: + context: The turn context. + notification: The meeting notification. + meeting_id: The meeting ID. If not provided, it will be extracted from the activity. + + Returns: + The meeting notification response. + + Raises: + ValueError: If required parameters are missing. + """ + activity = context.activity + + if meeting_id is None: + teams_channel_data: dict = activity.channel_data + meeting_id = teams_channel_data.get("meeting", {}).get("id", None) + + if not meeting_id: + raise ValueError("meeting_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.send_meeting_notification(meeting_id, notification) + + @staticmethod + async def send_message_to_list_of_users( + context: TurnContext, + activity: Activity, + tenant_id: str, + members: List[TeamsMember], + ) -> TeamsBatchOperationResponse: + """ + Sends a message to a list of users. + + Args: + context: The turn context. + activity: The activity to send. + tenant_id: The tenant ID. + members: The list of members. + + Returns: + The batch operation response. + + Raises: + ValueError: If required parameters are missing. + """ + if not activity: + raise ValueError("activity is required.") + if not tenant_id: + raise ValueError("tenant_id is required.") + if not members or len(members) == 0: + raise ValueError("members list is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.send_message_to_list_of_users( + activity, tenant_id, members + ) + + @staticmethod + async def send_message_to_all_users_in_tenant( + context: TurnContext, activity: Activity, tenant_id: str + ) -> TeamsBatchOperationResponse: + """ + Sends a message to all users in a tenant. + + Args: + context: The turn context. + activity: The activity to send. + tenant_id: The tenant ID. + + Returns: + The batch operation response. + + Raises: + ValueError: If required parameters are missing. + """ + if not activity: + raise ValueError("activity is required.") + if not tenant_id: + raise ValueError("tenant_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.send_message_to_all_users_in_tenant( + activity, tenant_id + ) + + @staticmethod + async def send_message_to_all_users_in_team( + context: TurnContext, activity: Activity, tenant_id: str, team_id: str + ) -> TeamsBatchOperationResponse: + """ + Sends a message to all users in a team. + + Args: + context: The turn context. + activity: The activity to send. + tenant_id: The tenant ID. + team_id: The team ID. + + Returns: + The batch operation response. + + Raises: + ValueError: If required parameters are missing. + """ + if not activity: + raise ValueError("activity is required.") + if not tenant_id: + raise ValueError("tenant_id is required.") + if not team_id: + raise ValueError("team_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.send_message_to_all_users_in_team( + activity, tenant_id, team_id + ) + + @staticmethod + async def send_message_to_list_of_channels( + context: TurnContext, + activity: Activity, + tenant_id: str, + members: List[TeamsMember], + ) -> TeamsBatchOperationResponse: + """ + Sends a message to a list of channels. + + Args: + context: The turn context. + activity: The activity to send. + tenant_id: The tenant ID. + members: The list of members. + + Returns: + The batch operation response. + + Raises: + ValueError: If required parameters are missing. + """ + if not activity: + raise ValueError("activity is required.") + if not tenant_id: + raise ValueError("tenant_id is required.") + if not members or len(members) == 0: + raise ValueError("members list is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.send_message_to_list_of_channels( + activity, tenant_id, members + ) + + @staticmethod + async def get_operation_state( + context: TurnContext, operation_id: str + ) -> BatchOperationStateResponse: + """ + Gets the operation state. + + Args: + context: The turn context. + operation_id: The operation ID. + + Returns: + The operation state response. + + Raises: + ValueError: If required parameters are missing. + """ + if not operation_id: + raise ValueError("operation_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.get_operation_state(operation_id) + + @staticmethod + async def get_failed_entries( + context: TurnContext, operation_id: str + ) -> BatchFailedEntriesResponse: + """ + Gets the failed entries of an operation. + + Args: + context: The turn context. + operation_id: The operation ID. + + Returns: + The failed entries response. + + Raises: + ValueError: If required parameters are missing. + """ + if not operation_id: + raise ValueError("operation_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.get_failed_entries(operation_id) + + @staticmethod + async def cancel_operation( + context: TurnContext, operation_id: str + ) -> CancelOperationResponse: + """ + Cancels an operation. + + Args: + context: The turn context. + operation_id: The operation ID. + + Returns: + The cancel operation response. + + Raises: + ValueError: If required parameters are missing. + """ + if not operation_id: + raise ValueError("operation_id is required.") + + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.cancel_operation(operation_id) + + @staticmethod + async def _get_member_internal( + context: TurnContext, conversation_id: str, user_id: str + ) -> TeamsChannelAccount: + """ + Internal method to get a member from a conversation. + + Args: + context: The turn context. + conversation_id: The conversation ID. + user_id: The user ID. + + Returns: + The member information. + + Raises: + ValueError: If required parameters are missing. + """ + rest_client = TeamsInfo._get_rest_client(context) + return await rest_client.get_conversation_member(conversation_id, user_id) + + @staticmethod + def _get_rest_client(context: TurnContext) -> TeamsConnectorClient: + """ + Gets the Teams connector client from the context. + + Args: + context: The turn context. + + Returns: + The Teams connector client. + + Raises: + ValueError: If the client is not available in the context. + """ + # TODO: Varify key + client = context.turn_state.get("ConnectorClient") + if not client: + raise ValueError("TeamsConnectorClient is not available in the context.") + return client diff --git a/libraries/Hosting/microsoft-agents-hosting-teams/pyproject.toml b/libraries/Hosting/microsoft-agents-hosting-teams/pyproject.toml new file mode 100644 index 00000000..47705c30 --- /dev/null +++ b/libraries/Hosting/microsoft-agents-hosting-teams/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "microsoft-agents-hosting-teams" +version = "0.0.0a1" +description = "A protocol library for Microsoft Agents" +authors = [{name = "Microsoft Corporation"}] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "microsoft-agents-connector", + "microsoft-agents-builder", + "aiohttp>=3.11.11", +] + +[project.urls] +"Homepage" = "https://github.com/microsoft/Agents-for-python" diff --git a/test_samples/teams_agent/app.py b/test_samples/teams_agent/app.py new file mode 100644 index 00000000..2dc05099 --- /dev/null +++ b/test_samples/teams_agent/app.py @@ -0,0 +1,128 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +from dotenv import load_dotenv +from aiohttp.web import Application, Request, Response, run_app, static + +from microsoft.agents.builder import RestChannelServiceClientFactory +from microsoft.agents.builder.state import UserState +from microsoft.agents.hosting.aiohttp import CloudAdapter, jwt_authorization_decorator +from microsoft.agents.authorization import ( + Connections, + AccessTokenProviderBase, + ClaimsIdentity, +) +from microsoft.agents.authentication.msal import MsalAuth +from microsoft.agents.storage import MemoryStorage + +from teams_handler import TeamsHandler +from teams_sso import TeamsSso +from teams_multi_feature import TeamsMultiFeature +from config import DefaultConfig + +load_dotenv() + +CONFIG = DefaultConfig() +AUTH_PROVIDER = MsalAuth(DefaultConfig()) + + +class DefaultConnection(Connections): + def get_default_connection(self) -> AccessTokenProviderBase: + pass + + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: + return AUTH_PROVIDER + + def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + return AUTH_PROVIDER + + +CHANNEL_CLIENT_FACTORY = RestChannelServiceClientFactory(CONFIG, DefaultConnection()) + +# Create adapter. +ADAPTER = CloudAdapter(CHANNEL_CLIENT_FACTORY) + +# Create the storage and user state (for SSO agent) +STORAGE = MemoryStorage() +USER_STATE = UserState(STORAGE) + + +def create_agent(agent_type: str): + """ + Create the appropriate agent based on configuration. + """ + if agent_type == "TeamsSso": + return TeamsSso(USER_STATE, CONFIG.CONNECTION_NAME, CONFIG.CLIENT_ID) + elif agent_type == "TeamsMultiFeature": + return TeamsMultiFeature() + else: # Default to TeamsHandler + return TeamsHandler() + + +# Create the agent based on configuration +AGENT = create_agent(CONFIG.AGENT_TYPE) + + +# Listen for incoming requests on /api/messages +@jwt_authorization_decorator +async def messages(req: Request) -> Response: + adapter: CloudAdapter = req.app["adapter"] + return await adapter.process(req, AGENT) + + +# Return the YouTube page +async def youtube(req: Request) -> Response: + return await render_page(req, "youtube.html") + + +# Return the custom form page +async def custom_form(req: Request) -> Response: + return await render_page(req, "customForm.html") + + +# Handle form submission +async def handle_form(req: Request) -> Response: + print("Data is being sent from the form") + return Response(text="Form data received") + + +async def render_page(req: Request, page_name: str) -> Response: + pages_path = pathlib.Path(__file__).parent / "pages" + if not pages_path.exists(): + pages_path.mkdir() + + page_file = pages_path / page_name + + if page_file.exists(): + with open(page_file, "r") as file: + content = file.read() + return Response(text=content, content_type="text/html") + else: + return Response(text="Page not found", status=404) + + +APP = Application() +APP.router.add_post("/api/messages", messages) +APP.router.add_get("/Youtube", youtube) +APP.router.add_get("/CustomForm", custom_form) +APP.router.add_post("/CustomForm", handle_form) + +# Add static file handling for CSS, JS, etc. +static_path = pathlib.Path(__file__).parent / "public" +if static_path.exists(): + APP.router.add_static("/public", static_path) + +APP["agent_configuration"] = CONFIG +APP["adapter"] = ADAPTER + +if __name__ == "__main__": + try: + port = CONFIG.PORT + print(f"\nServer listening on port {port} for appId {CONFIG.CLIENT_ID}") + run_app(APP, host="localhost", port=port) + except Exception as error: + raise error diff --git a/test_samples/teams_agent/cards/AdaptiveCard.json b/test_samples/teams_agent/cards/AdaptiveCard.json new file mode 100644 index 00000000..25d19fa7 --- /dev/null +++ b/test_samples/teams_agent/cards/AdaptiveCard.json @@ -0,0 +1,26 @@ + +{ + "version": "1.0", + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "text": "Enter Text Here", + "weight": "bolder", + "isSubtle": false + }, + { + "type": "Input.Text", + "id": "usertext", + "spacing": "none", + "isMultiLine": "true", + "placeholder": "add some text and submit" + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Submit" + } + ] +} diff --git a/test_samples/teams_agent/cards/AdaptiveCard_TaskModule.json b/test_samples/teams_agent/cards/AdaptiveCard_TaskModule.json new file mode 100644 index 00000000..a2d124e7 --- /dev/null +++ b/test_samples/teams_agent/cards/AdaptiveCard_TaskModule.json @@ -0,0 +1,112 @@ +{ + "type": "AdaptiveCard", + "body": [ + { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Publish Adaptive Card schema" + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "Image", + "style": "Person", + "url": "https://pbs.twimg.com/profile_images/3647943215/d7f12830b3c17a5a9e4afcc370e3a37e_400x400.jpeg", + "size": "Small" + } + ], + "width": "auto" + }, + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "Matt Hidinger", + "wrap": true + }, + { + "type": "TextBlock", + "spacing": "None", + "text": "Created {{DATE(2017-02-14T06:08:39Z,SHORT)}}", + "isSubtle": true, + "wrap": true + } + ], + "width": "stretch" + } + ] + } + ] + }, + { + "type": "Container", + "items": [ + { + "type": "TextBlock", + "text": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.", + "wrap": true + }, + { + "type": "FactSet", + "facts": [ + { + "title": "Board:", + "value": "Adaptive Card" + }, + { + "title": "List:", + "value": "Backlog" + }, + { + "title": "Assigned to:", + "value": "Matt Hidinger" + }, + { + "title": "Due date:", + "value": "Not set" + } + ] + } + ] + } + ], + "actions": [ + { + "type": "Action.ShowCard", + "title": "Set due date", + "card": { + "type": "AdaptiveCard", + "style": "emphasis", + "body": [ + { + "type": "Input.Date", + "id": "dueDate" + }, + { + "type": "Input.Text", + "id": "comment", + "placeholder": "Add a comment", + "isMultiline": true + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": "OK" + } + ] + } + } + ], + "version": "1.0" +} \ No newline at end of file diff --git a/test_samples/teams_agent/cards/RestaurantCard.json b/test_samples/teams_agent/cards/RestaurantCard.json new file mode 100644 index 00000000..dd8e26dc --- /dev/null +++ b/test_samples/teams_agent/cards/RestaurantCard.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "speak": "Tom's Pie is a pizza restaurant which is rated 9.3 by customers.", + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": 2, + "items": [ + { + "type": "TextBlock", + "text": "**PIZZA**" + }, + { + "type": "TextBlock", + "text": "Tom's Pie", + "weight": "bolder", + "size": "extraLarge", + "spacing": "none" + }, + { + "type": "TextBlock", + "text": "4.2 $", + "isSubtle": true, + "spacing": "none" + }, + { + "type": "TextBlock", + "text": "**Matt H. said** \"I'm compelled to give this place 5 stars due to the number of times I've chosen to eat here this past year!\"", + "size": "small", + "wrap": true + } + ] + }, + { + "type": "Column", + "width": 1, + "items": [ + { + "type": "Image", + "url": "https://picsum.photos/300?image=882", + "size": "auto" + } + ] + } + ] + } + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "More Info", + "url": "https://www.youtube.com/watch?v=CH2seLS5Wb0" + } + ] +} \ No newline at end of file diff --git a/test_samples/teams_agent/cards/UserProfileCard.json b/test_samples/teams_agent/cards/UserProfileCard.json new file mode 100644 index 00000000..fba3ca09 --- /dev/null +++ b/test_samples/teams_agent/cards/UserProfileCard.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "type": "AdaptiveCard", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "altText": "", + "url": "${imageUri}", + "style": "Person", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "${displayName}" + }, + { + "type": "Container", + "spacing": "Small", + "items": [ + { + "type": "TextBlock", + "text": "${toUpper(jobTitle)}", + "spacing": "Small" + }, + { + "type": "TextBlock", + "text": "${mail}", + "spacing": "None" + }, + { + "type": "TextBlock", + "text": "${givenName}", + "spacing": "None" + }, + { + "type": "TextBlock", + "text": "${surname}", + "spacing": "None" + } + ] + } + ] + } + ] + } + ] + +} \ No newline at end of file diff --git a/test_samples/teams_agent/config.py b/test_samples/teams_agent/config.py new file mode 100644 index 00000000..ee9c3299 --- /dev/null +++ b/test_samples/teams_agent/config.py @@ -0,0 +1,17 @@ +from os import environ +from microsoft.agents.authentication.msal import AuthTypes, MsalAuthConfiguration + + +class DefaultConfig(MsalAuthConfiguration): + """Teams Agent Configuration""" + + def __init__(self) -> None: + self.AUTH_TYPE = AuthTypes.client_secret + self.TENANT_ID = "" or environ.get("TENANT_ID") + self.CLIENT_ID = "" or environ.get("CLIENT_ID") + self.CLIENT_SECRET = "" or environ.get("CLIENT_SECRET") + self.CONNECTION_NAME = "" or environ.get("CONNECTION_NAME") + self.AGENT_TYPE = environ.get( + "AGENT_TYPE", "TeamsHandler" + ) # Default to TeamsHandler + self.PORT = 3978 diff --git a/test_samples/teams_agent/graph_client.py b/test_samples/teams_agent/graph_client.py new file mode 100644 index 00000000..f9a292d8 --- /dev/null +++ b/test_samples/teams_agent/graph_client.py @@ -0,0 +1,78 @@ +import aiohttp + + +class GraphClient: + """ + A simple Microsoft Graph client using aiohttp. + """ + + def __init__(self, token: str): + self.token = token + self.base_url = "https://graph.microsoft.com/v1.0" + + async def get_me(self): + """ + Get information about the current user. + """ + async with aiohttp.ClientSession() as session: + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + async with session.get(f"{self.base_url}/me", headers=headers) as response: + if response.status == 200: + return await response.json() + else: + error_text = await response.text() + raise Exception( + f"Error from Graph API: {response.status} - {error_text}" + ) + + async def get_user_photo(self): + """ + Get the current user's photo. + """ + async with aiohttp.ClientSession() as session: + headers = {"Authorization": f"Bearer {self.token}"} + async with session.get( + f"{self.base_url}/me/photo/$value", headers=headers + ) as response: + if response.status == 200: + return await response.read() + elif response.status == 404: + return None # No photo available + else: + error_text = await response.text() + raise Exception( + f"Error from Graph API: {response.status} - {error_text}" + ) + + async def get_calendar_events(self, start_datetime=None, end_datetime=None): + """ + Get the current user's calendar events. + """ + async with aiohttp.ClientSession() as session: + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + + url = f"{self.base_url}/me/events" + params = {} + + if start_datetime and end_datetime: + filter_query = f"start/dateTime ge '{start_datetime}' and end/dateTime le '{end_datetime}'" + params["$filter"] = filter_query + + params["$select"] = "subject,organizer,start,end,location" + params["$orderby"] = "start/dateTime" + params["$top"] = "10" + + async with session.get(url, headers=headers, params=params) as response: + if response.status == 200: + return await response.json() + else: + error_text = await response.text() + raise Exception( + f"Error from Graph API: {response.status} - {error_text}" + ) diff --git a/test_samples/teams_agent/helpers/task_module_ids.py b/test_samples/teams_agent/helpers/task_module_ids.py new file mode 100644 index 00000000..69c1a59b --- /dev/null +++ b/test_samples/teams_agent/helpers/task_module_ids.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Task Module ID constants.""" + + +from enum import Enum + + +class TaskModuleIds(str, Enum): + """ + Class containing constants for Task Module IDs. + """ + + AdaptiveCard = "AdaptiveCard" + YouTube = "YouTube" + CustomForm = "CustomForm" diff --git a/test_samples/teams_agent/helpers/task_module_response_factory.py b/test_samples/teams_agent/helpers/task_module_response_factory.py new file mode 100644 index 00000000..9a201194 --- /dev/null +++ b/test_samples/teams_agent/helpers/task_module_response_factory.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Factory for creating task module responses.""" + +from microsoft.agents.core.models.teams import ( + TaskModuleResponse, + TaskModuleTaskInfo, + TaskModuleResponseBase, +) + + +class TaskModuleResponseFactory: + """Factory for creating task module responses.""" + + @staticmethod + def to_task_module_response(task_info: TaskModuleTaskInfo) -> TaskModuleResponse: + """ + Convert a TaskModuleTaskInfo to a TaskModuleResponse. + + Args: + task_info: The task info to convert. + + Returns: + A TaskModuleResponse instance. + """ + return TaskModuleResponseFactory.create_response(task_info) + + @staticmethod + def create_response(task_info: TaskModuleTaskInfo) -> TaskModuleResponse: + """ + Create a TaskModuleResponse with type 'continue' and the provided task info as value. + + Args: + task_info: The task info to use as value. + + Returns: + A TaskModuleResponse instance. + """ + info = task_info.model_dump(by_alias=True, exclude_none=True) + task_module_response_base = TaskModuleResponseBase(type="continue", value=info) + + return TaskModuleResponse(task=task_module_response_base) + + @staticmethod + def create_message_response(message: str) -> TaskModuleResponse: + """ + Create a TaskModuleResponse with type 'message' and the provided message as value. + + Args: + message: The message to use as value. + + Returns: + A TaskModuleResponse instance. + """ + task_module_response_base = TaskModuleResponseBase( + type="message", value=message + ) + + return TaskModuleResponse(task=task_module_response_base) diff --git a/test_samples/teams_agent/helpers/task_module_ui_constants.py b/test_samples/teams_agent/helpers/task_module_ui_constants.py new file mode 100644 index 00000000..60fec83e --- /dev/null +++ b/test_samples/teams_agent/helpers/task_module_ui_constants.py @@ -0,0 +1,37 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Task Module UI constants for Teams applications.""" + +from .task_module_ids import TaskModuleIds +from .ui_settings import UISettings + + +class TaskModuleUIConstants: + """ + Class containing UI settings constants for various Task Modules. + """ + + AdaptiveCard: UISettings = UISettings( + width=400, + height=200, + title="Adaptive Card: Inputs", + id=TaskModuleIds.AdaptiveCard, + button_title="Adaptive Card", + ) + + YouTube: UISettings = UISettings( + width=1000, + height=700, + title="You Tube Video", + id=TaskModuleIds.YouTube, + button_title="You Tube", + ) + + CustomForm: UISettings = UISettings( + width=510, + height=450, + title="Custom Form", + id=TaskModuleIds.CustomForm, + button_title="Custom Form", + ) diff --git a/test_samples/teams_agent/helpers/ui_settings.py b/test_samples/teams_agent/helpers/ui_settings.py new file mode 100644 index 00000000..03215275 --- /dev/null +++ b/test_samples/teams_agent/helpers/ui_settings.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""UI Settings model for Teams task modules.""" + +from typing import Optional + + +class UISettings: + """ + Settings for configuring UI elements in Teams task modules. + + Attributes: + height: The height of the UI element. + width: The width of the UI element. + title: The title to display. + button_title: The text to display on the button. + id: The identifier for the UI element. + """ + + def __init__( + self, + height: Optional[int] = None, + width: Optional[int] = None, + title: Optional[str] = None, + button_title: Optional[str] = None, + id: Optional[str] = None, + ): + """ + Initialize UISettings with optional parameters. + Args: + height: The height of the UI element. + width: The width of the UI element. + title: The title to display. + button_title: The text to display on the button. + id: The identifier for the UI element. + """ + self.height = height + self.width = width + self.title = title + self.button_title = button_title + self.id = id diff --git a/test_samples/teams_agent/pages/customForm.html b/test_samples/teams_agent/pages/customForm.html new file mode 100644 index 00000000..aa696b28 --- /dev/null +++ b/test_samples/teams_agent/pages/customForm.html @@ -0,0 +1,154 @@ + + + + + + Custom Form + + + + +
+

Custom Form

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/test_samples/teams_agent/pages/youtube.html b/test_samples/teams_agent/pages/youtube.html new file mode 100644 index 00000000..b26f959b --- /dev/null +++ b/test_samples/teams_agent/pages/youtube.html @@ -0,0 +1,77 @@ + + + + + + YouTube Video + + + + +
+

Microsoft Teams Video

+
+ +
+ +
+ + + + \ No newline at end of file diff --git a/test_samples/teams_agent/teams_handler.py b/test_samples/teams_agent/teams_handler.py new file mode 100644 index 00000000..9b4e32e0 --- /dev/null +++ b/test_samples/teams_agent/teams_handler.py @@ -0,0 +1,233 @@ +from microsoft.agents.builder import MessageFactory, TurnContext +from microsoft.agents.hosting.teams import TeamsActivityHandler, TeamsInfo +from microsoft.agents.core.models import ChannelAccount, ConversationParameters +from microsoft.agents.core.models.teams import MeetingNotification + + +class TeamsHandler(TeamsActivityHandler): + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity("Hello from Python Teams handler!") + + async def on_message_activity(self, turn_context: TurnContext): + text = turn_context.activity.text.strip() + + if "getMember" in text: + member = await TeamsInfo.get_member( + turn_context, turn_context.activity.from_property.id + ) + await turn_context.send_activity( + MessageFactory.text(f"You mentioned me! {member}") + ) + elif "getPagedMembers" in text: + members = await TeamsInfo.get_paged_members(turn_context, 2) + member_emails = [m.email for m in members.members if m.email] + await turn_context.send_activity( + MessageFactory.text(f"members: {member_emails}") + ) + elif "getTeamChannels" in text: + channels = await TeamsInfo.get_team_channels(turn_context) + await turn_context.send_activity( + MessageFactory.text(f"list channels: {channels}") + ) + elif "getTeamDetails" in text: + team_details = await TeamsInfo.get_team_details(turn_context) + await turn_context.send_activity( + MessageFactory.text(f"Team details: {team_details}") + ) + elif "getMeetingInfo" in text: + meeting_info = await TeamsInfo.get_meeting_info(turn_context) + await turn_context.send_activity( + MessageFactory.text(f"Meeting Info: {meeting_info}") + ) + elif "getMeetingParticipant" in text: + meeting_participant = await TeamsInfo.get_meeting_participant(turn_context) + await turn_context.send_activity( + MessageFactory.text(f"Meeting participant: {meeting_participant}") + ) + elif "getPagedTeamMembers" in text: + members = await TeamsInfo.get_paged_team_members(turn_context, None, 2) + member_emails = [m.email for m in members.members if m.email] + await turn_context.send_activity( + MessageFactory.text(f"team members: {member_emails}") + ) + elif "getTeamMember" in text: + teams_channel_data = turn_context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id") + if not team_id: + await turn_context.send_activity( + MessageFactory.text("Team ID not found") + ) + return + + member = await TeamsInfo.get_team_member( + turn_context, team_id, turn_context.activity.from_property.id + ) + await turn_context.send_activity( + MessageFactory.text(f"team member: {member}") + ) + elif "sendMessageToTeamsChannel" in text: + teams_channel_data = turn_context.activity.channel_data + channel_id = teams_channel_data.get("channel", {}).get("id") + if not channel_id: + await turn_context.send_activity( + MessageFactory.text("Channel ID not found") + ) + return + + client_id = ( + turn_context.turn_state[turn_context.adapter.AGENT_IDENTITY_KEY].claims[ + "aud" + ], + ) + sent_result = await TeamsInfo.send_message_to_teams_channel( + turn_context, + MessageFactory.text("message from agent to channel"), + channel_id, + client_id, + ) + await turn_context.send_activity( + MessageFactory.text(f"sent: {sent_result}") + ) + elif "sendMeetingNotification" in text: + teams_channel_data: dict = turn_context.activity.channel_data + meeting_id = teams_channel_data.get("meeting", {}).get("id") + if not meeting_id: + await turn_context.send_activity( + MessageFactory.text("Meeting ID not found") + ) + return + + notification = MeetingNotification( + type="targetedMeetingNotification", + value={"recipients": ["rido"], "surfaces": []}, + ) + resp = await TeamsInfo.send_meeting_notification( + turn_context, notification, meeting_id + ) + await turn_context.send_activity( + MessageFactory.text(f"sendMeetingNotification: {resp}") + ) + elif "sendMessageToListOfUsers" in text: + members = await TeamsInfo.get_paged_members(turn_context, 2) + users = [{"id": m.id} for m in members.members if m.email] + + tenant_id = turn_context.activity.conversation.tenant_id + await TeamsInfo.send_message_to_list_of_users( + turn_context, + MessageFactory.text("message from agent to list of users"), + tenant_id, + users, + ) + await turn_context.send_activity( + MessageFactory.text("Messages sent to list of users") + ) + elif "sendMessageToAllUsersInTenant" in text: + tenant_id = turn_context.activity.conversation.tenant_id + batch_resp = await TeamsInfo.send_message_to_all_users_in_tenant( + turn_context, + MessageFactory.text("message from agent to all users"), + tenant_id, + ) + await turn_context.send_activity( + MessageFactory.text(f"Operation ID: {batch_resp.operation_id}") + ) + elif "sendMessageToAllUsersInTeam" in text: + teams_channel_data = turn_context.activity.channel_data + team_id = teams_channel_data.get("team", {}).get("id") + if not team_id: + await turn_context.send_activity( + MessageFactory.text("Team ID not found") + ) + return + + tenant_id = turn_context.activity.conversation.tenant_id + batch_resp = await TeamsInfo.send_message_to_all_users_in_team( + turn_context, + MessageFactory.text("message from agent to all users in team"), + tenant_id, + team_id, + ) + await turn_context.send_activity( + MessageFactory.text(f"Operation ID: {batch_resp.operation_id}") + ) + elif "sendMessageToListOfChannels" in text: + members = await TeamsInfo.get_paged_members(turn_context, 2) + users = [{"id": m.id} for m in members.members if m.email] + + tenant_id = turn_context.activity.conversation.tenant_id + await TeamsInfo.send_message_to_list_of_channels( + turn_context, + MessageFactory.text("message from agent to list of channels"), + tenant_id, + users, + ) + await turn_context.send_activity( + MessageFactory.text("Messages sent to list of channels") + ) + elif "msg all_members" in text: + await self.message_all_members(turn_context) + else: + await turn_context.send_activities( + [ + MessageFactory.text("Welcome to Python Teams handler!"), + MessageFactory.text( + "Options: getMember, getPagedMembers, getTeamChannels, getTeamDetails, getMeetingInfo, " + "getMeetingParticipant, getPagedTeamMembers, getTeamMember, sendMessageToTeamsChannel, " + "sendMeetingNotification, sendMessageToListOfUsers, sendMessageToAllUsersInTenant, " + "sendMessageToAllUsersInTeam, sendMessageToListOfChannels, msg all_members" + ), + ] + ) + + async def message_all_members(self, turn_context: TurnContext): + author = await TeamsInfo.get_member( + turn_context, turn_context.activity.from_property.id + ) + members_result = await TeamsInfo.get_paged_members(turn_context, 2) + + for member in members_result.members: + message = MessageFactory.text( + f"Hello {member.given_name} {member.surname}. I'm a Teams conversation agent from {author.email}" + ) + + convo_params = ConversationParameters( + members=[{"id": member.id}], + is_group=False, + agent=turn_context.activity.recipient, + tenant_id=turn_context.activity.conversation.tenant_id, + activity=message, + channel_data=turn_context.activity.channel_data, + ) + + async def conversation_callback(inner_context: TurnContext): + ref = inner_context.activity.get_conversation_reference() + + async def continue_callback(ctx: TurnContext): + await ctx.send_activity(message) + + await inner_context.adapter.continue_conversation( + inner_context.turn_state[ + turn_context.adapter.AGENT_IDENTITY_KEY + ].claims["aud"], + ref, + continue_callback, + ) + + await turn_context.adapter.create_conversation( + turn_context.turn_state[turn_context.adapter.AGENT_IDENTITY_KEY].claims[ + "aud" + ], + turn_context.activity.channel_id, + turn_context.activity.service_url, + "https://api.botframework.com", + convo_params, + conversation_callback, + ) + + await turn_context.send_activity( + MessageFactory.text("All messages have been sent.") + ) diff --git a/test_samples/teams_agent/teams_multi_feature.py b/test_samples/teams_agent/teams_multi_feature.py new file mode 100644 index 00000000..413494fe --- /dev/null +++ b/test_samples/teams_agent/teams_multi_feature.py @@ -0,0 +1,390 @@ +import json +from os import getenv + +from microsoft.agents.builder import MessageFactory, TurnContext +from microsoft.agents.core.models import ChannelAccount, Attachment +from microsoft.agents.core.models.teams import ( + TaskModuleResponse, + TaskModuleTaskInfo, + TaskModuleRequest, +) +from microsoft.agents.hosting.teams import ( + TeamsActivityHandler, + TeamsInfo, +) + +from helpers.task_module_response_factory import TaskModuleResponseFactory +from helpers.task_module_ids import TaskModuleIds +from helpers.ui_settings import UISettings +from helpers.task_module_ui_constants import TaskModuleUIConstants + + +class TeamsMultiFeature(TeamsActivityHandler): + """Teams handler implementing multiple advanced Teams features""" + + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity( + "Welcome to the Teams Multi-Feature demo!" + ) + await self._send_help_card(turn_context) + + async def on_message_activity(self, turn_context: TurnContext): + text = turn_context.activity.text.strip() if turn_context.activity.text else "" + + if "help" in text.lower(): + await self._send_help_card(turn_context) + elif "user profile" in text.lower(): + await self._send_user_profile_card(turn_context) + elif "restaurant" in text.lower(): + await self._send_restaurant_card(turn_context) + elif "task" in text.lower(): + await self._send_task_module_card(turn_context) + elif "youtube" in text.lower(): + await self._send_youtube_card(turn_context) + else: + await turn_context.send_activity( + MessageFactory.text( + "Type 'help' to see available commands, or " + "'user profile', 'restaurant', 'task', or 'youtube'" + ) + ) + + async def on_teams_task_module_fetch( + self, turn_context: TurnContext, task_module_request: TaskModuleRequest + ) -> TaskModuleResponse: + base_url = getenv("BASE_URL") + # Handle task module requests + task_module_type = task_module_request.data.get("data") + task_info: TaskModuleTaskInfo = None + + if task_module_type: + task_module_type = task_module_type.lower() + + if task_module_type == TaskModuleIds.YouTube.lower(): + task_info = self._set_task_info(TaskModuleUIConstants.YouTube) + task_info.url = task_info.fallback_url = f"{base_url}/Youtube" + elif task_module_type == TaskModuleIds.AdaptiveCard.lower(): + task_info = self._set_task_info(TaskModuleUIConstants.AdaptiveCard) + task_info.card = self._get_adaptive_card_content() + elif task_module_type == TaskModuleIds.CustomForm.lower(): + task_info = self._set_task_info(TaskModuleUIConstants.CustomForm) + task_info.url = task_info.fallback_url = f"{base_url}/CustomForm" + + response = TaskModuleResponseFactory.to_task_module_response(task_info) + + return response + + async def on_teams_task_module_submit( + self, turn_context: TurnContext, task_module_request + ): + # Handle task module submissions + data = task_module_request.data + message = f"Received task module submission: {json.dumps(data)}" + await turn_context.send_activity(MessageFactory.text(message)) + return TaskModuleResponseFactory.create_message_response(message) + + async def _send_help_card(self, turn_context: TurnContext): + """Send a help card with available commands""" + card_data = { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Teams Multi-Feature Demo - Available Commands", + }, + { + "type": "FactSet", + "facts": [ + {"title": "user profile", "value": "Show user profile card"}, + {"title": "restaurant", "value": "Show restaurant card"}, + {"title": "task", "value": "Show task module card"}, + {"title": "youtube", "value": "Show YouTube card"}, + ], + }, + ], + } + + attachment = Attachment( + content_type="application/vnd.microsoft.card.adaptive", content=card_data + ) + + await turn_context.send_activity(MessageFactory.attachment(attachment)) + + async def _send_user_profile_card(self, turn_context: TurnContext): + """Send a user profile card""" + try: + # Get the current user info + member = await TeamsInfo.get_member( + turn_context, turn_context.activity.from_property.id + ) + + card_data = { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "User Profile", + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "style": "Person", + "size": "Small", + } + ], + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": member.name, + "wrap": True, + }, + { + "type": "TextBlock", + "spacing": "None", + "text": f"Email: {member.email or 'Not available'}", + "wrap": True, + }, + ], + }, + ], + }, + ], + } + + attachment = Attachment( + content_type="application/vnd.microsoft.card.adaptive", + content=card_data, + ) + + await turn_context.send_activity(MessageFactory.attachment(attachment)) + + except Exception as e: + await turn_context.send_activity(f"Error retrieving user profile: {str(e)}") + + async def _send_restaurant_card(self, turn_context: TurnContext): + """Send a restaurant card example""" + card_data = { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Restaurant Recommendation", + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "size": "Small", + "url": "https://adaptivecards.io/images/Contoso.png", + } + ], + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "Contoso Cafe", + "wrap": True, + }, + { + "type": "TextBlock", + "spacing": "None", + "text": "⭐⭐⭐⭐★ (4.2) · $$", + "wrap": True, + }, + ], + }, + ], + }, + { + "type": "TextBlock", + "text": "Italian cuisine in a casual atmosphere", + "wrap": True, + }, + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "View Menu", + "url": "https://example.com/menu", + }, + { + "type": "Action.OpenUrl", + "title": "Order Online", + "url": "https://example.com/order", + }, + ], + } + + attachment = Attachment( + content_type="application/vnd.microsoft.card.adaptive", content=card_data + ) + + await turn_context.send_activity(MessageFactory.attachment(attachment)) + + async def _send_task_module_card(self, turn_context: TurnContext): + """Send a card that opens a task module""" + card_data = { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Task Module Demos", + }, + { + "type": "TextBlock", + "text": "Click the buttons below to open different types of task modules", + "wrap": True, + }, + ], + "actions": [ + { + "type": "Action.Submit", + "title": "Adaptive Card Form", + "data": { + "msteams": {"type": "task/fetch"}, + "data": "adaptiveCard", + }, + }, + { + "type": "Action.Submit", + "title": "YouTube Video", + "data": { + "msteams": {"type": "task/fetch"}, + "data": "Youtube", + }, + }, + { + "type": "Action.Submit", + "title": "Custom Form", + "data": { + "msteams": {"type": "task/fetch"}, + "data": "customForm", + }, + }, + ], + } + + attachment = Attachment( + content_type="application/vnd.microsoft.card.adaptive", content=card_data + ) + + await turn_context.send_activity(MessageFactory.attachment(attachment)) + + async def _send_youtube_card(self, turn_context: TurnContext): + """Send a card with a YouTube video""" + card_data = { + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.3", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": "Microsoft Teams", + }, + { + "type": "TextBlock", + "text": "Watch this video to learn more about Microsoft Teams", + "wrap": True, + }, + { + "type": "Image", + "url": "https://i.ytimg.com/vi/X8krAMdGvCQ/maxresdefault.jpg", + "size": "Large", + "selectAction": { + "type": "Action.Submit", + "title": "Watch Video", + "data": { + "msteams": {"type": "task/fetch"}, + "data": "Youtube", + }, + }, + }, + ], + } + + attachment = Attachment( + content_type="application/vnd.microsoft.card.adaptive", content=card_data + ) + + await turn_context.send_activity(MessageFactory.attachment(attachment)) + + def _get_adaptive_card_content(self): + """Get a basic adaptive card content""" + content = { + "version": "1.3", + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "text": "Enter Text Here", + "weight": "bolder", + "isSubtle": False, + }, + { + "type": "Input.Text", + "id": "usertext", + "spacing": "none", + "isMultiLine": "true", + "placeholder": "add some text and submit", + }, + ], + "actions": [{"type": "Action.Submit", "title": "Submit"}], + } + + # TODO: Fix card creation + return Attachment.model_validate( + { + "content_type": "application/vnd.microsoft.card.adaptive", + "content": content, + } + ) + + def _set_task_info(self, ui_constans: UISettings) -> TaskModuleTaskInfo: + """Set task info for the task module""" + task_info = TaskModuleTaskInfo( + title=ui_constans.title, height=ui_constans.height, width=ui_constans.width + ) + + return task_info diff --git a/test_samples/teams_agent/teams_sso.py b/test_samples/teams_agent/teams_sso.py new file mode 100644 index 00000000..7857884c --- /dev/null +++ b/test_samples/teams_agent/teams_sso.py @@ -0,0 +1,85 @@ +from microsoft.agents.builder import ( + ActivityHandler, + BasicOAuthFlow, + MessageFactory, + TurnContext, +) +from microsoft.agents.builder.state import UserState +from microsoft.agents.core.models import ChannelAccount +from microsoft.agents.hosting.teams import TeamsActivityHandler, TeamsInfo + +from graph_client import GraphClient + + +class TeamsSso(TeamsActivityHandler): + def __init__( + self, user_state: UserState, connection_name: str = None, app_id: str = None + ): + """ + Initializes a new instance of the TeamsSso class. + :param user_state: The user state. + :param connection_name: Connection name for OAuth. + :param app_id: Application ID. + """ + self.user_state = user_state + self.oauth_flow = BasicOAuthFlow(user_state, connection_name, app_id) + + async def on_members_added_activity( + self, members_added: list[ChannelAccount], turn_context: TurnContext + ): + for member in members_added: + if member.id != turn_context.activity.recipient.id: + await turn_context.send_activity( + "Hello and welcome to Teams SSO sample!" + ) + + async def on_message_activity(self, turn_context: TurnContext): + text = turn_context.activity.text.strip() if turn_context.activity.text else "" + + if text == "login": + await self.oauth_flow.get_oauth_token(turn_context) + elif text == "logout": + await self.oauth_flow.sign_out(turn_context) + await turn_context.send_activity( + MessageFactory.text("You have been signed out.") + ) + elif "getUserProfile" in text: + user_token = await self.oauth_flow.get_oauth_token(turn_context) + if user_token: + await self.send_logged_user_info(turn_context, user_token) + else: + await turn_context.send_activity( + MessageFactory.text("Please type 'login' to sign in first.") + ) + elif "getPagedMembers" in text: + members = await TeamsInfo.get_paged_members(turn_context, 2) + member_emails = [m.email for m in members.members if m.email] + await turn_context.send_activity( + MessageFactory.text(f"Team members: {member_emails}") + ) + else: + await turn_context.send_activity( + MessageFactory.text( + "Type 'login' to sign in, 'logout' to sign out, " + "'getUserProfile' to see your profile, or " + "'getPagedMembers' to see team members." + ) + ) + + async def on_turn(self, turn_context: TurnContext): + await super().on_turn(turn_context) + await self.user_state.save_changes(turn_context) + + async def send_logged_user_info(self, turn_context: TurnContext, token: str): + """ + Sends the logged user info. + :param turn_context: The turn context. + :param token: OAuth token. + """ + graph_client = GraphClient(token) + user_info = await graph_client.get_me() + message = ( + f"You are {user_info.get('displayName', 'Unknown')} " + f"and your email is {user_info.get('mail', 'Unknown')}." + ) + await turn_context.send_activity(MessageFactory.text(message))