Skip to content

Commit

Permalink
added headers but still doesn't work
Browse files Browse the repository at this point in the history
Rate limit · GitHub

Access has been restricted

You have triggered a rate limit.

Please wait a few minutes before you try again;
in some cases this may take up to an hour.

davidenders11 committed Nov 13, 2023
1 parent f97df4e commit 1f90872
Showing 5 changed files with 129 additions and 66 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -34,6 +34,10 @@ Another motivation for this app is to practice and refine the incorporation of A
- Fine-tune the AI model with all past sent emails (in the last year or so) from the user to create a more accurate tone/style
- Error check for token.json missing and remove the file and try again if so
- Answer [this question](https://stackoverflow.com/questions/66895957/google-api-with-python-error-when-trying-to-refresh-token) if you fixed the problem
- new functions:
- get_all_threads(query) which takes a from:email_address query and returns a string containing all threads
- might need to implement tiktoken for this so we don't run over openai limit
- modify openai prompt wording to be neutral for reply/new

## Documentation

8 changes: 6 additions & 2 deletions gmailtest.py
Original file line number Diff line number Diff line change
@@ -15,8 +15,12 @@
query = f"from:andrew@xostrucks.com"

# last_thread_id = gmail.get_most_recent_message_ids(query)["threadId"]
id = gmail.get_most_recent_message_ids(query)["id"]
payload = gmail.get_message_headers(id)
ids = gmail.get_most_recent_message_ids(query)
id = ids["id"]
threadId = ids["threadId"]
# print(id, threadId)
references, reply, subject = gmail.get_message_headers(id)
print(references, reply, subject)

# Find the "References" and "In-Reply-To" headers and extract their "value" fields

105 changes: 76 additions & 29 deletions gmailwrapper.py
Original file line number Diff line number Diff line change
@@ -23,7 +23,6 @@


class Gmail:

def __init__(self, logger):
self.logger = logger
self.auth()
@@ -38,7 +37,9 @@ def auth(self):
try:
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
except Exception as e:
self.logger.error(f"Error reading credentials from token.json: {e}\nRemoving token.json and re-authenticating")
self.logger.error(
f"Error reading credentials from token.json: {e}\nRemoving token.json and re-authenticating"
)
os.remove("token.json")
creds = None
pass
@@ -49,19 +50,28 @@ def auth(self):
self.logger.info("Credentials expired, sending refresh request")
creds.refresh(Request())
else:
self.logger.info("Credentials not found, sending auth request to local server")
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
self.logger.info(
"Credentials not found, sending auth request to local server"
)
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open("token.json", "w") as token:
self.logger.info("Saving credentials to token.json")
token.write(creds.to_json())
self.service = build("gmail", "v1", credentials=creds)
self.logger.info("Completed Gmail authentication flow")

def get_message_headers(self, message_id):
'''Get a message and return its References, In-Reply-To, and Subject headers'''
payload = self.service.users().messages().get(userId="me", id=message_id).execute()["payload"]
"""Get a message and return its References, In-Reply-To, and Subject headers"""
payload = (
self.service.users()
.messages()
.get(userId="me", id=message_id)
.execute()["payload"]
)
references_value = None
in_reply_to_value = None
subject = None
@@ -74,11 +84,11 @@ def get_message_headers(self, message_id):
elif header["name"] == "Subject":
subject = header["value"]

print("References Value:", references_value)
print("In-Reply-To Value:", in_reply_to_value)
print("Subject:", subject)
return
# print("References Value:", references_value)
# print("In-Reply-To Value:", in_reply_to_value)
# print("Subject:", subject)
return references_value, in_reply_to_value, subject

def get_most_recent_message_ids(self, query):
"""
Get the most recent message matching the query
@@ -105,29 +115,36 @@ def get_thread(self, thread_id):
"""
Get a thread and print each message including its sender and body
"""
response = self.service.users().threads().get(userId="me", id=thread_id).execute()
response = (
self.service.users().threads().get(userId="me", id=thread_id).execute()
)
messages = response["messages"]
thread = ""
for message in messages:

# specify sender in final string
for header in message["payload"]["headers"]:
if header["name"] == "From":
thread += f"From: {header['value']}"

# get messages content and add to string
for part in message["payload"]["parts"]:
if part["mimeType"] == "text/plain":
content = urlsafe_b64decode(part["body"]["data"]).decode()
# remove any lines that start with ">" as these are redundant
content = "\n".join(
[line for line in content.split("\n") if not line.startswith(">")]
[
line
for line in content.split("\n")
if not line.startswith(">")
]
)
thread += f"Body:\n{content}"
elif part["mimeType"] == "multipart/alternative":
for subpart in part["parts"]:
if subpart["mimeType"] == "text/plain":
content = urlsafe_b64decode(subpart["body"]["data"]).decode()
content = urlsafe_b64decode(
subpart["body"]["data"]
).decode()
# remove any lines that start with ">"
content = "\n".join(
[
@@ -140,14 +157,13 @@ def get_thread(self, thread_id):
break

return thread


# If I want draft replies to work, the subject needs to be the same as the original thread, and the thread id needs to be the same as the original thread
# And I need to also fill in the "In-Reply-To" header and the "References" header to be "Message-ID" of the most recent message in the thread and the "References"
# header to be the "References" header of the most recent plus the "Message-ID" of the most recent message in the thread
# I think these are set using the same message["To"] syntax as below, so like message["In-Reply-To"] = message["Message-ID"] of the most recent message in the thread
# headers and stuff docs here: https://datatracker.ietf.org/doc/html/rfc2822#section-2.2
def draft(self, content, other, subject, thread_id=None):

# If I want draft replies to work, the subject needs to be the same as the original thread, and the thread id needs to be the same as the original thread
# And I need to also fill in the "In-Reply-To" header and the "References" header to be "Message-ID" of the most recent message in the thread and the "References"
# header to be the "References" header of the most recent plus the "Message-ID" of the most recent message in the thread
# I think these are set using the same message["To"] syntax as below, so like message["In-Reply-To"] = message["Message-ID"] of the most recent message in the thread
# headers and stuff docs here: https://datatracker.ietf.org/doc/html/rfc2822#section-2.2
def new_draft(self, content, other, subject):
"""Create and insert a draft email.
Print the returned draft's message and id.
Returns: Draft object, including draft id and message meta data.
@@ -159,15 +175,46 @@ def draft(self, content, other, subject, thread_id=None):

message["To"] = other
message["From"] = self.me
if subject: message["Subject"] = subject
if subject:
message["Subject"] = subject

# encoded message
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
body = {"message": {"raw": encoded_message}}
# add thread id if replying
if thread_id:
body["threadId"] = str(thread_id)
draft = (
self.service.users().drafts().create(userId="me", body=body).execute()
)

except HttpError as error:
print(f"An error occurred: {error}")
draft = None

return draft

def reply_draft(
self, content, other, subject, thread_id, references_value, in_reply_to_value
):
"""Create and insert a draft email.
Print the returned draft's message and id.
Returns: Draft object, including draft id and message meta data.
"""
try:
message = EmailMessage()

message.set_content(content)

message["To"] = other
message["From"] = self.me
message["Subject"] = subject
message["References"] = references_value
message["In-Reply-To"] = in_reply_to_value

print(f"\n\n\nmessage before encoding: {message}\n\n\n")

# encoded message
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
body = {"message": {"raw": encoded_message}}
body["threadId"] = str(thread_id)

draft = (
self.service.users().drafts().create(userId="me", body=body).execute()
74 changes: 41 additions & 33 deletions main.py
Original file line number Diff line number Diff line change
@@ -6,19 +6,30 @@
from googleapiclient.discovery import build
import pandas as pd

parser = argparse.ArgumentParser(
description="Draft emails to specified recipients.")
parser = argparse.ArgumentParser(description="Draft emails to specified recipients.")
# stores a bool with the value of True if the flag is present, and False if it is not
parser.add_argument("recipients", type=str, help="The email address of a single recipient or an Excel file with an 'Email' column of recipients.")
parser.add_argument("--reply", action="store_true", help="Reply to the most recent email thread from the specified recipient.")
parser.add_argument("--verbose", action="store_true", help="Prints logging information to the console.")
parser.add_argument(
"recipients",
type=str,
help="The email address of a single recipient or an Excel file with an 'Email' column of recipients.",
)
parser.add_argument(
"--reply",
action="store_true",
help="Reply to the most recent email thread from the specified recipient.",
)
parser.add_argument(
"--verbose", action="store_true", help="Prints logging information to the console."
)
args = parser.parse_args()

logging.basicConfig(
level = logging.INFO if args.verbose else logging.WARNING,
format = '%(levelname)s:%(asctime)s:%(message)s')
level=logging.INFO if args.verbose else logging.WARNING,
format="%(levelname)s:%(asctime)s:%(message)s",
)
logger = logging.getLogger(__name__)


def main():
# initialize Gmail and OpenAI classes
openai = OpenAI(logger)
@@ -28,52 +39,49 @@ def main():
if "xlsx" in args.recipients:
workbook = pd.read_excel(args.recipients)
workbook.head()
args.recipients = [el for el in workbook['Email'] if isinstance(el, str)]
else:
args.recipients = [el for el in workbook["Email"] if isinstance(el, str)]
else:
args.recipients = [args.recipients]

# get user input
update = input(f"What new information would you like to tell your recipients?\n")
if not args.reply: subject = input(f"\nWhat would you like the subject of the email to be?\n")
if not args.reply:
new_subject = input(f"\nWhat would you like the subject of the email to be?\n")

# loop through recipients and create a draft for each
for address in args.recipients:
# get the most recent email thread from the specified recipient
query = f"from:{address}"
last_thread_id = gmail.get_most_recent_message_ids(query)["threadId"]
ids = gmail.get_most_recent_message_ids(query)
last_message_id = ids["id"]
last_thread_id = ids["threadId"]
thread = gmail.get_thread(last_thread_id)
logger.info("Retrieved last thread with target recipients")

# generate the draft and create it on gmail
content = openai.write_draft(thread, update, gmail.me, address)
logger.info("Draft has been generated, OpenAI call complete")
if args.reply:
gmail.draft(content, address, thread_id=last_thread_id)
(
references_value,
in_reply_to_value,
thread_subject,
) = gmail.get_message_headers(last_message_id)
gmail.reply_draft(
content,
address,
thread_subject,
last_thread_id,
references_value,
in_reply_to_value,
)
else:
gmail.draft(content, address, subject=subject)
gmail.new_draft(content, address, new_subject)
logger.info(
f"\nYour draft has been created!\nRecipient: {address}"+
"\nSubject: {subject}\nContent: {content}\n"
f"\nYour draft has been created!\nRecipient: {address}"
+ "\nSubject: {subject}\nContent: {content}\n"
)

# if batch, for loop through dict
# if reply, get most recent email thread from recipient, read and generate reply
# if new, read all past threads from recipient, generate new email
# if single recipient
# if reply, get most recent email thread from recipient, read and generate reply
# if new, generate new email

# new functions:
# parse args so that args recipient holds a list of recipients (single element if single recipient)
# get_all_threads(query) which takes a from:email_address query and returns a string containing all threads
# might need to implement tiktoken for this so we don't run over openai limit
# modify openai prompt wording to be neutral for reply/new
# modify gmail_create_draft to take a reply? boolean






if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions openaiwrapper.py
Original file line number Diff line number Diff line change
@@ -17,8 +17,8 @@ def auth(self):
def write_draft(self, thread, update, myself, other):
user_content = f"Below is a chain of messages between myself, {myself}, and {other}. I would like to draft a response to {other} based on this interaction and the following update. Please incorporate this new information and formulate a response to {other} that I can send. Only write the body of the response, do not include a subject. Make sure you use the previous thread as context for your response. \n\nPAST MESSAGES: \n###\n{thread}\n###\n\n NEW INFORMATION TO RELAY:\n###\n{update}\n###"
self.logger.info(f'Chat Completion prompt is: "{user_content}"')
# model = "gpt-3.5-turbo" # Use gpt-4 if you have access
model = "gpt-4-1106-preview" # Use gpt-4 if you have access
model = "gpt-3.5-turbo"
# model = "gpt-4-1106-preview" # Use gpt-4 if you have access
self.logger.info(f"Chat Completion model is: {model}")
gpt_response = openai.ChatCompletion.create(
model=model, messages=[{"role": "user", "content": user_content}]

0 comments on commit 1f90872

Please sign in to comment.