diff --git a/automon/integrations/google/gmail/client.py b/automon/integrations/google/gmail/client.py index 77de5f80..b039dadb 100644 --- a/automon/integrations/google/gmail/client.py +++ b/automon/integrations/google/gmail/client.py @@ -32,9 +32,6 @@ class AutomonLabels: 'resume': 'automon/resume', 'analyze': 'automon/analyze', "retry": 'automon/retry', - "test": 'automon/test', - 'error': 'automon/error', - 'drafted': 'automon/drafted', 'relevant': 'automon/relevant', "remote": 'automon/remote', "welcome": 'automon/welcome', @@ -70,14 +67,10 @@ def __init__(self): self.sent = Label(name='SENT', id='SENT') self.unread = Label(name='UNREAD', id='UNREAD') self.trash = Label(name='TRASH', id='TRASH') - self.drafted = Label(name=self.labels.get('drafted'), color=self._color_default) # allow auto reply self.auto_reply_enabled = Label(name=self.labels.get('auto_reply_enabled'), color=self._color_enabled) - # issues encountered - self.error = Label(name=self.labels.get('error'), color=self._color_error) - # retry draft self.retry = Label(name=self.labels.get('retry'), color=self._color_error) @@ -87,9 +80,6 @@ def __init__(self): # remote self.remote = Label(name=self.labels.get('remote'), color=self._color_default) - # test - self.test = Label(name=self.labels.get('test'), color=self._color_enabled) - # need user input self.user_action_required = Label(name=self.labels.get('user_action_required'), color=self._color_error) diff --git a/automon/integrations/google/gmail/tests/test_client_autoreply.py b/automon/integrations/google/gmail/tests/test_client_autoreply.py index d1121def..e3f13ad4 100644 --- a/automon/integrations/google/gmail/tests/test_client_autoreply.py +++ b/automon/integrations/google/gmail/tests/test_client_autoreply.py @@ -5,6 +5,9 @@ from automon.integrations.ollamaWrapper import OllamaClient from automon.integrations.google.gemini import GoogleGeminiClient +DEBUG = False +INFO = False + LoggingClient.logging.getLogger('httpx').setLevel(ERROR) LoggingClient.logging.getLogger('httpcore').setLevel(ERROR) LoggingClient.logging.getLogger('automon.integrations.ollamaWrapper.client').setLevel(DEBUG) @@ -12,11 +15,19 @@ LoggingClient.logging.getLogger('automon.integrations.ollamaWrapper.chat').setLevel(ERROR) LoggingClient.logging.getLogger('automon.integrations.requestsWrapper.client').setLevel(CRITICAL) LoggingClient.logging.getLogger('automon.integrations.google.oauth.config').setLevel(ERROR) -LoggingClient.logging.getLogger('automon.integrations.google.gemini.client').setLevel(ERROR) LoggingClient.logging.getLogger('automon.integrations.google.gemini.config').setLevel(ERROR) +LoggingClient.logging.getLogger('automon.integrations.google.gemini.client').setLevel(ERROR) LoggingClient.logging.getLogger('automon.integrations.google.gmail.client').setLevel(ERROR) LoggingClient.logging.getLogger('opentelemetry.instrumentation.instrumentor').setLevel(ERROR) +if INFO: + LoggingClient.logging.getLogger('automon.integrations.google.gemini.client').setLevel(INFO) + LoggingClient.logging.getLogger('automon.integrations.google.gmail.client').setLevel(INFO) + +if DEBUG: + LoggingClient.logging.getLogger('automon.integrations.google.gemini.client').setLevel(DEBUG) + LoggingClient.logging.getLogger('automon.integrations.google.gmail.client').setLevel(DEBUG) + USE_OLLAMA = False USE_GEMINI = True CHAT_FOREVER = False @@ -176,53 +187,59 @@ def main(): for _thread in email_search.threads: - print('.', end='') + # _thread = gmail.thread_get_automon('195da1cbcaa5573b') _first = _thread.automon_message_first _latest = _thread.automon_message_latest - if (labels.test in _first.automon_labels - ): - _FOUND = True - print('test', end='') - break + print(f"{_thread.id} :: {_first.payload.get_header('subject')} :: {_first.automon_labels}") - if (labels.retry in _first.automon_labels - or labels.retry in _latest.automon_labels - ): + if labels.resume in _first.automon_labels: + + if labels.sent in _latest.automon_labels: + continue + + if [x for x in _thread.messages + if labels.retry in x.automon_labels]: _FOUND = True print('retry', end='') break + if (labels.draft in _latest.automon_labels + and labels.trash not in _latest.automon_labels + ): + continue + + if labels.sent not in _latest.automon_labels: + _sent = False + for _message in _thread.messages: + if labels.sent in _message.automon_labels: + _sent = True + elif (labels.draft not in _message.automon_labels + and labels.trash not in _message.automon_labels + ): + _sent = False + + if not _sent: + _FOUND = True + print('new', end='') + break + continue + if (labels.analyze in _first.automon_labels ): _FOUND = True print('analyze', end='') break - if (labels.auto_reply_enabled in _first.automon_labels - or labels.auto_reply_enabled in _latest.automon_labels - and labels.sent not in _latest.automon_labels + if ((labels.auto_reply_enabled in _first.automon_labels + or labels.auto_reply_enabled in _latest.automon_labels) + and (labels.sent not in _latest.automon_labels) ): _FOUND = True print('auto', end='') break - if (gmail._userId != _first.automon_from_email() - and _first.automon_from_email().lower() == _latest.automon_from_email().lower()): - _FOUND = True - print('new', end='') - break - - if labels.resume in _first.automon_labels: - continue - - if labels.draft in _latest.automon_labels: - continue - - if labels.sent in _latest.automon_labels: - continue - if _FOUND: print(' :)') break @@ -242,9 +259,7 @@ def main(): or labels.auto_reply_enabled in _message.automon_labels ): _RETRY = True - gmail.messages_modify(id=_message.id, - removeLabelIds=[labels.retry, - labels.drafted]) + gmail.messages_modify(id=_message.id) # delete DRAFT if labels.draft in _message.automon_labels: @@ -256,7 +271,7 @@ def main(): email_selected = _thread resume_selected = resume_search.messages[0] - resume = resume_selected.automon_attachments().attachments[0].body.automon_data_html_text + resume = resume_selected.automon_attachments().attachments[0].parts[0].body.automon_data_html_text to = email_selected.automon_message_first.automon_from().get('value') from_ = email_selected.automon_message_first.automon_to().get('value') @@ -269,6 +284,9 @@ def main(): prompts_emails_all = [] for message in email_selected.messages: + if labels.draft in message.automon_labels: + continue + _message = f"{message.to_dict()}" import re @@ -311,21 +329,12 @@ def main(): GoogleGeminiClient.prompts.agent_machine_job_applicant, ) prompts.append( - f"MUST NOT have a reply if last email is not from the sender of the first email. \n" - f"EXCLUDE any email subject line. \n" - f"EXCLUDE any internal thought process. \n" - f"EXCLUDE any chain of thought process. \n" - f"EXCLUDE any conversational parts. \n" - f"MUST write in plain english. \n" - f"MUST write in first person. \n" - f"MUST respond as if in a conversation. \n" - f"MUST provide only the body of the response. \n" f"MUST check for a job description in the first email before continuing. \n" f"\n\n" f"Create a response. " ) - if labels.test in email_selected.automon_labels: + if labels.retry in email_selected.automon_message_first.automon_labels: response, model = run_llm(prompts=prompts, chat=True) else: response, model = run_llm(prompts=prompts, chat=False) @@ -349,18 +358,17 @@ def main(): draft_get = gmail.draft_get_automon(id=draft.id) gmail.messages_modify(id=email_selected.id, - addLabelIds=[labels.drafted, - labels.unread, - ], + addLabelIds=[labels.unread], + removeLabelIds=[labels.retry] ) - prompts = prompts_emails.copy() - prompts.append( - f"Respond only true or false, does any of the emails have the 'auto reply enabled' label name?" - ) - response, model = run_llm(prompts) - if gemini.true_or_false(response.lower()): + if (labels.auto_reply_enabled in email_selected.automon_message_latest.automon_labels + and labels.sent not in email_selected.automon_message_latest.automon_labels + ): draft_sent = gmail.draft_send(draft=draft) + gmail.messages_modify(id=email_selected.automon_message_first.id, + addLabelIds=[labels.unread], + ) prompts = [prompts_emails[0]] + prompts_resume prompts.append( @@ -372,7 +380,7 @@ def main(): prompts = [prompts_emails[0]] prompts.append( - f"Respond only true or false, is the job remote?" + f"Respond only true or false, is the job fully and completely remote, with no in-office days?" ) response, model = run_llm(prompts) if gemini.true_or_false(response.lower()): @@ -382,6 +390,8 @@ def main(): except Exception as error: + gmail.config.refresh_token() + import traceback llm_check = [ f"Tell me what the error from this stacktrace could be. \n" @@ -396,7 +406,7 @@ def main(): draft_subject=f"Bug Report", draft_body=bug_report, draft_to=[email_error]) - gmail.messages_modify(id=_draft.message.id, addLabelIds=[labels.error, + gmail.messages_modify(id=_draft.message.id, addLabelIds=[labels.retry, labels.unread, ]) diff --git a/automon/integrations/google/gmail/v1/__init__.py b/automon/integrations/google/gmail/v1/__init__.py index 626880b8..94b11961 100644 --- a/automon/integrations/google/gmail/v1/__init__.py +++ b/automon/integrations/google/gmail/v1/__init__.py @@ -264,7 +264,7 @@ class LabelListVisibility: class AutomonAttachments(DictUpdate): attachments: ['MessagePart'] - def __init__(self, attachments: ['MessagePart']): + def __init__(self, attachments: ['MessagePart'] = []): super().__init__() self.attachments = attachments @@ -937,6 +937,8 @@ class MessagePart(DictUpdate): def __init__(self): super().__init__() + self.headers = [] + def enhance(self): if hasattr(self, 'body'): @@ -950,9 +952,15 @@ def enhance(self): return self - def automon_attachments(self): + def automon_attachments(self) -> AutomonAttachments: if hasattr(self, 'parts'): return AutomonAttachments(attachments=self.parts) + return AutomonAttachments() + + def get_header(self, header: str) -> Headers: + for _header in self.headers: + if header.lower() in _header.name.lower(): + return _header def __repr__(self): if getattr(self, 'filename') and getattr(self, 'mimeType'): diff --git a/automon/integrations/google/oauth/config.py b/automon/integrations/google/oauth/config.py index 2328d033..0254944c 100644 --- a/automon/integrations/google/oauth/config.py +++ b/automon/integrations/google/oauth/config.py @@ -420,7 +420,12 @@ def userinfo(self): credentials=self.credentials, num_retries=30, ) - userinfo = service.userinfo().get().execute() + userinfo = None + while userinfo is None: + try: + userinfo = service.userinfo().get().execute() + except Exception as error: + logger.error(f'[GoogleAuthConfig] :: userinfo :: error :: {error=}') user_info = service.userinfo().get().execute() self.user_info = userinfo diff --git a/automon/integrations/ollamaWrapper/prompt_templates.py b/automon/integrations/ollamaWrapper/prompt_templates.py index eed0222c..ed17a0a2 100644 --- a/automon/integrations/ollamaWrapper/prompt_templates.py +++ b/automon/integrations/ollamaWrapper/prompt_templates.py @@ -3,8 +3,10 @@ class AgentTemplates: @property def agent_machine_job_applicant(self): return (f"You are the person in the resume. \n" - f"MUST INCLUDE only experience from the provided resume. \n" f"MUST Respond with the tone, the theme, and use of words from the resume. \n" + f"MUST Respond as a conversation instead of an email. \n" f"When presenting code, text blocks, or any structured information, present the content directly without any surrounding markers, prefixes, or suffixes such as '```text', '```code', or similar indicators. The response should consist solely of the requested information, formatted for readability but without extraneous characters or formatting elements. \n" f"Provide only the requested information, formatted appropriately for readability. Do not include greetings, acknowledgments, apologies, justifications, or any other text that is not directly part of the requested output. \n" - f"When generating an email response, ensure that the output *only* includes the body of the email. *Do not* include a subject line, greetings, or any extraneous information. The response should start directly with the content of the email body, formatted for readability. \n") + f"When generating an email response, ensure that the output *only* includes the body of the email. *Do not* include a subject line, greetings, or any extraneous information. The response should start directly with the content of the email body, formatted for readability. \n" + f"Compose a concise and professional email response to a recruiter expressing interest in a job opportunity. Highlight relevant skills and experience from the provided resume, focusing on alignment with the job description. Mention specific technologies and automation experience to demonstrate suitability for the role. Briefly inquire about the alignment of your skills with the recruiter's needs, aiming for a response length that is informative but not overly verbose. \n" + f"Never include a subject line in any response. Provide only the requested information, formatted appropriately for readability. Do not include greetings, acknowledgments, apologies, justifications, or any other text that is not directly part of the requested output. \n")