diff --git a/cookbook/models/perplexity/tool_use.py b/cookbook/models/perplexity/tool_use.py index e014d2b0cd..59739ca1db 100644 --- a/cookbook/models/perplexity/tool_use.py +++ b/cookbook/models/perplexity/tool_use.py @@ -10,4 +10,4 @@ show_tool_calls=True, markdown=True, ) -agent.print_response("Whats happening in France?", stream=True) +agent.print_response("Whats happening in France today 11 Feb 2025?", stream=True) diff --git a/cookbook/tools/gmail_tools.py b/cookbook/tools/gmail_tools.py index 285e235c52..60c268fcd6 100644 --- a/cookbook/tools/gmail_tools.py +++ b/cookbook/tools/gmail_tools.py @@ -5,7 +5,7 @@ from agno.agent import Agent from agno.models.google import Gemini from agno.tools.gmail import GmailTools - +from datetime import datetime, timedelta agent = Agent( name="Gmail Agent", model=Gemini(id="gemini-2.0-flash-exp"), @@ -21,8 +21,31 @@ debug_mode=True, ) -agent.print_response( - "summarize my last 5 emails with dates and key details, regarding ai agents", - markdown=True, - stream=True, +# agent.print_response( +# "summarize my last 5 emails with dates and key details, regarding ai agents", +# markdown=True, +# stream=True, +# ) + +tool = GmailTools( + credentials_path="storage/credentials.json", +) + + + +# print(tool.get_emails_by_context(context="Security", count=5)) + +thread_id = "194fa616c1f1aa92" +message_id = "CAPe88YDTC4sgdVuiVXR6y7xx8T4tRDi90JacwM9=e7WeSSGkig@mail.gmail.com" +resp = tool.send_email_reply( + thread_id=thread_id, + message_id=message_id, + to="willemcarel@gmail.com", + subject="Re: Security", + body="Hello, I am a security agent. I am here to help you with your security needs.", + cc="", ) + + +# resp = tool.get_latest_emails(count=1) +print(resp) \ No newline at end of file diff --git a/libs/agno/agno/tools/gmail.py b/libs/agno/agno/tools/gmail.py index b442aa52f1..d60260e7ba 100644 --- a/libs/agno/agno/tools/gmail.py +++ b/libs/agno/agno/tools/gmail.py @@ -101,8 +101,10 @@ def __init__( get_starred_emails: bool = True, get_emails_by_context: bool = True, get_emails_by_date: bool = True, + get_emails_by_thread: bool = True, create_draft_email: bool = True, send_email: bool = True, + send_email_reply: bool = True, search_emails: bool = True, creds: Optional[Credentials] = None, credentials_path: Optional[str] = None, @@ -118,9 +120,11 @@ def __init__( get_starred_emails (bool): Enable getting starred emails. Defaults to True. get_emails_by_context (bool): Enable getting emails by context. Defaults to True. get_emails_by_date (bool): Enable getting emails by date. Defaults to True. + get_emails_by_thread (bool): Enable getting emails by thread. Defaults to True. create_draft_email (bool): Enable creating draft emails. Defaults to True. send_email (bool): Enable sending emails. Defaults to True. search_emails (bool): Enable searching emails. Defaults to True. + send_email_reply (bool): Enable sending email replies. Defaults to True. creds (Optional[Credentials]): Pre-existing credentials. Defaults to None. credentials_path (Optional[str]): Path to credentials file. Defaults to None. token_path (Optional[str]): Path to token file. Defaults to None. @@ -146,6 +150,7 @@ def __init__( get_starred_emails, get_emails_by_context, get_emails_by_date, + get_emails_by_thread, search_emails, ] @@ -167,10 +172,14 @@ def __init__( self.register(self.get_emails_by_context) if get_emails_by_date: self.register(self.get_emails_by_date) + if get_emails_by_thread: + self.register(self.get_emails_by_thread) if create_draft_email: self.register(self.create_draft_email) if send_email: self.register(self.send_email) + if send_email_reply: + self.register(self.send_email_reply) if search_emails: self.register(self.search_emails) @@ -287,6 +296,27 @@ def get_unread_emails(self, count: int) -> str: except Exception as error: return f"Unexpected error retrieving unread emails: {type(error).__name__}: {error}" + @authenticate + def get_emails_by_thread(self, thread_id: str) -> str: + """ + Retrieve all emails from a specific thread. + + Args: + thread_id (str): The ID of the email thread. + + Returns: + str: Formatted string containing email thread details. + """ + try: + thread = self.service.users().threads().get(userId="me", id=thread_id).execute() # type: ignore + messages = thread.get("messages", []) + emails = self._get_message_details(messages) + return self._format_emails(emails) + except HttpError as error: + return f"Error retrieving emails from thread {thread_id}: {error}" + except Exception as error: + return f"Unexpected error retrieving emails from thread {thread_id}: {type(error).__name__}: {error}" + @authenticate def get_starred_emails(self, count: int) -> str: """ @@ -397,6 +427,33 @@ def send_email(self, to: str, subject: str, body: str, cc: Optional[str] = None) message = self.service.users().messages().send(userId="me", body=message).execute() # type: ignore return str(message) + @authenticate + def send_email_reply(self, thread_id: str, message_id: str, to: str, subject: str, body: str, cc: Optional[str] = None) -> str: + """ + Respond to an existing email thread. + + Args: + thread_id (str): The ID of the email thread to reply to. + message_id (str): The ID of the email being replied to. + to (str): Comma-separated recipient email addresses. + subject (str): Email subject (prefixed with "Re:" if not already). + body (str): Email body content. + cc (Optional[str]): Comma-separated CC email addresses (optional). + + Returns: + str: Stringified dictionary containing sent email details including id. + """ + self._validate_email_params(to, subject, body) + + # Ensure subject starts with "Re:" for consistency + if not subject.lower().startswith("re:"): + subject = f"Re: {subject}" + + body = body.replace("\n", "
") + message = self._create_message(to.split(","), subject, body, cc.split(",") if cc else None, thread_id, message_id) + message = self.service.users().messages().send(userId="me", body=message).execute() # type: ignore + return str(message) + @authenticate def search_emails(self, query: str, count: int) -> str: """ @@ -435,21 +492,46 @@ def _validate_email_params(self, to: str, subject: str, body: str) -> None: if body is None: raise ValueError("Email body cannot be None") - def _create_message(self, to: List[str], subject: str, body: str, cc: Optional[List[str]] = None) -> dict: - """Create email message""" + # def _create_message(self, to: List[str], subject: str, body: str, cc: Optional[List[str]] = None) -> dict: + # """Create email message""" + # body = body.replace("\\n", "\n") + # message = MIMEText(body, "html") + # message["to"] = ", ".join(to) + # message["from"] = "me" + # message["subject"] = subject + # if cc: + # message["cc"] = ", ".join(cc) + # return {"raw": base64.urlsafe_b64encode(message.as_bytes()).decode()} + + def _create_message(self, to: List[str], subject: str, body: str, cc: Optional[List[str]] = None, thread_id: Optional[str] = None, message_id: Optional[str] = None) -> dict: + body = body.replace("\\n", "\n") message = MIMEText(body, "html") message["to"] = ", ".join(to) message["from"] = "me" message["subject"] = subject + if cc: - message["cc"] = ", ".join(cc) - return {"raw": base64.urlsafe_b64encode(message.as_bytes()).decode()} + message["Cc"] = ", ".join(cc) + + # Add reply headers if this is a response + if thread_id and message_id: + message["In-Reply-To"] = message_id + message["References"] = message_id + + raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode() + email_data = {"raw": raw_message} + + # if thread_id: + email_data["threadId"] = thread_id + return email_data + def _get_message_details(self, messages: List[dict]) -> List[dict]: """Get details for list of messages""" details = [] for msg in messages: + print(msg) msg_data = self.service.users().messages().get(userId="me", id=msg["id"], format="full").execute() # type: ignore details.append( {