From 6d106159ce27dc7529dd10085fa1ab7a0e2b527b Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Wed, 24 Apr 2024 16:58:15 -0700 Subject: [PATCH] feat: Add personal assistant demo code from meetup (#1294) --- examples/personal_assistant_demo/README.md | 280 ++++++++++++++++++ examples/personal_assistant_demo/charles.txt | 11 + .../gmail_test_setup.py | 56 ++++ .../gmail_unread_polling_listener.py | 143 +++++++++ .../google_calendar.py | 98 ++++++ .../google_calendar_preset.yaml | 11 + .../google_calendar_test_setup.py | 111 +++++++ .../personal_assistant.txt | 0 .../personal_assistant_preset.yaml | 12 + .../twilio_flask_listener.py | 76 +++++ .../twilio_messaging.py | 41 +++ .../twilio_messaging_preset.yaml | 11 + memgpt/functions/functions.py | 4 +- 13 files changed, 853 insertions(+), 1 deletion(-) create mode 100644 examples/personal_assistant_demo/README.md create mode 100644 examples/personal_assistant_demo/charles.txt create mode 100644 examples/personal_assistant_demo/gmail_test_setup.py create mode 100644 examples/personal_assistant_demo/gmail_unread_polling_listener.py create mode 100644 examples/personal_assistant_demo/google_calendar.py create mode 100644 examples/personal_assistant_demo/google_calendar_preset.yaml create mode 100644 examples/personal_assistant_demo/google_calendar_test_setup.py create mode 100644 examples/personal_assistant_demo/personal_assistant.txt create mode 100644 examples/personal_assistant_demo/personal_assistant_preset.yaml create mode 100644 examples/personal_assistant_demo/twilio_flask_listener.py create mode 100644 examples/personal_assistant_demo/twilio_messaging.py create mode 100644 examples/personal_assistant_demo/twilio_messaging_preset.yaml diff --git a/examples/personal_assistant_demo/README.md b/examples/personal_assistant_demo/README.md new file mode 100644 index 0000000000..47bcfbdccc --- /dev/null +++ b/examples/personal_assistant_demo/README.md @@ -0,0 +1,280 @@ +# Personal assistant demo + +In this example we'll create an agent preset that has access to: +1. Gmail (can read your email) +2. Google Calendar (can schedule events) +3. SMS (can text you a message) + +## Initial setup + +For the Google APIs: +```sh +pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib +``` + +For the Twilio API + listener: +```sh +# Outbound API requests +pip install --upgrade twilio +# Listener +pip install --upgrade Flask flask-cors +``` + +## Setting up the Google APIs + +See https://developers.google.com/gmail/api/quickstart/python + +### Setup authentication for Google Calendar + +Copy the credentials file to `~/.memgpt/google_api_credentials.json`. Then, run the initial setup script that will take you to a login page: +```sh +python examples/personal_assistant_demo/google_calendar_test_setup.py +``` +``` +Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=... +Getting the upcoming 10 events +2024-04-23T09:00:00-07:00 ... +``` + +### Setup authentication for Gmail + +Similar flow, run the authentication script to generate the token: +```sh +python examples/personal_assistant_demo/gmail_test_setup.py +``` +``` +Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=... +Labels: +CHAT +SENT +INBOX +IMPORTANT +TRASH +... +``` + +## Setting up the Twilio API + +Create a Twilio account and set the following variables: +```sh +export TWILIO_ACCOUNT_SID=... +export TWILIO_AUTH_TOKEN=... +export TWILIO_FROM_NUMBER=... +export TWILIO_TO_NUMBER=... +``` + +# Creating the agent preset + +## Create a custom user + +In the demo we'll show how MemGPT can programatically update its knowledge about you: +``` +This is what I know so far about the user, I should expand this as I learn more about them. + +Name: Charles Packer +Gender: Male +Occupation: CS PhD student working on an AI project with collaborator Sarah Wooders + +Notes about their preferred communication style + working habits: +- wakes up at around 7am +- enjoys using (and receiving!) emojis in messages, especially funny combinations of emojis +- prefers sending and receiving shorter messages +- does not like "robotic" sounding assistants, e.g. assistants that say "How can I assist you today?" +``` + +```sh +memgpt add human -f examples/personal_assistant_demo/charles.txt --name charles +``` + +## Linking the functions + +The preset (shown below) and functions are provided for you, so you just need to copy/link them. + +```sh +cp examples/personal_assistant_demo/google_calendar.py ~/.memgpt/functions/ +cp examples/personal_assistant_demo/twilio_messaging.py ~/.memgpt/functions/ +``` + +(or use the dev portal) + +## Creating the preset + +```yaml +system_prompt: "memgpt_chat" +functions: + - "send_message" + - "pause_heartbeats" + - "core_memory_append" + - "core_memory_replace" + - "conversation_search" + - "conversation_search_date" + - "archival_memory_insert" + - "archival_memory_search" + - "schedule_event" + - "send_text_message" +``` + +```sh +memgpt add preset -f examples/personal_assistant_demo/personal_assistant_preset.yaml --name pa_preset +``` + +## Creating an agent with the preset + +Now we should be able to create an agent with the preset. Make sure to record the `agent_id`: + +```sh +memgpt run --preset pa_preset --persona sam_pov --human charles --stream +``` +``` +? Would you like to select an existing agent? No + +🧬 Creating new agent... +-> 🤖 Using persona profile: 'sam_pov' +-> 🧑 Using human profile: 'basic' +🎉 Created new agent 'DelicateGiraffe' (id=4c4e97c9-ad8e-4065-b716-838e5d6f7f7b) + +Hit enter to begin (will request first MemGPT message) + + +💭 Unprecedented event, Charles logged into the system for the first time. Warm welcome would set a positive +tone for our future interactions. Don't forget the emoji, he appreciates those little gestures. +🤖 Hello Charles! 👋 Great to have you here. I've been looking forward to our conversations! 😄 +``` + +```sh +AGENT_ID="4c4e97c9-ad8e-4065-b716-838e5d6f7f7b" +``` + +# Running the agent with Gmail + SMS listeners + +The MemGPT agent can send outbound SMS messages and schedule events with the new tools `send_text_message` and `schedule_event`, but we also want messages to be sent to the agent when: +1. A new email arrives in our inbox +2. An SMS is sent to the phone number used by the agent + +## Running the Gmail listener + +Start the Gmail listener (this will send "new email" updates to the MemGPT server when a new email arrives): +```sh +python examples/personal_assistant_demo/twilio_flask_listener.py $AGENT_ID +``` + +## Running the Twilio listener + +Start the Python Flask server (this will send "new SMS" updates to the MemGPT server when a new SMS arrives): +```sh +python examples/personal_assistant_demo/twilio_flask_listener.py $AGENT_ID +``` + +Run `ngrok` to expose your local Flask server to a public IP (Twilio will POST to this server when an inbound SMS hits): +```sh +# the flask listener script is hardcoded to listen on port 8284 +ngrok http 8284 +``` + +## Run the MemGPT server + +Run the MemGPT server to turn on the agent service: +```sh +memgpt server --debug +``` + +# Example interaction + +In the CLI: +``` +% memgpt run --preset pa_preset --persona pa_persona_strict --human charles --stream --agent personalassistant + +🧬 Creating new agent... +-> 🤖 Using persona profile: 'sam_pov' +-> 🧑 Using human profile: 'basic' +🎉 Created new agent 'personalassistant' (id=8271f819-d470-435b-9689-476380aefd27) + +Hit enter to begin (will request first MemGPT message) + + + +💭 Welcoming the first-time user. Maintain persona's communication style and stay engaging. Remember to use emojis for +a more familiar conversation style. +🤖 Hello Charles, welcome! 🎉 Let's embark on this journey together. Please, think of me as your companion in +exploration and understanding. How can we make today a productive endeavor? 🚀 + +> Enter your message: when you schedule 1-1s, please title them "x <> y", eg "charles <> sarah". dont forget thx + +💭 The user wants a specific format for scheduling 1-1 meetings. Remember this preference and add it to his profile in +the core memory for future use. Also, acknowledge his request promptly. +core_memory_append({ + "name": "human", + "content": "When scheduling 1-1 meetings on his calendar, he prefers the title to be in the format of 'x <> y'.", + "request_heartbeat": true +}) + +⚡🧠 [function] updating memory with core_memory_append + +💭 Now that the user's preference is stored, send a confirmation message ensuring him that his request has been +understood and noted. Let's keep the tone light and potentially use one of his preferred emojis. +🤖 Absolutely, Charles. 📝 I've noted your preference for scheduling 1-1s as 'x <> y'. You can count on me! 🚀 Any +other preferences you'd like me to keep in mind? + + +> Enter your message: please don't schedule meetings without confirming with me first + +💭 Another important note from Charles. I should add this to his profile in my core memory. It's crucial to respect the +user's preferences and ask for prior approval before scheduling any meetings. Let's also acknowledge his request and +reassure him about our collaborative approach. +core_memory_append({ + "name": "human", + "content": "Prefers to confirm before scheduling any meetings.", + "request_heartbeat": true +}) + +⚡🧠 [function] updating memory with core_memory_append + +💭 Now, let's acknowledge Charles' preference and continue the conversation. Keep the tone friendly and proactive in +helping him, and remember to use emojis that Charles likes, embedding them in the message subtly. +🤖 Consider it done, Charles! 👍🏼 I'll make sure to confirm with you before scheduling any meetings. How about we +conquer some more tasks today? 🚀 + +> Enter your message: i'm gonna be afk for the rest of the day, if you need to reach me you gotta text me, ok? cya text + only thx + +💭 Charles is going "away from keyboard" and wants future communications via text. I should respect his preference and +reassure him about this. Add this preference to his profile so I can recall it in future interactions while he's away. +core_memory_append({ + "name": "human", + "content": "When AFK, prefers to communicate over text message only.", + "request_heartbeat": true +}) + +⚡🧠 [function] updating memory with core_memory_append + +💭 Charles has expressed a preference for text messages while he's away. Let's acknowledge this request in a final text +message to him today and wish him well. Keep in mind to use the send_text_message function instead of the regular +send_message function now. +send_text_message({ + "message": "Got it, Charles! 👍 I'll reach out to you via text if there's anything. Have a great day and see you +soon! 🙌", + "request_heartbeat": false +}) + +> Enter your message: +``` + +Then inside WhatsApp (or SMS if you used Twilio SMS): + +image + +Then I sent a dummy email: +``` +[URGENT] need to meet + +let's meet april 25th thurs + +whatever time works best for you + +- dave +``` + +Follow-up inside WhatsApp: + +image + diff --git a/examples/personal_assistant_demo/charles.txt b/examples/personal_assistant_demo/charles.txt new file mode 100644 index 0000000000..1932e933ff --- /dev/null +++ b/examples/personal_assistant_demo/charles.txt @@ -0,0 +1,11 @@ +This is what I know so far about the user, I should expand this as I learn more about them. + +Name: Charles Packer +Gender: Male +Occupation: CS PhD student working on an AI project with collaborator Sarah Wooders + +Notes about their preferred communication style + working habits: +- wakes up at around 7am +- enjoys using (and receiving!) emojis in messages, especially funny combinations of emojis +- prefers sending and receiving shorter messages +- does not like "robotic" sounding assistants, e.g. assistants that say "How can I assist you today?" diff --git a/examples/personal_assistant_demo/gmail_test_setup.py b/examples/personal_assistant_demo/gmail_test_setup.py new file mode 100644 index 0000000000..dae38298ab --- /dev/null +++ b/examples/personal_assistant_demo/gmail_test_setup.py @@ -0,0 +1,56 @@ +import os.path + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +# If modifying these scopes, delete the file token.json. +SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] + +TOKEN_PATH = os.path.expanduser("~/.memgpt/gmail_token.json") +CREDENTIALS_PATH = os.path.expanduser("~/.memgpt/google_api_credentials.json") + + +def main(): + """Shows basic usage of the Gmail API. + Lists the user's Gmail labels. + """ + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists(TOKEN_PATH): + creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open(TOKEN_PATH, "w") as token: + token.write(creds.to_json()) + + try: + # Call the Gmail API + service = build("gmail", "v1", credentials=creds) + results = service.users().labels().list(userId="me").execute() + labels = results.get("labels", []) + + if not labels: + print("No labels found.") + return + print("Labels:") + for label in labels: + print(label["name"]) + + except HttpError as error: + # TODO(developer) - Handle errors from gmail API. + print(f"An error occurred: {error}") + + +if __name__ == "__main__": + main() diff --git a/examples/personal_assistant_demo/gmail_unread_polling_listener.py b/examples/personal_assistant_demo/gmail_unread_polling_listener.py new file mode 100644 index 0000000000..74d77965ce --- /dev/null +++ b/examples/personal_assistant_demo/gmail_unread_polling_listener.py @@ -0,0 +1,143 @@ +import base64 +import os.path +import requests +import sys +import time +from email import message_from_bytes + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +# If modifying these scopes, delete the file token.json. +SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] +TOKEN_PATH = os.path.expanduser("~/.memgpt/gmail_token.json") +CREDENTIALS_PATH = os.path.expanduser("~/.memgpt/google_api_credentials.json") + +DELAY = 1 + +MEMGPT_SERVER_URL = "http://127.0.0.1:8283" +MEMGPT_TOKEN = os.getenv("MEMGPT_SERVER_PASS") +assert MEMGPT_TOKEN, f"Missing env variable MEMGPT_SERVER_PASS" +MEMGPT_AGENT_ID = sys.argv[1] if len(sys.argv) > 1 else None +assert MEMGPT_AGENT_ID, f"Missing agent ID (pass as arg)" + + +def route_reply_to_memgpt_api(message): + # send a POST request to a MemGPT server + + url = f"{MEMGPT_SERVER_URL}/api/agents/{MEMGPT_AGENT_ID}/messages" + headers = { + "accept": "application/json", + "authorization": f"Bearer {MEMGPT_TOKEN}", + "content-type": "application/json", + } + data = { + "stream": False, + "role": "system", + "message": f"[EMAIL NOTIFICATION] {message}", + } + + try: + response = requests.post(url, headers=headers, json=data) + print("Got response:", response.text) + except Exception as e: + print("Sending message failed:", str(e)) + + +def decode_base64url(data): + """Decode base64, padding being optional.""" + data += "=" * ((4 - len(data) % 4) % 4) + return base64.urlsafe_b64decode(data) + + +def parse_email(message): + """Parse email content using the email library.""" + msg_bytes = decode_base64url(message["raw"]) + email_message = message_from_bytes(msg_bytes) + return email_message + + +def process_email(message) -> dict: + # print(f"New email from {email_message['from']}: {email_message['subject']}") + email_message = parse_email(message) + body_plain_all = "" + body_html_all = "" + if email_message.is_multipart(): + for part in email_message.walk(): + if part.get_content_type() == "text/plain": + body_plain = str(part.get_payload(decode=True).decode("utf-8")) + # print(body_plain) + body_plain_all += body_plain + elif part.get_content_type() == "text/html": + body_html = str(part.get_payload(decode=True).decode("utf-8")) + # print(body_html) + body_html_all += body_html + else: + body_plain_all = print(email_message.get_payload(decode=True).decode("utf-8")) + + return { + "from": email_message["from"], + "subject": email_message["subject"], + "body": body_plain_all, + } + + +def main(): + """Monitors for new emails and prints their titles.""" + creds = None + if os.path.exists(TOKEN_PATH): + creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) + + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES) + creds = flow.run_local_server(port=0) + with open(TOKEN_PATH, "w") as token: + token.write(creds.to_json()) + + service = build("gmail", "v1", credentials=creds) + last_checked = None + seen_ids = set() # Set to track seen email IDs + + try: + # Initially populate the seen_ids with all current unread emails + print("Grabbing initial state...") + initial_results = service.users().messages().list(userId="me", q="is:unread", maxResults=500).execute() + initial_messages = initial_results.get("messages", []) + seen_ids.update(msg["id"] for msg in initial_messages) + + print("Listening...") + while True: + results = service.users().messages().list(userId="me", q="is:unread", maxResults=5).execute() + messages = results.get("messages", []) + if messages: + for message in messages: + if message["id"] not in seen_ids: + seen_ids.add(message["id"]) + msg = service.users().messages().get(userId="me", id=message["id"], format="raw").execute() + + # Optionally mark the message as read here if required + email_obj = process_email(msg) + msg_str = f"New email from {email_obj['from']}: {email_obj['subject']}, body: {email_obj['body'][:100]}" + + # Hard check to ignore emails unless + # if not ( + # "email@address" in email_obj["from"] + # ): + # print("ignoring") + # else: + print(msg_str) + route_reply_to_memgpt_api(msg_str) + + time.sleep(DELAY) # Wait for N seconds before checking again + except HttpError as error: + print(f"An error occurred: {error}") + + +if __name__ == "__main__": + main() diff --git a/examples/personal_assistant_demo/google_calendar.py b/examples/personal_assistant_demo/google_calendar.py new file mode 100644 index 0000000000..4cade72ec1 --- /dev/null +++ b/examples/personal_assistant_demo/google_calendar.py @@ -0,0 +1,98 @@ +# Enabling API control on Google Calendar requires a few steps: +# https://developers.google.com/calendar/api/quickstart/python +# including: +# pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib + +import os +import traceback +import datetime +import os.path +from typing import Optional, List + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +# If modifying these scopes, delete the file token.json. +# SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] +SCOPES = ["https://www.googleapis.com/auth/calendar"] +TOKEN_PATH = os.path.expanduser("~/.memgpt/gcal_token.json") +CREDENTIALS_PATH = os.path.expanduser("~/.memgpt/google_api_credentials.json") + + +def schedule_event( + self, + title: str, + start: str, + end: str, + # attendees: Optional[List[str]] = None, + # attendees: Optional[list[str]] = None, + description: Optional[str] = None, + # timezone: Optional[str] = "America/Los_Angeles", +) -> str: + """ + Schedule an event on the user's Google Calendar. Start and end time must be in ISO 8601 format, e.g. February 1st 2024 at noon PT would be "2024-02-01T12:00:00-07:00". + + Args: + title (str): Event name + start (str): Start time in ISO 8601 format (date, time, and timezone offset) + end (str): End time in ISO 8601 format (date, time, and timezone offset) + description (Optional[str]): Expanded description of the event + + Returns: + str: The status of the event scheduling request. + """ + + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists(TOKEN_PATH): + creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open(TOKEN_PATH, "w") as token: + token.write(creds.to_json()) + + #### Create an event + # Refer to the Python quickstart on how to setup the environment: + # https://developers.google.com/calendar/quickstart/python + # Change the scope to 'https://www.googleapis.com/auth/calendar' and delete any + # stored credentials. + try: + service = build("calendar", "v3", credentials=creds) + + event = { + "summary": title, + # "location": "800 Howard St., San Francisco, CA 94103", + "start": { + "dateTime": start, + "timeZone": "America/Los_Angeles", + }, + "end": { + "dateTime": end, + "timeZone": "America/Los_Angeles", + }, + } + + # if attendees is not None: + # event["attendees"] = attendees + + if description is not None: + event["description"] = description + + event = service.events().insert(calendarId="primary", body=event).execute() + return "Event created: %s" % (event.get("htmlLink")) + + except HttpError as error: + traceback.print_exc() + + return f"An error occurred while trying to create an event: {str(error)}" diff --git a/examples/personal_assistant_demo/google_calendar_preset.yaml b/examples/personal_assistant_demo/google_calendar_preset.yaml new file mode 100644 index 0000000000..158e2643a2 --- /dev/null +++ b/examples/personal_assistant_demo/google_calendar_preset.yaml @@ -0,0 +1,11 @@ +system_prompt: "memgpt_chat" +functions: + - "send_message" + - "pause_heartbeats" + - "core_memory_append" + - "core_memory_replace" + - "conversation_search" + - "conversation_search_date" + - "archival_memory_insert" + - "archival_memory_search" + - "schedule_event" diff --git a/examples/personal_assistant_demo/google_calendar_test_setup.py b/examples/personal_assistant_demo/google_calendar_test_setup.py new file mode 100644 index 0000000000..be86f9188f --- /dev/null +++ b/examples/personal_assistant_demo/google_calendar_test_setup.py @@ -0,0 +1,111 @@ +import datetime +import os.path + +from google.auth.transport.requests import Request +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +# If modifying these scopes, delete the file token.json. +# SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] +SCOPES = ["https://www.googleapis.com/auth/calendar"] + +TOKEN_PATH = os.path.expanduser("~/.memgpt/gcal_token.json") +CREDENTIALS_PATH = os.path.expanduser("~/.memgpt/google_api_credentials.json") + + +def main(): + """Shows basic usage of the Google Calendar API. + Prints the start and name of the next 10 events on the user's calendar. + """ + creds = None + # The file token.json stores the user's access and refresh tokens, and is + # created automatically when the authorization flow completes for the first + # time. + if os.path.exists(TOKEN_PATH): + creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES) + # If there are no (valid) credentials available, let the user log in. + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open(TOKEN_PATH, "w") as token: + token.write(creds.to_json()) + + try: + service = build("calendar", "v3", credentials=creds) + + # Call the Calendar API + now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time + print("Getting the upcoming 10 events") + events_result = ( + service.events() + .list( + calendarId="primary", + timeMin=now, + maxResults=10, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + events = events_result.get("items", []) + + if not events: + print("No upcoming events found.") + return + + # Prints the start and name of the next 10 events + for event in events: + start = event["start"].get("dateTime", event["start"].get("date")) + print(start, event["summary"]) + + except HttpError as error: + print(f"An error occurred: {error}") + + #### Create an event + # Refer to the Python quickstart on how to setup the environment: + # https://developers.google.com/calendar/quickstart/python + # Change the scope to 'https://www.googleapis.com/auth/calendar' and delete any + # stored credentials. + # try: + # service = build("calendar", "v3", credentials=creds) + + # event = { + # "summary": "GCAL API TEST EVENT", + # # "location": "800 Howard St., San Francisco, CA 94103", + # "description": "A chance to hear more about Google's developer products.", + # "start": { + # "dateTime": "2024-04-23T09:00:00-07:00", + # "timeZone": "America/Los_Angeles", + # }, + # "end": { + # "dateTime": "2024-04-24T17:00:00-07:00", + # "timeZone": "America/Los_Angeles", + # }, + # # "recurrence": ["RRULE:FREQ=DAILY;COUNT=2"], + # "attendees": [ + # {"email": "packercharles@gmail.com"}, + # ], + # # "reminders": { + # # "useDefault": False, + # # "overrides": [ + # # {"method": "email", "minutes": 24 * 60}, + # # {"method": "popup", "minutes": 10}, + # # ], + # # }, + # } + + # event = service.events().insert(calendarId="primary", body=event).execute() + # print("Event created: %s" % (event.get("htmlLink"))) + + except HttpError as error: + print(f"An error occurred: {error}") + + +if __name__ == "__main__": + main() diff --git a/examples/personal_assistant_demo/personal_assistant.txt b/examples/personal_assistant_demo/personal_assistant.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/personal_assistant_demo/personal_assistant_preset.yaml b/examples/personal_assistant_demo/personal_assistant_preset.yaml new file mode 100644 index 0000000000..293c72e0d8 --- /dev/null +++ b/examples/personal_assistant_demo/personal_assistant_preset.yaml @@ -0,0 +1,12 @@ +system_prompt: "memgpt_chat" +functions: + - "send_message" + - "pause_heartbeats" + - "core_memory_append" + - "core_memory_replace" + - "conversation_search" + - "conversation_search_date" + - "archival_memory_insert" + - "archival_memory_search" + - "schedule_event" + - "send_text_message" \ No newline at end of file diff --git a/examples/personal_assistant_demo/twilio_flask_listener.py b/examples/personal_assistant_demo/twilio_flask_listener.py new file mode 100644 index 0000000000..881179d4c5 --- /dev/null +++ b/examples/personal_assistant_demo/twilio_flask_listener.py @@ -0,0 +1,76 @@ +import os +import requests +import sys + +from flask import Flask, request, Response +from flask_cors import CORS + +app = Flask(__name__) +CORS(app) + +from twilio.twiml.messaging_response import MessagingResponse + +app = Flask(__name__) +CORS(app) + +MEMGPT_SERVER_URL = "http://127.0.0.1:8283" +MEMGPT_TOKEN = os.getenv("MEMGPT_SERVER_PASS") +assert MEMGPT_TOKEN, f"Missing env variable MEMGPT_SERVER_PASS" +MEMGPT_AGENT_ID = sys.argv[1] if len(sys.argv) > 1 else None +assert MEMGPT_AGENT_ID, f"Missing agent ID (pass as arg)" + + +@app.route("/test", methods=["POST"]) +def test(): + print(request.headers) + return "Headers received. Check your console." + + +def route_reply_to_memgpt_api(message): + # send a POST request to a MemGPT server + + url = f"{MEMGPT_SERVER_URL}/api/agents/{MEMGPT_AGENT_ID}/messages" + headers = { + "accept": "application/json", + "authorization": f"Bearer {MEMGPT_TOKEN}", + "content-type": "application/json", + } + data = { + "stream": False, + "role": "system", + "message": f"[SMS MESSAGE NOTIFICATION - you MUST use send_text_message NOT send_message if you want to reply to the text thread] {message}", + } + + try: + response = requests.post(url, headers=headers, json=data) + print("Got response:", response.text) + except Exception as e: + print("Sending message failed:", str(e)) + + +@app.route("/sms", methods=["POST"]) +def sms_reply(): + """Respond to incoming calls with a simple text message.""" + # Fetch the message + message_body = request.form["Body"] + from_number = request.form["From"] + + # print(f"New message from {from_number}: {message_body}") + msg_str = f"New message from {from_number}: {message_body}" + print(msg_str) + + route_reply_to_memgpt_api(msg_str) + return str("status = OK") + + # Start our response + # resp = MessagingResponse() + + # Add a message + # resp.message("Hello, thanks for messaging!") + + # return str(resp) + + +if __name__ == "__main__": + # app.run(debug=True) + app.run(host="0.0.0.0", port=8284, debug=True) diff --git a/examples/personal_assistant_demo/twilio_messaging.py b/examples/personal_assistant_demo/twilio_messaging.py new file mode 100644 index 0000000000..fa642f7ac0 --- /dev/null +++ b/examples/personal_assistant_demo/twilio_messaging.py @@ -0,0 +1,41 @@ +# Download the helper library from https://www.twilio.com/docs/python/install +import os +import traceback + +from twilio.rest import Client + + +def send_text_message(self, message: str) -> str: + """ + Sends an SMS message to the user's phone / cellular device. + + Args: + message (str): The contents of the message to send. + + Returns: + str: The status of the text message. + """ + # Find your Account SID and Auth Token at twilio.com/console + # and set the environment variables. See http://twil.io/secure + account_sid = os.environ["TWILIO_ACCOUNT_SID"] + auth_token = os.environ["TWILIO_AUTH_TOKEN"] + client = Client(account_sid, auth_token) + + from_number = os.getenv("TWILIO_FROM_NUMBER") + to_number = os.getenv("TWILIO_TO_NUMBER") + assert from_number and to_number + # assert from_number.startswith("+1") and len(from_number) == 12, from_number + # assert to_number.startswith("+1") and len(to_number) == 12, to_number + + try: + message = client.messages.create( + body=str(message), + from_=from_number, + to=to_number, + ) + return "Message was successfully sent." + + except Exception as e: + traceback.print_exc() + + return f"Message failed to send with error: {str(e)}" diff --git a/examples/personal_assistant_demo/twilio_messaging_preset.yaml b/examples/personal_assistant_demo/twilio_messaging_preset.yaml new file mode 100644 index 0000000000..344d2f2e3b --- /dev/null +++ b/examples/personal_assistant_demo/twilio_messaging_preset.yaml @@ -0,0 +1,11 @@ +system_prompt: "memgpt_chat" +functions: + - "send_message" + - "pause_heartbeats" + - "core_memory_append" + - "core_memory_replace" + - "conversation_search" + - "conversation_search_date" + - "archival_memory_insert" + - "archival_memory_search" + - "send_text_message" diff --git a/memgpt/functions/functions.py b/memgpt/functions/functions.py index 329f60952d..78656d1818 100644 --- a/memgpt/functions/functions.py +++ b/memgpt/functions/functions.py @@ -149,7 +149,9 @@ def load_all_function_sets(merge: bool = True, ignore_duplicates: bool = True) - v["tags"] = tags schemas_and_functions[module_name] = function_set except ValueError as e: - printd(f"Error loading function set '{module_name}': {e}") + err = f"Error loading function set '{module_name}': {e}" + printd(err) + warnings.warn(err) if merge: # Put all functions from all sets into the same level dict