From 01c6e7b3bbad05d613c50cfa8487c4ad22c3502e Mon Sep 17 00:00:00 2001 From: Stan Girard Date: Fri, 10 May 2024 16:56:51 +0200 Subject: [PATCH] feat(email): Add email sender tool and update image generator tool (#2579) This pull request adds a new email sender tool and updates the image generator tool. --- backend/modules/assistant/ito/ito.py | 6 +- .../modules/brain/integrations/GPT4/Brain.py | 24 ++++-- backend/modules/tools/__init__.py | 2 +- backend/modules/tools/email_sender.py | 81 +++++++++++++++++++ backend/modules/tools/image_generator.py | 3 +- backend/modules/tools/web_search.py | 5 ++ 6 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 backend/modules/tools/email_sender.py diff --git a/backend/modules/assistant/ito/ito.py b/backend/modules/assistant/ito/ito.py index 0a92fbb5f..dc0081781 100644 --- a/backend/modules/assistant/ito/ito.py +++ b/backend/modules/assistant/ito/ito.py @@ -9,13 +9,13 @@ from typing import List, Optional from fastapi import UploadFile from logger import get_logger -from modules.user.service.user_usage import UserUsage from modules.assistant.dto.inputs import InputAssistant from modules.assistant.ito.utils.pdf_generator import PDFGenerator, PDFModel from modules.chat.controller.chat.utils import update_user_usage from modules.contact_support.controller.settings import ContactsSettings from modules.upload.controller.upload_routes import upload_file from modules.user.entity.user_identity import UserIdentity +from modules.user.service.user_usage import UserUsage from packages.emails.send_email import send_email from pydantic import BaseModel from unidecode import unidecode @@ -111,8 +111,8 @@ class ITO(BaseModel): """ params = { - "from": mail_from, - "to": mail_to, + "sender": mail_from, + "to": [mail_to], "subject": "Quivr Ingestion Processed", "reply_to": "no-reply@quivr.app", "html": body, diff --git a/backend/modules/brain/integrations/GPT4/Brain.py b/backend/modules/brain/integrations/GPT4/Brain.py index d57b8f55b..0d46190c6 100644 --- a/backend/modules/brain/integrations/GPT4/Brain.py +++ b/backend/modules/brain/integrations/GPT4/Brain.py @@ -1,6 +1,6 @@ import json import operator -from typing import Annotated, AsyncIterable, List, Sequence, TypedDict +from typing import Annotated, AsyncIterable, List, Optional, Sequence, TypedDict from uuid import UUID from langchain.tools import BaseTool @@ -15,7 +15,12 @@ from modules.brain.knowledge_brain_qa import KnowledgeBrainQA from modules.chat.dto.chats import ChatQuestion from modules.chat.dto.outputs import GetChatHistoryOutput from modules.chat.service.chat_service import ChatService -from modules.tools import ImageGeneratorTool, URLReaderTool, WebSearchTool +from modules.tools import ( + EmailSenderTool, + ImageGeneratorTool, + URLReaderTool, + WebSearchTool, +) class AgentState(TypedDict): @@ -37,8 +42,8 @@ class GPT4Brain(KnowledgeBrainQA): KnowledgeBrainQA (_type_): A brain that store the knowledge internaly """ - tools: List[BaseTool] = [WebSearchTool(), ImageGeneratorTool(), URLReaderTool()] - tool_executor: ToolExecutor = ToolExecutor(tools) + tools: Optional[List[BaseTool]] = None + tool_executor: Optional[ToolExecutor] = None model_function: ChatOpenAI = None def __init__( @@ -48,6 +53,13 @@ class GPT4Brain(KnowledgeBrainQA): super().__init__( **kwargs, ) + self.tools = [ + WebSearchTool(), + ImageGeneratorTool(), + URLReaderTool(), + EmailSenderTool(user_email=self.user_email), + ] + self.tool_executor = ToolExecutor(tools=self.tools) def calculate_pricing(self): return 3 @@ -164,7 +176,7 @@ class GPT4Brain(KnowledgeBrainQA): transformed_history, streamed_chat_history = ( self.initialize_streamed_chat_history(chat_id, question) ) - filtered_history = self.filter_history(transformed_history, 20, 2000) + filtered_history = self.filter_history(transformed_history, 40, 2000) response_tokens = [] config = {"metadata": {"conversation_id": str(chat_id)}} @@ -232,7 +244,7 @@ class GPT4Brain(KnowledgeBrainQA): transformed_history, _ = self.initialize_streamed_chat_history( chat_id, question ) - filtered_history = self.filter_history(transformed_history, 20, 2000) + filtered_history = self.filter_history(transformed_history, 40, 2000) config = {"metadata": {"conversation_id": str(chat_id)}} prompt = ChatPromptTemplate.from_messages( diff --git a/backend/modules/tools/__init__.py b/backend/modules/tools/__init__.py index 476e2f20f..753df27d5 100644 --- a/backend/modules/tools/__init__.py +++ b/backend/modules/tools/__init__.py @@ -1,4 +1,4 @@ from .image_generator import ImageGeneratorTool from .web_search import WebSearchTool from .url_reader import URLReaderTool - +from .email_sender import EmailSenderTool \ No newline at end of file diff --git a/backend/modules/tools/email_sender.py b/backend/modules/tools/email_sender.py new file mode 100644 index 000000000..2806702a0 --- /dev/null +++ b/backend/modules/tools/email_sender.py @@ -0,0 +1,81 @@ +# Extract and combine content recursively +from typing import Dict, Optional, Type + +from langchain.callbacks.manager import ( + AsyncCallbackManagerForToolRun, + CallbackManagerForToolRun, +) +from langchain.pydantic_v1 import BaseModel as BaseModelV1 +from langchain.pydantic_v1 import Field as FieldV1 +from langchain_community.document_loaders import PlaywrightURLLoader +from langchain_core.tools import BaseTool +from logger import get_logger +from models import BrainSettings +from modules.contact_support.controller.settings import ContactsSettings +from packages.emails.send_email import send_email +from pydantic import BaseModel + +logger = get_logger(__name__) + + +class EmailInput(BaseModelV1): + text: str = FieldV1( + ..., + title="text", + description="text to send in HTML email format. Use pretty formating, use bold, italic, next line, etc...", + ) + + +class EmailSenderTool(BaseTool): + user_email: str + name = "email-sender" + description = "useful for when you need to send an email." + args_schema: Type[BaseModel] = EmailInput + brain_settings: BrainSettings = BrainSettings() + contact_settings: ContactsSettings = ContactsSettings() + + def _run( + self, text: str, run_manager: Optional[CallbackManagerForToolRun] = None + ) -> Dict: + + html_body = f""" +
+ Quivr Logo +
+
+ """ + html_body += f""" + {text} + """ + logger.debug(f"Email body: {html_body}") + logger.debug(f"Email to: {self.user_email}") + logger.debug(f"Email from: {self.contact_settings.resend_contact_sales_from}") + try: + r = send_email( + { + "sender": self.contact_settings.resend_contact_sales_from, + "to": self.user_email, + "reply_to": "no-reply@quivr.app", + "subject": "Email from your assistant", + "html": html_body, + } + ) + logger.info("Resend response", r) + except Exception as e: + logger.error(f"Error sending email: {e}") + return {"content": "Error sending email because of error: " + str(e)} + + return {"content": "Email sent"} + + async def _arun( + self, url: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None + ) -> Dict: + """Run the tool asynchronously.""" + loader = PlaywrightURLLoader(urls=[url], remove_selectors=["header", "footer"]) + data = loader.load() + + extracted_content = "" + for page in data: + extracted_content += page.page_content + + return {"content": extracted_content} diff --git a/backend/modules/tools/image_generator.py b/backend/modules/tools/image_generator.py index debbc1b31..e3b009c07 100644 --- a/backend/modules/tools/image_generator.py +++ b/backend/modules/tools/image_generator.py @@ -57,5 +57,6 @@ class ImageGeneratorTool(BaseTool): n=1, ) image_url = response.data[0].url + revised_prompt = response.data[0].revised_prompt # Make the url a markdown image - return f"![Generated Image]({image_url})" + return f"{revised_prompt} \n ![Generated Image]({image_url}) " diff --git a/backend/modules/tools/web_search.py b/backend/modules/tools/web_search.py index 1b7f7e412..7f7349016 100644 --- a/backend/modules/tools/web_search.py +++ b/backend/modules/tools/web_search.py @@ -78,3 +78,8 @@ class WebSearchTool(BaseTool): def _format_result(self, title: str, description: str, url: str) -> str: return f"**{title}**\n{description}\n{url}" + + +if __name__ == "__main__": + tool = WebSearchTool() + print(tool.run("python"))