From f31f371d539ba8a206aad46b015108b786f23da7 Mon Sep 17 00:00:00 2001 From: Zhou Tuo <45249333+FromCSUZhou@users.noreply.github.com> Date: Wed, 7 Feb 2024 02:49:47 +0000 Subject: [PATCH 1/3] add email_login tool and add email summarization scenario example --- examples/email_summary.py | 25 ++++++++++++++ metagpt/tools/libs/__init__.py | 10 +++++- metagpt/tools/libs/email_login.py | 57 +++++++++++++++++++++++++++++++ metagpt/tools/tool_type.py | 4 +++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 examples/email_summary.py create mode 100644 metagpt/tools/libs/email_login.py diff --git a/examples/email_summary.py b/examples/email_summary.py new file mode 100644 index 000000000..39c6df1c1 --- /dev/null +++ b/examples/email_summary.py @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +""" +@Date : 2024/02/07 +@Author : Tuo Zhou +@File : email_summary.py +""" + +from metagpt.roles.ci.code_interpreter import CodeInterpreter + + +async def main(): + # prompt_response = """I will give you your Outlook email account(englishgpt@outlook.com) and password(the outlook_email_password item in the environment variable). You need to find the latest email in my inbox with the sender's suffix @qq.com and reply to him "Thank you! I have received your email~""""" + prompt_summary = """I will give you your Outlook email account(englishgpt@outlook.com) and password(outlook_email_password item in the environment variable). + Firstly, Please help me present the latest 5 senders and full letter contents. + Then, summarize each of the 5 emails into one sentence with Chinese(you can do this by yourself, don't need import other models to do this) and output them in a markdown format.""" + # ci_response = CodeInterpreter(goal=prompt_response, use_tools=True) + ci_summary = CodeInterpreter(goal=prompt_summary, use_tools=True) + + await ci_summary.run(prompt_summary) + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/metagpt/tools/libs/__init__.py b/metagpt/tools/libs/__init__.py index c9767c1e5..91596fd3d 100644 --- a/metagpt/tools/libs/__init__.py +++ b/metagpt/tools/libs/__init__.py @@ -10,6 +10,14 @@ from metagpt.tools.libs import ( sd_engine, gpt_v_generator, web_scraping, + email_login, ) -_ = data_preprocess, feature_engineering, sd_engine, gpt_v_generator, web_scraping # Avoid pre-commit error +_ = ( + data_preprocess, + feature_engineering, + sd_engine, + gpt_v_generator, + web_scraping, + email_login, +) # Avoid pre-commit error diff --git a/metagpt/tools/libs/email_login.py b/metagpt/tools/libs/email_login.py new file mode 100644 index 000000000..946e294eb --- /dev/null +++ b/metagpt/tools/libs/email_login.py @@ -0,0 +1,57 @@ +from imap_tools import MailBox + +from metagpt.tools.tool_registry import register_tool +from metagpt.tools.tool_type import ToolType + + +@register_tool(tool_type=ToolType.EMAIL_LOGIN.type_name) +def email_login_imap(email_address, email_password): + """ + Use imap_tools package to log in to your email (the email that supports IMAP protocol) to verify and return the account object. + + Args: + email_address (str): Email address that needs to be logged in and linked. + email_password (str): Password for the email address that needs to be logged in and linked. + + Returns: + object: The imap_tools's MailBox object returned after successfully connecting to the mailbox through imap_tools, including various information about this account (email, etc.), or None if login fails. + """ + + # Define a dictionary mapping email domains to their IMAP server addresses + imap_servers = { + "outlook.com": "imap-mail.outlook.com", # Outlook + "163.com": "imap.163.com", # 163 Mail + "qq.com": "imap.qq.com", # QQ Mail + "gmail.com": "imap.gmail.com", # Gmail + "yahoo.com": "imap.mail.yahoo.com", # Yahoo Mail + "icloud.com": "imap.mail.me.com", # iCloud Mail + "hotmail.com": "imap-mail.outlook.com", # Hotmail (同 Outlook) + "live.com": "imap-mail.outlook.com", # Live (同 Outlook) + "sina.com": "imap.sina.com", # Sina Mail + "sohu.com": "imap.sohu.com", # Sohu Mail + "yahoo.co.jp": "imap.mail.yahoo.co.jp", # Yahoo Mail Japan + "yandex.com": "imap.yandex.com", # Yandex Mail + "mail.ru": "imap.mail.ru", # Mail.ru + "aol.com": "imap.aol.com", # AOL Mail + "gmx.com": "imap.gmx.com", # GMX Mail + "zoho.com": "imap.zoho.com", # Zoho Mail + } + + # Extract the domain from the email address + domain = email_address.split("@")[-1] + + # Determine the correct IMAP server + imap_server = imap_servers.get(domain) + + if not imap_server: + print(f"IMAP server for {domain} not found.") + return None + + # Attempt to log in to the email account + try: + mailbox = MailBox(imap_server).login(email_address, email_password) + print("Login successful") + return mailbox + except Exception as e: + print(f"Login failed: {e}") + return None diff --git a/metagpt/tools/tool_type.py b/metagpt/tools/tool_type.py index 6fa971c56..5a0c66a03 100644 --- a/metagpt/tools/tool_type.py +++ b/metagpt/tools/tool_type.py @@ -17,6 +17,10 @@ class ToolType(Enum): desc="Only for changing value inplace.", usage_prompt=DATA_PREPROCESS_PROMPT, ) + EMAIL_LOGIN = ToolTypeDef( + name="email_login", + desc="For logging to an email.", + ) FEATURE_ENGINEERING = ToolTypeDef( name="feature_engineering", desc="Only for creating new columns for input data.", From 4a991b4cd4eb0bdfb415b8ffb8bb02c3d33f10e2 Mon Sep 17 00:00:00 2001 From: Zhou Tuo <45249333+FromCSUZhou@users.noreply.github.com> Date: Wed, 7 Feb 2024 07:59:54 +0000 Subject: [PATCH 2/3] add email_login unit test --- examples/email_summary.py | 11 +++-- metagpt/tools/libs/email_login.py | 45 +++++++++--------- tests/metagpt/tools/libs/test_email_login.py | 50 ++++++++++++++++++++ 3 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 tests/metagpt/tools/libs/test_email_login.py diff --git a/examples/email_summary.py b/examples/email_summary.py index 39c6df1c1..0862991da 100644 --- a/examples/email_summary.py +++ b/examples/email_summary.py @@ -9,14 +9,15 @@ from metagpt.roles.ci.code_interpreter import CodeInterpreter async def main(): - # prompt_response = """I will give you your Outlook email account(englishgpt@outlook.com) and password(the outlook_email_password item in the environment variable). You need to find the latest email in my inbox with the sender's suffix @qq.com and reply to him "Thank you! I have received your email~""""" - prompt_summary = """I will give you your Outlook email account(englishgpt@outlook.com) and password(outlook_email_password item in the environment variable). + # For email response prompt + # prompt = """I will give you your Outlook email account(englishgpt@outlook.com) and password(the outlook_email_password item in the environment variable). You need to find the latest email in my inbox with the sender's suffix @qq.com and reply to him "Thank you! I have received your email~""""" + prompt = """I will give you your Outlook email account(englishgpt@outlook.com) and password(outlook_email_password item in the environment variable). Firstly, Please help me present the latest 5 senders and full letter contents. Then, summarize each of the 5 emails into one sentence with Chinese(you can do this by yourself, don't need import other models to do this) and output them in a markdown format.""" - # ci_response = CodeInterpreter(goal=prompt_response, use_tools=True) - ci_summary = CodeInterpreter(goal=prompt_summary, use_tools=True) - await ci_summary.run(prompt_summary) + ci = CodeInterpreter(use_tools=True) + + await ci.run(prompt) if __name__ == "__main__": diff --git a/metagpt/tools/libs/email_login.py b/metagpt/tools/libs/email_login.py index 946e294eb..77772e15a 100644 --- a/metagpt/tools/libs/email_login.py +++ b/metagpt/tools/libs/email_login.py @@ -1,8 +1,29 @@ from imap_tools import MailBox +from metagpt.logs import logger from metagpt.tools.tool_registry import register_tool from metagpt.tools.tool_type import ToolType +# Define a dictionary mapping email domains to their IMAP server addresses +IMAP_SERVERS = { + "outlook.com": "imap-mail.outlook.com", # Outlook + "163.com": "imap.163.com", # 163 Mail + "qq.com": "imap.qq.com", # QQ Mail + "gmail.com": "imap.gmail.com", # Gmail + "yahoo.com": "imap.mail.yahoo.com", # Yahoo Mail + "icloud.com": "imap.mail.me.com", # iCloud Mail + "hotmail.com": "imap-mail.outlook.com", # Hotmail (同 Outlook) + "live.com": "imap-mail.outlook.com", # Live (同 Outlook) + "sina.com": "imap.sina.com", # Sina Mail + "sohu.com": "imap.sohu.com", # Sohu Mail + "yahoo.co.jp": "imap.mail.yahoo.co.jp", # Yahoo Mail Japan + "yandex.com": "imap.yandex.com", # Yandex Mail + "mail.ru": "imap.mail.ru", # Mail.ru + "aol.com": "imap.aol.com", # AOL Mail + "gmx.com": "imap.gmx.com", # GMX Mail + "zoho.com": "imap.zoho.com", # Zoho Mail +} + @register_tool(tool_type=ToolType.EMAIL_LOGIN.type_name) def email_login_imap(email_address, email_password): @@ -17,34 +38,14 @@ def email_login_imap(email_address, email_password): object: The imap_tools's MailBox object returned after successfully connecting to the mailbox through imap_tools, including various information about this account (email, etc.), or None if login fails. """ - # Define a dictionary mapping email domains to their IMAP server addresses - imap_servers = { - "outlook.com": "imap-mail.outlook.com", # Outlook - "163.com": "imap.163.com", # 163 Mail - "qq.com": "imap.qq.com", # QQ Mail - "gmail.com": "imap.gmail.com", # Gmail - "yahoo.com": "imap.mail.yahoo.com", # Yahoo Mail - "icloud.com": "imap.mail.me.com", # iCloud Mail - "hotmail.com": "imap-mail.outlook.com", # Hotmail (同 Outlook) - "live.com": "imap-mail.outlook.com", # Live (同 Outlook) - "sina.com": "imap.sina.com", # Sina Mail - "sohu.com": "imap.sohu.com", # Sohu Mail - "yahoo.co.jp": "imap.mail.yahoo.co.jp", # Yahoo Mail Japan - "yandex.com": "imap.yandex.com", # Yandex Mail - "mail.ru": "imap.mail.ru", # Mail.ru - "aol.com": "imap.aol.com", # AOL Mail - "gmx.com": "imap.gmx.com", # GMX Mail - "zoho.com": "imap.zoho.com", # Zoho Mail - } - # Extract the domain from the email address domain = email_address.split("@")[-1] # Determine the correct IMAP server - imap_server = imap_servers.get(domain) + imap_server = IMAP_SERVERS.get(domain) if not imap_server: - print(f"IMAP server for {domain} not found.") + logger.error(f"IMAP server for {domain} not found.") return None # Attempt to log in to the email account diff --git a/tests/metagpt/tools/libs/test_email_login.py b/tests/metagpt/tools/libs/test_email_login.py new file mode 100644 index 000000000..fd8d41506 --- /dev/null +++ b/tests/metagpt/tools/libs/test_email_login.py @@ -0,0 +1,50 @@ +import os +from unittest.mock import Mock, patch + +import pytest + +from metagpt.tools.libs.email_login import email_login_imap + +# Configuration for the test IMAP servers +TEST_IMAP_SERVERS = {"outlook.com": "imap-mail.outlook.com"} + +# Setup correct and incorrect email information +correct_email_address = "englishgpt@outlook.com" +correct_email_password = os.environ.get("outlook_email_password") +incorrect_email_address = "test@unknown.com" +incorrect_email_password = "incorrect_password" + + +@pytest.fixture +def imap_server_setup(): + # Use patch to mock the behavior of MailBox from the correct module path + with patch("metagpt.tools.libs.email_login.MailBox") as mock_mailbox: + # Setup for successful login + mock_mail_instance = Mock() + mock_mail_instance.login.return_value = mock_mail_instance + mock_mailbox.return_value = mock_mail_instance + yield mock_mail_instance + + +def test_email_login_imap_success(imap_server_setup): + # Mock successful login + mailbox = email_login_imap(correct_email_address, correct_email_password) + assert mailbox is not None + # Correctly assert that the login method of the MailBox mock was called with the correct arguments + imap_server_setup.login.assert_called_with(correct_email_address, correct_email_password) + + +def test_email_login_imap_failure_due_to_incorrect_server(imap_server_setup): + # Attempt to login with an incorrect server + mailbox = email_login_imap(incorrect_email_address, incorrect_email_password) + assert mailbox is None + + +def test_email_login_imap_failure_due_to_wrong_credentials(imap_server_setup): + # Configure mock to throw an exception to simulate login failure due to incorrect credentials + imap_server_setup.login.side_effect = Exception("Login failed") + # Attempt to login which should simulate a failure + mailbox = email_login_imap(correct_email_address, incorrect_email_password) + assert mailbox is None + # Verify that the login method was called with the expected arguments + imap_server_setup.login.assert_called_with(correct_email_address, incorrect_email_password) From 3369c9e53671313d73ae57a17c010e3de786fb41 Mon Sep 17 00:00:00 2001 From: Zhou Tuo <45249333+FromCSUZhou@users.noreply.github.com> Date: Wed, 7 Feb 2024 08:59:32 +0000 Subject: [PATCH 3/3] modify by comment --- examples/email_summary.py | 9 +++++---- metagpt/tools/libs/email_login.py | 4 ++-- tests/metagpt/tools/libs/test_email_login.py | 16 +++++++--------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/examples/email_summary.py b/examples/email_summary.py index 0862991da..dd8dd8c8e 100644 --- a/examples/email_summary.py +++ b/examples/email_summary.py @@ -10,10 +10,11 @@ from metagpt.roles.ci.code_interpreter import CodeInterpreter async def main(): # For email response prompt - # prompt = """I will give you your Outlook email account(englishgpt@outlook.com) and password(the outlook_email_password item in the environment variable). You need to find the latest email in my inbox with the sender's suffix @qq.com and reply to him "Thank you! I have received your email~""""" - prompt = """I will give you your Outlook email account(englishgpt@outlook.com) and password(outlook_email_password item in the environment variable). - Firstly, Please help me present the latest 5 senders and full letter contents. - Then, summarize each of the 5 emails into one sentence with Chinese(you can do this by yourself, don't need import other models to do this) and output them in a markdown format.""" + email_account = "your_email_account" + # prompt = f"""I will give you your Outlook email account({email_account}) and password(email_password item in the environment variable). You need to find the latest email in my inbox with the sender's suffix @qq.com and reply to him "Thank you! I have received your email~""""" + prompt = f"""I will give you your Outlook email account({email_account}) and password(email_password item in the environment variable). + Firstly, Please help me fetch the latest 5 senders and full letter contents. + Then, summarize each of the 5 emails into one sentence(you can do this by yourself, no need import other models to do this) and output them in a markdown format.""" ci = CodeInterpreter(use_tools=True) diff --git a/metagpt/tools/libs/email_login.py b/metagpt/tools/libs/email_login.py index 77772e15a..8fd77274c 100644 --- a/metagpt/tools/libs/email_login.py +++ b/metagpt/tools/libs/email_login.py @@ -51,8 +51,8 @@ def email_login_imap(email_address, email_password): # Attempt to log in to the email account try: mailbox = MailBox(imap_server).login(email_address, email_password) - print("Login successful") + logger.info("Login successful") return mailbox except Exception as e: - print(f"Login failed: {e}") + logger.error(f"Login failed: {e}") return None diff --git a/tests/metagpt/tools/libs/test_email_login.py b/tests/metagpt/tools/libs/test_email_login.py index fd8d41506..c18d15c7d 100644 --- a/tests/metagpt/tools/libs/test_email_login.py +++ b/tests/metagpt/tools/libs/test_email_login.py @@ -1,5 +1,4 @@ import os -from unittest.mock import Mock, patch import pytest @@ -16,14 +15,13 @@ incorrect_email_password = "incorrect_password" @pytest.fixture -def imap_server_setup(): - # Use patch to mock the behavior of MailBox from the correct module path - with patch("metagpt.tools.libs.email_login.MailBox") as mock_mailbox: - # Setup for successful login - mock_mail_instance = Mock() - mock_mail_instance.login.return_value = mock_mail_instance - mock_mailbox.return_value = mock_mail_instance - yield mock_mail_instance +def imap_server_setup(mocker): + # Use the mocker fixture to mock the MailBox class + mock_mailbox = mocker.patch("metagpt.tools.libs.email_login.MailBox") + mock_mail_instance = mocker.Mock() + mock_mail_instance.login.return_value = mock_mail_instance + mock_mailbox.return_value = mock_mail_instance + return mock_mail_instance def test_email_login_imap_success(imap_server_setup):