From 5a03af0e887513b98d4d65a5763ddd89086ef3d7 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 2 Oct 2024 13:18:11 +0530 Subject: [PATCH 01/40] added a webhook polling class --- mindsdb/interfaces/chatbot/polling.py | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index 971332b7a28..aa19b6dc741 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -1,3 +1,4 @@ +from functools import reduce import time from mindsdb_sql.parser.ast import Identifier, Select, Insert @@ -159,3 +160,35 @@ def run(self, stop_event): # def send_message(self, message: ChatBotMessage): # # self.chat_task.chat_handler.realtime_send(message) + + +class WebhookPolling(BasePolling): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def run(self, request): + p_params = self.params["polling"] + chat_id_attr = p_params["chat_id_attr"] + + chat_id = self._parse_request(request, chat_id_attr) + chat_memory = self.chat_task.memory.get_chat(chat_id) + + message = ChatBotMessage( + ChatBotMessage.Type.DIRECT, + self._parse_request(request, p_params["message_attr"]), + self._parse_request(request, p_params["from_attr"]), + self._parse_request(request, p_params["to_attr"]), + request=request + ) + + self.chat_task.on_message(chat_memory, message) + + def send_message(self, message: ChatBotMessage): + self.chat_task.chat_handler.respond(message) + + def _parse_request(self, request, key): + if isinstance(key, list): + return reduce(lambda x, y: x[y], key, request) + + else: + return request[key] \ No newline at end of file From f27ad42cd74635100ad404094f08086b4d5fbf70 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 2 Oct 2024 13:20:27 +0530 Subject: [PATCH 02/40] extended chat bot message with optional kwargs --- mindsdb/interfaces/chatbot/types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mindsdb/interfaces/chatbot/types.py b/mindsdb/interfaces/chatbot/types.py index 89b3e238fb2..f0cc6c944e3 100644 --- a/mindsdb/interfaces/chatbot/types.py +++ b/mindsdb/interfaces/chatbot/types.py @@ -22,12 +22,13 @@ class Type(Enum): DIRECT = 1 CHANNEL = 2 - def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None): + def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None, **kwargs): self.type = type self.text = text self.user = user self.destination = destination self.sent_at = sent_at or dt.datetime.now() + self.kwargs = kwargs class Function: From 8640ed22931c232a133532a49ddc2e7802cec9e4 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 2 Oct 2024 13:21:02 +0530 Subject: [PATCH 03/40] extended the chat bot task to support webhooks --- mindsdb/interfaces/chatbot/chatbot_task.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 2b7552eb618..45646cca21c 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -9,7 +9,7 @@ from mindsdb.utilities import log -from .polling import MessageCountPolling, RealtimePolling +from .polling import MessageCountPolling, RealtimePolling, WebhookPolling from .memory import DBMemory, HandlerMemory from .chatbot_executor import MultiModeBotExecutor, BotExecutor, AgentExecutor @@ -59,6 +59,11 @@ def run(self, stop_event): elif polling == 'realtime': self.chat_pooling = RealtimePolling(self, chat_params) self.memory = DBMemory(self, chat_params) + + elif polling == 'webhook': + self.chat_pooling = WebhookPolling(self, chat_params) + self.memory = DBMemory(self, chat_params) + else: raise Exception(f"Not supported polling: {polling}") @@ -101,7 +106,8 @@ def _on_message(self, chat_memory, message: ChatBotMessage): # In Slack direct messages are treated as channels themselves. user=bot_username, destination=chat_id, - sent_at=dt.datetime.now() + sent_at=dt.datetime.now(), + **message.kwargs ) # send to chat adapter From d55ec66e678790d76f1260794f0f5a52d2e9fdc0 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 2 Oct 2024 13:21:19 +0530 Subject: [PATCH 04/40] created an API endpoint for the webhook to call --- mindsdb/api/http/namespaces/chatbots.py | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mindsdb/api/http/namespaces/chatbots.py b/mindsdb/api/http/namespaces/chatbots.py index 9dc1c1ac19d..0845305b0f5 100644 --- a/mindsdb/api/http/namespaces/chatbots.py +++ b/mindsdb/api/http/namespaces/chatbots.py @@ -11,6 +11,7 @@ from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController from mindsdb.interfaces.model.functions import PredictorRecordNotFound from mindsdb.interfaces.storage.db import Predictor +from mindsdb.interfaces.chatbot.chatbot_task import ChatBotTask def create_chatbot(project_name, name, chatbot): @@ -287,3 +288,35 @@ def delete(self, project_name, chatbot_name): chatbot_controller.delete_chatbot(chatbot_name, project_name=project_name) return '', HTTPStatus.NO_CONTENT + + +@ns_conf.route('//chatbots//messages') +@ns_conf.param('project_name', 'Name of the project') +@ns_conf.param('chatbot_name', 'Name of the chatbot') +class ChatBotMessagesResource(Resource): + @ns_conf.doc('post_chatbot_message') + def post(self, project_name, chatbot_name): + '''Post a message to a chatbot''' + # Get the contents of the request. + req = request.json + + # Get the chatbot from the controller. + chatbot_controller = ChatBotController() + try: + existing_chatbot = chatbot_controller.get_chatbot(chatbot_name, project_name=project_name) + if existing_chatbot is None: + return http_error( + HTTPStatus.NOT_FOUND, + 'Chatbot not found', + f'Chatbot with name {chatbot_name} does not exist' + ) + except ValueError: + # Project needs to exist. + return http_error( + HTTPStatus.NOT_FOUND, + 'Project not found', + f'Project with name {project_name} does not exist' + ) + + chatbot_task = ChatBotTask(task_id=None, object_id=existing_chatbot["id"]) + chatbot_task.run(req) From 89c8265e65dd4dbe3e5d2dd46ef6203ec5eab392 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 2 Oct 2024 13:25:32 +0530 Subject: [PATCH 05/40] updated the MS Teams handler to support webhook polling --- .../ms_teams_handler/ms_teams_handler.py | 62 ++++++++++++++----- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py index f8369521a4f..427ba8e49d4 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py +++ b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py @@ -1,5 +1,10 @@ +import os from typing import Text, Dict +from botframework.connector import ConnectorClient +from botframework.connector.auth import MicrosoftAppCredentials +from botbuilder.schema import Activity, ActivityTypes +from botbuilder.schema import ChannelAccount from mindsdb.utilities import log from mindsdb_sql import parse_sql from mindsdb.integrations.utilities.handlers.auth_utilities import MSGraphAPIAuthManager @@ -143,17 +148,11 @@ def get_chat_config(self) -> Dict: params = { 'polling': { - 'type': 'message_count', - 'table': 'chats', - 'chat_id_col': 'id', - 'count_col': 'lastMessagePreview_id' - }, - 'chat_table': { - 'name': 'chat_messages', - 'chat_id_col': 'chatId', - 'username_col': 'from_user_displayName', - 'text_col': 'body_content', - 'time_col': 'createdDateTime', + 'type': 'webhook', + 'chat_id_attr': ['conversation', 'id'], + 'message_attr': 'text', + 'from_attr': ['from', 'id'], + 'to_attr': ['recipient', 'id'], } } @@ -170,7 +169,42 @@ def get_my_user_name(self) -> Text: Name of the signed in user. """ - connection = self.connect() - user_profile = connection.get_user_profile() + # connection = self.connect() + # user_profile = connection.get_user_profile() - return user_profile['displayName'] \ No newline at end of file + # return user_profile['displayName'] + return None + + def respond(self, message): + """ + Respond to a message. + + Parameters + ---------- + message: Dict + Message to respond to. + + Returns + ------- + Dict + Response to the message. + """ + RECIPIENT_ID = message.destination + TEXT = message.text + SERVICE_URL = message.kwargs['request']['serviceUrl'] + CHANNEL_ID = message.kwargs['request']['channelId'] + BOT_ID = message.kwargs['request']['from']['id'] + CONVERSATION_ID = message.kwargs['request']['conversation']['id'] + + credentials = MicrosoftAppCredentials( + self.connection_data["client_id"], + self.connection_data["client_secret"] + ) + connector = ConnectorClient(credentials, base_url=SERVICE_URL) + + connector.conversations.send_to_conversation(CONVERSATION_ID, Activity( + type=ActivityTypes.message, + channel_id=CHANNEL_ID, + recipient=ChannelAccount(id=RECIPIENT_ID), + from_property=ChannelAccount(id=BOT_ID), + text=TEXT)) From 82614a0e912a1922c49d89d901a7e1b1a169bf92 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 3 Oct 2024 00:37:11 +0530 Subject: [PATCH 06/40] made a couple of formatting changes --- .../ms_teams_handler/ms_teams_handler.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py index 427ba8e49d4..dec4bfa52d1 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py +++ b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py @@ -189,6 +189,7 @@ def respond(self, message): Dict Response to the message. """ + RECIPIENT_ID = message.destination TEXT = message.text SERVICE_URL = message.kwargs['request']['serviceUrl'] @@ -202,9 +203,12 @@ def respond(self, message): ) connector = ConnectorClient(credentials, base_url=SERVICE_URL) - connector.conversations.send_to_conversation(CONVERSATION_ID, Activity( - type=ActivityTypes.message, - channel_id=CHANNEL_ID, - recipient=ChannelAccount(id=RECIPIENT_ID), - from_property=ChannelAccount(id=BOT_ID), - text=TEXT)) + connector.conversations.send_to_conversation( + CONVERSATION_ID, + Activity( + type=ActivityTypes.message, + channel_id=CHANNEL_ID, + recipient=ChannelAccount(id=RECIPIENT_ID), + from_property=ChannelAccount(id=BOT_ID), + text=TEXT) + ) From cb5728d383a7bab9687a2299479d3b13d4ed8cf4 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Thu, 3 Oct 2024 19:04:31 +0530 Subject: [PATCH 07/40] moved the common initialization code from run() to __init__() --- mindsdb/interfaces/chatbot/chatbot_task.py | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index e4a51f9e5da..1107f210a0a 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -23,34 +23,40 @@ class ChatBotTask(BaseTask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.bot_id = self.object_id - self.agent_id = None - - self.session = SessionController() - - def run(self, stop_event): - - # TODO check deleted, raise errors - # TODO checks on delete predictor / project/ integration bot_record = db.ChatBots.query.get(self.bot_id) self.base_model_name = bot_record.model_name - self.agent_id = bot_record.agent_id self.project_name = db.Project.query.get(bot_record.project_id).name self.project_datanode = self.session.datahub.get(self.project_name) + self.agent_id = bot_record.agent_id + if self.agent_id is not None: + self.bot_executor_cls = AgentExecutor + elif self.bot_params.get('modes') is None: + self.bot_executor_cls = BotExecutor + else: + self.bot_executor_cls = MultiModeBotExecutor + database_name = db.Integration.query.get(bot_record.database_id).name self.chat_handler = self.session.integration_controller.get_data_handler(database_name) if not isinstance(self.chat_handler, APIChatHandler): raise Exception(f"Can't use chat database: {database_name}") - + # get chat handler info self.bot_params = bot_record.params or {} chat_params = self.chat_handler.get_chat_config() self.bot_params['bot_username'] = self.chat_handler.get_my_user_name() + self.session = SessionController() + + def run(self, stop_event): + + # TODO check deleted, raise errors + # TODO checks on delete predictor / project/ integration + polling = chat_params['polling']['type'] if polling == 'message_count': chat_params = chat_params['tables'] if 'tables' in chat_params else [chat_params] @@ -68,13 +74,6 @@ def run(self, stop_event): else: raise Exception(f"Not supported polling: {polling}") - if self.agent_id is not None: - self.bot_executor_cls = AgentExecutor - elif self.bot_params.get('modes') is None: - self.bot_executor_cls = BotExecutor - else: - self.bot_executor_cls = MultiModeBotExecutor - self.chat_pooling.run(stop_event) def on_message(self, chat_memory, message: ChatBotMessage, table_name=None): From a7de713aa2a69d35b6ddc64c3e7607b143df1542 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Fri, 4 Oct 2024 02:17:44 +0530 Subject: [PATCH 08/40] moved more common code to init() --- mindsdb/interfaces/chatbot/chatbot_task.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 1107f210a0a..69940d819a1 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -48,15 +48,6 @@ def __init__(self, *args, **kwargs): self.bot_params = bot_record.params or {} chat_params = self.chat_handler.get_chat_config() - self.bot_params['bot_username'] = self.chat_handler.get_my_user_name() - - self.session = SessionController() - - def run(self, stop_event): - - # TODO check deleted, raise errors - # TODO checks on delete predictor / project/ integration - polling = chat_params['polling']['type'] if polling == 'message_count': chat_params = chat_params['tables'] if 'tables' in chat_params else [chat_params] @@ -74,6 +65,15 @@ def run(self, stop_event): else: raise Exception(f"Not supported polling: {polling}") + self.bot_params['bot_username'] = self.chat_handler.get_my_user_name() + + self.session = SessionController() + + def run(self, stop_event): + + # TODO check deleted, raise errors + # TODO checks on delete predictor / project/ integration + self.chat_pooling.run(stop_event) def on_message(self, chat_memory, message: ChatBotMessage, table_name=None): From 7dc6c2187a358715d6a1afb0615bf34d0b93dbf3 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Sat, 5 Oct 2024 23:43:37 +0530 Subject: [PATCH 09/40] added a on_webhook() func to task and polling --- mindsdb/api/http/namespaces/chatbots.py | 2 +- mindsdb/interfaces/chatbot/chatbot_task.py | 9 ++++++--- mindsdb/interfaces/chatbot/polling.py | 13 ++++++------- mindsdb/interfaces/chatbot/types.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mindsdb/api/http/namespaces/chatbots.py b/mindsdb/api/http/namespaces/chatbots.py index 0845305b0f5..30725c82ab3 100644 --- a/mindsdb/api/http/namespaces/chatbots.py +++ b/mindsdb/api/http/namespaces/chatbots.py @@ -319,4 +319,4 @@ def post(self, project_name, chatbot_name): ) chatbot_task = ChatBotTask(task_id=None, object_id=existing_chatbot["id"]) - chatbot_task.run(req) + chatbot_task.on_webhook(req) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 69940d819a1..bf11fae2c3f 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -24,6 +24,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.bot_id = self.object_id + self.session = SessionController() + bot_record = db.ChatBots.query.get(self.bot_id) self.base_model_name = bot_record.model_name @@ -67,8 +69,6 @@ def __init__(self, *args, **kwargs): self.bot_params['bot_username'] = self.chat_handler.get_my_user_name() - self.session = SessionController() - def run(self, stop_event): # TODO check deleted, raise errors @@ -107,7 +107,7 @@ def _on_message(self, chat_memory, message: ChatBotMessage, table_name=None): user=bot_username, destination=chat_id, sent_at=dt.datetime.now(), - **message.kwargs + request=message.request ) # send to chat adapter @@ -116,3 +116,6 @@ def _on_message(self, chat_memory, message: ChatBotMessage, table_name=None): # send to history chat_memory.add_to_history(response_message) + + def on_webhook(self, request): + self.chat_pooling.on_webhook(request) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index 37f767faadb..bee28834554 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -175,24 +175,23 @@ class WebhookPolling(BasePolling): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def run(self, request): + def on_webhook(self, request): p_params = self.params["polling"] - chat_id_attr = p_params["chat_id_attr"] - chat_id = self._parse_request(request, chat_id_attr) + chat_id = self._parse_request(request, p_params["chat_id_path"]) chat_memory = self.chat_task.memory.get_chat(chat_id) message = ChatBotMessage( ChatBotMessage.Type.DIRECT, - self._parse_request(request, p_params["message_attr"]), - self._parse_request(request, p_params["from_attr"]), - self._parse_request(request, p_params["to_attr"]), + text=self._parse_request(request, p_params["message_path"]), + user=self._parse_request(request, p_params["from_path"]), + destination=self._parse_request(request, p_params["to_path"]), request=request ) self.chat_task.on_message(chat_memory, message) - def send_message(self, message: ChatBotMessage): + def send_message(self, message: ChatBotMessage, table_name=None): self.chat_task.chat_handler.respond(message) def _parse_request(self, request, key): diff --git a/mindsdb/interfaces/chatbot/types.py b/mindsdb/interfaces/chatbot/types.py index f0cc6c944e3..0ff7cc2fdde 100644 --- a/mindsdb/interfaces/chatbot/types.py +++ b/mindsdb/interfaces/chatbot/types.py @@ -22,13 +22,13 @@ class Type(Enum): DIRECT = 1 CHANNEL = 2 - def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None, **kwargs): + def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None, request=None): self.type = type self.text = text self.user = user self.destination = destination self.sent_at = sent_at or dt.datetime.now() - self.kwargs = kwargs + self.request = request class Function: From 4228830d35286b3774758432fcd3cc5e3543b99a Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 16:45:35 +0530 Subject: [PATCH 10/40] added the webhook_token col to the ChatBots model --- mindsdb/interfaces/storage/db.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mindsdb/interfaces/storage/db.py b/mindsdb/interfaces/storage/db.py index f9b5db08599..386c194858e 100644 --- a/mindsdb/interfaces/storage/db.py +++ b/mindsdb/interfaces/storage/db.py @@ -351,6 +351,7 @@ class ChatBots(Base): DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now ) created_at = Column(DateTime, default=datetime.datetime.now) + webhook_token = Column(String) def as_dict(self) -> Dict: return { @@ -360,6 +361,7 @@ def as_dict(self) -> Dict: "agent_id": self.agent_id, "model_name": self.model_name, "params": self.params, + "webhook_token": self.webhook_token, "created_at": self.created_at, "database_id": self.database_id, } From 37e0f4e8be88a4146f98702ec4d68408a9ae0ec1 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 16:46:04 +0530 Subject: [PATCH 11/40] created the Alembic revision for the change to the database model --- ...39a82b_added_webhook_token_to_chat_bots.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py diff --git a/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py b/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py new file mode 100644 index 00000000000..64627ff062b --- /dev/null +++ b/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py @@ -0,0 +1,29 @@ +"""added webhook_token to chat_bots + +Revision ID: 6c57ed39a82b +Revises: 8e17ff6b75e9 +Create Date: 2024-10-07 16:40:14.141878 + +""" +from alembic import op +import sqlalchemy as sa +import mindsdb.interfaces.storage.db # noqa + + + +# revision identifiers, used by Alembic. +revision = '6c57ed39a82b' +down_revision = '8e17ff6b75e9' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('chat_bots', schema=None) as batch_op: + batch_op.add_column(sa.Column('webhook_token', sa.VARCHAR(), nullable=True)) + + +def downgrade(): + with op.batch_alter_table('chat_bots', schema=None) as batch_op: + batch_op.drop_column('webhook_token') + From 45dd6e1f37ab970942ace257baef5f3cb95ca71f Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 22:49:12 +0530 Subject: [PATCH 12/40] added the API namespace for webhooks --- mindsdb/api/http/namespaces/configs/webhooks.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 mindsdb/api/http/namespaces/configs/webhooks.py diff --git a/mindsdb/api/http/namespaces/configs/webhooks.py b/mindsdb/api/http/namespaces/configs/webhooks.py new file mode 100644 index 00000000000..09d463f9b82 --- /dev/null +++ b/mindsdb/api/http/namespaces/configs/webhooks.py @@ -0,0 +1,3 @@ +from flask_restx import Namespace + +ns_conf = Namespace('webhooks', description='API to perform operations that read and write MindsDB databases') From 43004c642f1d4d1607c610343136051aed4a1381 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 22:49:32 +0530 Subject: [PATCH 13/40] added the route for handling messages via webhooks --- mindsdb/api/http/namespaces/webhooks.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 mindsdb/api/http/namespaces/webhooks.py diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py new file mode 100644 index 00000000000..c5d83b85d29 --- /dev/null +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -0,0 +1,19 @@ +from flask_restx import Resource + +from mindsdb.api.http.namespaces.configs.webhooks import ns_conf +from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController +from mindsdb.metrics.metrics import api_endpoint_metrics + + +@ns_conf.route('/chatbots/') +class ChatbotWebhooks(Resource): + @ns_conf.doc('chatbots_webhook') + @api_endpoint_metrics('POST', '/chatbots/') + def post(self, webhook_token): + """ + This endpoint is used to receive messages posted by bots from different platforms. + + :param webhook_token: The token of the webhook. It is used to uniquely identify the webhook. + """ + chatbot_controller = ChatBotController() + return chatbot_controller.on_webhook(webhook_token) \ No newline at end of file From db2790ebafe511a4c4506f420a56bbd819cd873e Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 22:54:48 +0530 Subject: [PATCH 14/40] passed the request from the endpoint to the controller --- mindsdb/api/http/namespaces/webhooks.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index c5d83b85d29..c9f5105e138 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -1,3 +1,4 @@ +from flask import request from flask_restx import Resource from mindsdb.api.http.namespaces.configs.webhooks import ns_conf @@ -9,11 +10,13 @@ class ChatbotWebhooks(Resource): @ns_conf.doc('chatbots_webhook') @api_endpoint_metrics('POST', '/chatbots/') - def post(self, webhook_token): + def post(self, webhook_token: str) -> None: """ This endpoint is used to receive messages posted by bots from different platforms. :param webhook_token: The token of the webhook. It is used to uniquely identify the webhook. """ - chatbot_controller = ChatBotController() - return chatbot_controller.on_webhook(webhook_token) \ No newline at end of file + request_data = request.json + + chat_bot_controller = ChatBotController() + return chat_bot_controller.on_webhook(webhook_token, request_data) \ No newline at end of file From c0c5cdffdfeb470a301474d07e22812c0c0d17a3 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 22:58:05 +0530 Subject: [PATCH 15/40] added an on_webhook() method to ChatBotController --- .../interfaces/chatbot/chatbot_controller.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index f32471f13c9..895ff0df754 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -1,6 +1,7 @@ from typing import Dict, List from mindsdb.interfaces.agents.agents_controller import AgentsController +from mindsdb.interfaces.chatbot.chatbot_task import ChatBotTask from mindsdb.interfaces.database.projects import ProjectController from mindsdb.interfaces.storage import db @@ -321,3 +322,21 @@ def delete_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb'): db.session.delete(bot_rec) db.session.commit() + + def on_webhook(self, webhook_token: str, request: dict) -> None: + """ + Handles incoming webhook requests. + + Parameters: + webhook_token (str): The token to uniquely identify the webhook. + request (dict): The incoming webhook request. + """ + chat_bot = db.ChatBots.query.filter_by(webhook_token=webhook_token).first() + + if chat_bot is None: + raise Exception(f"No chat bot exists for webhook token: {webhook_token}") + + chat_bot_task = ChatBotTask(task_id=None, object_id=chat_bot.id) + chat_bot_task.on_webhook(request) + + From 67b10aaf2f8afacf9e8cd1b8c3b512b17c5447e8 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 23:31:12 +0530 Subject: [PATCH 16/40] removed the on_webhook() method from WebhookPolling --- mindsdb/interfaces/chatbot/polling.py | 23 ----------------------- mindsdb/interfaces/chatbot/types.py | 3 +-- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index bee28834554..ed2f28c5089 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -175,28 +175,5 @@ class WebhookPolling(BasePolling): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def on_webhook(self, request): - p_params = self.params["polling"] - - chat_id = self._parse_request(request, p_params["chat_id_path"]) - chat_memory = self.chat_task.memory.get_chat(chat_id) - - message = ChatBotMessage( - ChatBotMessage.Type.DIRECT, - text=self._parse_request(request, p_params["message_path"]), - user=self._parse_request(request, p_params["from_path"]), - destination=self._parse_request(request, p_params["to_path"]), - request=request - ) - - self.chat_task.on_message(chat_memory, message) - def send_message(self, message: ChatBotMessage, table_name=None): self.chat_task.chat_handler.respond(message) - - def _parse_request(self, request, key): - if isinstance(key, list): - return reduce(lambda x, y: x[y], key, request) - - else: - return request[key] \ No newline at end of file diff --git a/mindsdb/interfaces/chatbot/types.py b/mindsdb/interfaces/chatbot/types.py index 0ff7cc2fdde..89b3e238fb2 100644 --- a/mindsdb/interfaces/chatbot/types.py +++ b/mindsdb/interfaces/chatbot/types.py @@ -22,13 +22,12 @@ class Type(Enum): DIRECT = 1 CHANNEL = 2 - def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None, request=None): + def __init__(self, type: Type, text: str, user: str, destination: str = None, sent_at: dt.datetime = None): self.type = type self.text = text self.user = user self.destination = destination self.sent_at = sent_at or dt.datetime.now() - self.request = request class Function: From 77878d91267fe2c40320b1d70271e518317dbd6d Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 23:34:15 +0530 Subject: [PATCH 17/40] added on_webhook method to ChatBotTask and updated o_message to take chat_id --- mindsdb/interfaces/chatbot/chatbot_task.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index bf11fae2c3f..0a91bee4077 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -76,10 +76,10 @@ def run(self, stop_event): self.chat_pooling.run(stop_event) - def on_message(self, chat_memory, message: ChatBotMessage, table_name=None): + def on_message(self, chat_id, message: ChatBotMessage, table_name=None): try: - self._on_message(chat_memory, message, table_name) + self._on_message(chat_id, message, table_name) except (SystemExit, KeyboardInterrupt): raise except Exception: @@ -87,9 +87,10 @@ def on_message(self, chat_memory, message: ChatBotMessage, table_name=None): logger.error(error) self.set_error(str(error)) - def _on_message(self, chat_memory, message: ChatBotMessage, table_name=None): + def _on_message(self, chat_id, message: ChatBotMessage, table_name=None): # add question to history # TODO move it to realtime pooling + chat_memory = self.memory.get_chat(chat_id, table_name=table_name) chat_memory.add_to_history(message) logger.debug(f'>>chatbot {chat_memory.chat_id} in: {message.text}') @@ -117,5 +118,12 @@ def _on_message(self, chat_memory, message: ChatBotMessage, table_name=None): # send to history chat_memory.add_to_history(response_message) - def on_webhook(self, request): - self.chat_pooling.on_webhook(request) + def on_webhook(self, request: dict) -> None: + """ + Handle incoming webhook requests. + Passes the request to the chat handler along with the callback method. + + Args: + request (dict): The incoming webhook request. + """ + self.chat_handler.on_webhook(request, self.on_message) From dc8121a8d9bb8a2b65ae409f987045d3d84776d7 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 23:34:58 +0530 Subject: [PATCH 18/40] updated polling call on_message with chat ID instead of memory --- mindsdb/interfaces/chatbot/polling.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index ed2f28c5089..b3c682a36b1 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -69,7 +69,7 @@ def run(self, stop_event): message = None if message: - self.chat_task.on_message(chat_memory, message, table_name=chat_params["chat_table"]["name"]) + self.chat_task.on_message(chat_id, message, table_name=chat_params["chat_table"]["name"]) except Exception as e: logger.error(e) @@ -157,8 +157,7 @@ def _callback(self, row, key): chat_id = row[t_params["chat_id_col"]] - chat_memory = self.chat_task.memory.get_chat(chat_id) - self.chat_task.on_message(chat_memory, message) + self.chat_task.on_message(chat_id, message) def run(self, stop_event): t_params = self.params["chat_table"] From ea2ab9a8fb1cd7022c46ad0e2fb35ec9f528f8b4 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Mon, 7 Oct 2024 23:35:56 +0530 Subject: [PATCH 19/40] removed the request attr from ChatBotMessage --- mindsdb/api/http/namespaces/webhooks.py | 5 +++-- mindsdb/interfaces/chatbot/chatbot_controller.py | 3 ++- mindsdb/interfaces/chatbot/chatbot_task.py | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index c9f5105e138..6340bf749b5 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -13,8 +13,9 @@ class ChatbotWebhooks(Resource): def post(self, webhook_token: str) -> None: """ This endpoint is used to receive messages posted by bots from different platforms. - - :param webhook_token: The token of the webhook. It is used to uniquely identify the webhook. + + Args: + webhook_token (str): The token of the webhook. It is used to uniquely identify the webhook. """ request_data = request.json diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 895ff0df754..7817b076155 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -326,8 +326,9 @@ def delete_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb'): def on_webhook(self, webhook_token: str, request: dict) -> None: """ Handles incoming webhook requests. + Finds the chat bot associated with the webhook token and passes the request to the chat bot task. - Parameters: + Args: webhook_token (str): The token to uniquely identify the webhook. request (dict): The incoming webhook request. """ diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 0a91bee4077..55094ccf0ae 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -107,8 +107,7 @@ def _on_message(self, chat_id, message: ChatBotMessage, table_name=None): # In Slack direct messages are treated as channels themselves. user=bot_username, destination=chat_id, - sent_at=dt.datetime.now(), - request=message.request + sent_at=dt.datetime.now() ) # send to chat adapter From eb10ca542dba0cdf0aacff1edb7e487a045d7840 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 00:08:50 +0530 Subject: [PATCH 20/40] added an on_webhook() to the MS Teams handler --- .../ms_teams_handler/ms_teams_handler.py | 83 ++++++++++++------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py index dec4bfa52d1..62208da274f 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py +++ b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py @@ -1,5 +1,5 @@ import os -from typing import Text, Dict +from typing import Text, Dict, Callable from botframework.connector import ConnectorClient from botframework.connector.auth import MicrosoftAppCredentials @@ -16,6 +16,7 @@ ) from mindsdb.integrations.libs.api_handler import APIChatHandler from mindsdb.integrations.handlers.ms_teams_handler.ms_teams_tables import ChannelsTable, ChannelMessagesTable, ChatsTable, ChatMessagesTable +from mindsdb.interfaces.chatbot.types import ChatBotMessage logger = log.getLogger(__name__) @@ -57,6 +58,11 @@ def __init__(self, name: str, **kwargs): chat_messages_data = ChatMessagesTable(self) self._register_table("chat_messages", chat_messages_data) + self.service_url = None + self.channel_id = None + self.bot_id = None + self.conversation_id = None + def connect(self) -> MSGraphAPITeamsClient: """ Set up the connection required by the handler. @@ -148,11 +154,7 @@ def get_chat_config(self) -> Dict: params = { 'polling': { - 'type': 'webhook', - 'chat_id_attr': ['conversation', 'id'], - 'message_attr': 'text', - 'from_attr': ['from', 'id'], - 'to_attr': ['recipient', 'id'], + 'type': 'webhook' } } @@ -167,48 +169,67 @@ def get_my_user_name(self) -> Text: ------- Text Name of the signed in user. - """ + """ - # connection = self.connect() - # user_profile = connection.get_user_profile() - - # return user_profile['displayName'] return None - def respond(self, message): + def on_webhook(self, request: Dict, callback: Callable) -> None: """ - Respond to a message. + Handle a webhook request. Parameters ---------- - message: Dict - Message to respond to. + request: Dict + The incoming webhook request. - Returns - ------- - Dict - Response to the message. + callback: Callable + Callback function to call after parsing the request. + """ + + self.service_url = request["serviceUrl"] + self.channel_id = request["channelId"] + self.bot_id = request["from"]["id"] + self.conversation_id = request["conversation"]["id"] + + chat_bot_message = ChatBotMessage( + ChatBotMessage.Type.DIRECT, + text=request["text"], + user=request["from"]["id"], + destination=request["recipient"]["id"] + ) + + callback( + chat_id=request['conversation']['id'], + message=chat_bot_message + ) + + def respond(self, message: ChatBotMessage) -> None: """ + Send a response to the chatbot. - RECIPIENT_ID = message.destination - TEXT = message.text - SERVICE_URL = message.kwargs['request']['serviceUrl'] - CHANNEL_ID = message.kwargs['request']['channelId'] - BOT_ID = message.kwargs['request']['from']['id'] - CONVERSATION_ID = message.kwargs['request']['conversation']['id'] + Parameters + ---------- + message: ChatBotMessage + The message to send. + """ credentials = MicrosoftAppCredentials( self.connection_data["client_id"], self.connection_data["client_secret"] ) - connector = ConnectorClient(credentials, base_url=SERVICE_URL) + connector = ConnectorClient(credentials, base_url=self.service_url) connector.conversations.send_to_conversation( - CONVERSATION_ID, + self.conversation_id, Activity( type=ActivityTypes.message, - channel_id=CHANNEL_ID, - recipient=ChannelAccount(id=RECIPIENT_ID), - from_property=ChannelAccount(id=BOT_ID), - text=TEXT) + channel_id=self.channel_id, + recipient=ChannelAccount( + id=message.destination + ), + from_property=ChannelAccount( + id=self.bot_id + ), + text=message.text ) + ) From 275553a5fde9069f6f97395ec405b8ec6b0ffbec Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 09:25:39 +0530 Subject: [PATCH 21/40] added a method to get chatbot by ID and added the webhook token to update --- .../interfaces/chatbot/chatbot_controller.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 7817b076155..57e39ec2207 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -77,6 +77,57 @@ def get_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb') -> db.Ch 'last_error': task.last_error, } return bot_obj + + def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: + ''' + Gets a chatbot by id. + + Parameters: + chatbot_id (int): The id of the chatbot + + Returns: + bot (db.ChatBots): The database chatbot object + ''' + + query = db.session.query( + db.ChatBots, db.Tasks + ).join( + db.Tasks, db.ChatBots.id == db.Tasks.object_id + ).filter( + db.ChatBots.id == chatbot_id, + db.Tasks.object_type == self.OBJECT_TYPE, + db.Tasks.company_id == ctx.company_id, + ) + + query_result = query.first() + if query_result is None: + return None + bot, task = query_result + + # Include DB, Agent, and Task information in response. + session = SessionController() + database_names = { + i['id']: i['name'] + for i in session.database_controller.get_list() + } + + agent = self.agents_controller.get_agent_by_id(bot.agent_id) + agent_obj = agent.as_dict() if agent is not None else None + bot_obj = { + 'id': bot.id, + 'name': bot.name, + 'project': self.project_controller.get(bot.project_id).name, + 'agent': agent_obj, + 'database_id': bot.database_id, # TODO remove in future + 'database': database_names.get(bot.database_id, '?'), + 'model_name': bot.model_name, + 'params': bot.params, + 'created_at': bot.created_at, + 'is_running': task.active, + 'last_error': task.last_error, + 'webhook_token': bot.webhook_token, + } + return bot_obj def get_chatbots(self, project_name: str = 'mindsdb') -> List[dict]: ''' @@ -228,7 +279,8 @@ def update_chatbot( agent_name: str = None, database_id: int = None, is_running: bool = None, - params: Dict[str, str] = None): + params: Dict[str, str] = None, + webhook_token: str = None) -> db.ChatBots: ''' Updates a chatbot in the database, creating it if it doesn't already exist. @@ -291,6 +343,10 @@ def update_chatbot( existing_params = existing_chatbot_rec.params or {} params.update(existing_params) existing_chatbot_rec.params = params + + if webhook_token is not None: + existing_chatbot_rec.webhook_token = webhook_token + db.session.commit() return existing_chatbot_rec From bde34a1601cc926540d8de309135f6c228049176 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 09:33:44 +0530 Subject: [PATCH 22/40] added a run() method to WebhookPolling --- mindsdb/interfaces/chatbot/polling.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index b3c682a36b1..ba46ec29caf 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -1,10 +1,11 @@ -from functools import reduce +import secrets import time from mindsdb_sql.parser.ast import Identifier, Select, Insert from mindsdb.utilities import log from mindsdb.utilities.context import context as ctx +from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController from .types import ChatBotMessage, BotException @@ -174,5 +175,21 @@ class WebhookPolling(BasePolling): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + def run(self, stop_event): + # If a webhook token is not set for the chatbot, generate a new one. + chat_bot_controller = ChatBotController() + chat_bot = chat_bot_controller.get_chatbot_by_id(self.chat_task.object_id) + + if not chat_bot.webhook_token: + chat_bot_controller.update_chatbot( + name=chat_bot.name, + project_name=chat_bot.project_name, + webhook_token=secrets.token_urlsafe(16), + ) + + # Do nothing, as the webhook is handled by a task instantiated for each request. + while not stop_event.is_set(): + time.sleep(1) + def send_message(self, message: ChatBotMessage, table_name=None): self.chat_task.chat_handler.respond(message) From f875f6b90d09884d334066c5b835b2d150e8e3dc Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 10:58:06 +0530 Subject: [PATCH 23/40] registered the new webhooks namespace --- mindsdb/api/http/initialize.py | 2 ++ mindsdb/api/http/namespaces/configs/webhooks.py | 2 +- mindsdb/api/http/namespaces/webhooks.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mindsdb/api/http/initialize.py b/mindsdb/api/http/initialize.py index fde90446971..390068b424a 100644 --- a/mindsdb/api/http/initialize.py +++ b/mindsdb/api/http/initialize.py @@ -38,6 +38,7 @@ from mindsdb.api.http.namespaces.tree import ns_conf as tree_ns from mindsdb.api.http.namespaces.views import ns_conf as views_ns from mindsdb.api.http.namespaces.util import ns_conf as utils_ns +from mindsdb.api.http.namespaces.webhooks import ns_conf as webhooks_ns from mindsdb.interfaces.database.integrations import integration_controller from mindsdb.interfaces.database.database import DatabaseController from mindsdb.interfaces.file.file_controller import FileController @@ -246,6 +247,7 @@ def root_index(path): api.add_namespace(ns) api.add_namespace(default_ns) api.add_namespace(auth_ns) + api.add_namespace(webhooks_ns) @api.errorhandler(Exception) def handle_exception(e): diff --git a/mindsdb/api/http/namespaces/configs/webhooks.py b/mindsdb/api/http/namespaces/configs/webhooks.py index 09d463f9b82..bbea6f6f225 100644 --- a/mindsdb/api/http/namespaces/configs/webhooks.py +++ b/mindsdb/api/http/namespaces/configs/webhooks.py @@ -1,3 +1,3 @@ from flask_restx import Namespace -ns_conf = Namespace('webhooks', description='API to perform operations that read and write MindsDB databases') +ns_conf = Namespace('webhooks', description='API to receive messages from bots') diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index 6340bf749b5..a04e4758239 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -9,7 +9,7 @@ @ns_conf.route('/chatbots/') class ChatbotWebhooks(Resource): @ns_conf.doc('chatbots_webhook') - @api_endpoint_metrics('POST', '/chatbots/') + @api_endpoint_metrics('POST', '/webhooks/chatbots/') def post(self, webhook_token: str) -> None: """ This endpoint is used to receive messages posted by bots from different platforms. From 6539e789b8d12737bac5f7854d58dfeb6af8def9 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 10:58:49 +0530 Subject: [PATCH 24/40] fixed bugs in WebhookPolling --- mindsdb/interfaces/chatbot/polling.py | 33 ++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index ba46ec29caf..7ed9592e941 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -1,11 +1,11 @@ import secrets +import threading import time from mindsdb_sql.parser.ast import Identifier, Select, Insert from mindsdb.utilities import log from mindsdb.utilities.context import context as ctx -from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController from .types import ChatBotMessage, BotException @@ -172,18 +172,31 @@ def run(self, stop_event): class WebhookPolling(BasePolling): + """ + Polling class for handling webhooks. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def run(self, stop_event): + def run(self, stop_event: threading.Event) -> None: + """ + Run the webhook polling. + Check if a webhook token is set for the chatbot. If not, generate a new one. + Then, do nothing, as the webhook is handled by a task instantiated for each request. + + Args: + stop_event (threading.Event): Event to stop the polling. + """ # If a webhook token is not set for the chatbot, generate a new one. + from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController + chat_bot_controller = ChatBotController() chat_bot = chat_bot_controller.get_chatbot_by_id(self.chat_task.object_id) - if not chat_bot.webhook_token: + if not chat_bot["webhook_token"]: chat_bot_controller.update_chatbot( - name=chat_bot.name, - project_name=chat_bot.project_name, + chatbot_name=chat_bot["name"], + project_name=chat_bot["project"], webhook_token=secrets.token_urlsafe(16), ) @@ -191,5 +204,13 @@ def run(self, stop_event): while not stop_event.is_set(): time.sleep(1) - def send_message(self, message: ChatBotMessage, table_name=None): + def send_message(self, message: ChatBotMessage, table_name: str = None) -> None: + """ + Send a message (response) to the chatbot. + Pass the message to the chatbot handler to respond. + + Args: + message (ChatBotMessage): The message to send. + table_name (str): The name of the table to send the message to. Defaults to None. + """ self.chat_task.chat_handler.respond(message) From c8eeb87d658892c6b0b29416855fe701d98a3e84 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 11:02:46 +0530 Subject: [PATCH 25/40] updated the MS Teams handler with the new implementation --- .../ms_teams_handler/ms_teams_handler.py | 111 ++++++++++++------ 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py index 62208da274f..756a0d5c5b2 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py +++ b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py @@ -40,62 +40,110 @@ def __init__(self, name: str, **kwargs): connection_data = kwargs.get("connection_data", {}) self.connection_data = connection_data - self.handler_storage = kwargs['handler_storage'] + # self.handler_storage = kwargs['handler_storage'] self.kwargs = kwargs self.connection = None self.is_connected = False - channels_data = ChannelsTable(self) - self._register_table("channels", channels_data) + # channels_data = ChannelsTable(self) + # self._register_table("channels", channels_data) - channel_messages_data = ChannelMessagesTable(self) - self._register_table("channel_messages", channel_messages_data) + # channel_messages_data = ChannelMessagesTable(self) + # self._register_table("channel_messages", channel_messages_data) - chats_data = ChatsTable(self) - self._register_table("chats", chats_data) + # chats_data = ChatsTable(self) + # self._register_table("chats", chats_data) - chat_messages_data = ChatMessagesTable(self) - self._register_table("chat_messages", chat_messages_data) + # chat_messages_data = ChatMessagesTable(self) + # self._register_table("chat_messages", chat_messages_data) self.service_url = None self.channel_id = None self.bot_id = None self.conversation_id = None - def connect(self) -> MSGraphAPITeamsClient: + # def connect(self) -> MSGraphAPITeamsClient: + # """ + # Set up the connection required by the handler. + + # Returns + # ------- + # MSGraphAPITeamsClient + # Client object for accessing the Microsoft Graph API. + # """ + + # if self.is_connected and self.connection.check_connection(): + # return self.connection + + # # initialize the auth manager for the Microsoft Graph API + # ms_graph_api_auth_manager = MSGraphAPIAuthManager( + # handler_storage=self.handler_storage, + # scopes=self.connection_data.get('scopes', ms_teams_handler_config.DEFAULT_SCOPES), + # client_id=self.connection_data["client_id"], + # client_secret=self.connection_data["client_secret"], + # tenant_id=self.connection_data["tenant_id"], + # code=self.connection_data.get('code') + # ) + + # # get access token from the auth manager for the Microsoft Graph API + # access_token = ms_graph_api_auth_manager.get_access_token() + + # # pass the access token to the client for access to the Microsoft Graph API + # self.connection = MSGraphAPITeamsClient(access_token) + + # self.is_connected = True + + # return self.connection + + def connect(self) -> MicrosoftAppCredentials: """ Set up the connection required by the handler. Returns ------- - MSGraphAPITeamsClient - Client object for accessing the Microsoft Graph API. + MicrosoftAppCredentials + Client object for interacting with the Microsoft Teams app. """ - if self.is_connected and self.connection.check_connection(): + if self.is_connected: return self.connection - # initialize the auth manager for the Microsoft Graph API - ms_graph_api_auth_manager = MSGraphAPIAuthManager( - handler_storage=self.handler_storage, - scopes=self.connection_data.get('scopes', ms_teams_handler_config.DEFAULT_SCOPES), - client_id=self.connection_data["client_id"], - client_secret=self.connection_data["client_secret"], - tenant_id=self.connection_data["tenant_id"], - code=self.connection_data.get('code') + self.connection = MicrosoftAppCredentials( + app_id=self.connection_data["client_id"], + password=self.connection_data["client_secret"] ) - # get access token from the auth manager for the Microsoft Graph API - access_token = ms_graph_api_auth_manager.get_access_token() - - # pass the access token to the client for access to the Microsoft Graph API - self.connection = MSGraphAPITeamsClient(access_token) - self.is_connected = True return self.connection + # def check_connection(self) -> StatusResponse: + # """ + # Check connection to the handler. + + # Returns + # ------- + # StatusResponse + # Response object with the status of the connection. + # """ + + # response = StatusResponse(False) + + # try: + # connection = self.connect() + # connection.check_connection() + # response.success = True + # response.copy_storage = True + # except Exception as e: + # logger.error(f'Error connecting to Microsoft Teams: {e}!') + # response.success = False + # response.error_message = str(e) + + # self.is_connected = response.success + + # return response + def check_connection(self) -> StatusResponse: """ Check connection to the handler. @@ -109,10 +157,8 @@ def check_connection(self) -> StatusResponse: response = StatusResponse(False) try: - connection = self.connect() - connection.check_connection() + self.connect() response.success = True - response.copy_storage = True except Exception as e: logger.error(f'Error connecting to Microsoft Teams: {e}!') response.success = False @@ -213,10 +259,7 @@ def respond(self, message: ChatBotMessage) -> None: The message to send. """ - credentials = MicrosoftAppCredentials( - self.connection_data["client_id"], - self.connection_data["client_secret"] - ) + credentials = self.connect() connector = ConnectorClient(credentials, base_url=self.service_url) connector.conversations.send_to_conversation( From 896afc05e195b80a7e40cabc3eca307e367cc9b8 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 11:23:30 +0530 Subject: [PATCH 26/40] updated controller to return webhook_token when getting chatbots --- mindsdb/interfaces/chatbot/chatbot_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 57e39ec2207..a181713f24d 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -75,6 +75,7 @@ def get_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb') -> db.Ch 'created_at': bot.created_at, 'is_running': task.active, 'last_error': task.last_error, + 'webhook_token': bot.webhook_token, } return bot_obj @@ -184,6 +185,7 @@ def get_chatbots(self, project_name: str = 'mindsdb') -> List[dict]: 'created_at': bot.created_at, 'is_running': task.active, 'last_error': task.last_error, + 'webhook_token': bot.webhook_token, } ) From fdec23340ba4f1da13b745c4c76d0ad5c37c9c89 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 11:23:57 +0530 Subject: [PATCH 27/40] added the webhook_token col to internal chatbots table --- mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py b/mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py index 9fe8cc5d4d7..5dc0d5bd4f5 100644 --- a/mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py +++ b/mindsdb/api/executor/datahub/datanodes/mindsdb_tables.py @@ -291,6 +291,7 @@ class ChatbotsTable(MdbTable): "PARAMS", "IS_RUNNING", "LAST_ERROR", + "WEBHOOK_TOKEN", ] @classmethod From 892097910c31c9f435772683e2bfaae21838e299 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 11:26:58 +0530 Subject: [PATCH 28/40] removed the ChatBotMessagesResource --- mindsdb/api/http/namespaces/chatbots.py | 33 ------------------------- 1 file changed, 33 deletions(-) diff --git a/mindsdb/api/http/namespaces/chatbots.py b/mindsdb/api/http/namespaces/chatbots.py index 30725c82ab3..9dc1c1ac19d 100644 --- a/mindsdb/api/http/namespaces/chatbots.py +++ b/mindsdb/api/http/namespaces/chatbots.py @@ -11,7 +11,6 @@ from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController from mindsdb.interfaces.model.functions import PredictorRecordNotFound from mindsdb.interfaces.storage.db import Predictor -from mindsdb.interfaces.chatbot.chatbot_task import ChatBotTask def create_chatbot(project_name, name, chatbot): @@ -288,35 +287,3 @@ def delete(self, project_name, chatbot_name): chatbot_controller.delete_chatbot(chatbot_name, project_name=project_name) return '', HTTPStatus.NO_CONTENT - - -@ns_conf.route('//chatbots//messages') -@ns_conf.param('project_name', 'Name of the project') -@ns_conf.param('chatbot_name', 'Name of the chatbot') -class ChatBotMessagesResource(Resource): - @ns_conf.doc('post_chatbot_message') - def post(self, project_name, chatbot_name): - '''Post a message to a chatbot''' - # Get the contents of the request. - req = request.json - - # Get the chatbot from the controller. - chatbot_controller = ChatBotController() - try: - existing_chatbot = chatbot_controller.get_chatbot(chatbot_name, project_name=project_name) - if existing_chatbot is None: - return http_error( - HTTPStatus.NOT_FOUND, - 'Chatbot not found', - f'Chatbot with name {chatbot_name} does not exist' - ) - except ValueError: - # Project needs to exist. - return http_error( - HTTPStatus.NOT_FOUND, - 'Project not found', - f'Project with name {project_name} does not exist' - ) - - chatbot_task = ChatBotTask(task_id=None, object_id=existing_chatbot["id"]) - chatbot_task.on_webhook(req) From b0a54b9ca1b11c76ee29df394ccae89e9a927926 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 14:05:14 +0530 Subject: [PATCH 29/40] increased the token length --- mindsdb/interfaces/chatbot/polling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index 7ed9592e941..750e29da7c3 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -197,7 +197,7 @@ def run(self, stop_event: threading.Event) -> None: chat_bot_controller.update_chatbot( chatbot_name=chat_bot["name"], project_name=chat_bot["project"], - webhook_token=secrets.token_urlsafe(16), + webhook_token=secrets.token_urlsafe(32), ) # Do nothing, as the webhook is handled by a task instantiated for each request. From a10848267ab985df67baa367d32aa26307995955 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 14:24:15 +0530 Subject: [PATCH 30/40] cleaned up the logic for getting a chatbot --- .../interfaces/chatbot/chatbot_controller.py | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index a181713f24d..10cdcf519ee 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -49,35 +49,7 @@ def get_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb') -> db.Ch db.Tasks.company_id == ctx.company_id, ) - query_result = query.first() - if query_result is None: - return None - bot, task = query_result - - # Include DB, Agent, and Task information in response. - session = SessionController() - database_names = { - i['id']: i['name'] - for i in session.database_controller.get_list() - } - - agent = self.agents_controller.get_agent_by_id(bot.agent_id) - agent_obj = agent.as_dict() if agent is not None else None - bot_obj = { - 'id': bot.id, - 'name': bot.name, - 'project': project_name, - 'agent': agent_obj, - 'database_id': bot.database_id, # TODO remove in future - 'database': database_names.get(bot.database_id, '?'), - 'model_name': bot.model_name, - 'params': bot.params, - 'created_at': bot.created_at, - 'is_running': task.active, - 'last_error': task.last_error, - 'webhook_token': bot.webhook_token, - } - return bot_obj + return self._get_chatbot(query, project) def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: ''' @@ -100,6 +72,19 @@ def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: db.Tasks.company_id == ctx.company_id, ) + return self._get_chatbot(query) + + def _get_chatbot(self, query, project: db.Project = None) -> db.ChatBots: + ''' + Gets a chatbot by query. + + Parameters: + query: The query to get the chatbot + + Returns: + bot (db.ChatBots): The database chatbot object + ''' + query_result = query.first() if query_result is None: return None @@ -117,7 +102,7 @@ def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: bot_obj = { 'id': bot.id, 'name': bot.name, - 'project': self.project_controller.get(bot.project_id).name, + 'project': project.name if project else self.project_controller.get(bot.project_id).name, 'agent': agent_obj, 'database_id': bot.database_id, # TODO remove in future 'database': database_names.get(bot.database_id, '?'), @@ -128,6 +113,7 @@ def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: 'last_error': task.last_error, 'webhook_token': bot.webhook_token, } + return bot_obj def get_chatbots(self, project_name: str = 'mindsdb') -> List[dict]: From 7556ad913f0a8c725e210745a339a5148529c5bb Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 14:43:19 +0530 Subject: [PATCH 31/40] improved the on_webhook() method in ChatBotController --- .../interfaces/chatbot/chatbot_controller.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 10cdcf519ee..11d85a144cd 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -376,12 +376,26 @@ def on_webhook(self, webhook_token: str, request: dict) -> None: webhook_token (str): The token to uniquely identify the webhook. request (dict): The incoming webhook request. """ - chat_bot = db.ChatBots.query.filter_by(webhook_token=webhook_token).first() + query = db.session.query( + db.ChatBots, db.Tasks + ).join( + db.Tasks, db.ChatBots.id == db.Tasks.object_id + ).filter( + db.ChatBots.webhook_token == webhook_token, + db.Tasks.object_type == self.OBJECT_TYPE, + db.Tasks.company_id == ctx.company_id, + ) + result = query.first() + + chat_bot, task = result if result is not None else (None, None) if chat_bot is None: raise Exception(f"No chat bot exists for webhook token: {webhook_token}") - chat_bot_task = ChatBotTask(task_id=None, object_id=chat_bot.id) + if not task.active: + raise Exception(f"Chat bot is not running: {chat_bot.name}") + + chat_bot_task = ChatBotTask(task_id=task.id, object_id=chat_bot.id) chat_bot_task.on_webhook(request) From d72ac0439f494da4dd5308726ed266ed53f65d5c Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 14:51:10 +0530 Subject: [PATCH 32/40] re-introduced chat_memory as a param to on_message --- mindsdb/interfaces/chatbot/chatbot_task.py | 10 ++++++---- mindsdb/interfaces/chatbot/polling.py | 7 +++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 55094ccf0ae..06160c96e62 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -76,10 +76,12 @@ def run(self, stop_event): self.chat_pooling.run(stop_event) - def on_message(self, chat_id, message: ChatBotMessage, table_name=None): + def on_message(self, message: ChatBotMessage, chat_id=None, chat_memory=None, table_name=None): + if not chat_id and chat_memory: + raise Exception('chat_id or chat_memory should be provided') try: - self._on_message(chat_id, message, table_name) + self._on_message(message, chat_id, chat_memory, table_name) except (SystemExit, KeyboardInterrupt): raise except Exception: @@ -87,10 +89,10 @@ def on_message(self, chat_id, message: ChatBotMessage, table_name=None): logger.error(error) self.set_error(str(error)) - def _on_message(self, chat_id, message: ChatBotMessage, table_name=None): + def _on_message(self, message: ChatBotMessage, chat_id, chat_memory, table_name=None): # add question to history # TODO move it to realtime pooling - chat_memory = self.memory.get_chat(chat_id, table_name=table_name) + chat_memory = chat_memory if chat_memory else self.memory.get_chat(chat_id, table_name=table_name) chat_memory.add_to_history(message) logger.debug(f'>>chatbot {chat_memory.chat_id} in: {message.text}') diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index 750e29da7c3..d00bc9acb34 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -70,7 +70,7 @@ def run(self, stop_event): message = None if message: - self.chat_task.on_message(chat_id, message, table_name=chat_params["chat_table"]["name"]) + self.chat_task.on_message(message, chat_memory=chat_memory, table_name=chat_params["chat_table"]["name"]) except Exception as e: logger.error(e) @@ -158,7 +158,7 @@ def _callback(self, row, key): chat_id = row[t_params["chat_id_col"]] - self.chat_task.on_message(chat_id, message) + self.chat_task.on_message(message, chat_id=chat_id) def run(self, stop_event): t_params = self.params["chat_table"] @@ -201,8 +201,7 @@ def run(self, stop_event: threading.Event) -> None: ) # Do nothing, as the webhook is handled by a task instantiated for each request. - while not stop_event.is_set(): - time.sleep(1) + stop_event.wait() def send_message(self, message: ChatBotMessage, table_name: str = None) -> None: """ From 7e53726796bc1b47d1f4f94dc9419fd191dc0695 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 16:14:04 +0530 Subject: [PATCH 33/40] fixed lint issues --- mindsdb/api/http/namespaces/webhooks.py | 4 ++-- mindsdb/interfaces/chatbot/chatbot_controller.py | 9 ++++----- mindsdb/interfaces/chatbot/chatbot_task.py | 4 ++-- mindsdb/interfaces/chatbot/polling.py | 2 +- ...0-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py | 2 -- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index a04e4758239..f987c807d00 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -13,9 +13,9 @@ class ChatbotWebhooks(Resource): def post(self, webhook_token: str) -> None: """ This endpoint is used to receive messages posted by bots from different platforms. - + Args: - webhook_token (str): The token of the webhook. It is used to uniquely identify the webhook. + webhook_token (str): The token of the webhook. It is used to uniquely identify the webhook. """ request_data = request.json diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 11d85a144cd..0c24f83f084 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -50,7 +50,7 @@ def get_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb') -> db.Ch ) return self._get_chatbot(query, project) - + def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: ''' Gets a chatbot by id. @@ -73,7 +73,7 @@ def get_chatbot_by_id(self, chatbot_id: int) -> db.ChatBots: ) return self._get_chatbot(query) - + def _get_chatbot(self, query, project: db.Project = None) -> db.ChatBots: ''' Gets a chatbot by query. @@ -391,11 +391,10 @@ def on_webhook(self, webhook_token: str, request: dict) -> None: if chat_bot is None: raise Exception(f"No chat bot exists for webhook token: {webhook_token}") - + if not task.active: raise Exception(f"Chat bot is not running: {chat_bot.name}") - + chat_bot_task = ChatBotTask(task_id=task.id, object_id=chat_bot.id) chat_bot_task.on_webhook(request) - diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 06160c96e62..797166cde28 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -45,7 +45,7 @@ def __init__(self, *args, **kwargs): self.chat_handler = self.session.integration_controller.get_data_handler(database_name) if not isinstance(self.chat_handler, APIChatHandler): raise Exception(f"Can't use chat database: {database_name}") - + # get chat handler info self.bot_params = bot_record.params or {} @@ -92,7 +92,7 @@ def on_message(self, message: ChatBotMessage, chat_id=None, chat_memory=None, ta def _on_message(self, message: ChatBotMessage, chat_id, chat_memory, table_name=None): # add question to history # TODO move it to realtime pooling - chat_memory = chat_memory if chat_memory else self.memory.get_chat(chat_id, table_name=table_name) + chat_memory = chat_memory if chat_memory else self.memory.get_chat(chat_id, table_name=table_name) chat_memory.add_to_history(message) logger.debug(f'>>chatbot {chat_memory.chat_id} in: {message.text}') diff --git a/mindsdb/interfaces/chatbot/polling.py b/mindsdb/interfaces/chatbot/polling.py index d00bc9acb34..ec38a8f4e19 100644 --- a/mindsdb/interfaces/chatbot/polling.py +++ b/mindsdb/interfaces/chatbot/polling.py @@ -185,7 +185,7 @@ def run(self, stop_event: threading.Event) -> None: Then, do nothing, as the webhook is handled by a task instantiated for each request. Args: - stop_event (threading.Event): Event to stop the polling. + stop_event (threading.Event): Event to stop the polling. """ # If a webhook token is not set for the chatbot, generate a new one. from mindsdb.interfaces.chatbot.chatbot_controller import ChatBotController diff --git a/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py b/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py index 64627ff062b..2189edbc259 100644 --- a/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py +++ b/mindsdb/migrations/versions/2024-10-07_6c57ed39a82b_added_webhook_token_to_chat_bots.py @@ -10,7 +10,6 @@ import mindsdb.interfaces.storage.db # noqa - # revision identifiers, used by Alembic. revision = '6c57ed39a82b' down_revision = '8e17ff6b75e9' @@ -26,4 +25,3 @@ def upgrade(): def downgrade(): with op.batch_alter_table('chat_bots', schema=None) as batch_op: batch_op.drop_column('webhook_token') - From a999898d15f4ce5a0b898a991691b429f12e3a78 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 18:12:14 +0530 Subject: [PATCH 34/40] added a simple mechanism to persist chatbot memory --- mindsdb/api/http/namespaces/webhooks.py | 5 ++++- mindsdb/interfaces/chatbot/chatbot_controller.py | 9 ++++++++- mindsdb/interfaces/chatbot/chatbot_task.py | 6 ++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index f987c807d00..62d1916e12d 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -6,6 +6,9 @@ from mindsdb.metrics.metrics import api_endpoint_metrics +chat_bot_memory = {} + + @ns_conf.route('/chatbots/') class ChatbotWebhooks(Resource): @ns_conf.doc('chatbots_webhook') @@ -20,4 +23,4 @@ def post(self, webhook_token: str) -> None: request_data = request.json chat_bot_controller = ChatBotController() - return chat_bot_controller.on_webhook(webhook_token, request_data) \ No newline at end of file + return chat_bot_controller.on_webhook(webhook_token, request_data, chat_bot_memory) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 0c24f83f084..7766864beb7 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -1,5 +1,6 @@ from typing import Dict, List +from flask import Flask from mindsdb.interfaces.agents.agents_controller import AgentsController from mindsdb.interfaces.chatbot.chatbot_task import ChatBotTask from mindsdb.interfaces.database.projects import ProjectController @@ -367,7 +368,7 @@ def delete_chatbot(self, chatbot_name: str, project_name: str = 'mindsdb'): db.session.commit() - def on_webhook(self, webhook_token: str, request: dict) -> None: + def on_webhook(self, webhook_token: str, request: dict, chat_bot_memory: dict): """ Handles incoming webhook requests. Finds the chat bot associated with the webhook token and passes the request to the chat bot task. @@ -396,5 +397,11 @@ def on_webhook(self, webhook_token: str, request: dict) -> None: raise Exception(f"Chat bot is not running: {chat_bot.name}") chat_bot_task = ChatBotTask(task_id=task.id, object_id=chat_bot.id) + + if webhook_token in chat_bot_memory: + chat_bot_task.set_memory(chat_bot_memory[webhook_token]) + else: + chat_bot_memory[webhook_token] = chat_bot_task.get_memory() + chat_bot_task.on_webhook(request) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index 797166cde28..bf13c196b94 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -128,3 +128,9 @@ def on_webhook(self, request: dict) -> None: request (dict): The incoming webhook request. """ self.chat_handler.on_webhook(request, self.on_message) + + def get_memory(self): + return self.memory + + def set_memory(self, memory): + self.memory = memory From e7c6360676efe3e781de482b2b9f7e504ec62084 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 18:23:21 +0530 Subject: [PATCH 35/40] fixed lint issues --- .../interfaces/chatbot/chatbot_controller.py | 4 +--- mindsdb/interfaces/chatbot/chatbot_task.py | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index 7766864beb7..adc7fed044e 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -1,6 +1,5 @@ from typing import Dict, List -from flask import Flask from mindsdb.interfaces.agents.agents_controller import AgentsController from mindsdb.interfaces.chatbot.chatbot_task import ChatBotTask from mindsdb.interfaces.database.projects import ProjectController @@ -402,6 +401,5 @@ def on_webhook(self, webhook_token: str, request: dict, chat_bot_memory: dict): chat_bot_task.set_memory(chat_bot_memory[webhook_token]) else: chat_bot_memory[webhook_token] = chat_bot_task.get_memory() - - chat_bot_task.on_webhook(request) + chat_bot_task.on_webhook(request) diff --git a/mindsdb/interfaces/chatbot/chatbot_task.py b/mindsdb/interfaces/chatbot/chatbot_task.py index bf13c196b94..de0a135fa25 100644 --- a/mindsdb/interfaces/chatbot/chatbot_task.py +++ b/mindsdb/interfaces/chatbot/chatbot_task.py @@ -10,7 +10,7 @@ from mindsdb.utilities import log from .polling import MessageCountPolling, RealtimePolling, WebhookPolling -from .memory import DBMemory, HandlerMemory +from .memory import BaseMemory, DBMemory, HandlerMemory from .chatbot_executor import MultiModeBotExecutor, BotExecutor, AgentExecutor from .types import ChatBotMessage @@ -129,8 +129,20 @@ def on_webhook(self, request: dict) -> None: """ self.chat_handler.on_webhook(request, self.on_message) - def get_memory(self): + def get_memory(self) -> BaseMemory: + """ + Get the memory of the chatbot task. + + Returns: + BaseMemory: The memory of the chatbot task. + """ return self.memory - - def set_memory(self, memory): + + def set_memory(self, memory: BaseMemory) -> None: + """ + Set the memory of the chatbot task. + + Args: + memory (BaseMemory): The memory to set for the chatbot task. + """ self.memory = memory From 21e33369ca56f5c616963ba3075a99eb4420beb6 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Tue, 8 Oct 2024 20:24:42 +0530 Subject: [PATCH 36/40] removed unused code in the MS Teams handler --- .../ms_teams_handler/ms_teams_handler.py | 89 +------------------ 1 file changed, 2 insertions(+), 87 deletions(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py index 756a0d5c5b2..0934a134eba 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py +++ b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_handler.py @@ -1,21 +1,16 @@ -import os from typing import Text, Dict, Callable -from botframework.connector import ConnectorClient -from botframework.connector.auth import MicrosoftAppCredentials from botbuilder.schema import Activity, ActivityTypes from botbuilder.schema import ChannelAccount +from botframework.connector import ConnectorClient +from botframework.connector.auth import MicrosoftAppCredentials from mindsdb.utilities import log from mindsdb_sql import parse_sql -from mindsdb.integrations.utilities.handlers.auth_utilities import MSGraphAPIAuthManager -from mindsdb.integrations.handlers.ms_teams_handler.settings import ms_teams_handler_config -from mindsdb.integrations.handlers.ms_teams_handler.ms_graph_api_teams_client import MSGraphAPITeamsClient from mindsdb.integrations.libs.response import ( HandlerStatusResponse as StatusResponse, ) from mindsdb.integrations.libs.api_handler import APIChatHandler -from mindsdb.integrations.handlers.ms_teams_handler.ms_teams_tables import ChannelsTable, ChannelMessagesTable, ChatsTable, ChatMessagesTable from mindsdb.interfaces.chatbot.types import ChatBotMessage logger = log.getLogger(__name__) @@ -35,67 +30,20 @@ def __init__(self, name: str, **kwargs): name (str): name of particular handler instance **kwargs: arbitrary keyword arguments. """ - super().__init__(name) connection_data = kwargs.get("connection_data", {}) self.connection_data = connection_data - # self.handler_storage = kwargs['handler_storage'] self.kwargs = kwargs self.connection = None self.is_connected = False - # channels_data = ChannelsTable(self) - # self._register_table("channels", channels_data) - - # channel_messages_data = ChannelMessagesTable(self) - # self._register_table("channel_messages", channel_messages_data) - - # chats_data = ChatsTable(self) - # self._register_table("chats", chats_data) - - # chat_messages_data = ChatMessagesTable(self) - # self._register_table("chat_messages", chat_messages_data) - self.service_url = None self.channel_id = None self.bot_id = None self.conversation_id = None - # def connect(self) -> MSGraphAPITeamsClient: - # """ - # Set up the connection required by the handler. - - # Returns - # ------- - # MSGraphAPITeamsClient - # Client object for accessing the Microsoft Graph API. - # """ - - # if self.is_connected and self.connection.check_connection(): - # return self.connection - - # # initialize the auth manager for the Microsoft Graph API - # ms_graph_api_auth_manager = MSGraphAPIAuthManager( - # handler_storage=self.handler_storage, - # scopes=self.connection_data.get('scopes', ms_teams_handler_config.DEFAULT_SCOPES), - # client_id=self.connection_data["client_id"], - # client_secret=self.connection_data["client_secret"], - # tenant_id=self.connection_data["tenant_id"], - # code=self.connection_data.get('code') - # ) - - # # get access token from the auth manager for the Microsoft Graph API - # access_token = ms_graph_api_auth_manager.get_access_token() - - # # pass the access token to the client for access to the Microsoft Graph API - # self.connection = MSGraphAPITeamsClient(access_token) - - # self.is_connected = True - - # return self.connection - def connect(self) -> MicrosoftAppCredentials: """ Set up the connection required by the handler. @@ -105,7 +53,6 @@ def connect(self) -> MicrosoftAppCredentials: MicrosoftAppCredentials Client object for interacting with the Microsoft Teams app. """ - if self.is_connected: return self.connection @@ -118,32 +65,6 @@ def connect(self) -> MicrosoftAppCredentials: return self.connection - # def check_connection(self) -> StatusResponse: - # """ - # Check connection to the handler. - - # Returns - # ------- - # StatusResponse - # Response object with the status of the connection. - # """ - - # response = StatusResponse(False) - - # try: - # connection = self.connect() - # connection.check_connection() - # response.success = True - # response.copy_storage = True - # except Exception as e: - # logger.error(f'Error connecting to Microsoft Teams: {e}!') - # response.success = False - # response.error_message = str(e) - - # self.is_connected = response.success - - # return response - def check_connection(self) -> StatusResponse: """ Check connection to the handler. @@ -153,7 +74,6 @@ def check_connection(self) -> StatusResponse: StatusResponse Response object with the status of the connection. """ - response = StatusResponse(False) try: @@ -182,7 +102,6 @@ def native_query(self, query: Text) -> StatusResponse: StatusResponse Response object with the result of the query. """ - ast = parse_sql(query, dialect="mindsdb") return self.query(ast) @@ -197,7 +116,6 @@ def get_chat_config(self) -> Dict: Dict Configuration for the chatbot. """ - params = { 'polling': { 'type': 'webhook' @@ -216,7 +134,6 @@ def get_my_user_name(self) -> Text: Text Name of the signed in user. """ - return None def on_webhook(self, request: Dict, callback: Callable) -> None: @@ -231,7 +148,6 @@ def on_webhook(self, request: Dict, callback: Callable) -> None: callback: Callable Callback function to call after parsing the request. """ - self.service_url = request["serviceUrl"] self.channel_id = request["channelId"] self.bot_id = request["from"]["id"] @@ -258,7 +174,6 @@ def respond(self, message: ChatBotMessage) -> None: message: ChatBotMessage The message to send. """ - credentials = self.connect() connector = ConnectorClient(credentials, base_url=self.service_url) From 03bd3142af52cd3cc19c5dabe9010ec22cd9b524 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 9 Oct 2024 00:32:19 +0530 Subject: [PATCH 37/40] removed unnecessary files --- .../ms_graph_api_teams_client.py | 318 --------- .../ms_teams_handler/ms_teams_tables.py | 630 ------------------ 2 files changed, 948 deletions(-) delete mode 100644 mindsdb/integrations/handlers/ms_teams_handler/ms_graph_api_teams_client.py delete mode 100644 mindsdb/integrations/handlers/ms_teams_handler/ms_teams_tables.py diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_graph_api_teams_client.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_graph_api_teams_client.py deleted file mode 100644 index 46ed582f611..00000000000 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_graph_api_teams_client.py +++ /dev/null @@ -1,318 +0,0 @@ -from typing import Text, List, Dict, Optional -from mindsdb.integrations.utilities.handlers.api_utilities.microsoft.ms_graph_api_utilities import MSGraphAPIBaseClient - - -class MSGraphAPITeamsClient(MSGraphAPIBaseClient): - """ - The Microsoft Graph API client for the Microsoft Teams handler. - This client is used for accessing the Microsoft Teams specific endpoints of the Microsoft Graph API. - Several common methods for submitting requests, fetching data, etc. are inherited from the base class. - """ - - def _get_group_ids(self) -> List[Text]: - """ - Get all group IDs related to Microsoft Teams. - - Returns - ------- - List[Text] - The group IDs. - """ - - if not self._group_ids: - api_url = self._get_api_url("groups") - # only get the id and resourceProvisioningOptions fields - params = {"$select": "id,resourceProvisioningOptions"} - groups = self._get_response_value_unsafe(self._make_request(api_url, params=params)) - # filter out only the groups that are related to Microsoft Teams - self._group_ids = [item["id"] for item in groups if "Team" in item["resourceProvisioningOptions"]] - - return self._group_ids - - def _get_channel_ids(self, group_id: Text) -> List[Text]: - """ - Get all channel IDs related to a group. - - Parameters - ---------- - group_id : Text - The ID of the group. - - Returns - ------- - List[Text] - The channel IDs for that group. - """ - - api_url = self._get_api_url(f"teams/{group_id}/channels") - channels_ids = self._get_response_value_unsafe(self._make_request(api_url)) - - return channels_ids - - def get_channel(self, group_id: Text, channel_id: Text) -> Dict: - """ - Get a channel by its ID and the ID of the group that it belongs to. - - Parameters - ---------- - group_id : str - The ID of the group that the channel belongs to. - - channel_id : str - The ID of the channel. - - Returns - ------- - Dict - The channel data. - """ - - api_url = self._get_api_url(f"teams/{group_id}/channels/{channel_id}") - channel = self._make_request(api_url) - # add the group ID to the channel data - channel.update({"teamId": group_id}) - - last_message = self.get_last_channel_message(group_id, channel_id) - channel["lastMessagePreview_id"] = last_message.get("id") - - return channel - - def get_channels(self) -> List[Dict]: - """ - Get all channels. - - Returns - ------- - List[Dict] - The channels data. - """ - - channels = [] - for group_id in self._get_group_ids(): - for group_channels in self._fetch_data(f"teams/{group_id}/channels", pagination=False): - for group_channel in group_channels: - # add the group ID to the channel data - group_channel.update({"teamId": group_id}) - - last_message = self.get_last_channel_message(group_id, group_channel["id"]) - group_channel["lastMessagePreview_id"] = last_message.get("id") - - channels.extend(group_channels) - - return channels - - def get_channel_message(self, group_id: Text, channel_id: Text, message_id: Text) -> Dict: - """ - Get a channel message by its ID and the IDs of the group and channel that it belongs to. - - Parameters - ---------- - group_id : str - The ID of the group that the channel belongs to. - - channel_id : str - The ID of the channel that the message belongs to. - - message_id : str - The ID of the message. - - Returns - ------- - Dict - The channel message data. - """ - - api_url = self._get_api_url(f"teams/{group_id}/channels/{channel_id}/messages/{message_id}") - message = self._make_request(api_url) - - return message - - def get_channel_messages(self) -> List[Dict]: - """ - Get all channel messages. - - Returns - ------- - List[Dict] - The channel messages data. - """ - - channel_messages = [] - for group_id in self._get_group_ids(): - for channel_id in self._get_channel_ids(group_id): - for messages in self._fetch_data(f"teams/{group_id}/channels/{channel_id['id']}/messages"): - channel_messages.extend(messages) - - return channel_messages - - def get_last_channel_message(self, group_id: Text, channel_id: Text) -> Dict: - """ - Get the last message in a channel. - - Parameters - ---------- - group_id : Text - The ID of the group that the channel belongs to. - - channel_id : Text - The ID of the channel. - - Returns - ------- - Dict - The last message data. - """ - - api_url = self._get_api_url(f"teams/{group_id}/channels/{channel_id}/messages") - # get the last message only - messages = self._get_response_value_unsafe(self._make_request(api_url, params={"$top": 1})) - - return messages[0] if messages else {} - - def send_channel_message(self, group_id: Text, channel_id: Text, message: Text, subject: Optional[Text] = None) -> None: - """ - Send a message to a channel. - - Parameters - ---------- - group_id : Text - The ID of the group that the channel belongs to. - - channel_id : Text - The ID of the channel. - - message : Text - The message to send. - - subject : Text, Optional - The subject of the message. - """ - - api_url = self._get_api_url(f"teams/{group_id}/channels/{channel_id}/messages") - data = { - "subject": subject, - "body": { - "content": message - } - } - - self._make_request(api_url, data=data, method="POST") - - def get_chat(self, chat_id: Text) -> Dict: - """ - Get a chat by its ID. - - Parameters - ---------- - chat_id : str - The ID of the chat. - """ - - api_url = self._get_api_url(f"chats/{chat_id}") - # expand the response with the last message preview - chat = self._make_request(api_url, params={"$expand": "lastMessagePreview"}) - - return chat - - def get_chats(self) -> List[Dict]: - """ - Get all chats. - - Returns - ------- - List[Dict] - The chats data. - """ - - chats = [] - for chat in self._fetch_data("chats", params={"$expand": "lastMessagePreview"}): - chats.extend(chat) - - return chats - - def get_chat_message(self, chat_id: Text, message_id: Text) -> Dict: - """ - Get a chat message by its ID and the ID of the chat that it belongs to. - - Parameters - ---------- - chat_id : str - The ID of the chat that the message belongs to. - - message_id : str - The ID of the message. - - Returns - ------- - Dict - The chat message data. - """ - - api_url = self._get_api_url(f"chats/{chat_id}/messages/{message_id}") - message = self._make_request(api_url) - - return message - - def get_chat_messages(self, chat_id: Text) -> List[Dict]: - """ - Get all chat messages for a chat. - - Parameters - ---------- - chat_id : str - The ID of the chat. - - Returns - ------- - List[Dict] - The chat messages data. - """ - - chat_messages = [] - for messages in self._fetch_data(f"chats/{chat_id}/messages"): - chat_messages.extend(messages) - - return chat_messages - - def get_all_chat_messages(self) -> List[Dict]: - """ - Get all chat messages for all chats. - - Returns - ------- - List[Dict] - The chat messages data. - """ - - chat_messages = [] - for chat_id in [chat["id"] for chat in self.get_chats()]: - for messages in self._fetch_data(f"chats/{chat_id}/messages"): - chat_messages.extend(messages) - - return chat_messages - - def send_chat_message(self, chat_id: Text, message: Text, subject: Optional[Text] = None) -> None: - """ - Send a message to a chat. - - Parameters - ---------- - chat_id : Text - The ID of the chat. - - message : Text - The message to send. - - subject : Text, Optional - The subject of the message. - """ - - api_url = self._get_api_url(f"chats/{chat_id}/messages") - data = { - "subject": subject, - "body": { - "content": message - } - } - - self._make_request(api_url, data=data, method="POST") diff --git a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_tables.py b/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_tables.py deleted file mode 100644 index 09a8d6d8381..00000000000 --- a/mindsdb/integrations/handlers/ms_teams_handler/ms_teams_tables.py +++ /dev/null @@ -1,630 +0,0 @@ -import pandas as pd -from typing import Text, List, Dict, Any - -from mindsdb.utilities import log -from mindsdb_sql.parser import ast -from mindsdb.integrations.libs.api_handler import APITable - -from mindsdb.integrations.handlers.ms_teams_handler.settings import ms_teams_handler_config - -from mindsdb.integrations.utilities.handlers.query_utilities.insert_query_utilities import INSERTQueryParser -from mindsdb.integrations.utilities.handlers.query_utilities.select_query_utilities import SELECTQueryParser, SELECTQueryExecutor - -logger = log.getLogger(__name__) - - -class ChatsTable(APITable): - """ - The Microsoft Teams Chats Table implementation. - """ - - def select(self, query: ast.Select) -> pd.DataFrame: - """ - Pulls data from the "GET /chats" and "GET /chats/{chat_id} Microsoft Graph API endpoints. - - Parameters - ---------- - query : ast.Select - Given SQL SELECT query. - - Returns - ------- - pd.DataFrame - Microsoft Teams Chats matching the query. - - Raises - ------ - ValueError - If the query contains an unsupported target (column). - - NotImplementedError - If the query contains an unsupported condition. - """ - - select_statement_parser = SELECTQueryParser( - query, - 'chats', - self.get_columns() - ) - - selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query() - - # only the = operator is supported for the id column - id = None - for op, arg1, arg2 in where_conditions: - if arg1 == 'id': - if op == "=": - id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for id column.") - - chats_df = pd.json_normalize(self.get_chats(id), sep='_') - - # if the id is given, remove the id column from the where conditions as it has already been evaluated in the API call (get_chats) - if id: - where_conditions = [where_condition for where_condition in where_conditions if where_condition[1] not in ['id']] - - select_statement_executor = SELECTQueryExecutor( - chats_df, - selected_columns, - where_conditions, - order_by_conditions, - result_limit if query.limit else None - ) - - chats_df = select_statement_executor.execute_query() - - return chats_df - - def get_chats(self, chat_id: Text = None) -> List[Dict[Text, Any]]: - """ - Calls the API client to get the chats from the Microsoft Graph API. - - Parameters - ---------- - chat_id: Text - The chat id to get the chat from. - - Returns - ------- - List[Dict[Text, Any]] - The chats from the Microsoft Graph API. - """ - - api_client = self.handler.connect() - - # if the chat_id is given, get the chat with that id from the API - if chat_id: - chats = [api_client.get_chat(chat_id)] - # if the chat_id is not given, get all the chats - else: - chats = api_client.get_chats() - - for chat in chats: - last_message_preview = chat.get("lastMessagePreview") - - # keep only the lastMessagePreview_id and lastMessagePreview_createdDateTime columns - if last_message_preview: - chat["lastMessagePreview_id"] = last_message_preview.get("id") - chat["lastMessagePreview_createdDateTime"] = last_message_preview.get("createdDateTime") - del chat["lastMessagePreview"] - del chat["lastMessagePreview@odata.context"] - - return chats - - def get_columns(self) -> List[Text]: - """ - Returns the columns of the Chats Table. - - Returns - ------- - List[Text] - The columns of the Chats Table. - """ - - return ms_teams_handler_config.CHATS_TABLE_COLUMNS - -class ChatMessagesTable(APITable): - """ - The Microsoft Teams Chat Messages Table implementation. - """ - - def select(self, query: ast.Select) -> pd.DataFrame: - """ - Pulls data from the "GET /chats/{chat_id}/messages" and "GET /chats/{chat_id}/messages/{message_id}" Microsoft Graph API endpoints. - - Parameters - ---------- - query : ast.Select - Given SQL SELECT query. - - Returns - ------- - pd.DataFrame - Microsoft Teams Chat Messages matching the query. - - Raises - ------ - ValueError - If the query contains an unsupported target (column). - - NotImplementedError - If the query contains an unsupported condition. - """ - - select_statement_parser = SELECTQueryParser( - query, - 'chat_messages', - self.get_columns() - ) - - selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query() - - # only the = operator is supported for the id and chatId columns - chat_id, message_id = None, None - for op, arg1, arg2 in where_conditions: - if arg1 == 'id': - if op == "=": - message_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for id column.") - - if arg1 == 'chatId': - if op == "=": - chat_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for chatId column.") - - messages_df = pd.json_normalize(self.get_messages(chat_id, message_id), sep='_') - - # if both chat_id and message_id are given, remove the id and chatId columns from the where conditions as they have already been evaluated in the API call (get_messages) - if chat_id and message_id: - where_conditions = [where_condition for where_condition in where_conditions if where_condition[1] not in ['id', 'chatId']] - # if only the chat_id is given, remove the chatId column from the where conditions as it has already been evaluated in the API call (get_messages) - elif chat_id: - where_conditions = [where_condition for where_condition in where_conditions if where_condition[1] not in ['chatId']] - - select_statement_executor = SELECTQueryExecutor( - messages_df, - selected_columns, - where_conditions, - order_by_conditions, - result_limit if query.limit else None - ) - - messages_df = select_statement_executor.execute_query() - - return messages_df - - def get_messages(self, chat_id: Text = None, message_id: Text = None) -> List[Dict[Text, Any]]: - """ - Calls the API client to get the messages from the Microsoft Graph API. - If all parameters are None, it will return all the messages from all the chats. - If only the chat_id is given, it will return all the messages from that chat. - - Parameters - ---------- - chat_id: Text - The chat id to get the messages from. - - message_id: Text - The message id to get the message from. - - Returns - ------- - List[Dict[Text, Any]] - The messages from the Microsoft Graph API. - """ - - api_client = self.handler.connect() - - # if both chat_id and message_id are given, get the message with that id from the API - if message_id and chat_id: - chat_messages = [api_client.get_chat_message(chat_id, message_id)] - # if only the chat_id is given, get all the messages from that chat - elif chat_id: - chat_messages = api_client.get_chat_messages(chat_id) - # if no parameters are given or only the message_id is given, get all the messages from all the chats - else: - chat_messages = api_client.get_all_chat_messages() - - return chat_messages - - def insert(self, query: ast.Insert) -> None: - """ - Inserts data into the "POST /chats/{chat_id}/messages" Microsoft Graph API endpoint. - - Parameters - ---------- - query : ast.Insert - Given SQL INSERT query. - - Returns - ------- - None - - Raises - ------ - UnsupportedColumnException - If the query contains an unsupported column. - - MandatoryColumnException - If the query is missing a mandatory column. - - ColumnCountMismatchException - If the number of columns does not match the number of values. - """ - - # only the chatId, subject and body_content columns are supported - # chatId and body_content are mandatory - insert_statement_parser = INSERTQueryParser( - query, - supported_columns=["chatId", "subject", "body_content"], - mandatory_columns=["chatId", "body_content"], - ) - - messages_to_send = insert_statement_parser.parse_query() - - self.send_messages(messages_to_send) - - def send_messages(self, messages_to_send: List[Dict[Text, Any]]) -> None: - """ - Calls the API client to send the messages to the Microsoft Graph API. - - Parameters - ---------- - messages_to_send: List[Dict[Text, Any]] - The messages to send to the Microsoft Graph API. - - Returns - ------- - None - """ - - api_client = self.handler.connect() - - # send each message through the API to the chat with the given id - for message in messages_to_send: - api_client.send_chat_message( - chat_id=message["chatId"], - message=message["body_content"], - subject=message.get("subject") - ) - - def get_columns(self) -> List[Text]: - """ - Returns the columns of the Chat Messages Table. - - Returns - ------- - List[Text] - The columns of the Chat Messages Table. - """ - - return ms_teams_handler_config.CHAT_MESSAGES_TABLE_COLUMNS - - class ChatMessageRepliesTable(APITable): - """ - The Microsoft Chat Message Replies Table implementation. - """ - pass - -class ChannelsTable(APITable): - """ - The Microsoft Channels Table implementation. - """ - - def select(self, query: ast.Select) -> pd.DataFrame: - """ - Pulls data from the "GET /teams/{group_id}/channels" and "GET teams/{group_id}/channels/{channel_id}" Microsoft Graph API endpoints. - - Parameters - ---------- - query : ast.Select - Given SQL SELECT query. - - Returns - ------- - pd.DataFrame - Microsoft Teams Channels matching the query. - - Raises - ------ - ValueError - If the query contains an unsupported target (column). - - NotImplementedError - If the query contains an unsupported condition. - """ - - select_statement_parser = SELECTQueryParser( - query, - 'channels', - self.get_columns() - ) - - selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query() - - # only the = operator is supported for the id and teamId columns - channel_id, team_id = None, None - for op, arg1, arg2 in where_conditions: - if arg1 == 'id': - if op == "=": - channel_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for id column.") - - if arg1 == 'teamId': - if op == "=": - team_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for teamId column.") - - channels_df = pd.json_normalize(self.get_channels(channel_id, team_id)) - - # if both channel_id and team_id are given, remove the id and teamId columns from the where conditions as they have already been evaluated in the API call (get_channels) - if channel_id and team_id: - where_conditions = [where_condition for where_condition in where_conditions if where_condition[1] not in ['id', 'teamId']] - - select_statement_executor = SELECTQueryExecutor( - channels_df, - selected_columns, - where_conditions, - order_by_conditions, - result_limit if query.limit else None - ) - - channels_df = select_statement_executor.execute_query() - - return channels_df - - def get_channels(self, channel_id: Text = None, team_id: Text = None) -> List[Dict[Text, Any]]: - """ - Calls the API client to get the channels from the Microsoft Graph API. - If all parameters are None, it will return all the channels from all the teams. - - Parameters - ---------- - channel_id: Text - The channel id to get the channel from. - - team_id: Text - The team id to get the channels from. - - Returns - ------- - List[Dict[Text, Any]] - The channels from the Microsoft Graph API. - """ - - api_client = self.handler.connect() - - # if both channel_id and team_id are given, get the channel with that id from the API - if channel_id and team_id: - return [api_client.get_channel(team_id, channel_id)] - # if no parameter are given or only the team_id is given, get all the channels - else: - return api_client.get_channels() - - def get_columns(self) -> List[Text]: - """ - Returns the columns of the Channels Table. - - Returns - ------- - List[Text] - The columns of the Channels Table. - """ - - return ms_teams_handler_config.CHANNELS_TABLE_COLUMNS - -class ChannelMessagesTable(APITable): - """ - The Microsoft Teams Channel Messages Table implementation. - """ - - def select(self, query: ast.Select) -> pd.DataFrame: - """ - Pulls data from the "GET /teams/{group_id}/channels/{channel_id}/messages" and "GET /teams/{group_id}/channels/{channel_id}/messages/{message_id}" Microsoft Graph API endpoints. - - Parameters - ---------- - query: ast.Select - Given SQL SELECT query. - - Returns - ------- - pd.DataFrame - Microsoft Teams Channel Messages matching the query. - - Raises - ------ - ValueError - If the query contains an unsupported target (column). - - NotImplementedError - If the query contains an unsupported condition. - """ - - select_statement_parser = SELECTQueryParser( - query, - 'channel_messages', - self.get_columns() - ) - - selected_columns, where_conditions, order_by_conditions, result_limit = select_statement_parser.parse_query() - - # only the = operator is supported for the id, channelIdentity_teamId and channelIdentity_channelId columns - team_id, channel_id, message_id = None, None, None - for op, arg1, arg2 in where_conditions: - if arg1 == 'id': - if op == "=": - message_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for id column.") - - if arg1 == 'channelIdentity_teamId': - if op == "=": - team_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for id column.") - - if arg1 == 'channelIdentity_channelId': - if op == "=": - channel_id = arg2 - else: - raise NotImplementedError("Only '=' operator is supported for teamId column.") - - messages_df = pd.json_normalize(self.get_messages(team_id, channel_id, message_id), sep='_') - - # if all parameters are given, remove the id, channelIdentity_teamId and channelIdentity_channelId columns from the where conditions as they have already been evaluated in the API call (get_messages) - if team_id and channel_id and message_id: - where_conditions = [where_condition for where_condition in where_conditions if where_condition[1] not in ['id', 'channelIdentity_teamId', 'channelIdentity_channelId']] - - select_statement_executor = SELECTQueryExecutor( - messages_df, - selected_columns, - where_conditions, - order_by_conditions, - result_limit if query.limit else None - ) - - messages_df = select_statement_executor.execute_query() - - return messages_df - - def get_messages(self, team_id: Text = None, channel_id: Text = None, message_id: Text = None) -> List[Dict[Text, Any]]: - """ - Calls the API client to get the messages from the Microsoft Graph API. - If all parameters are None, it will return all the messages from all the channels from all the teams. - - Parameters - ---------- - team_id: Text - The team id to get the messages from. - - channel_id: Text - The channel id to get the messages from. - - message_id: Text - The message id to get the message from. - """ - - api_client = self.handler.connect() - - # if all parameters are given, get the message with that id from that channel from that team from the API - if message_id and channel_id and team_id: - channel_message = api_client.get_channel_message(team_id, channel_id, message_id) - # add the missing eventDetail attribute to the channel message - channel_message['eventDetail'] = { - '@odata.type': None, - 'visibleHistoryStartDateTime': None, - 'members': None, - 'channelId': None, - 'channelDisplayName': None, - 'initiator': { - 'application': { - '@odata.type': None, - 'id': None, - 'displayName': None, - 'applicationIdentityType': None - }, - 'device': None, - 'user': { - '@odata.type': None, - 'id': None, - 'displayName': None, - 'userIdentityType': None, - 'tenantId': None - } - } - } - - return [channel_message] - # for any other combination of parameters, get all the messages from all the channels from all the teams - else: - return api_client.get_channel_messages() - - def insert(self, query: ast.Insert) -> None: - """ - Inserts data into the "POST /teams/{group_id}/channels/{channel_id}/messages" Microsoft Graph API endpoint. - - Parameters - ---------- - query : ast.Insert - Given SQL INSERT query. - - Returns - ------- - None - - Raises - ------ - UnsupportedColumnException - If the query contains an unsupported column. - - MandatoryColumnException - If the query is missing a mandatory column. - - ColumnCountMismatchException - If the number of columns does not match the number of values. - """ - - # only the channelIdentity_teamId, channelIdentity_channelId, subject and body_content columns are supported - # channelIdentity_teamId and channelIdentity_channelId are mandatory - insert_statement_parser = INSERTQueryParser( - query, - supported_columns=["channelIdentity_teamId", "channelIdentity_channelId", "subject", "body_content"], - mandatory_columns=["channelIdentity_teamId", "channelIdentity_channelId", "body_content"], - ) - - messages_to_send = insert_statement_parser.parse_query() - - self.send_messages(messages_to_send) - - def send_messages(self, messages_to_send: List[Dict[Text, Any]]) -> None: - """ - Calls the API client to send the messages to the Microsoft Graph API. - - Parameters - ---------- - messages_to_send: List[Dict[Text, Any]] - The messages to send to the Microsoft Graph API. - - Returns - ------- - None - - Raises - ------ - None - """ - - api_client = self.handler.connect() - - # send each message through the API to the channel with the given id from the team with the given id - for message in messages_to_send: - api_client.send_channel_message( - group_id=message["channelIdentity_teamId"], - channel_id=message["channelIdentity_channelId"], - message=message["body_content"], - subject=message.get("subject") - ) - - def get_columns(self) -> List[Text]: - """ - Returns the columns of the Channel Messages Table. - - Returns - ------- - list - The columns of the Channel Messages Table. - """ - - return ms_teams_handler_config.CHANNEL_MESSAGES_TABLE_COLUMNS - -class ChannelMessageRepliesTable(APITable): - """ - The Microsoft Teams Channel Message Replies Table implementation. - """ - pass From 1fcee248f6de55db320ae3fb67bcc55a210ebd23 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 9 Oct 2024 00:32:27 +0530 Subject: [PATCH 38/40] updated the dependencies --- .../integrations/handlers/ms_teams_handler/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mindsdb/integrations/handlers/ms_teams_handler/requirements.txt b/mindsdb/integrations/handlers/ms_teams_handler/requirements.txt index 0f0cb6a9ecf..e7440fcfadf 100644 --- a/mindsdb/integrations/handlers/ms_teams_handler/requirements.txt +++ b/mindsdb/integrations/handlers/ms_teams_handler/requirements.txt @@ -1 +1,2 @@ -pydantic-settings >= 2.1.0 \ No newline at end of file +botframework-connector +botbuilder-schema \ No newline at end of file From 7f5c3e5e18148b53536fc4b59b1e0ebee569e421 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 9 Oct 2024 00:50:57 +0530 Subject: [PATCH 39/40] updated the script for checking requirements with new dependencies --- mindsdb/api/http/namespaces/webhooks.py | 3 +++ mindsdb/interfaces/chatbot/chatbot_controller.py | 1 + tests/scripts/check_requirements.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/mindsdb/api/http/namespaces/webhooks.py b/mindsdb/api/http/namespaces/webhooks.py index 62d1916e12d..d7e849d28bd 100644 --- a/mindsdb/api/http/namespaces/webhooks.py +++ b/mindsdb/api/http/namespaces/webhooks.py @@ -6,6 +6,9 @@ from mindsdb.metrics.metrics import api_endpoint_metrics +# Stores the memory of the various chat-bots mapped by their webhook tokens. +# This is required because each time a new request is made, a new instance of the ChatBotTask is created. +# This causes the memory to be lost. chat_bot_memory = {} diff --git a/mindsdb/interfaces/chatbot/chatbot_controller.py b/mindsdb/interfaces/chatbot/chatbot_controller.py index adc7fed044e..20ea6a34809 100644 --- a/mindsdb/interfaces/chatbot/chatbot_controller.py +++ b/mindsdb/interfaces/chatbot/chatbot_controller.py @@ -375,6 +375,7 @@ def on_webhook(self, webhook_token: str, request: dict, chat_bot_memory: dict): Args: webhook_token (str): The token to uniquely identify the webhook. request (dict): The incoming webhook request. + chat_bot_memory (dict): The memory of the various chat-bots mapped by their webhook tokens. """ query = db.session.query( db.ChatBots, db.Tasks diff --git a/tests/scripts/check_requirements.py b/tests/scripts/check_requirements.py index 5a76c930d11..55b84beecee 100644 --- a/tests/scripts/check_requirements.py +++ b/tests/scripts/check_requirements.py @@ -123,6 +123,8 @@ def get_requirements_from_file(path): "auto-ts": ["auto_ts"], "llama-index-readers-web": ["llama_index"], "llama-index-embeddings-openai": ["llama_index"], + "botframework-connector": ["botframework"], + "botbuilder-schema": ["botbuilder"], } # We use this to exit with a non-zero status code if any check fails From 40cf3700ecd106b52b6b1854527951a57819f003 Mon Sep 17 00:00:00 2001 From: Minura Punchihewa Date: Wed, 9 Oct 2024 00:55:19 +0530 Subject: [PATCH 40/40] removed the unnecessary settings file --- .../handlers/ms_teams_handler/settings.py | 360 ------------------ 1 file changed, 360 deletions(-) delete mode 100644 mindsdb/integrations/handlers/ms_teams_handler/settings.py diff --git a/mindsdb/integrations/handlers/ms_teams_handler/settings.py b/mindsdb/integrations/handlers/ms_teams_handler/settings.py deleted file mode 100644 index 24f0e22a0d4..00000000000 --- a/mindsdb/integrations/handlers/ms_teams_handler/settings.py +++ /dev/null @@ -1,360 +0,0 @@ -from typing import List -from pydantic_settings import BaseSettings - - -class MSTeamsHandlerConfig(BaseSettings): - """ - Settings for the Microsoft Teams handler. - - Attributes - ---------- - DEFAULT_SCOPES: List - Default scopes for querying the Microsoft Graph API. - - CHATS_TABLE_COLUMNS: List - Columns for the chats table. - - CHAT_MESSAGES_TABLE_COLUMNS: List - Columns for the chat messages table. - - CHANNELS_TABLE_COLUMNS: List - Columns for the channels table. - - CHANNEL_MESSAGES_TABLE_COLUMNS: List - Columns for the channel messages table. - - TEST_CHAT_DATA: Dict - Test data for the chats table. - - TEST_CHATS_DATA: List[Dict] - Test data for the chats table. - - TEST_CHAT_MESSAGE_DATA: Dict - Test data for the chat messages table. - - TEST_CHAT_MESSAGES_DATA: List[Dict] - Test data for the chat messages table. - - TEST_CHANNEL_DATA: Dict - Test data for the channels table. - - TEST_CHANNELS_DATA: List[Dict] - Test data for the channels table. - - TEST_CHANNEL_MESSAGE_DATA: Dict - Test data for the channel messages table. - - TEST_CHANNEL_MESSAGES_DATA: List[Dict] - Test data for the channel messages table. - - TEST_GROUP_DATA: dict - - TEST_CHANNEL_ID_DATA: dict - """ - - DEFAULT_SCOPES: List = [ - "https://graph.microsoft.com/.default", - ] - - CHATS_TABLE_COLUMNS: List = [ - "id", - "topic", - "createdDateTime", - "lastUpdatedDateTime", - "chatType", - "webUrl", - "tenantId", - "onlineMeetingInfo", - "viewpoint_isHidden", - "viewpoint_lastMessageReadDateTime", - "lastMessagePreview_id", - "lastMessagePreview_createdDateTime", - ] - - CHAT_MESSAGES_TABLE_COLUMNS: List = [ - "id", - "replyToId", - "etag", - "messageType", - "createdDateTime", - "lastModifiedDateTime", - "lastEditedDateTime", - "deletedDateTime", - "subject", - "summary", - "chatId", - "importance", - "locale", - "webUrl", - "channelIdentity", - "policyViolation", - "attachments", - "mentions", - "reactions", - "from_application", - "from_device", - "from_user_@odata.type", - "from_user_id", - "from_user_displayName", - "from_user_userIdentityType", - "from_user_tenantId", - "body_contentType", - "body_content" - ] - - CHANNELS_TABLE_COLUMNS: List = [ - "id", - "createdDateTime", - "displayName", - "description", - "isFavoriteByDefault", - "email", - "tenantId", - "webUrl", - "membershipType", - "teamId", - "lastMessagePreview_id" - ] - - CHANNEL_MESSAGES_TABLE_COLUMNS: List = [ - "id", - "replyToId", - "etag", - "messageType", - "createdDateTime", - "lastModifiedDateTime", - "lastEditedDateTime", - "deletedDateTime", - "subject", - "summary", - "chatId", - "importance", - "locale", - "webUrl", - "policyViolation", - "attachments", - "mentions", - "reactions", - "from_application", - "from_device", - "from_user_@odata.type", - "from_user_id", - "from_user_displayName", - "from_user_userIdentityType", - "from_user_tenantId", - "body_contentType", - "body_content", - "channelIdentity_teamId", - "channelIdentity_channelId", - "eventDetail_@odata.type", - "eventDetail_visibleHistoryStartDateTime", - "eventDetail_members", - "eventDetail_initiator_device", - "eventDetail_initiator_application_@odata.type", - "eventDetail_initiator_application_id", - "eventDetail_initiator_application_displayName", - "eventDetail_initiator_application_applicationIdentityType", - "eventDetail_channelId", - "eventDetail_channelDisplayName", - "eventDetail_initiator_user_@odata.type", - "eventDetail_initiator_user_id", - "eventDetail_initiator_user_displayName", - "eventDetail_initiator_user_userIdentityType", - "eventDetail_initiator_user_tenantId" - ] - - TEST_CHAT_DATA: dict = { - '@odata.context': 'test_context', - 'id': 'test_id', - 'topic': None, - 'createdDateTime': '2023-11-20T10:25:19.553Z', - 'lastUpdatedDateTime': '2023-11-20T10:25:19.553Z', - 'chatType': 'oneOnOne', - 'webUrl': 'https://teams.test', - 'tenantId': 'test_tenant_id', - 'onlineMeetingInfo': None, - 'viewpoint': { - 'isHidden': False, - 'lastMessageReadDateTime': '2023-12-08T17:09:34.214Z' - }, - 'lastMessagePreview@odata.context': 'https://graph.test', - 'lastMessagePreview': { - 'id': '1702055374214', - 'createdDateTime': '2023-12-08T17:09:34.214Z', - 'isDeleted': False, - 'messageType': 'message', - 'eventDetail': None, - 'body': { - 'contentType': 'text', - 'content': '\n\nTest message.' - }, - 'from': { - 'application': None, - 'device': None, - 'user': {} - } - } - } - - TEST_CHATS_DATA: List[dict] = [TEST_CHAT_DATA] - - TEST_CHAT_MESSAGE_DATA: dict = { - '@odata.context': 'test_context', - 'id': 'test_id', - 'replyToId': None, - 'etag': 'test_etag', - 'messageType': 'message', - 'createdDateTime': '2023-12-08T17:09:22.241Z', - 'lastModifiedDateTime': '2023-12-08T17:09:22.241Z', - 'lastEditedDateTime': None, - 'deletedDateTime': None, - 'subject': None, - 'summary': None, - 'chatId': 'test_chat_id', - 'importance': 'normal', - 'locale': 'en-us', - 'webUrl': None, - 'channelIdentity': None, - 'policyViolation': None, - 'attachments': [], - 'mentions': [], - 'reactions': [], - 'from': { - 'application': None, - 'device': None, - 'user': { - '@odata.type': 'test_type', - 'id': 'test_user_id', - 'displayName': 'test_user_display_name', - 'userIdentityType': 'aadUser', - 'tenantId': 'test_tenant_id' - } - }, - 'body': { - 'contentType': 'text', - 'content': '\n\nTest message.' - }, - 'eventDetail': { - '@odata.type': 'test_type', - 'visibleHistoryStartDateTime': '2023-12-08T17:09:22.241Z', - 'members': [], - 'initiator': { - 'device': None, - 'application': None, - 'user': { - '@odata.type': 'test_type', - 'id': 'test_user_id', - 'displayName': 'test_user_display_name', - 'userIdentityType': 'aadUser', - 'tenantId': 'test_tenant_id' - } - }, - } - } - - TEST_CHAT_MESSAGES_DATA: List[dict] = [TEST_CHAT_MESSAGE_DATA] - - TEST_CHANNEL_DATA: dict = { - '@odata.context': 'test_context', - 'id': 'test_id', - 'createdDateTime': '2023-11-17T22:54:33.055Z', - 'displayName': 'test_display_name', - 'description': None, - 'isFavoriteByDefault': None, - 'email': 'test@test.com', - 'tenantId': 'test_tenant_id', - 'webUrl': 'https://teams.test', - 'membershipType': 'standard', - 'teamId': 'test_team_id' - } - - TEST_CHANNELS_DATA: List[dict] = [TEST_CHANNEL_DATA] - - TEST_CHANNEL_MESSAGE_DATA: dict = { - '@odata.context': 'test_context', - 'id': 'test_id', - 'replyToId': None, - 'etag': 'test_etag', - 'messageType': 'message', - 'createdDateTime': '2023-11-30T16:52:50.18Z', - 'lastModifiedDateTime': '2023-11-30T16:52:50.18Z', - 'lastEditedDateTime': None, - 'deletedDateTime': None, - 'subject': 'Test Subject', - 'summary': None, - 'chatId': None, - 'importance': - 'normal', - 'locale': 'en-us', - 'webUrl': 'https://teams.test', - 'policyViolation': None, - 'attachments': [], - 'mentions': [], - 'reactions': [], - 'from': { - 'application': None, - 'device': None, - 'user': { - '@odata.type': 'test_type', - 'id': 'test_user_id', - 'displayName': 'test_user_display_name', - 'userIdentityType': 'aadUser', - 'tenantId': 'test_tenant_id' - } - }, - 'body': { - 'contentType': 'text', - 'content': '\n\nTest message.' - }, - 'channelIdentity': { - 'teamId': 'test_team_id', - 'channelId': 'test_channel_id' - }, - 'eventDetail': { - '@odata.type': 'test_type', - 'visibleHistoryStartDateTime': '2023-11-30T16:52:50.18Z', - 'members': [], - 'initiator': { - 'device': None, - 'application': { - '@odata.type': 'test_type', - 'id': 'test_app_id', - 'displayName': 'test_app_display_name', - 'applicationIdentityType': 'bot' - }, - 'user': { - '@odata.type': 'test_type', - 'id': 'test_user_id', - 'displayName': 'test_user_display_name', - 'userIdentityType': 'aadUser', - 'tenantId': 'test_tenant_id' - } - }, - 'channelId': 'test_channel_id', - 'channelDisplayName': 'test_channel_display_name' - } - } - - TEST_CHANNEL_MESSAGES_DATA: List[dict] = [TEST_CHANNEL_MESSAGE_DATA] - - TEST_GROUP_DATA: dict = { - '@odata.context': 'test_context', - 'value': [ - { - 'id': 'test_team_id', - 'resourceProvisioningOptions': ['Team'], - } - ] - } - - TEST_CHANNEL_ID_DATA: dict = { - '@odata.context': 'test_context', - '@odata.count': 1, - 'value': [ - { - 'id': 'test_channel_id', - } - ] - } - -ms_teams_handler_config = MSTeamsHandlerConfig() \ No newline at end of file