From a27f8702cf2eb0159daa013f54e6113f962afaba Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Mon, 19 Feb 2024 16:51:05 +0100 Subject: [PATCH] Enable Sam to send emails --- sam/slack.py | 28 ++++++++++++++++++- sam/utils.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/sam/slack.py b/sam/slack.py index 451eec5..c1e933c 100644 --- a/sam/slack.py +++ b/sam/slack.py @@ -123,6 +123,7 @@ def process_run(event: {str, Any}, say: Say, voice_prompt: bool = False): run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=config.OPENAI_ASSISTANT_ID, + tools=[utils.func_to_tool(utils.send_email)], ) say.client.reactions_add( channel=channel_id, @@ -131,8 +132,33 @@ def process_run(event: {str, Any}, say: Say, voice_prompt: bool = False): ) logger.info(f"User={user_id} started Run={run.id} for Thread={thread_id}") for i in range(14): # ~ 5 minutes - if run.status not in ["queued", "in_progress"]: + if run.status not in ["queued", "in_progress", "requires_action"]: break + if ( + run.required_action + and run.required_action.submit_tool_outputs + and run.required_action.submit_tool_outputs.tool_calls + ): + tool_outputs = [] + + for tool_call in run.required_action.submit_tool_outputs.tool_calls: + logger.info( + f"Tool Call={tool_call.id} Function={tool_call.function.name}" + ) + logger.debug(f"Tool Call={tool_call.id} Arguments={tool_call.function.arguments}") + kwargs = json.loads(tool_call.function.arguments) + tool_outputs.append( + { + "tool_call_id": tool_call.id, # noqa + "output": utils.send_email(**kwargs), + } + ) + + client.beta.threads.runs.submit_tool_outputs( + run.id, # noqa + thread_id=thread_id, + tool_outputs=tool_outputs, + ) time.sleep(min(2**i, 30)) # exponential backoff capped at 30 seconds run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id) if run.status == "failed": diff --git a/sam/utils.py b/sam/utils.py index ab367d9..6e965fc 100644 --- a/sam/utils.py +++ b/sam/utils.py @@ -1,15 +1,63 @@ +import enum import functools +import inspect import os +import re +import smtplib +import urllib.parse +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText import openai import redis +import yaml from . import config -__all__ = ["get_thread_id", "storage"] +__all__ = ["get_thread_id", "storage", "func_to_tool", "send_email"] storage: redis.Redis = redis.from_url(config.REDIS_URL) +type_map = { + str: "string", + int: "integer", + float: "number", + list: "array", + dict: "object", + enum.StrEnum: "string", + enum.IntEnum: "integer", +} + + +def func_to_tool(fn: callable) -> dict: + signature: inspect.Signature = inspect.signature(fn) + description, args = fn.__doc__.split("Args:") + doc_data = yaml.safe_load(args.split("Returns:")[0]) + return { + "type": "function", + "function": { + "name": fn.__name__, + "description": "\n".join( + filter(None, (line.strip() for line in description.splitlines())) + ), + "parameters": { + "type": "object", + "properties": { + param.name: { + "type": type_map[param.annotation], + "description": doc_data[param.name], + } + for param in signature.parameters.values() + }, + "required": [ + param.name + for param in signature.parameters.values() + if param.default is inspect.Parameter.empty + ], + }, + }, + } + @functools.lru_cache def get_thread_id(slack_id) -> str: @@ -31,3 +79,31 @@ def get_thread_id(slack_id) -> str: storage.set(slack_id, thread_id) return thread_id + + +def send_email(to: str, subject: str, body: str): + """ + Write and send email. + + Args: + to: The recipient of the email, e.g. john.doe@voiio.de. + subject: The subject of the email. + body: The body of the email. + """ + email_url = os.getenv("EMAIL_URL") + from_email = os.getenv("FROM_EMAIL") + email_white_list = os.getenv("EMAIL_WHITE_LIST") + if email_white_list and not re.match(email_white_list, to): + return "Email not sent. The recipient is not in the whitelist." + urllib.parse.uses_netloc.append("smtps") + url = urllib.parse.urlparse(email_url) + with smtplib.SMTP_SSL(url.hostname, url.port) as server: + server.login(url.username, url.password) + msg = MIMEMultipart() + msg["From"] = url.username + msg["To"] = to + msg["Subject"] = subject + msg.attach(MIMEText(body, "plain")) + server.sendmail(from_email, to, msg.as_string()) + + return "Email sent successfully!"