Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gmail tools improvements and updates #2092

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions cookbook/tools/gmail_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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="[email protected]",
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)
90 changes: 86 additions & 4 deletions libs/agno/agno/tools/gmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -146,6 +150,7 @@ def __init__(
get_starred_emails,
get_emails_by_context,
get_emails_by_date,
get_emails_by_thread,
search_emails,
]

Expand All @@ -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)

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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", "<br>")
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:
"""
Expand Down Expand Up @@ -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(
{
Expand Down
Loading