From 820cbd60c7efb59f459c01b19e73d121e49ebeb6 Mon Sep 17 00:00:00 2001 From: Zhufeng-Qiu Date: Sun, 13 Jul 2025 06:03:03 -0700 Subject: [PATCH] upload week5 exercise: personal knowledge assistant with local file, Gmail, outlook and Google Workspace files --- .../Gmail_API_Credential_Guide.ipynb | 154 ++ ...oogle_Workspace_API_Credential_Guide.ipynb | 294 +++ .../Outlook_API_Credential_Guide.ipynb | 178 ++ ...xercise_Personal_Knowledge_Assistant.ipynb | 1862 +++++++++++++++++ .../credentials/gmail_credentials.json | 3 + .../google_drive_workspace_credentials.json | 3 + .../local-knowledge-base/image/JPEG.jpg | Bin 0 -> 10884 bytes .../ms_office/LLMGooglePDF.pdf | Bin 0 -> 16124 bytes .../ms_office/Presentation.pptx | Bin 0 -> 33991 bytes .../local-knowledge-base/ms_office/excel.xlsx | Bin 0 -> 9609 bytes .../local-knowledge-base/ms_office/word.docx | Bin 0 -> 12139 bytes .../local-knowledge-base/text/Epub.epub | Bin 0 -> 2138 bytes .../local-knowledge-base/text/HTML.html | 9 + .../local-knowledge-base/text/MD.md | 1 + .../local-knowledge-base/text/PDF.pdf | Bin 0 -> 14864 bytes .../local-knowledge-base/text/text.txt | 1 + 16 files changed, 2505 insertions(+) create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/Gmail_API_Credential_Guide.ipynb create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/Google_Workspace_API_Credential_Guide.ipynb create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/Outlook_API_Credential_Guide.ipynb create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/Week5_Exercise_Personal_Knowledge_Assistant.ipynb create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/gmail_credentials.json create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/google_drive_workspace_credentials.json create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/image/JPEG.jpg create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/LLMGooglePDF.pdf create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/Presentation.pptx create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/excel.xlsx create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/word.docx create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/Epub.epub create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/HTML.html create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/MD.md create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/PDF.pdf create mode 100644 week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/text.txt diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Gmail_API_Credential_Guide.ipynb b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Gmail_API_Credential_Guide.ipynb new file mode 100644 index 0000000..1f5e1c6 --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Gmail_API_Credential_Guide.ipynb @@ -0,0 +1,154 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "35177005-196a-48b3-bf92-fa37d84693f4", + "metadata": {}, + "source": [ + "# Gmail API Credential Guide" + ] + }, + { + "cell_type": "markdown", + "id": "7bcad9ee-cd11-4b12-834d-9f1ddcefb190", + "metadata": {}, + "source": [ + "Use Gmail API to Read Your Emails\n", + "1. Set up a Google Cloud Project\n", + "\n", + " Go to Google Cloud Platform(GCP) Console\n", + "\n", + " Create a new project\n", + "\n", + "2. Enable the Gmail API for that project\n", + "\n", + " Select the created project and go to \"APIs & services\" page\n", + "\n", + " Click \"+ Enable APIs and services\" button, search \"Gmail API\" and enable it\n", + "\n", + "3. Go to \"OAuth Consent Screen\" and configure:\n", + "\n", + " Choose External and Fill in app name, dedveloper email, etc.\n", + "\n", + "4. Create OAuth Credentials\n", + "\n", + " Go to APIs & Services > Credentials\n", + "\n", + " Click \"+ Create Credentials\" > \"OAuth client ID\"\n", + "\n", + " Choose Desktop App\n", + "\n", + " Download the generated credentials.json\n", + "\n", + " Sometimes, GCP will navigate you to \"Google Auth Platform\" > \"Clients\", and you can click \"+ Create client\" here to create the OAuth Credentials\n", + "\n", + " \n", + "5. Add Test Users for Gmail API OAuth Access\n", + " \n", + " Go to \"APIs & Services\" > \"OAuth consent screen\" > \"Audience\" > \"Test Users\"\n", + "\n", + " Add the email account from which you want to extract email content.\n", + "\n", + "\n", + "6. Create 'credentials' folders to store gmail credential and user tokens" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc86bec0-bda8-4e9e-9c85-423179a99981", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4270e52e-378c-4127-bd52-1d082e9834e0", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import print_function\n", + "import os.path\n", + "import base64\n", + "import re\n", + "from email import message_from_bytes\n", + "from google.oauth2.credentials import Credentials\n", + "from google_auth_oauthlib.flow import InstalledAppFlow\n", + "from googleapiclient.discovery import build\n", + "\n", + "# If modifying these SCOPES, delete the token.json\n", + "SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']\n", + "PORT = 18000\n", + "\n", + "def main():\n", + " creds = None\n", + " # token.json stores the user's access and refresh tokens\n", + " if os.path.exists('token.json'):\n", + " creds = Credentials.from_authorized_user_file('token.json', SCOPES)\n", + " else:\n", + " flow = InstalledAppFlow.from_client_secrets_file('credentials/gmail_credentials.json', SCOPES)\n", + " creds = flow.run_local_server(port=PORT)\n", + " with open('token.json', 'w') as token:\n", + " token.write(creds.to_json())\n", + "\n", + " service = build('gmail', 'v1', credentials=creds)\n", + "\n", + " # Get the latest message\n", + " results = service.users().messages().list(userId='me', maxResults=1).execute()\n", + " messages = results.get('messages', [])\n", + "\n", + " if not messages:\n", + " print(\"No messages found.\")\n", + " return\n", + "\n", + " msg = service.users().messages().get(userId='me', id=messages[0]['id'], format='raw').execute()\n", + " raw_msg = base64.urlsafe_b64decode(msg['raw'].encode('ASCII'))\n", + " email_message = message_from_bytes(raw_msg)\n", + "\n", + " subject = email_message['Subject']\n", + " print(\"Subject:\", subject)\n", + "\n", + " # Extract text/plain body\n", + " for part in email_message.walk():\n", + " if part.get_content_type() == 'text/plain':\n", + " print(\"Body:\")\n", + " print(part.get_payload(decode=True).decode('utf-8'))\n", + "\n", + "if __name__ == '__main__':\n", + " main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff68e06-3cfb-48ae-9dad-fa431d0d548a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Google_Workspace_API_Credential_Guide.ipynb b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Google_Workspace_API_Credential_Guide.ipynb new file mode 100644 index 0000000..c300ec4 --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Google_Workspace_API_Credential_Guide.ipynb @@ -0,0 +1,294 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "35177005-196a-48b3-bf92-fa37d84693f4", + "metadata": {}, + "source": [ + "# Google Workspace API Credential Guide" + ] + }, + { + "cell_type": "markdown", + "id": "7bcad9ee-cd11-4b12-834d-9f1ddcefb190", + "metadata": {}, + "source": [ + "Use Google Drive API to Read files in Google Workspace \n", + "1. Set up a Google Cloud Project\n", + "\n", + " Go to Google Cloud Platform(GCP) Console\n", + "\n", + " Create a new project\n", + "\n", + "2. Enable the Gmail API for that project\n", + "\n", + " Select the created project and go to \"APIs & services\" page\n", + "\n", + " Click \"+ Enable APIs and services\" button, enable these APIs: Google Drive API, Google Docs API, Google Sheets API, and Google Slides API \n", + "\n", + "3. Go to \"OAuth Consent Screen\" and configure:\n", + "\n", + " Choose External and Fill in app name, dedveloper email, etc.\n", + "\n", + "4. Create OAuth Credentials\n", + "\n", + " Go to APIs & Services > Credentials\n", + "\n", + " Click \"+ Create Credentials\" > \"OAuth client ID\"\n", + "\n", + " Choose Desktop App\n", + "\n", + " Download the generated credentials.json\n", + "\n", + " Sometimes, GCP will navigate you to \"Google Auth Platform\" > \"Clients\", and you can click \"+ Create client\" here to create the OAuth Credentials\n", + "\n", + " \n", + "5. Add Test Users for Gmail API OAuth Access\n", + " \n", + " Go to \"APIs & Services\" > \"OAuth consent screen\" > \"Audience\" > \"Test Users\"\n", + "\n", + " Add the email account from which you want to extract email content.\n", + "\n", + "\n", + "6. Create 'credentials' folders to store google workspace credential and user tokens" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc86bec0-bda8-4e9e-9c85-423179a99981", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install PyPDF2\n", + "# !pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4270e52e-378c-4127-bd52-1d082e9834e0", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff68e06-3cfb-48ae-9dad-fa431d0d548a", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69c20f2d-2f49-408c-8700-f12d6745efd3", + "metadata": {}, + "outputs": [], + "source": [ + "from google_auth_oauthlib.flow import InstalledAppFlow\n", + "from googleapiclient.discovery import build\n", + "from google.oauth2.credentials import Credentials\n", + "from googleapiclient.http import MediaIoBaseDownload\n", + "import os\n", + "\n", + "import io\n", + "from PyPDF2 import PdfReader\n", + "from langchain.vectorstores import Chroma\n", + "from langchain.embeddings import OpenAIEmbeddings\n", + "from langchain.schema import Document\n", + "\n", + "GOOGLE_WORKSPACE_SCOPES = [\"https://www.googleapis.com/auth/drive.readonly\",\n", + " 'https://www.googleapis.com/auth/documents.readonly',\n", + " 'https://www.googleapis.com/auth/spreadsheets.readonly',\n", + " 'https://www.googleapis.com/auth/presentations.readonly'\n", + " ]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7164903b-be81-46b2-8c04-886397599c27", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_google_doc(docs_service, file_id):\n", + " doc = docs_service.documents().get(documentId=file_id).execute()\n", + " content = \"\"\n", + " for elem in doc.get(\"body\", {}).get(\"content\", []):\n", + " if \"paragraph\" in elem:\n", + " for run in elem[\"paragraph\"][\"elements\"]:\n", + " content += run.get(\"textRun\", {}).get(\"content\", \"\")\n", + " return content.strip()\n", + "\n", + "def extract_google_sheet(service, file_id):\n", + " # Get spreadsheet metadata\n", + " spreadsheet = service.spreadsheets().get(spreadsheetId=file_id).execute()\n", + " all_text = \"\"\n", + "\n", + " # Loop through each sheet\n", + " for sheet in spreadsheet.get(\"sheets\", []):\n", + " title = sheet[\"properties\"][\"title\"]\n", + " result = service.spreadsheets().values().get(\n", + " spreadsheetId=file_id,\n", + " range=title\n", + " ).execute()\n", + "\n", + " values = result.get(\"values\", [])\n", + " sheet_text = f\"### Sheet: {title} ###\\n\"\n", + " sheet_text += \"\\n\".join([\", \".join(row) for row in values])\n", + " all_text += sheet_text + \"\\n\\n\"\n", + "\n", + " return all_text.strip()\n", + "\n", + "\n", + "def extract_google_slide(slides_service, file_id):\n", + " pres = slides_service.presentations().get(presentationId=file_id).execute()\n", + " text = \"\"\n", + " for slide in pres.get(\"slides\", []):\n", + " for element in slide.get(\"pageElements\", []):\n", + " shape = element.get(\"shape\")\n", + " if shape:\n", + " for p in shape.get(\"text\", {}).get(\"textElements\", []):\n", + " if \"textRun\" in p:\n", + " text += p[\"textRun\"][\"content\"]\n", + " return text.strip()\n", + "\n", + "def extract_pdf_from_drive(drive_service, file_id, filename='downloaded.pdf'):\n", + " request = drive_service.files().get_media(fileId=file_id)\n", + " fh = io.BytesIO()\n", + " downloader = MediaIoBaseDownload(fh, request)\n", + " done = False\n", + " while not done:\n", + " _, done = downloader.next_chunk()\n", + " fh.seek(0)\n", + " reader = PdfReader(fh)\n", + " return \"\\n\".join([page.extract_text() for page in reader.pages if page.extract_text()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f2edc68-f9f8-4cba-810e-159bea4fe4ac", + "metadata": {}, + "outputs": [], + "source": [ + "def get_creds():\n", + " if os.path.exists(\"token.json\"):\n", + " creds = Credentials.from_authorized_user_file(\"token.json\", SCOPES)\n", + " else:\n", + " flow = InstalledAppFlow.from_client_secrets_file(\"credentials/google_drive_workspace_credentials.json\", SCOPES)\n", + " creds = flow.run_local_server(port=0)\n", + " with open(\"token.json\", \"w\") as token:\n", + " token.write(creds.to_json())\n", + " return creds\n", + " \n", + "\n", + "def get_folder_id_by_name(drive_service, folder_name):\n", + " query = f\"mimeType='application/vnd.google-apps.folder' and name='{folder_name}' and trashed=false\"\n", + " results = drive_service.files().list(\n", + " q=query,\n", + " fields=\"files(id, name)\",\n", + " pageSize=1\n", + " ).execute()\n", + "\n", + " folders = results.get(\"files\", [])\n", + " if not folders:\n", + " raise ValueError(f\"❌ Folder named '{folder_name}' not found.\")\n", + " return folders[0]['id']\n", + "\n", + "\n", + "def extract_docs_from_google_workspace(folder_name):\n", + " info = \"\"\n", + " \n", + " creds = get_creds()\n", + "\n", + " file_types = {\n", + " 'application/vnd.google-apps.document': lambda fid: extract_google_doc(docs_service, fid),\n", + " 'application/vnd.google-apps.spreadsheet': lambda fid: extract_google_sheet(sheets_service, fid),\n", + " 'application/vnd.google-apps.presentation': lambda fid: extract_google_slide(slides_service, fid),\n", + " 'application/pdf': lambda fid: extract_pdf_from_drive(drive_service, fid),\n", + " }\n", + " \n", + " drive_service = build(\"drive\", \"v3\", credentials=creds)\n", + " docs_service = build('docs', 'v1', credentials=creds)\n", + " sheets_service = build('sheets', 'v4', credentials=creds)\n", + " slides_service = build('slides', 'v1', credentials=creds)\n", + "\n", + " folder_id = get_folder_id_by_name(drive_service, folder_name)\n", + " info += f\"Collection files from folder: {folder_name}\\n\"\n", + " \n", + " query = (\n", + " f\"'{folder_id}' in parents and (\"\n", + " 'mimeType=\"application/vnd.google-apps.document\" or '\n", + " 'mimeType=\"application/vnd.google-apps.spreadsheet\" or '\n", + " 'mimeType=\"application/vnd.google-apps.presentation\" or '\n", + " 'mimeType=\"application/pdf\")'\n", + " )\n", + " \n", + " results = drive_service.files().list(\n", + " q=query,\n", + " fields=\"files(id, name, mimeType)\",\n", + " pageSize=20\n", + " ).execute()\n", + "\n", + " docs = []\n", + " summary_info = {\n", + " 'application/vnd.google-apps.document': {'file_type': 'Google Doc', 'count': 0},\n", + " 'application/vnd.google-apps.spreadsheet': {'file_type': 'Google Sheet', 'count': 0},\n", + " 'application/vnd.google-apps.presentation': {'file_type': 'Google Silde', 'count': 0},\n", + " 'application/pdf': {'file_type': 'PDF', 'count': 0}\n", + " }\n", + " for file in results.get(\"files\", []):\n", + " extractor = file_types.get(file['mimeType'])\n", + " if extractor:\n", + " try:\n", + " content = extractor(file[\"id\"])\n", + " if content:\n", + " docs.append(Document(page_content=content, metadata={\"source\": file[\"name\"]}))\n", + " summary_info[file['mimeType']]['count'] += 1\n", + " except Exception as e:\n", + " print(f\"❌ Error processing {file['name']}: {e}\")\n", + " \n", + " total = 0;\n", + " for file_type, element in summary_info.items():\n", + " total += element['count']\n", + " info += f\"Found {element['count']} {element['file_type']} files\\n\"\n", + " info += f\"Total documents loaded: {total}\"\n", + " return docs, info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a9da5c9-415c-4856-973a-627a1790f38d", + "metadata": {}, + "outputs": [], + "source": [ + "docs, info = extract_docs_from_google_workspace(\"google_workspace_knowledge_base\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Outlook_API_Credential_Guide.ipynb b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Outlook_API_Credential_Guide.ipynb new file mode 100644 index 0000000..785d5dd --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Outlook_API_Credential_Guide.ipynb @@ -0,0 +1,178 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "35177005-196a-48b3-bf92-fa37d84693f4", + "metadata": {}, + "source": [ + "# Outlook API Credential Guide" + ] + }, + { + "cell_type": "markdown", + "id": "7bcad9ee-cd11-4b12-834d-9f1ddcefb190", + "metadata": {}, + "source": [ + "Extract Outlook Emails via Microsoft Graph API\n", + "\n", + "1. Register Your App on Azure Portal\n", + "\n", + " Go to Azure Portal > Azure Active Directory > App registrations\n", + "\n", + " Click “New registration”\n", + "\n", + " Choose Mobole/Desktop app\n", + " \n", + " After creation, note the Application (client) ID\n", + "\n", + "2. API Permissions\n", + "\n", + " Go to API permissions tab\n", + "\n", + " Click Add permission > Microsoft Graph > Delegated\n", + "\n", + " Choose: Mail.Read\n", + "\n", + " Click Grant admin consent\n", + "\n", + "3. Allow public client flows\n", + "\n", + " Navigate to: Azure Active Directory > App registrations > Your App\n", + "\n", + " Go to Authentication tab\n", + "\n", + " Under \"Advanced settings\" → \"Allow public client flows\", set to \"Yes\"\n", + "\n", + " Save changes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc86bec0-bda8-4e9e-9c85-423179a99981", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install msal requests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4270e52e-378c-4127-bd52-1d082e9834e0", + "metadata": {}, + "outputs": [], + "source": [ + "from msal import PublicClientApplication\n", + "import os\n", + "from dotenv import load_dotenv\n", + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff68e06-3cfb-48ae-9dad-fa431d0d548a", + "metadata": {}, + "outputs": [], + "source": [ + "load_dotenv()\n", + "\n", + "CLIENT_ID = os.getenv(\"AZURE_CLIENT_ID\")\n", + "AUTHORITY = \"https://login.microsoftonline.com/common\" \n", + "SCOPES = [\"Mail.Read\"]\n", + "\n", + "app = PublicClientApplication(CLIENT_ID, authority=AUTHORITY)\n", + "\n", + "flow = app.initiate_device_flow(scopes=SCOPES)\n", + "print(\"Go to:\", flow[\"verification_uri\"])\n", + "print(\"Enter code:\", flow[\"user_code\"])\n", + "\n", + "result = app.acquire_token_by_device_flow(flow)\n", + "\n", + "if \"access_token\" not in result:\n", + " raise Exception(\"Failed to authenticate:\", result)\n", + "\n", + "access_token = result[\"access_token\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c7f97da-68cc-4923-b280-1ddf7e5b7aa3", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Granted scopes:\", result.get(\"scope\"))\n", + "\n", + "headers = {\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Prefer\": \"outlook.body-content-type='text'\"\n", + "}\n", + "\n", + "query = (\n", + " \"https://graph.microsoft.com/v1.0/me/messages\"\n", + " \"?$top=1\"\n", + " \"&$select=id,subject,receivedDateTime,body\"\n", + ")\n", + "\n", + "all_emails = []\n", + "\n", + "while query:\n", + " response = requests.get(query, headers=headers)\n", + "\n", + " if not response.ok:\n", + " print(response.text)\n", + " print(f\"❌ HTTP {response.status_code}: {response.text}\")\n", + " break\n", + "\n", + " try:\n", + " res = response.json()\n", + " except ValueError:\n", + " print(\"❌ Invalid JSON:\", response.text)\n", + " break\n", + "\n", + " for msg in res.get(\"value\", []):\n", + " all_emails.append({\n", + " \"id\": msg.get(\"id\"),\n", + " \"subject\": msg.get(\"subject\", \"\"),\n", + " \"body\": msg.get(\"body\", {}).get(\"content\", \"\"),\n", + " \"date\": msg.get(\"receivedDateTime\", \"\")\n", + " })\n", + "\n", + " query = res.get(\"@odata.nextLink\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e29493b6-0a9e-4106-93c9-e58ff6aa0f97", + "metadata": {}, + "outputs": [], + "source": [ + "all_emails" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Week5_Exercise_Personal_Knowledge_Assistant.ipynb b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Week5_Exercise_Personal_Knowledge_Assistant.ipynb new file mode 100644 index 0000000..9bab26f --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/Week5_Exercise_Personal_Knowledge_Assistant.ipynb @@ -0,0 +1,1862 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e096ce5d-71a1-4fde-b171-8b9fed16cd7b", + "metadata": {}, + "source": [ + "# Personal Knowledge Assistant" + ] + }, + { + "cell_type": "markdown", + "id": "7bcad9ee-cd11-4b12-834d-9f1ddcefb190", + "metadata": {}, + "source": [ + "## Week 5 exercise\n", + "\n", + "\n", + "### Features:\n", + "1. Chat powered of uploaded knowlege\n", + "\n", + " The system prompt is designed to make the chatbot simulate a person based on the provided documents.\n", + "\n", + "2. Load files from local system\n", + "\n", + " Reuse code from bluebells1 [Wk5-final-multi-doc-type-KB.ipynb](../Wk5-final-multi-doc-type-KB.ipynb). Really appreciate it!\n", + "\n", + " Choose a folder located in the same directory as this script to extract content from. You can also specify subfolders to exclude from the extraction.\n", + "\n", + "3. Load emails from Gmail\n", + "\n", + " Enter an alias first, and a Google popup will guide you to grant permissions and log in, then extract emails for your specified time range\n", + "\n", + "4. Load emails from Outlook\n", + "\n", + " First, enter an alias. After clicking the 'Get Verification Code' button, a URI and code will appear in the 'Verification Instructions' textbox. Visit the Outlook website using the code, and follow the guide to grant permissions and complete the login.\n", + " Then, extract emails for your specified time range\n", + " \n", + "5. Load files from Google Workspace\n", + "\n", + " Enter with an alias first, and Google popup will guide you to grant permissions and log in, then extract emails for your specified folder in your Google Drive\n", + "\n", + "\n", + "### TO-DO Features:\n", + "1. Load messages from Slack\n", + "2. Use local inference/embedding models (llama) instead of relying on OpenAI-hosted models \n", + "3. Optimize Gmail/Outlook/Google Workspace login logic\n", + "4. Label different files. For example, extract prrivate and work emails respectively and store them into different vector stores\n", + "5. Add vector visualization\n", + "\n", + "### Requirements:\n", + "1. Store gmail credential json file under the 'credentials' folder\n", + "\n", + " The setup and configuration steps for Gmail API are in this guide: [Gmail_API_Credential_Guide](./Gmail_API_Credential_Guide.ipynb)\n", + "\n", + "2. Set AZURE_CLIENT_ID in .env file\n", + "\n", + " The setup and configuration steps for Outlook API are in this guide: [Outlook_API_Credential_Guide](./Outlook_API_Credential_Guide.ipynb)\n", + "\n", + "\n", + "3. Store google workspace credential json file under the 'credentials' folder\n", + "\n", + " The setup and configuration steps for Gmail API are in this guide: [Google_Workspace_API_Credential_Guide](./Google_Workspace_API_Credential_Guide.ipynb)\n", + "\n", + "The directories should be structured before launch as follows:\n", + "\n", + " ```text\n", + " The project/\n", + " │\n", + " ├── credentials/ <-- Need to create and store manually before launch; download from Google Cloud Plafotm(GCP)\n", + " │ ├── gmail_credentials.json\n", + " │ └── google_workspace_credentials.json\n", + " ├── tokens/ <-- Automatically created and saved\n", + " │ ├── gmail_tokens \n", + " │ │ └── gmail_token_{alias}.json\n", + " │ ├── google_workspace_tokens\n", + " │ └── outlook_tokens\n", + " ├── vector_index/ <-- Need to create manually before launch\n", + " │ ├── local_vector_index\n", + " │ ├── google_workspace_vector_index\n", + " │ ├── gmail_vector_index\n", + " │ └── output_vector_index\n", + " └── ***.ipynb <-- Script" + ] + }, + { + "cell_type": "markdown", + "id": "99c271af-9054-4066-9583-65a9253cb70a", + "metadata": {}, + "source": [ + "Feel free to contact me via zhufqiu@gmail.com or via [Linkedin](https://www.linkedin.com/in/zhufeng-zephyr-qiu/) if you have any questions about this project. If you have better idea about system prompt, chunk config or search_kwargs, I will be happy to talk with you!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc86bec0-bda8-4e9e-9c85-423179a99981", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install pymupdf\n", + "# !pip install openpyxl\n", + "# !pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4270e52e-378c-4127-bd52-1d082e9834e0", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import base64\n", + "from datetime import datetime\n", + "from email import message_from_bytes\n", + "from email.utils import parsedate_to_datetime\n", + "\n", + "from google.auth.transport.requests import Request\n", + "from google.oauth2.credentials import Credentials\n", + "from google_auth_oauthlib.flow import InstalledAppFlow\n", + "from googleapiclient.discovery import build\n", + "\n", + "from langchain_openai import OpenAIEmbeddings, ChatOpenAI\n", + "from langchain.vectorstores import FAISS\n", + "from langchain.schema import Document\n", + "from langchain.text_splitter import CharacterTextSplitter\n", + "from langchain_chroma import Chroma\n", + "from langchain.memory import ConversationBufferMemory\n", + "from langchain.chains import ConversationalRetrievalChain\n", + "from langchain.chains import ConversationChain\n", + "from langchain.retrievers import MergerRetriever\n", + "from collections import defaultdict\n", + "from langchain.document_loaders import (\n", + " DirectoryLoader, TextLoader, \n", + " Docx2txtLoader,\n", + " TextLoader,\n", + " PyPDFLoader,\n", + " UnstructuredExcelLoader,\n", + " BSHTMLLoader\n", + ")\n", + "import glob\n", + "from dotenv import load_dotenv\n", + "import gradio as gr\n", + "import tiktoken\n", + "\n", + "from msal import PublicClientApplication\n", + "import requests\n", + "from datetime import datetime, timezone\n", + "import json\n", + "import shutil\n", + "\n", + "from PIL import Image\n", + "import pytesseract\n", + "import fitz\n", + "import ebooklib\n", + "from ebooklib import epub\n", + "import io\n", + "\n", + "from langchain.prompts.chat import (\n", + " ChatPromptTemplate,\n", + " SystemMessagePromptTemplate,\n", + " HumanMessagePromptTemplate\n", + ")\n", + "from langchain.prompts import PromptTemplate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3478cbe-2854-4011-b1b4-70be3f1623fd", + "metadata": {}, + "outputs": [], + "source": [ + "MODEL = \"gpt-4o-mini\"\n", + "load_dotenv(override=True)\n", + "os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', 'your-key-if-not-using-env')" + ] + }, + { + "cell_type": "markdown", + "id": "a5195792-f6e1-43a1-9c5f-d6f8c84a253f", + "metadata": {}, + "source": [ + "### If it is your first time to create VECTOR_DIR and its sub-folder, you should create them, close this script and re-open it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ca9623f-fb8c-45d1-a968-370c92762924", + "metadata": {}, + "outputs": [], + "source": [ + "LOCAL_VECTOR_DIR = 'vector_index/local_vector_index'\n", + "GMAIL_VECTOR_DIR = 'vector_index/gmail_vector_index'\n", + "OUTLOOK_VECTOR_DIR = \"vector_index/outlook_vector_index\"\n", + "GOOGLE_WORKSPACE_VECTOR_DIR = 'vector_index/google_workspace_vector_index'\n", + "SLACK_VECTOR_DIR = 'vector_index/slack_vector_index'\n", + "\n", + "os.makedirs(LOCAL_VECTOR_DIR, exist_ok=True)\n", + "os.makedirs(GMAIL_VECTOR_DIR, exist_ok=True)\n", + "os.makedirs(OUTLOOK_VECTOR_DIR, exist_ok=True)\n", + "os.makedirs(GOOGLE_WORKSPACE_VECTOR_DIR, exist_ok=True)" + ] + }, + { + "cell_type": "markdown", + "id": "b0f2a2ee-c9fb-49ad-8e09-919a7a7130ea", + "metadata": {}, + "source": [ + "#### Utilize functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f185451f-2e2a-4ebb-a570-8b7349f3df48", + "metadata": {}, + "outputs": [], + "source": [ + "def get_num_tokens(text, model=\"text-embedding-3-large\"):\n", + " enc = tiktoken.encoding_for_model(model)\n", + " return len(enc.encode(text))\n", + "\n", + "def batch_chunks(chunks, max_tokens=250000, model=\"text-embedding-3-large\"):\n", + " batches = []\n", + " current_batch = []\n", + " current_tokens = 0\n", + "\n", + " for doc in chunks:\n", + " doc_tokens = get_num_tokens(doc.page_content, model)\n", + " if current_tokens + doc_tokens > max_tokens:\n", + " batches.append(current_batch)\n", + " current_batch = [doc]\n", + " current_tokens = doc_tokens\n", + " else:\n", + " current_batch.append(doc)\n", + " current_tokens += doc_tokens\n", + "\n", + " if current_batch:\n", + " batches.append(current_batch)\n", + " \n", + " return batches" + ] + }, + { + "cell_type": "markdown", + "id": "a5546fd7-46bf-4a36-8eef-7b4192f247e9", + "metadata": {}, + "source": [ + "### 1. Local" + ] + }, + { + "cell_type": "markdown", + "id": "937c4f19-5e5b-46b8-b15d-f7ceddd81384", + "metadata": {}, + "source": [ + "Reuse code from bluebells1 [Wk5-final-multi-doc-type-KB.ipynb](../Wk5-final-multi-doc-type-KB.ipynb). Really appreciate it!\n", + "\n", + "Advanced features:\n", + "1. ImgLoader added to load image file (png, jpg, jpeg)\n", + "2. Add logic to use DocumentLoader, extract files and show summary in Gradio textbox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74b85882-c2d6-42af-9079-9f2a61d9eb72", + "metadata": {}, + "outputs": [], + "source": [ + "from ebooklib import epub\n", + "from bs4 import BeautifulSoup\n", + "from langchain.document_loaders.base import BaseLoader\n", + "\n", + "class EpubLoader(BaseLoader):\n", + " def __init__(self, file_path: str):\n", + " self.file_path = file_path\n", + "\n", + " def load(self) -> list[Document]:\n", + " book = epub.read_epub(self.file_path)\n", + " text = ''\n", + " for item in book.get_items():\n", + " if item.get_type() == ebooklib.ITEM_DOCUMENT:\n", + " soup = BeautifulSoup(item.get_content().decode(\"utf-8\"), 'html.parser')\n", + " extracted = soup.get_text().strip()\n", + " if extracted:\n", + " text += extracted + '\\n\\n'\n", + "\n", + " return [Document(page_content=text.strip(), metadata={\"source\": self.file_path})]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85f94e96-83e1-4b5a-ad63-373a37474d25", + "metadata": {}, + "outputs": [], + "source": [ + "from pptx import Presentation\n", + "\n", + "class PptxLoader(BaseLoader):\n", + " def __init__(self, file_path: str):\n", + " self.file_path = file_path\n", + "\n", + " def load(self) -> list[Document]:\n", + " prs = Presentation(self.file_path)\n", + " text = ''\n", + " for slide in prs.slides:\n", + " for shape in slide.shapes:\n", + " if hasattr(shape, \"text\") and shape.text:\n", + " text += shape.text + '\\n'\n", + "\n", + " return [Document(page_content=text, metadata={\"source\": self.file_path})]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd3932ce-5179-4e83-9a2c-bdefc37028aa", + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "import pytesseract\n", + "\n", + "class ImgLoader(BaseLoader):\n", + " def __init__(self, file_path: str):\n", + " self.file_path = file_path\n", + "\n", + " def load(self) -> list[Document]:\n", + " text = ''\n", + " try:\n", + " text = pytesseract.image_to_string(Image.open(self.file_path))\n", + " except Exception as e:\n", + " print(f\"OCR failed for {path}: {e}\")\n", + " return [Document(page_content=text, metadata={\"source\": self.file_path})]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "427e758a-77ab-4de1-ae14-8f2f233ea6db", + "metadata": {}, + "outputs": [], + "source": [ + "# Class based version of document loader which can be expanded more easily for other document types. (Currently includes file types: docx, txt (windows encoding), xlsx, pdfs, epubs, pptx)\n", + "\n", + "class DocumentLoader:\n", + " \"\"\"A clean, extensible document loader for multiple file types.\"\"\"\n", + " \n", + " def __init__(self, base_path, exclude_folders=None):\n", + " self.base_path = base_path\n", + " self.documents = []\n", + " self.exclude_folders = exclude_folders or []\n", + " self.print_info = \"\"\n", + " \n", + " # Configuration for different file types\n", + " self.loader_config = {\n", + " 'docx': {\n", + " 'loader_cls': Docx2txtLoader,\n", + " 'glob_pattern': \"**/*.docx\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'txt': {\n", + " 'loader_cls': TextLoader,\n", + " 'glob_pattern': \"**/*.txt\",\n", + " 'loader_kwargs': {\"encoding\": 'utf-8'},\n", + " 'post_process': None\n", + " },\n", + " 'md': {\n", + " 'loader_cls': TextLoader,\n", + " 'glob_pattern': \"**/*.md\",\n", + " 'loader_kwargs': {\"encoding\": 'utf-8'},\n", + " 'post_process': None\n", + " },\n", + " 'pdf': {\n", + " 'loader_cls': PyPDFLoader,\n", + " 'glob_pattern': \"**/*.pdf\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'xlsx': {\n", + " 'loader_cls': UnstructuredExcelLoader,\n", + " 'glob_pattern': \"**/*.xlsx\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'html': {\n", + " 'loader_cls': BSHTMLLoader,\n", + " 'glob_pattern': \"**/*.html\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'epub': {\n", + " 'loader_cls': EpubLoader,\n", + " 'glob_pattern': \"**/*.epub\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': self._process_epub_metadata\n", + " },\n", + " 'pptx': {\n", + " 'loader_cls': PptxLoader,\n", + " 'glob_pattern': \"**/*.pptx\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'png': {\n", + " 'loader_cls': ImgLoader,\n", + " 'glob_pattern': \"**/*.png\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'jpeg': {\n", + " 'loader_cls': ImgLoader,\n", + " 'glob_pattern': \"**/*.jpeg\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " },\n", + " 'jpg': {\n", + " 'loader_cls': ImgLoader,\n", + " 'glob_pattern': \"**/*.jpg\",\n", + " 'loader_kwargs': {},\n", + " 'post_process': None\n", + " }\n", + " }\n", + " \n", + " def _get_epub_metadata(self, file_path):\n", + " \"\"\"Extract metadata from EPUB files.\"\"\"\n", + " try:\n", + " book = epub.read_epub(file_path)\n", + " title = book.get_metadata('DC', 'title')[0][0] if book.get_metadata('DC', 'title') else None\n", + " author = book.get_metadata('DC', 'creator')[0][0] if book.get_metadata('DC', 'creator') else None\n", + " return title, author\n", + " except Exception as e:\n", + " self.print_info += f\"Error extracting EPUB metadata: {e}\\n\"\n", + " return None, None\n", + " \n", + " def _process_epub_metadata(self, doc) -> None:\n", + " \"\"\"Post-process EPUB documents to add metadata.\"\"\"\n", + " title, author = self._get_epub_metadata(doc.metadata['source'])\n", + " doc.metadata[\"author\"] = author\n", + " doc.metadata[\"title\"] = title\n", + " \n", + " def _load_file_type(self, folder, file_type, config):\n", + " \"\"\"Load documents of a specific file type from a folder.\"\"\"\n", + " try:\n", + " loader = DirectoryLoader(\n", + " folder, \n", + " glob=config['glob_pattern'], \n", + " loader_cls=config['loader_cls'],\n", + " loader_kwargs=config['loader_kwargs']\n", + " )\n", + " docs = loader.load()\n", + " self.print_info += f\"Found {len(docs)} .{file_type} files\\n\"\n", + " \n", + " # Apply post-processing if defined\n", + " if config['post_process']:\n", + " for doc in docs:\n", + " config['post_process'](doc)\n", + " \n", + " return docs\n", + " \n", + " except Exception as e:\n", + " self.print_info += f\"Error loading .{file_type} files: {e}\\n\"\n", + " return []\n", + " \n", + " def load_all(self):\n", + " \"\"\"Load all documents from configured folders.\"\"\"\n", + " all_folders = [f for f in glob.glob(self.base_path) if os.path.isdir(f)]\n", + "\n", + " #filter out excluded folders\n", + " folders = []\n", + " for folder in all_folders:\n", + " folder_name = os.path.basename(folder)\n", + " if folder_name not in self.exclude_folders:\n", + " folders.append(folder)\n", + " else:\n", + " self.print_info += f\"Excluded folder: {folder_name}\\n\"\n", + " \n", + " self.print_info += f\"Scanning folders (directories only):{folders}\\n\" \n", + " \n", + " self.documents = []\n", + " \n", + " for folder in folders:\n", + " doc_type = os.path.basename(folder)\n", + " self.print_info += f\"\\nProcessing folder: {doc_type}\\n\"\n", + " \n", + " for file_type, config in self.loader_config.items():\n", + " docs = self._load_file_type(folder, file_type, config)\n", + " \n", + " # Add doc_type metadata to all documents\n", + " for doc in docs:\n", + " doc.metadata[\"doc_type\"] = doc_type\n", + " self.documents.append(doc)\n", + " \n", + " self.print_info += f\"\\nTotal documents loaded: {len(self.documents)}\\n\"\n", + " return self.documents\n", + " \n", + " def add_file_type(self, extension, loader_cls, glob_pattern=None, \n", + " loader_kwargs=None, post_process=None):\n", + " \"\"\"Add support for a new file type.\"\"\"\n", + " self.loader_config[extension] = {\n", + " 'loader_cls': loader_cls,\n", + " 'glob_pattern': glob_pattern or f\"**/*.{extension}\",\n", + " 'loader_kwargs': loader_kwargs or {},\n", + " 'post_process': post_process\n", + " }\n", + "\n", + "# load\n", + "# loader = DocumentLoader(\"local-knowledge-base/**\", exclude_folders=[\"Music\", \"Online Courses\", \"Fitness\"])\n", + "# documents = loader.load_all()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53e65a63-29fd-4db3-91f0-246cc2b61941", + "metadata": {}, + "outputs": [], + "source": [ + "def local_embed_and_store(docs):\n", + " text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + " chunks = [doc for doc in text_splitter.split_documents(docs) if doc.page_content.strip()]\n", + "\n", + " if not chunks:\n", + " return \"⚠️ No non-empty chunks to embed. Skipping vectorstore update.\"\n", + "\n", + " embeddings = OpenAIEmbeddings()\n", + "\n", + " vectorstore = None\n", + " if os.path.exists(LOCAL_VECTOR_DIR):\n", + " vectorstore = Chroma(persist_directory=LOCAL_VECTOR_DIR, embedding_function=embeddings)\n", + " else:\n", + " if chunks:\n", + " vectorstore = Chroma.from_documents(documents=chunks[:1], embedding=embeddings, persist_directory=LOCAL_VECTOR_DIR)\n", + " chunks = chunks[1:]\n", + " else:\n", + " return \"⚠️ No chunks to create new vectorstore.\"\n", + " \n", + " batches = batch_chunks(chunks)\n", + " total = 1 if not os.path.exists(LOCAL_VECTOR_DIR) else 0\n", + " \n", + " for batch in batches:\n", + " vectorstore.add_documents(batch)\n", + " total += len(batch)\n", + "\n", + " info = \"\"\n", + " info += f\"Vectorstore updated with {total} new chunks.\\n\"\n", + " num_docs = vectorstore._collection.count()\n", + " info += f\"Vectorstore contains {num_docs} chunks.\\n\"\n", + " return info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0a70a0e-08cd-4827-b42b-9a5394ff6dec", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_local_folder(folder_path=\"local-knowledge-base\", exclude=\"\"):\n", + "\n", + " # try:\n", + " info = f\"Process files under: {folder_path}\\n\"\n", + " loader = DocumentLoader(os.path.join(folder_path, \"**\"), exclude_folders=[folder.strip() for folder in exclude.split(',')])\n", + " docs = loader.load_all()\n", + " info += loader.print_info\n", + " if not docs:\n", + " return info + \"No valid files found in the given range.\"\n", + " info += f\"Fetched {len(docs)} files.\\n\"\n", + " info += local_embed_and_store(docs)\n", + " return info\n", + "\n", + " # except Exception as e:\n", + " # return f\"❌ Extraction failed: {str(e)}\"" + ] + }, + { + "cell_type": "markdown", + "id": "0e47d670-8c50-4744-8fbd-78112fa941dd", + "metadata": {}, + "source": [ + "### 2. Gmail" + ] + }, + { + "cell_type": "markdown", + "id": "4d52fe40-65e3-4d82-9999-1ed3e4cbae0a", + "metadata": {}, + "source": [ + "#### Store gmail credential json file under the credentials folder\n", + "\n", + "To avoid complicated steps and focus on LLMs stuff, I chose to utilize the Gmail API in test mode.\n", + "\n", + "I have included the setup and configuration steps in this guide:\n", + "[Gmail_API_Credential_Guide](./Gmail_API_Credential_Guide.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f32c4e4-fa7a-42a1-9ef8-b981af02f585", + "metadata": {}, + "outputs": [], + "source": [ + "GMAIL_SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']\n", + "GMAIL_CREDENTIALS_FILE = 'credentials/gmail_credentials.json'\n", + "GMAIL_TOKEN_DIR = 'tokens/gmail_tokens'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db344254-8c92-4e82-8414-40b3bef56db5", + "metadata": {}, + "outputs": [], + "source": [ + "def gmail_get_credentials(account_alias):\n", + " token_path = os.path.join(GMAIL_TOKEN_DIR, f'gmail_token_{account_alias}.json')\n", + " creds = None\n", + " if os.path.exists(token_path):\n", + " creds = Credentials.from_authorized_user_file(token_path, GMAIL_SCOPES)\n", + " if not creds or not creds.valid:\n", + " if creds and creds.expired and creds.refresh_token:\n", + " creds.refresh(Request())\n", + " else:\n", + " flow = InstalledAppFlow.from_client_secrets_file(GMAIL_CREDENTIALS_FILE, GMAIL_SCOPES)\n", + " creds = flow.run_local_server(port=0)\n", + " with open(token_path, 'w') as token_file:\n", + " token_file.write(creds.to_json())\n", + " return creds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "119558f0-4d35-4737-ad8a-eef516b540d2", + "metadata": {}, + "outputs": [], + "source": [ + "def parse_message(service, msg_id):\n", + " msg = service.users().messages().get(userId='me', id=msg_id, format='raw').execute()\n", + " raw_msg = base64.urlsafe_b64decode(msg['raw'].encode('ASCII'))\n", + " email_message = message_from_bytes(raw_msg)\n", + " subject = email_message['Subject'] or \"(No Subject)\"\n", + " date = parsedate_to_datetime(email_message['Date'])\n", + " sender = email_message['From'] or \"\"\n", + " to = email_message['To'] or \"\"\n", + " cc = email_message['Cc'] or \"\"\n", + " body = \"\"\n", + " \n", + " for part in email_message.walk():\n", + " if part.get_content_type() == 'text/plain' and not part.get('Content-Disposition'):\n", + " body = part.get_payload(decode=True).decode('utf-8', errors='ignore')\n", + " break\n", + "\n", + " content = f\"\"\"Subject: {subject}\n", + " From: {sender}\n", + " To: {to}\n", + " Cc: {cc}\n", + " {body}\n", + " \"\"\"\n", + " return {\n", + " \"id\": msg_id,\n", + " \"subject\": subject,\n", + " \"date\": date,\n", + " \"body\": content\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "481d0500-6270-47ec-bc30-44400c86dff2", + "metadata": {}, + "outputs": [], + "source": [ + "def fetch_emails(service, start_date, end_date):\n", + " query = (\n", + " f\"(category:primary OR is:important OR is:starred OR is:snoozed OR is:sent OR in:chats OR label:SCHEDULED) \"\n", + " f\"after:{start_date} before:{end_date} -in:spam -in:trash -category:promotions -category:forums\"\n", + " ) \n", + " \n", + " all_messages = []\n", + " page_token = None\n", + "\n", + " while True:\n", + " response = service.users().messages().list(userId='me', q=query, pageToken=page_token).execute()\n", + " messages = response.get('messages', [])\n", + " print(f\"Found {len(messages)} sub-messages.\")\n", + " all_messages.extend(messages)\n", + " page_token = response.get('nextPageToken')\n", + " if not page_token:\n", + " break\n", + " print(f\"Total messages fetched: {len(all_messages)}\")\n", + " parsed_emails = []\n", + " for msg in all_messages:\n", + " parsed = parse_message(service, msg['id'])\n", + " if parsed:\n", + " parsed_emails.append(parsed)\n", + " \n", + " return parsed_emails\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aebb1598-e95d-4a7b-9d40-44afb62f587d", + "metadata": {}, + "outputs": [], + "source": [ + "def gmail_embed_and_store(emails, account):\n", + " docs = []\n", + " for email in emails:\n", + " content = f\"Subject: {email['subject']}\\n\\n{email['body']}\"\n", + " doc = Document(\n", + " page_content=content.strip(),\n", + " metadata={\n", + " \"date\": str(email['date']),\n", + " \"gmail_id\": email['id'],\n", + " \"account\": account\n", + " }\n", + " )\n", + " docs.append(doc)\n", + "\n", + " text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + " chunks = [doc for doc in text_splitter.split_documents(docs) if doc.page_content.strip()]\n", + "\n", + " if not chunks:\n", + " return \"⚠️ No non-empty chunks to embed. Skipping vectorstore update.\"\n", + "\n", + " embeddings = OpenAIEmbeddings()\n", + "\n", + " vectorstore = None\n", + " if os.path.exists(GMAIL_VECTOR_DIR):\n", + " vectorstore = Chroma(persist_directory=GMAIL_VECTOR_DIR, embedding_function=embeddings)\n", + " else:\n", + " if chunks:\n", + " vectorstore = Chroma.from_documents(documents=chunks[:1], embedding=embeddings, persist_directory=GMAIL_VECTOR_DIR)\n", + " chunks = chunks[1:]\n", + " else:\n", + " return \"⚠️ No chunks to create new vectorstore.\"\n", + " \n", + " batches = batch_chunks(chunks)\n", + " total = 1 if not os.path.exists(GMAIL_VECTOR_DIR) else 0\n", + " \n", + " for batch in batches:\n", + " vectorstore.add_documents(batch)\n", + " total += len(batch)\n", + "\n", + " info = \"\"\n", + " info += f\"Vectorstore updated with {total} new chunks from {account}.\\n\"\n", + " num_docs = vectorstore._collection.count()\n", + " info += f\"Vectorstore contains {num_docs} chunks.\\n\"\n", + " return info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3d67265-ef62-4104-ae79-783e6d20d31c", + "metadata": {}, + "outputs": [], + "source": [ + "def login_gmail(alias):\n", + " try:\n", + " creds = gmail_get_credentials(alias)\n", + " service = build('gmail', 'v1', credentials=creds)\n", + " profile = service.users().getProfile(userId='me').execute()\n", + " email = profile.get(\"emailAddress\")\n", + "\n", + " # Store in session\n", + " SESSION_STATE[\"gmail_service\"] = service\n", + " SESSION_STATE[\"gmail_email\"] = email\n", + " SESSION_STATE[\"gmail_alias\"] = alias\n", + "\n", + " return f\"✅ Logged in as: {email}\"\n", + " except Exception as e:\n", + " return f\"❌ Login failed: {str(e)}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69cb6320-7ef0-49bb-8893-d51d6d2cd87c", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_gmail(start_date, end_date):\n", + " service = SESSION_STATE.get(\"gmail_service\")\n", + " email_address = SESSION_STATE.get(\"gmail_email\")\n", + "\n", + " if not service:\n", + " return \"❌ Please login first.\"\n", + "\n", + " # try:\n", + " info = f\"Connected to: {email_address}\\n\"\n", + " emails = fetch_emails(service, start_date, end_date)\n", + "\n", + " if not emails:\n", + " return info + \"No emails found in the given range.\"\n", + " info += f\"Fetched {len(emails)} emails.\\n\"\n", + " info += gmail_embed_and_store(emails, account=email_address)\n", + " return info\n", + "\n", + " # except Exception as e:\n", + " # return f\"❌ Extraction failed: {str(e)}\"" + ] + }, + { + "cell_type": "markdown", + "id": "b049fee6-5b51-4458-b089-6a11c6050492", + "metadata": {}, + "source": [ + "### 3. Outlook" + ] + }, + { + "cell_type": "markdown", + "id": "7660ec50-23ca-476f-97f7-42b764de46fa", + "metadata": {}, + "source": [ + "#### Set AZURE_CLIENT_ID in .env file\n", + "\n", + "I have included the setup and configuration steps in this guide:\n", + "[Outlook_API_Credential_Guide](./Outlook_API_Credential_Guide.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1f2b0d2-d2c0-414f-be53-c3bc74ceb6a6", + "metadata": {}, + "outputs": [], + "source": [ + "load_dotenv()\n", + "\n", + "OUTLOOK_TOKEN_DIR = \"tokens/outlook_tokens\"\n", + "OUTLOOK_CLIENT_ID = os.getenv(\"AZURE_CLIENT_ID\")\n", + "OUTLOOK_AUTHORITY = \"https://login.microsoftonline.com/common\" \n", + "OUTLOOK_SCOPES = [\"Mail.Read\", \"User.Read\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2197700b-1103-4ba0-b929-28fea4af6881", + "metadata": {}, + "outputs": [], + "source": [ + "def fetch_outlook_emails(access_token, start_date, end_date):\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {access_token}\",\n", + " \"Prefer\": \"outlook.body-content-type='text'\"\n", + " }\n", + "\n", + " # Filter format: yyyy-mm-ddTHH:MM:SSZ\n", + " query = (\n", + " \"https://graph.microsoft.com/v1.0/me/messages\"\n", + " f\"?$top=100\"\n", + " \"&$select=id,subject,receivedDateTime,body,sender,toRecipients,ccRecipients\"\n", + " )\n", + "\n", + " all_emails = []\n", + "\n", + " while query:\n", + " response = requests.get(query, headers=headers)\n", + " if not response.ok:\n", + " print(f\"❌ HTTP {response.status_code}: {response.text}\")\n", + " break\n", + "\n", + " res = response.json()\n", + " for msg in res.get(\"value\", []):\n", + " received = msg.get(\"receivedDateTime\", \"\")\n", + " try:\n", + " received_dt = datetime.fromisoformat(received.replace(\"Z\", \"+00:00\"))\n", + " except Exception:\n", + " continue\n", + "\n", + " if not (start_date <= received_dt <= end_date):\n", + " continue\n", + "\n", + " email_data = {\n", + " \"id\": msg.get(\"id\"),\n", + " \"subject\": msg.get(\"subject\", \"\"),\n", + " \"body\": msg.get(\"body\", {}).get(\"content\", \"\"),\n", + " \"sender\": msg.get(\"sender\", {}).get(\"emailAddress\", {}).get(\"address\", \"\"),\n", + " \"to\": [r[\"emailAddress\"][\"address\"] for r in msg.get(\"toRecipients\", [])],\n", + " \"cc\": [r[\"emailAddress\"][\"address\"] for r in msg.get(\"ccRecipients\", [])],\n", + " \"date\": received_dt.isoformat()\n", + " }\n", + "\n", + " all_emails.append(email_data)\n", + "\n", + " query = res.get(\"@odata.nextLink\")\n", + "\n", + " print(f\"✅ Total emails extracted: {len(all_emails)}\")\n", + " return all_emails" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d9759ad-47fa-4f3a-8e67-59b59ccccfd9", + "metadata": {}, + "outputs": [], + "source": [ + "def outlook_embed_and_store(emails):\n", + " if not emails:\n", + " return \"No emails to embed.\\n\"\n", + "\n", + " docs = []\n", + " for email in emails:\n", + " content = (\n", + " f\"Subject: {email['subject']}\\n\"\n", + " f\"From: {email['sender']}\\n\"\n", + " f\"To: {', '.join(email['to'])}\\n\"\n", + " f\"CC: {', '.join(email['cc'])}\\n\\n\"\n", + " f\"{email['body']}\"\n", + " )\n", + " doc = Document(\n", + " page_content=content,\n", + " metadata={\"date\": email[\"date\"], \"outlook_id\": email[\"id\"]}\n", + " )\n", + " docs.append(doc)\n", + "\n", + " text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + " chunks = [doc for doc in text_splitter.split_documents(docs) if doc.page_content.strip()]\n", + "\n", + " if not chunks:\n", + " return \"⚠️ No non-empty chunks to embed. Skipping vectorstore update.\"\n", + "\n", + " embeddings = OpenAIEmbeddings()\n", + "\n", + " vectorstore = None\n", + " if os.path.exists(OUTLOOK_VECTOR_DIR):\n", + " vectorstore = Chroma(persist_directory=OUTLOOK_VECTOR_DIR, embedding_function=embeddings)\n", + " else:\n", + " if chunks:\n", + " vectorstore = Chroma.from_documents(documents=chunks[:1], embedding=embeddings, persist_directory=OUTLOOK_VECTOR_DIR)\n", + " chunks = chunks[1:]\n", + " else:\n", + " return \"⚠️ No chunks to create new vectorstore.\\n\"\n", + " \n", + " batches = batch_chunks(chunks)\n", + " total = 1 if not os.path.exists(OUTLOOK_VECTOR_DIR) else 0\n", + " \n", + " for batch in batches:\n", + " vectorstore.add_documents(batch)\n", + " total += len(batch)\n", + "\n", + " info = \"\"\n", + " info += f\"✅ Vectorstore updated with {total} chunks.\\n\"\n", + " num_docs = vectorstore._collection.count()\n", + " info += f\"Vectorstore contains {num_docs} chunks.\\n\"\n", + " return info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1feb49b5-9df0-4232-a233-c6ceb97361a7", + "metadata": {}, + "outputs": [], + "source": [ + "def login_outlook(alias):\n", + " # try:\n", + " token_path = os.path.join(OUTLOOK_TOKEN_DIR, f\"outlook_token_{alias}.json\")\n", + " SESSION_STATE[\"outlook_alias\"] = alias\n", + " access_token = None\n", + "\n", + " # Load existing token\n", + " if os.path.exists(token_path):\n", + " with open(token_path, \"r\") as f:\n", + " result = json.load(f)\n", + " access_token = result.get(\"access_token\")\n", + "\n", + " # If no token, run device flow\n", + " if not access_token:\n", + " app = PublicClientApplication(OUTLOOK_CLIENT_ID, authority=OUTLOOK_AUTHORITY)\n", + " flow = app.initiate_device_flow(scopes=OUTLOOK_SCOPES)\n", + "\n", + " if \"user_code\" not in flow:\n", + " return \"❌ Failed to initiate device login.\"\n", + "\n", + " print(\"🔗 Visit:\", flow[\"verification_uri\"])\n", + " print(\"🔐 Enter code:\", flow[\"user_code\"])\n", + "\n", + " result = app.acquire_token_by_device_flow(flow)\n", + "\n", + " if \"access_token\" not in result:\n", + " return f\"❌ Login failed: {result.get('error_description', 'Unknown error')}\"\n", + "\n", + " access_token = result[\"access_token\"]\n", + "\n", + " with open(token_path, \"w\") as f:\n", + " json.dump(result, f)\n", + "\n", + " # Get user's email via Microsoft Graph\n", + " headers = {\"Authorization\": f\"Bearer {access_token}\"}\n", + " user_info = requests.get(\"https://graph.microsoft.com/v1.0/me\", headers=headers).json()\n", + " email = user_info.get(\"mail\") or user_info.get(\"userPrincipalName\")\n", + "\n", + " # Store in session\n", + " SESSION_STATE[\"outlook_token\"] = access_token\n", + " SESSION_STATE[\"outlook_email\"] = email\n", + "\n", + " return f\"✅ Logged in to Outlook as: {email}\"\n", + "\n", + " # except Exception as e:\n", + " # return f\"❌ Login failed: {str(e)}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e11523b-a757-459c-8c4c-1ceef586439f", + "metadata": {}, + "outputs": [], + "source": [ + "def start_outlook_login(alias):\n", + " token_path = os.path.join(OUTLOOK_TOKEN_DIR, f\"outlook_token_{alias}.json\")\n", + " access_token = None\n", + " SESSION_STATE[\"outlook_token_path\"] = token_path\n", + " \n", + " # Load existing token\n", + " if os.path.exists(token_path):\n", + " return True, \"This alias already verified\"\n", + "\n", + " # If no token, run device flow\n", + " if not access_token:\n", + " app = PublicClientApplication(OUTLOOK_CLIENT_ID, authority=OUTLOOK_AUTHORITY)\n", + " flow = app.initiate_device_flow(scopes=OUTLOOK_SCOPES)\n", + "\n", + " if \"user_code\" not in flow:\n", + " return False, \"❌ Failed to initiate device login.\"\n", + "\n", + " # Store the flow for next step\n", + " SESSION_STATE[\"outlook_alias\"] = alias\n", + " SESSION_STATE[\"outlook_app\"] = app\n", + " SESSION_STATE[\"outlook_flow\"] = flow\n", + " \n", + " msg = f\"🔗 Visit: {flow['verification_uri']}\\n🔐 Enter code: {flow['user_code']}\"\n", + " return False, \"🔄 Waiting for verification...\\n\" + msg\n", + "\n", + "def finish_outlook_login():\n", + " flag = SESSION_STATE.get(\"outlook_login_flag\")\n", + " token_path = SESSION_STATE.get(\"outlook_token_path\")\n", + " if flag:\n", + " with open(token_path, \"r\") as f:\n", + " result = json.load(f)\n", + " access_token = result.get(\"access_token\")\n", + " else: \n", + " app = SESSION_STATE.get(\"outlook_app\")\n", + " flow = SESSION_STATE.get(\"outlook_flow\")\n", + " \n", + " result = app.acquire_token_by_device_flow(flow)\n", + " \n", + " if \"access_token\" not in result:\n", + " return f\"❌ Login failed: {result.get('error_description', 'Unknown error')}\"\n", + " \n", + " access_token = result[\"access_token\"]\n", + " \n", + " with open(token_path, \"w\") as f:\n", + " json.dump(result, f)\n", + " \n", + "\n", + " # Get user's email via Microsoft Graph\n", + " headers = {\"Authorization\": f\"Bearer {access_token}\"}\n", + " user_info = requests.get(\"https://graph.microsoft.com/v1.0/me\", headers=headers).json()\n", + " email = user_info.get(\"mail\") or user_info.get(\"userPrincipalName\")\n", + "\n", + " # Store in session\n", + " SESSION_STATE[\"outlook_token\"] = access_token\n", + " SESSION_STATE[\"outlook_email\"] = email\n", + "\n", + " return f\"✅ Logged in to Outlook as: {email}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3faea0d-723d-41e3-9683-db92dd918aba", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_outlook_emails(start, end, alias):\n", + " try:\n", + " start_date = datetime.strptime(start.strip(), \"%Y/%m/%d\").replace(tzinfo=timezone.utc)\n", + " end_date = datetime.strptime(end.strip(), \"%Y/%m/%d\").replace(tzinfo=timezone.utc)\n", + " except ValueError:\n", + " return \"❌ Invalid date format. Use YYYY/MM/DD.\"\n", + "\n", + " access_token = SESSION_STATE[\"outlook_token\"]\n", + "\n", + " if not access_token:\n", + " return f\"❌ No access token found for '{alias}'. Please login first.\"\n", + "\n", + " info = \"\"\n", + " try:\n", + " emails = fetch_outlook_emails(access_token, start_date, end_date)\n", + " if not emails:\n", + " return f\"❌ No email found.\"\n", + " info += f\"✅ Extracted and embedded {len(emails)} Outlook emails.\\n\"\n", + " info += outlook_embed_and_store(emails)\n", + " return info\n", + " except Exception as e:\n", + " return f\"❌ Error: {str(e)}\"\n" + ] + }, + { + "cell_type": "markdown", + "id": "0c030701-8f16-4101-a501-f310ce61871c", + "metadata": {}, + "source": [ + "### 4. Google Workspace" + ] + }, + { + "cell_type": "markdown", + "id": "4b04baa3-0dfe-491a-974e-c1b97c978031", + "metadata": {}, + "source": [ + "#### Store google workspace credential json file under the credentials folder\n", + "\n", + "To avoid complicated steps and focus on LLMs stuff, I chose to utilize the Google Drive/Workspace API in test mode.\n", + "\n", + "I have included the setup and configuration steps in this guide:\n", + "[Google_Workspace_API_Credential_Guide](./Google_Workspace_API_Credential_Guide.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1aeb8a99-d039-4550-8cac-f9370e7d7401", + "metadata": {}, + "outputs": [], + "source": [ + "GOOGLE_WORKSPACE_SCOPES = [\n", + " 'https://www.googleapis.com/auth/gmail.readonly',\n", + " 'https://www.googleapis.com/auth/drive.readonly',\n", + " 'https://www.googleapis.com/auth/documents.readonly',\n", + " 'https://www.googleapis.com/auth/spreadsheets.readonly',\n", + " 'https://www.googleapis.com/auth/presentations.readonly'\n", + "]\n", + "GOOGLE_WORKSPACE_CREDENTIALS_FILE = 'credentials/google_drive_workspace_credentials.json'\n", + "GOOGLE_WORKSPACE_TOKEN_DIR = 'tokens/google_workspace_tokens'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0d7c1ad3-d288-42a7-bc7b-4ddae0f3aaa3", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_google_doc(docs_service, file_id):\n", + " doc = docs_service.documents().get(documentId=file_id).execute()\n", + " content = \"\"\n", + " for elem in doc.get(\"body\", {}).get(\"content\", []):\n", + " if \"paragraph\" in elem:\n", + " for run in elem[\"paragraph\"][\"elements\"]:\n", + " content += run.get(\"textRun\", {}).get(\"content\", \"\")\n", + " return content.strip()\n", + "\n", + "def extract_google_sheet(service, file_id):\n", + " # Get spreadsheet metadata\n", + " spreadsheet = service.spreadsheets().get(spreadsheetId=file_id).execute()\n", + " all_text = \"\"\n", + "\n", + " # Loop through each sheet\n", + " for sheet in spreadsheet.get(\"sheets\", []):\n", + " title = sheet[\"properties\"][\"title\"]\n", + " result = service.spreadsheets().values().get(\n", + " spreadsheetId=file_id,\n", + " range=title\n", + " ).execute()\n", + "\n", + " values = result.get(\"values\", [])\n", + " sheet_text = f\"### Sheet: {title} ###\\n\"\n", + " sheet_text += \"\\n\".join([\", \".join(row) for row in values])\n", + " all_text += sheet_text + \"\\n\\n\"\n", + "\n", + " return all_text.strip()\n", + "\n", + "\n", + "def extract_google_slide(slides_service, file_id):\n", + " pres = slides_service.presentations().get(presentationId=file_id).execute()\n", + " text = \"\"\n", + " for slide in pres.get(\"slides\", []):\n", + " for element in slide.get(\"pageElements\", []):\n", + " shape = element.get(\"shape\")\n", + " if shape:\n", + " for p in shape.get(\"text\", {}).get(\"textElements\", []):\n", + " if \"textRun\" in p:\n", + " text += p[\"textRun\"][\"content\"]\n", + " return text.strip()\n", + "\n", + "def extract_pdf_from_drive(drive_service, file_id):\n", + " request = drive_service.files().get_media(fileId=file_id)\n", + " fh = io.BytesIO()\n", + " downloader = MediaIoBaseDownload(fh, request)\n", + " done = False\n", + " while not done:\n", + " _, done = downloader.next_chunk()\n", + " fh.seek(0)\n", + " reader = PdfReader(fh)\n", + " return \"\\n\".join([page.extract_text() for page in reader.pages if page.extract_text()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "005640a5-b7b3-4397-be1c-d86dccafdc61", + "metadata": {}, + "outputs": [], + "source": [ + "def login_google_workspace(alias):\n", + " try:\n", + " creds = google_workspace_get_creds(alias)\n", + " service = build('gmail', 'v1', credentials=creds)\n", + " profile = service.users().getProfile(userId='me').execute()\n", + " email = profile.get(\"emailAddress\")\n", + "\n", + " drive_service = build(\"drive\", \"v3\", credentials=creds)\n", + " docs_service = build('docs', 'v1', credentials=creds)\n", + " sheets_service = build('sheets', 'v4', credentials=creds)\n", + " slides_service = build('slides', 'v1', credentials=creds)\n", + "\n", + " # Store in session\n", + " SESSION_STATE[\"google_workspace_drive_service\"] = drive_service\n", + " SESSION_STATE[\"google_workspace_docs_service\"] = docs_service\n", + " SESSION_STATE[\"google_workspace_sheets_service\"] = sheets_service\n", + " SESSION_STATE[\"google_workspace_slides_service\"] = slides_service\n", + " SESSION_STATE[\"google_workspace_email\"] = email\n", + " SESSION_STATE[\"google_workspace_alias\"] = alias\n", + "\n", + " return f\"✅ Logged in as: {email}\"\n", + " except Exception as e:\n", + " return f\"❌ Login failed: {str(e)}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2677d0aa-d61d-45e2-994a-b4707d839b48", + "metadata": {}, + "outputs": [], + "source": [ + "def google_workspace_get_creds(account_alias):\n", + " token_path = os.path.join(GOOGLE_WORKSPACE_TOKEN_DIR, f'google_workspace_token_{account_alias}.json')\n", + " \n", + " if os.path.exists(token_path):\n", + " creds = Credentials.from_authorized_user_file(token_path, GOOGLE_WORKSPACE_SCOPES)\n", + " else:\n", + " flow = InstalledAppFlow.from_client_secrets_file(GOOGLE_WORKSPACE_CREDENTIALS_FILE, GOOGLE_WORKSPACE_SCOPES)\n", + " creds = flow.run_local_server(port=0)\n", + " with open(\"token.json\", \"w\") as token:\n", + " token.write(creds.to_json())\n", + " return creds\n", + " \n", + "\n", + "def get_folder_id_by_name(drive_service, folder_name):\n", + " query = f\"mimeType='application/vnd.google-apps.folder' and name='{folder_name}' and trashed=false\"\n", + " results = drive_service.files().list(\n", + " q=query,\n", + " fields=\"files(id, name)\",\n", + " pageSize=1\n", + " ).execute()\n", + "\n", + " folders = results.get(\"files\", [])\n", + " if not folders:\n", + " raise ValueError(f\"❌ Folder named '{folder_name}' not found.\")\n", + " return folders[0]['id']\n", + "\n", + "\n", + "def extract_docs_from_google_workspace(folder_name):\n", + " info = \"\"\n", + "\n", + " file_types = {\n", + " 'application/vnd.google-apps.document': lambda fid: extract_google_doc(docs_service, fid),\n", + " 'application/vnd.google-apps.spreadsheet': lambda fid: extract_google_sheet(sheets_service, fid),\n", + " 'application/vnd.google-apps.presentation': lambda fid: extract_google_slide(slides_service, fid),\n", + " 'application/pdf': lambda fid: extract_pdf_from_drive(drive_service, fid),\n", + " }\n", + "\n", + " drive_service = SESSION_STATE.get(\"google_workspace_drive_service\")\n", + " docs_service = SESSION_STATE.get(\"google_workspace_docs_service\")\n", + " sheets_service = SESSION_STATE.get(\"google_workspace_sheets_service\")\n", + " slides_service = SESSION_STATE.get(\"google_workspace_slides_service\")\n", + " \n", + " if not drive_service or not docs_service or not sheets_service or not slides_service: \n", + " return None, \"Please login first.\\n\"\n", + " \n", + "\n", + " folder_id = get_folder_id_by_name(drive_service, folder_name)\n", + " print(\"folder_id\")\n", + " print(folder_id)\n", + " info += f\"Collection files from folder: {folder_name}\\n\"\n", + " \n", + " query = (\n", + " f\"'{folder_id}' in parents and (\"\n", + " 'mimeType=\"application/vnd.google-apps.document\" or '\n", + " 'mimeType=\"application/vnd.google-apps.spreadsheet\" or '\n", + " 'mimeType=\"application/vnd.google-apps.presentation\" or '\n", + " 'mimeType=\"application/pdf\")'\n", + " )\n", + " \n", + " results = drive_service.files().list(\n", + " q=query,\n", + " fields=\"files(id, name, mimeType)\",\n", + " pageSize=20\n", + " ).execute()\n", + "\n", + " docs = []\n", + " summary_info = {\n", + " 'application/vnd.google-apps.document': {'file_type': 'Google Doc', 'count': 0},\n", + " 'application/vnd.google-apps.spreadsheet': {'file_type': 'Google Sheet', 'count': 0},\n", + " 'application/vnd.google-apps.presentation': {'file_type': 'Google Silde', 'count': 0},\n", + " 'application/pdf': {'file_type': 'PDF', 'count': 0}\n", + " }\n", + " for file in results.get(\"files\", []):\n", + " print(file['mimeType'])\n", + " extractor = file_types.get(file['mimeType'])\n", + " if extractor:\n", + " try:\n", + " content = extractor(file[\"id\"])\n", + " if content:\n", + " docs.append(Document(page_content=content, metadata={\"source\": file[\"name\"]}))\n", + " summary_info[file['mimeType']]['count'] += 1\n", + " print(file['mimeType'])\n", + " print(summary_info[file['mimeType']]['count'])\n", + " except Exception as e:\n", + " print(f\"❌ Error processing {file['name']}: {e}\")\n", + " \n", + " total = 0;\n", + " for file_type, element in summary_info.items():\n", + " total += element['count']\n", + " info += f\"Found {element['count']} {element['file_type']} files\\n\"\n", + " info += f\"Total documents loaded: {total}\\n\"\n", + " return docs, info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5e7aee3-a7fe-4dd1-ada4-e7290cb1d1c4", + "metadata": {}, + "outputs": [], + "source": [ + "def google_workspace_embed_and_store(docs):\n", + " text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)\n", + " chunks = [doc for doc in text_splitter.split_documents(docs) if doc.page_content.strip()]\n", + "\n", + " if not chunks:\n", + " return \"⚠️ No non-empty chunks to embed. Skipping vectorstore update.\"\n", + "\n", + " embeddings = OpenAIEmbeddings()\n", + "\n", + " vectorstore = None\n", + " if os.path.exists(GOOGLE_WORKSPACE_VECTOR_DIR):\n", + " vectorstore = Chroma(persist_directory=GOOGLE_WORKSPACE_VECTOR_DIR, embedding_function=embeddings)\n", + " else:\n", + " if chunks:\n", + " vectorstore = Chroma.from_documents(documents=chunks[:1], embedding=embeddings, persist_directory=GOOGLE_WORKSPACE_VECTOR_DIR)\n", + " chunks = chunks[1:]\n", + " else:\n", + " return \"⚠️ No chunks to create new vectorstore.\"\n", + " \n", + " batches = batch_chunks(chunks)\n", + " total = 1 if not os.path.exists(GOOGLE_WORKSPACE_VECTOR_DIR) else 0\n", + " \n", + " for batch in batches:\n", + " vectorstore.add_documents(batch)\n", + " total += len(batch)\n", + "\n", + " info = \"\"\n", + " info += f\"Vectorstore updated with {total} new chunks.\\n\"\n", + " num_docs = vectorstore._collection.count()\n", + " info += f\"Vectorstore contains {num_docs} chunks.\\n\"\n", + " return info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9fd47dcc-03be-4ff2-8e13-406067242c0d", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_google_workspace_folder(folder_path):\n", + "\n", + " # try:\n", + " info = f\"Process files under: {folder_path}\\n\"\n", + " docs, embed_store_info = extract_docs_from_google_workspace(folder_path)\n", + " info += embed_store_info\n", + " if not docs:\n", + " return info + \"No valid files found in the given range.\"\n", + " info += f\"Fetched {len(docs)} files.\\n\"\n", + " info += google_workspace_embed_and_store(docs)\n", + " return info\n", + "\n", + " # except Exception as e:\n", + " # return f\"❌ Extraction failed: {str(e)}\"" + ] + }, + { + "cell_type": "markdown", + "id": "59794946-dfdd-40b7-909d-f8290d628242", + "metadata": {}, + "source": [ + "### 5. Slack" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33c6bf19-f685-4654-9fda-06ec32afd2e5", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "9de15f01-7749-46df-9526-306c51310797", + "metadata": {}, + "source": [ + "### 6. Gradio UI" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d613a92f-e16b-4cc0-a454-7fbae162f27b", + "metadata": {}, + "outputs": [], + "source": [ + "VECTOR_DIR = [LOCAL_VECTOR_DIR, GMAIL_VECTOR_DIR, OUTLOOK_VECTOR_DIR, GOOGLE_WORKSPACE_VECTOR_DIR, SLACK_VECTOR_DIR]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d6f2df3-8471-443c-94da-dde496a9a02d", + "metadata": {}, + "outputs": [], + "source": [ + "# system prompt\n", + "prompt_template = PromptTemplate(\n", + " input_variables=[\"question\", \"context\", \"chat_history\"],\n", + " template=\"\"\"\n", + "You are a personal assistant trained on the user's private documents, emails, and notes.\n", + "Your role is to answer questions as if you are the user themself — based on their experiences, thoughts, habits, personality, and preferences reflected in the uploaded materials.\n", + "Also, you are having a conversation with the user. Use the chat history to understand the context of the conversation.\n", + "At the beginning of each conversation, ask the user what name they would like to assign to you. If the user later requests a name change, update your name accordingly without delay.\n", + "\n", + "Use the retrieved documents to:\n", + "- Summarize the user's background, actions, and communication patterns\n", + "- Simulate how the user would respond to questions\n", + "- Infer personality traits, professional history, and personal interests\n", + "\n", + "Always cite the type of source (e.g., email, resume, journal) when appropriate. If no relevant information is available, say so honestly.\n", + "\n", + "You must never make assumptions beyond what the user's data reveals.\n", + "\n", + "Chat History:\n", + "{chat_history}\n", + "\n", + "Retrieved Context:\n", + "{context}\n", + "\n", + "User Question:\n", + "{question}\n", + "\"\"\"\n", + ")\n", + "\n", + "llm = ChatOpenAI(temperature=0.7, model_name=MODEL)\n", + "memory = ConversationBufferMemory(memory_key='chat_history', return_messages=True)\n", + "embeddings = OpenAIEmbeddings()\n", + "retrievers = []\n", + "for vec_dir in VECTOR_DIR:\n", + " if os.path.exists(vec_dir):\n", + " vectorstore = Chroma(persist_directory=vec_dir, embedding_function=embeddings)\n", + " retriever = vectorstore.as_retriever(search_kwargs={\"k\": 10})\n", + " retrievers.append(retriever)\n", + "\n", + "merged_retriever = MergerRetriever(retrievers=retrievers)\n", + "conversation_chain = ConversationalRetrievalChain.from_llm(\n", + " llm=llm, \n", + " retriever=merged_retriever, \n", + " memory=memory,\n", + " combine_docs_chain_kwargs={\"prompt\": prompt_template}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30861ea3-1005-4fe0-b06b-060571b382bc", + "metadata": {}, + "outputs": [], + "source": [ + "def chat_with_rag(user_input, chat_history):\n", + " result = conversation_chain.invoke({\"question\": user_input})\n", + " answer = result[\"answer\"]\n", + " chat_history.append({\"role\": \"user\", \"content\": user_input})\n", + " chat_history.append({\"role\": \"assistant\", \"content\": answer})\n", + " return \"\", chat_history" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55be9260-4a05-4f7b-990a-3979ea0a49c3", + "metadata": {}, + "outputs": [], + "source": [ + "def delete_knowledge(delete_type):\n", + " global conversation_chain, retrievers\n", + " \n", + " if delete_type == \"Local Folder\":\n", + " vector_dir = LOCAL_VECTOR_DIR\n", + " elif delete_type == \"Gmail\":\n", + " vector_dir = GMAIL_VECTOR_DIR\n", + " elif delete_type == \"Outlook\":\n", + " vector_dir = OUTLOOK_VECTOR_DIR\n", + " elif delete_type == \"Google Workspace\":\n", + " vector_dir = GOOGLE_WORKSPACE_VECTOR_DIR\n", + " elif delete_type == \"Slack\":\n", + " vector_dir = SLACK_VECTOR_DIR\n", + " \n", + " if os.path.exists(vector_dir):\n", + " Chroma(persist_directory=vector_dir, embedding_function=embeddings).delete_collection()\n", + " retrievers = []\n", + " for vec_dir in VECTOR_DIR:\n", + " if os.path.exists(vec_dir):\n", + " vectorstore = Chroma(persist_directory=vec_dir, embedding_function=embeddings)\n", + " retriever = vectorstore.as_retriever(search_kwargs={\"k\": 10})\n", + " retrievers.append(retriever)\n", + " \n", + " merged_retriever = MergerRetriever(retrievers=retrievers)\n", + " conversation_chain = ConversationalRetrievalChain.from_llm(llm=llm, retriever=merged_retriever, memory=memory)\n", + " return \"Deleted successfully.\"\n", + " else:\n", + " return \"Vector store does not exist.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02f80830-8dd2-4a6a-aca3-0c79f28e703a", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "with gr.Blocks(title=\"Personla Knowledge Assistant\", theme=gr.themes.Citrus(), css=\"\"\"\n", + ".selected {\n", + " background-color: orange !important;\n", + " box-shadow: 0 4px 12px rgba(255, 140, 0, 0.5) !important;\n", + " color: black;\n", + "}\n", + ".unselected {\n", + " background-color: gray !important;\n", + " box-shadow: 0 4px 12px rgba(128, 128, 128, 0.4);\n", + " color: white;\n", + "}\n", + ".gr-button-stop {\n", + " background-color: #cf142b !important;\n", + " color: white !important;\n", + " box-shadow: 0 4px 12px rgba(128, 128, 128, 0.4);\n", + "}\n", + "\"\"\") as ui:\n", + " SESSION_STATE = {\n", + " \"gmail_service\": None, \"gmail_email\": None, \"gmail_alias\": None,\n", + " \"outlook_email\": None, \"outlook_alias\": None,\n", + " \"outlook_login_app\": None, \"outlook_login_flow\": None,\n", + " \"outlook_token_path\": None,\n", + " \"google_workspace_email\": None, \"google_workspace_alias\": None, \n", + " \"google_workspace_drive_service\": None, \"google_workspace_docs_service\": None,\n", + " \"google_workspace_sheets_service\": None, \"google_workspace_slides_service\": None\n", + " }\n", + " outlook_login_flag = gr.State(False)\n", + " current_selected = gr.State(\"\")\n", + " section_names = [\"Local Folder\", \"Gmail\", \"Outlook\", \"Google Workspace\", \"Slack\"]\n", + "\n", + " def show_section(current_selected, current_section):\n", + " updates = []\n", + " if current_selected == current_section:\n", + "\n", + " for sec in section_names:\n", + " updates.append(gr.update(visible=False))\n", + " for sec in section_names:\n", + " updates.append(gr.update(elem_classes=[\"unselected\"]))\n", + " updates.append(\"\")\n", + " else:\n", + " updates = []\n", + " for sec in section_names:\n", + " if sec == current_selected:\n", + " updates.append(gr.update(visible=True))\n", + " else:\n", + " updates.append(gr.update(visible=False))\n", + " for sec in section_names:\n", + " if sec == current_selected:\n", + " updates.append(gr.update(elem_classes=[\"selected\"]))\n", + " else:\n", + " updates.append(gr.update(elem_classes=[\"unselected\"]))\n", + " updates.append(current_selected)\n", + " return tuple(updates)\n", + "\n", + " \n", + " \n", + " gr.Markdown(\"## Personal Knowledge Assistant\")\n", + "\n", + " chatbot = gr.Chatbot(label=\"Chat\", show_copy_button=True, type=\"messages\")\n", + " user_input = gr.Textbox(\n", + " placeholder=\"Talk with your personal knowledge assistant...\",\n", + " label=\"Enter Message\",\n", + " lines=1\n", + " )\n", + " user_input.submit(\n", + " fn=chat_with_rag,\n", + " inputs=[user_input, chatbot],\n", + " outputs=[user_input, chatbot]\n", + " )\n", + " \n", + " gr.HTML(\"
\")\n", + "\n", + " with gr.Row():\n", + " local_folder_show_up = gr.Button(\"Local folder\", elem_id=\"local-folder-btn\", elem_classes=[\"unselected\"])\n", + " gmail_show_up = gr.Button(\"Gmail\", elem_id=\"gmail-btn\", elem_classes=[\"unselected\"])\n", + " outlook_show_up = gr.Button(\"Outlook\", elem_id=\"outlook-btn\", elem_classes=[\"unselected\"])\n", + " google_workspace_show_up = gr.Button(\"Google Workspace\", elem_id=\"google_workspace-btn\", elem_classes=[\"unselected\"])\n", + " slack_show_up = gr.Button(\"Slack\", elem_id=\"Slack-btn\", elem_classes=[\"unselected\"])\n", + " \n", + " local_input = gr.Textbox(value=\"Local Folder\", visible=False)\n", + " gmail_input = gr.Textbox(value=\"Gmail\", visible=False)\n", + " outlook_input = gr.Textbox(value=\"Outlook\", visible=False)\n", + " workspace_input = gr.Textbox(value=\"Google Workspace\", visible=False)\n", + " slack_input = gr.Textbox(value=\"Slack\", visible=False)\n", + " \n", + " local_folder_section = gr.Column(visible=False)\n", + " gmail_section = gr.Column(visible=False)\n", + " outlook_section = gr.Column(visible=False)\n", + " google_workspace_section = gr.Column(visible=False)\n", + " slack_section = gr.Column(visible=False)\n", + "\n", + "\n", + " with local_folder_section:\n", + " gr.Markdown(\"### Local Documents Extractor\")\n", + "\n", + " with gr.Row():\n", + " local_folder_input = gr.Textbox(label=\"Folder Path\", info=\"All subfolders under the selected folder will be extracted.\", value=\"local-knowledge-base\")\n", + " with gr.Row():\n", + " local_exclude_folder_input = gr.Textbox(label=\"Folders to Exclude\", info=\"\\u00A0\", placeholder=\"Join by comma. e.g. dir1, dir2\")\n", + " with gr.Row(): \n", + " local_extract_button = gr.Button(\"Extract Local Documents\")\n", + " with gr.Row(): \n", + " local_extract_log = gr.Textbox(label=\"Extraction Log\", lines=15)\n", + "\n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row(): \n", + " local_delete_button = gr.Button(\"Delete Local Knowledge\", elem_classes=[\"gr-button-stop\"])\n", + " with gr.Row(): \n", + " local_delete_log = gr.Textbox(label=\"Delete Log\", lines=1)\n", + " \n", + " local_delete_button.click(fn=delete_knowledge, inputs=local_input, outputs=local_delete_log)\n", + " local_extract_button.click(fn=extract_local_folder, inputs=[local_folder_input, local_exclude_folder_input], outputs=local_extract_log)\n", + " \n", + " with gmail_section:\n", + " gr.Markdown(\"### Local Documents Extractor\")\n", + " \n", + " with gr.Row():\n", + " gmail_alias_input = gr.Textbox(label=\"Gmail Alias (e.g., zhufqiu)\", placeholder=\"Gmail alias\") \n", + " with gr.Row():\n", + " gmail_login_log = gr.Textbox(label=\"Login Status\", lines=1)\n", + " with gr.Row():\n", + " gmail_login_btn = gr.Button(\"Login\")\n", + " \n", + " gr.HTML(\"
\")\n", + "\n", + " with gr.Row():\n", + " gmail_start_date = gr.Textbox(label=\"Start Date (YYYY/MM/DD)\")\n", + " gmail_end_date = gr.Textbox(label=\"End Date (YYYY/MM/DD)\")\n", + " with gr.Row(): \n", + " gmail_extract_btn = gr.Button(\"Extract Gmail Emails\")\n", + " with gr.Row(): \n", + " gmail_extract_log = gr.Textbox(label=\"Extraction Log\", lines=15)\n", + "\n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row(): \n", + " gmail_delete_button = gr.Button(\"Delete Gmail Knowledge\", elem_classes=[\"gr-button-stop\"])\n", + " with gr.Row(): \n", + " gmail_delete_log = gr.Textbox(label=\"Delete Log\", lines=1)\n", + " \n", + " gmail_delete_button.click(fn=delete_knowledge, inputs=gmail_input, outputs=gmail_delete_log)\n", + " gmail_login_btn.click(fn=login_gmail, inputs=gmail_alias_input, outputs=gmail_login_log)\n", + " gmail_extract_btn.click(fn=extract_gmail, inputs=[gmail_start_date, gmail_end_date], outputs=gmail_extract_log)\n", + " \n", + " with outlook_section:\n", + " gr.Markdown(\"### Outlook Email Extractor\")\n", + "\n", + " with gr.Row():\n", + " outlook_alias = gr.Textbox(label=\"Outlook Alias(e.g., zhufqiu)\", placeholder=\"Outlook alias\")\n", + "\n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row():\n", + " outlook_verify_info = gr.Textbox(label=\"Verification Instructions\", lines=3)\n", + " with gr.Row():\n", + " outlook_start_login_btn = gr.Button(\"Get Verification Code\")\n", + "\n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row():\n", + " outlook_login_log = gr.Textbox(label=\"Login Status\", info=\"\", lines=1)\n", + " with gr.Row():\n", + " outlook_finish_login_btn = gr.Button(\"Login\")\n", + " \n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row():\n", + " outlook_start_date = gr.Textbox(label=\"Start Date (YYYY/MM/DD)\")\n", + " outlook_end_date = gr.Textbox(label=\"End Date (YYYY/MM/DD)\")\n", + " \n", + " with gr.Row():\n", + " outlook_extract_btn = gr.Button(\"Extract Outlook Emails\")\n", + " \n", + " with gr.Row():\n", + " outlook_log = gr.Textbox(label=\"Extraction Log\", lines=15)\n", + "\n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row(): \n", + " outlook_delete_button = gr.Button(\"Delete Outlook Knowledge\", elem_classes=[\"gr-button-stop\"])\n", + " with gr.Row(): \n", + " outlook_delete_log = gr.Textbox(label=\"Delete Log\", lines=1)\n", + " \n", + " outlook_delete_button.click(fn=delete_knowledge, inputs=outlook_input, outputs=outlook_delete_log)\n", + " outlook_start_login_btn.click(fn=start_outlook_login, inputs=outlook_alias, outputs=[outlook_login_flag, outlook_verify_info])\n", + " outlook_finish_login_btn.click(fn=finish_outlook_login, outputs=outlook_login_log)\n", + " outlook_extract_btn.click(fn=extract_outlook_emails, inputs=[outlook_start_date, outlook_end_date], outputs=outlook_log)\n", + "\n", + " with google_workspace_section:\n", + " gr.Markdown(\"### Google Workspace Extractor\")\n", + "\n", + " with gr.Row():\n", + " google_workspace_alias_input = gr.Textbox(label=\"Google Account Alias (e.g., zhufqiu)\", placeholder=\"Google Account alias\") \n", + " with gr.Row():\n", + " google_workspace_login_log = gr.Textbox(label=\"Login Status\", lines=1)\n", + " with gr.Row():\n", + " google_workspace_login_btn = gr.Button(\"Login\")\n", + " \n", + " gr.HTML(\"
\")\n", + "\n", + " with gr.Row():\n", + " google_workspace_folder_input = gr.Textbox(label=\"Folder Path\", info=\"All files under the selected folder will be extracted.\", value=\"google_workspace_knowledge_base\")\n", + " with gr.Row(): \n", + " google_workspace_extract_button = gr.Button(\"Extract Google Workspace Documents\")\n", + " \n", + " with gr.Row(): \n", + " google_workspace_extract_log = gr.Textbox(label=\"Extraction Log\", lines=15)\n", + " \n", + " gr.HTML(\"
\")\n", + " \n", + " with gr.Row(): \n", + " google_workspace_delete_button = gr.Button(\"Delete Google Workspace Knowledge\", elem_classes=[\"gr-button-stop\"])\n", + " with gr.Row(): \n", + " google_workspace_delete_log = gr.Textbox(label=\"Delete Log\", lines=1)\n", + " \n", + " google_workspace_delete_button.click(fn=delete_knowledge, inputs=workspace_input, outputs=google_workspace_delete_log)\n", + " google_workspace_login_btn.click(fn=login_google_workspace, inputs=google_workspace_alias_input, outputs=google_workspace_login_log)\n", + " google_workspace_extract_button.click(fn=extract_google_workspace_folder, inputs=google_workspace_folder_input, outputs=google_workspace_extract_log)\n", + " \n", + " with slack_section:\n", + " gr.Markdown(\"Slack part\")\n", + " gr.Markdown(\"To be developed\")\n", + " \n", + " switch_outputs = [\n", + " local_folder_section, gmail_section, outlook_section, google_workspace_section, slack_section,\n", + " local_folder_show_up, gmail_show_up, outlook_show_up, google_workspace_show_up, slack_show_up,\n", + " current_selected\n", + " ]\n", + "\n", + " gmail_show_up.click(fn=show_section, inputs=[gmail_input, current_selected], outputs=switch_outputs)\n", + " local_folder_show_up.click(fn=show_section, inputs=[local_input, current_selected], outputs=switch_outputs)\n", + " outlook_show_up.click(fn=show_section, inputs=[outlook_input, current_selected], outputs=switch_outputs)\n", + " google_workspace_show_up.click(fn=show_section, inputs=[workspace_input, current_selected], outputs=switch_outputs)\n", + " slack_show_up.click(fn=show_section, inputs=[slack_input, current_selected], outputs=switch_outputs)" + ] + }, + { + "cell_type": "markdown", + "id": "d98536e1-9be1-4b52-8535-dfa4778bb7d8", + "metadata": {}, + "source": [ + "### 7. Launch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff68e06-3cfb-48ae-9dad-fa431d0d548a", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Logout all the gmail accounts before launch\n", + "if os.path.exists(GMAIL_TOKEN_DIR):\n", + " shutil.rmtree(GMAIL_TOKEN_DIR)\n", + "os.makedirs(GMAIL_TOKEN_DIR, exist_ok=True)\n", + "\n", + "# Logout all the outlook accounts before launch\n", + "if os.path.exists(OUTLOOK_TOKEN_DIR):\n", + " shutil.rmtree(OUTLOOK_TOKEN_DIR)\n", + "os.makedirs(OUTLOOK_TOKEN_DIR, exist_ok=True)\n", + "\n", + "# Logout all the google accounts before launch\n", + "if os.path.exists(GOOGLE_WORKSPACE_TOKEN_DIR):\n", + " shutil.rmtree(GOOGLE_WORKSPACE_TOKEN_DIR)\n", + "os.makedirs(GOOGLE_WORKSPACE_TOKEN_DIR, exist_ok=True)\n", + "\n", + "ui.launch()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d31bf212-896e-492c-9e3f-88ea5001ab9e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/gmail_credentials.json b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/gmail_credentials.json new file mode 100644 index 0000000..43b09df --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/gmail_credentials.json @@ -0,0 +1,3 @@ +// delete key + +{"installed":{"client_id":"196620306719-vr5i30l44mqmkmnp7j96iavjfqsfl41f.apps.googleusercontent.com","project_id":"llms-personal-knowledge","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["http://localhost"]}} \ No newline at end of file diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/google_drive_workspace_credentials.json b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/google_drive_workspace_credentials.json new file mode 100644 index 0000000..b5af177 --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/credentials/google_drive_workspace_credentials.json @@ -0,0 +1,3 @@ +// delete key + +{"installed":{"client_id":"196620306719-7qvdhd86sau3ngmrrlcb1314us9nuli4.apps.googleusercontent.com","project_id":"llms-personal-knowledge","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["http://localhost"]}} \ No newline at end of file diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/image/JPEG.jpg b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/image/JPEG.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ac004a6d3a16139abddff098b304aedc439c79c GIT binary patch literal 10884 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 literal 0 HcmV?d00001 diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/Presentation.pptx b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/Presentation.pptx new file mode 100644 index 0000000000000000000000000000000000000000..876b4e4e4a59396f01c2111e7eb5aeccd17b6bd0 GIT binary patch literal 33991 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 literal 0 HcmV?d00001 diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/excel.xlsx b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/excel.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8e577476687499279a7ce6197030d905844c5b9c GIT binary patch literal 9609 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 literal 0 HcmV?d00001 diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/word.docx b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/ms_office/word.docx new file mode 100644 index 0000000000000000000000000000000000000000..ac58076572f658e38ed3938d833ba5b42db9f96a GIT binary patch literal 12139 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 literal 0 HcmV?d00001 diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/Epub.epub b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/Epub.epub new file mode 100644 index 0000000000000000000000000000000000000000..45fe318ed325b0421b719b017e15940fd8b857c3 GIT binary patch literal 2138 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 SIzsqAedh-r9MIL+=iMLgxM(r} literal 0 HcmV?d00001 diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/HTML.html b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/HTML.html new file mode 100644 index 0000000..94fca83 --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/HTML.html @@ -0,0 +1,9 @@ + + + + My First Web Page + + +

Zephyr won ZHTML award

+ + \ No newline at end of file diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/MD.md b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/MD.md new file mode 100644 index 0000000..ac8709f --- /dev/null +++ b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/MD.md @@ -0,0 +1 @@ +Zephyr won ZMD award diff --git a/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/PDF.pdf b/week5/community-contributions/Week5_Exercise_Personal_Knowledge/local-knowledge-base/text/PDF.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0de975e0211c7cd70c4652b5e94bc67da11c5aba GIT binary patch literal 14864 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