walkerhsu
commited on
Commit
·
c74033b
1
Parent(s):
132b0f4
init
Browse files- agents/__pycache__/gmail_agent.cpython-313.pyc +0 -0
- agents/__pycache__/google_calendar_agent.cpython-313.pyc +0 -0
- agents/gmail_agent.py +107 -0
- agents/google_calendar_agent.py +287 -0
- app.py +77 -0
- requirements.txt +4 -0
- utils/__pycache__/auth.cpython-313.pyc +0 -0
- utils/__pycache__/llm.cpython-313.pyc +0 -0
- utils/auth.py +25 -0
- utils/llm.py +22 -0
agents/__pycache__/gmail_agent.cpython-313.pyc
ADDED
|
Binary file (5.61 kB). View file
|
|
|
agents/__pycache__/google_calendar_agent.cpython-313.pyc
ADDED
|
Binary file (12 kB). View file
|
|
|
agents/gmail_agent.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import json
|
| 3 |
+
import base64
|
| 4 |
+
from googleapiclient.discovery import build
|
| 5 |
+
from utils.auth import authenticate_google_services
|
| 6 |
+
|
| 7 |
+
class Gmail_Agent:
|
| 8 |
+
def __init__(self, creds=None):
|
| 9 |
+
if not creds:
|
| 10 |
+
creds = authenticate_google_services()
|
| 11 |
+
self.service = build('gmail', 'v1', credentials=creds)
|
| 12 |
+
|
| 13 |
+
def read_gmail_messages(self, queries, max_results=5):
|
| 14 |
+
query = " ".join(queries)
|
| 15 |
+
results = self.service.users().messages().list(userId='me', maxResults=max_results, q=query).execute()
|
| 16 |
+
messages = results.get('messages', [])
|
| 17 |
+
return messages
|
| 18 |
+
|
| 19 |
+
def format_messages(self, messages):
|
| 20 |
+
formatted_content = "\n\n==========\n\n"
|
| 21 |
+
for message in messages:
|
| 22 |
+
msg = self.service.users().messages().get(userId='me', id=message['id']).execute()
|
| 23 |
+
import json
|
| 24 |
+
with open('results.json', 'w', encoding='utf-8') as f:
|
| 25 |
+
json.dump(msg, f)
|
| 26 |
+
internal_Date = int(msg['internalDate'])
|
| 27 |
+
formatted_Date = datetime.fromtimestamp(internal_Date / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
| 28 |
+
formatted_content += f"信件日期: {formatted_Date}\n"
|
| 29 |
+
content = self._parse_gmail_response(msg['payload']['parts'])
|
| 30 |
+
formatted_content+=f"信件內容: {content}\n\n==========\n\n"
|
| 31 |
+
return formatted_content
|
| 32 |
+
|
| 33 |
+
def send_email(self, to, subject, message_content):
|
| 34 |
+
try:
|
| 35 |
+
from email.message import EmailMessage
|
| 36 |
+
message = EmailMessage()
|
| 37 |
+
message.set_content(message_content)
|
| 38 |
+
message["To"] = ", ".join(to)
|
| 39 |
+
message["Subject"] = subject
|
| 40 |
+
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
| 41 |
+
create_message = {"raw": encoded_message}
|
| 42 |
+
send_message = (
|
| 43 |
+
self.service.users()
|
| 44 |
+
.messages()
|
| 45 |
+
.send(userId="me", body=create_message)
|
| 46 |
+
.execute()
|
| 47 |
+
)
|
| 48 |
+
print(f'Message Id: {send_message["id"]}')
|
| 49 |
+
formatted_content = "✅ Gmail 已傳送給所有使用者,請至您的 Gmail 寄件備份確認!"
|
| 50 |
+
except Exception as error:
|
| 51 |
+
print(f"An error occurred: {error}")
|
| 52 |
+
send_message = None
|
| 53 |
+
formatted_content = f"❌ 信件未能成功寄出,以下是錯誤訊息(英文):{error}"
|
| 54 |
+
return formatted_content
|
| 55 |
+
|
| 56 |
+
def get_all_tools(self):
|
| 57 |
+
LLM_tools = []
|
| 58 |
+
LLM_tools.append({
|
| 59 |
+
"name": "query_gmail_tool",
|
| 60 |
+
"description": "query the gmails with specified queries",
|
| 61 |
+
"parameters": {
|
| 62 |
+
"type": "object",
|
| 63 |
+
"properties": {
|
| 64 |
+
"queries": {
|
| 65 |
+
"type": "array",
|
| 66 |
+
"items": {"type": "string"},
|
| 67 |
+
"description": "Queries that can be used to filter the gmails. (e.g., ['before:2025/06/01', 'after:2025/05/01', 'from:abd@gmail.com'])",
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
"required": ["queries"]
|
| 71 |
+
},
|
| 72 |
+
})
|
| 73 |
+
LLM_tools.append({
|
| 74 |
+
"name": "send_email_tool",
|
| 75 |
+
"description": "Send an email to a specified recipient with a specified subject and message",
|
| 76 |
+
"parameters": {
|
| 77 |
+
"type": "object",
|
| 78 |
+
"properties": {
|
| 79 |
+
"to": {
|
| 80 |
+
"type": "array",
|
| 81 |
+
"items": {"type": "string"},
|
| 82 |
+
"description": "An array of email addresses of all the recipients"
|
| 83 |
+
},
|
| 84 |
+
"subject": {
|
| 85 |
+
"type": "string",
|
| 86 |
+
"description": "The subject of the email"
|
| 87 |
+
},
|
| 88 |
+
"message_content": {
|
| 89 |
+
"type": "string",
|
| 90 |
+
"description": "The content of the email"
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"required": ["to", "subject", "message_content"]
|
| 94 |
+
}
|
| 95 |
+
})
|
| 96 |
+
return LLM_tools
|
| 97 |
+
|
| 98 |
+
def _parse_gmail_response(self, parts):
|
| 99 |
+
for part in parts:
|
| 100 |
+
if "parts" in part:
|
| 101 |
+
content = self._parse_gmail_response(part["parts"])
|
| 102 |
+
if content:
|
| 103 |
+
return content
|
| 104 |
+
if part['mimeType'] == 'text/plain' and part['body']['size'] != 0:
|
| 105 |
+
decoded_data = base64.urlsafe_b64decode(part['body']['data']).decode('utf-8')
|
| 106 |
+
return decoded_data
|
| 107 |
+
return None
|
agents/google_calendar_agent.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from googleapiclient.discovery import build
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from utils.auth import authenticate_google_services
|
| 4 |
+
|
| 5 |
+
class GoogleCalendar_Agent:
|
| 6 |
+
def __init__(self, creds=None, timezone="Asia/Taipei"):
|
| 7 |
+
if not creds:
|
| 8 |
+
creds = authenticate_google_services()
|
| 9 |
+
self.service = build("calendar", "v3", credentials=creds)
|
| 10 |
+
self.timezone = timezone
|
| 11 |
+
|
| 12 |
+
def list_events(self, n: int=5, queries=None) -> str:
|
| 13 |
+
now = datetime.utcnow().isoformat() + 'Z'
|
| 14 |
+
events_result = self.service.events().list(
|
| 15 |
+
calendarId='primary',
|
| 16 |
+
timeMin=now,
|
| 17 |
+
maxResults=n,
|
| 18 |
+
singleEvents=True,
|
| 19 |
+
orderBy='startTime'
|
| 20 |
+
).execute()
|
| 21 |
+
events = events_result.get('items', [])
|
| 22 |
+
if not events:
|
| 23 |
+
return "🔕 找不到即將到來的行事曆事件。"
|
| 24 |
+
output = "📅 您接下來的行事曆事件如下:\n"
|
| 25 |
+
for event in events:
|
| 26 |
+
start = event['start'].get('dateTime', event['start'].get('date'))
|
| 27 |
+
summary = event.get('summary', 'No title')
|
| 28 |
+
output += f"• {summary} at {start}\n"
|
| 29 |
+
return output
|
| 30 |
+
|
| 31 |
+
def create_event(self,
|
| 32 |
+
summary: str,
|
| 33 |
+
start_time: str,
|
| 34 |
+
end_time: str,
|
| 35 |
+
location: str = None,
|
| 36 |
+
description: str = None,
|
| 37 |
+
attendees: list = None,
|
| 38 |
+
reminder_minutes: int = None) -> str:
|
| 39 |
+
event = {
|
| 40 |
+
'summary': summary,
|
| 41 |
+
'start': {'dateTime': start_time, 'timeZone': self.timezone},
|
| 42 |
+
'end': {'dateTime': end_time, 'timeZone': self.timezone}
|
| 43 |
+
}
|
| 44 |
+
if location:
|
| 45 |
+
event['location'] = location
|
| 46 |
+
if description:
|
| 47 |
+
event['description'] = description
|
| 48 |
+
if attendees:
|
| 49 |
+
event['attendees'] = [{'email': email} for email in attendees]
|
| 50 |
+
if reminder_minutes:
|
| 51 |
+
event['reminders'] = {
|
| 52 |
+
'useDefault': False,
|
| 53 |
+
'overrides': [{'method': 'popup', 'minutes': reminder_minutes}]
|
| 54 |
+
}
|
| 55 |
+
try:
|
| 56 |
+
created_event = self.service.events().insert(
|
| 57 |
+
calendarId='primary',
|
| 58 |
+
sendUpdates='all',
|
| 59 |
+
body=event
|
| 60 |
+
).execute()
|
| 61 |
+
return f"""✅ 已成功建立事件:\n標題 : {summary}\n開始時間 : {start_time}\n結束時間 : {end_time}\n地點 : {location or '無'}\n說明 : {description or '無'}\n參與者 : {', '.join(attendees) if attendees else '無'}\n提醒時間 : {reminder_minutes if reminder_minutes is not None else '使用預設提醒'}\n時區 : {self.timezone}\n事件連結 : {created_event.get('htmlLink')}"""
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return f"❌ 建立事件失敗,以下是錯誤訊息(英文): {e}"
|
| 64 |
+
|
| 65 |
+
def find_event(self, query: str,) -> list:
|
| 66 |
+
now = datetime.utcnow().isoformat() + 'Z'
|
| 67 |
+
events_result = self.service.events().list(
|
| 68 |
+
calendarId='primary',
|
| 69 |
+
timeMin=now,
|
| 70 |
+
singleEvents=True,
|
| 71 |
+
orderBy='startTime',
|
| 72 |
+
q=query
|
| 73 |
+
).execute()
|
| 74 |
+
return events_result.get('items', [])
|
| 75 |
+
|
| 76 |
+
def update_event(self, query: str,
|
| 77 |
+
new_summary=None,
|
| 78 |
+
new_location=None,
|
| 79 |
+
new_description=None,
|
| 80 |
+
new_start=None,
|
| 81 |
+
new_end=None,
|
| 82 |
+
new_attendees=None,
|
| 83 |
+
reminder_minutes=None):
|
| 84 |
+
events = self.find_event(query)
|
| 85 |
+
if not events:
|
| 86 |
+
return f"❌ 找不到包含「{query}」的事件"
|
| 87 |
+
if all(arg is None for arg in [
|
| 88 |
+
new_summary, new_location, new_description,
|
| 89 |
+
new_start, new_end, new_attendees, reminder_minutes
|
| 90 |
+
]):
|
| 91 |
+
return f"⚠️ 沒有指定任何要更新的欄位,未執行更新。"
|
| 92 |
+
for event in events:
|
| 93 |
+
event_id = event['id']
|
| 94 |
+
try:
|
| 95 |
+
if new_summary:
|
| 96 |
+
event['summary'] = new_summary
|
| 97 |
+
if new_location:
|
| 98 |
+
event['location'] = new_location
|
| 99 |
+
if new_description:
|
| 100 |
+
event['description'] = new_description
|
| 101 |
+
if new_attendees:
|
| 102 |
+
event['attendees'] = [{'email': email} for email in new_attendees]
|
| 103 |
+
if reminder_minutes:
|
| 104 |
+
event['reminders'] = {
|
| 105 |
+
'useDefault': False,
|
| 106 |
+
'overrides': [{'method': 'popup', 'minutes': reminder_minutes}]
|
| 107 |
+
}
|
| 108 |
+
if new_start or new_end:
|
| 109 |
+
event['start']['dateTime'] = new_start
|
| 110 |
+
event['end']['dateTime'] = new_end
|
| 111 |
+
event['start']['timeZone'] = 'Asia/Taipei'
|
| 112 |
+
event['end']['timeZone'] = 'Asia/Taipei'
|
| 113 |
+
updated_event = self.service.events().update(
|
| 114 |
+
calendarId='primary',
|
| 115 |
+
eventId=event_id,
|
| 116 |
+
sendUpdates='all',
|
| 117 |
+
body=event
|
| 118 |
+
).execute()
|
| 119 |
+
return f"✅ 已成功更新事件:{updated_event['summary']}"
|
| 120 |
+
except Exception as e:
|
| 121 |
+
return f"❌ 更新事件失敗,以下是錯誤訊息(英文): {e}"
|
| 122 |
+
|
| 123 |
+
def delete_event(self, query: str):
|
| 124 |
+
now = datetime.utcnow().isoformat() + 'Z'
|
| 125 |
+
event_results = self.find_event(query)
|
| 126 |
+
if len(event_results) == 0:
|
| 127 |
+
return f"❌ 找不到包含「{query}」的事件"
|
| 128 |
+
for event in event_results:
|
| 129 |
+
target_id = event["id"]
|
| 130 |
+
try:
|
| 131 |
+
self.service.events().delete(
|
| 132 |
+
calendarId='primary',
|
| 133 |
+
eventId=target_id,
|
| 134 |
+
sendUpdates='all'
|
| 135 |
+
).execute()
|
| 136 |
+
return f"🗑️ 已刪除事件:{event['summary']}(ID: {target_id})"
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return f"❌ 刪除事件失敗(ID: {target_id}),以下是錯誤訊息(英文): {e}"
|
| 139 |
+
|
| 140 |
+
def get_event_details(self, query: str):
|
| 141 |
+
events = self.find_event(query)
|
| 142 |
+
if not events:
|
| 143 |
+
return f"❌ 找不到符合 '{query}' 的事件!"
|
| 144 |
+
event = events[0]
|
| 145 |
+
summary = event.get('summary', 'No Title')
|
| 146 |
+
start_time = event['start'].get('dateTime', event['start'].get('date'))
|
| 147 |
+
end_time = event['end'].get('dateTime', event['end'].get('date'))
|
| 148 |
+
formatted_string = f"事件: {summary}\n開始時間: {start_time}\n結束時間: {end_time}"
|
| 149 |
+
attendees = event.get('attendees', [])
|
| 150 |
+
emails = [attendee.get('email') for attendee in attendees if attendee.get('email')]
|
| 151 |
+
if emails:
|
| 152 |
+
formatted_string += f"\n參與者: {', '.join(emails)}"
|
| 153 |
+
else:
|
| 154 |
+
formatted_string += "\n無參與者"
|
| 155 |
+
return formatted_string
|
| 156 |
+
|
| 157 |
+
def get_all_tools(self):
|
| 158 |
+
LLM_tools = []
|
| 159 |
+
LLM_tools.append({
|
| 160 |
+
"name": "create_Calendar_event",
|
| 161 |
+
"description": "Create a new Google Calendar event by specifying summary, start time, and end time, and optionally location, description, attendees, and reminder.",
|
| 162 |
+
"parameters": {
|
| 163 |
+
"type": "object",
|
| 164 |
+
"properties": {
|
| 165 |
+
"summary": {
|
| 166 |
+
"type": "string",
|
| 167 |
+
"description": "Title or summary of the event (e.g., 'Lunch with Alice')"
|
| 168 |
+
},
|
| 169 |
+
"start_time": {
|
| 170 |
+
"type": "string",
|
| 171 |
+
"format": "date-time",
|
| 172 |
+
"description": "Start time of the event in ISO format (e.g., '2025-07-01T14:00:00')"
|
| 173 |
+
},
|
| 174 |
+
"end_time": {
|
| 175 |
+
"type": "string",
|
| 176 |
+
"format": "date-time",
|
| 177 |
+
"description": "End time of the event in ISO format (e.g., '2025-07-01T15:00:00')"
|
| 178 |
+
},
|
| 179 |
+
"location": {
|
| 180 |
+
"type": "string",
|
| 181 |
+
"description": "Location of the event (e.g., 'Taipei 101')"
|
| 182 |
+
},
|
| 183 |
+
"description": {
|
| 184 |
+
"type": "string",
|
| 185 |
+
"description": "Detailed description of the event"
|
| 186 |
+
},
|
| 187 |
+
"attendees": {
|
| 188 |
+
"type": "array",
|
| 189 |
+
"items": {"type": "string"},
|
| 190 |
+
"description": "List of email addresses to invite to the event"
|
| 191 |
+
},
|
| 192 |
+
"reminder_minutes": {
|
| 193 |
+
"type": "integer",
|
| 194 |
+
"description": "Number of minutes before the event to trigger a popup reminder"
|
| 195 |
+
}
|
| 196 |
+
},
|
| 197 |
+
"required": ["summary", "start_time", "end_time"]
|
| 198 |
+
},
|
| 199 |
+
})
|
| 200 |
+
LLM_tools.append({
|
| 201 |
+
"name": "list_Calendar_events",
|
| 202 |
+
"description": "List your upcoming Google Calendar events.",
|
| 203 |
+
"parameters": {
|
| 204 |
+
"type": "object",
|
| 205 |
+
"properties": {
|
| 206 |
+
"n": {
|
| 207 |
+
"type": "integer",
|
| 208 |
+
"default": 5,
|
| 209 |
+
"description": "Number of upcoming events to list (default: 5)"
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
})
|
| 214 |
+
LLM_tools.append({
|
| 215 |
+
"name": "update_Calendar_event",
|
| 216 |
+
"description": "Update a Google Calendar event using a keyword query to find it. You can update summary, location, description, start/end time, attendees, or reminders.",
|
| 217 |
+
"parameters": {
|
| 218 |
+
"type": "object",
|
| 219 |
+
"properties": {
|
| 220 |
+
"query": {
|
| 221 |
+
"type": "string",
|
| 222 |
+
"description": "Keyword to find the event to update (e.g., 'meeting with Bob')"
|
| 223 |
+
},
|
| 224 |
+
"new_summary": {
|
| 225 |
+
"type": "string",
|
| 226 |
+
"description": "New summary/title for the event"
|
| 227 |
+
},
|
| 228 |
+
"new_location": {
|
| 229 |
+
"type": "string",
|
| 230 |
+
"description": "New location for the event"
|
| 231 |
+
},
|
| 232 |
+
"new_description": {
|
| 233 |
+
"type": "string",
|
| 234 |
+
"description": "New description for the event"
|
| 235 |
+
},
|
| 236 |
+
"new_start": {
|
| 237 |
+
"type": "string",
|
| 238 |
+
"format": "date-time",
|
| 239 |
+
"description": "New start time in ISO format"
|
| 240 |
+
},
|
| 241 |
+
"new_end": {
|
| 242 |
+
"type": "string",
|
| 243 |
+
"format": "date-time",
|
| 244 |
+
"description": "New end time in ISO format"
|
| 245 |
+
},
|
| 246 |
+
"new_attendees": {
|
| 247 |
+
"type": "array",
|
| 248 |
+
"items": {"type": "string"},
|
| 249 |
+
"description": "New list of attendees (email addresses)"
|
| 250 |
+
},
|
| 251 |
+
"reminder_minutes": {
|
| 252 |
+
"type": "integer",
|
| 253 |
+
"description": "Updated popup reminder time in minutes"
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
"required": ["query"]
|
| 257 |
+
}
|
| 258 |
+
})
|
| 259 |
+
LLM_tools.append({
|
| 260 |
+
"name": "delete_Calendar_event",
|
| 261 |
+
"description": "Delete a Google Calendar event using a keyword query to find it.",
|
| 262 |
+
"parameters": {
|
| 263 |
+
"type": "object",
|
| 264 |
+
"properties": {
|
| 265 |
+
"query": {
|
| 266 |
+
"type": "string",
|
| 267 |
+
"description": "Keyword to find the event to delete (e.g., 'meeting with Bob')"
|
| 268 |
+
}
|
| 269 |
+
},
|
| 270 |
+
"required": ["query"]
|
| 271 |
+
}
|
| 272 |
+
})
|
| 273 |
+
LLM_tools.append({
|
| 274 |
+
"name": "get_event_details",
|
| 275 |
+
"description": "Find the attendee email addresses and the summary of a specific event, based on a keyword.",
|
| 276 |
+
"parameters": {
|
| 277 |
+
"type": "object",
|
| 278 |
+
"properties": {
|
| 279 |
+
"query": {
|
| 280 |
+
"type": "string",
|
| 281 |
+
"description": "Keyword to find the event and extract attendees and summary of that event."
|
| 282 |
+
}
|
| 283 |
+
},
|
| 284 |
+
"required": ["query"]
|
| 285 |
+
}
|
| 286 |
+
})
|
| 287 |
+
return LLM_tools
|
app.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from utils.auth import authenticate_google_services
|
| 4 |
+
from utils.llm import Google_Gemini_LLM
|
| 5 |
+
from agents.gmail_agent import Gmail_Agent
|
| 6 |
+
from agents.google_calendar_agent import GoogleCalendar_Agent
|
| 7 |
+
|
| 8 |
+
# Authenticate and initialize agents
|
| 9 |
+
creds = authenticate_google_services()
|
| 10 |
+
GMAIL_AGENT = Gmail_Agent(creds)
|
| 11 |
+
CALENDAR_Agent = GoogleCalendar_Agent(creds)
|
| 12 |
+
|
| 13 |
+
# Initialize LLM
|
| 14 |
+
# Use os.environ.get instead of userdata.get
|
| 15 |
+
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
| 16 |
+
if not GOOGLE_API_KEY:
|
| 17 |
+
raise ValueError("GOOGLE_API_KEY environment variable not set")
|
| 18 |
+
|
| 19 |
+
MODEL = "gemini-1.5-flash"
|
| 20 |
+
YOUR_AI_AGENT = Google_Gemini_LLM(GOOGLE_API_KEY, MODEL)
|
| 21 |
+
YOUR_AI_AGENT.append_function_tools(GMAIL_AGENT.get_all_tools())
|
| 22 |
+
YOUR_AI_AGENT.append_function_tools(CALENDAR_Agent.get_all_tools())
|
| 23 |
+
|
| 24 |
+
def generate_response(user_prompt, history):
|
| 25 |
+
response = YOUR_AI_AGENT.generate_content(user_prompt)
|
| 26 |
+
while(response.function_call):
|
| 27 |
+
function_name = response.function_call.name
|
| 28 |
+
function_args = response.function_call.args
|
| 29 |
+
if function_name == "query_gmail_tool":
|
| 30 |
+
messages = GMAIL_AGENT.read_gmail_messages(**function_args)
|
| 31 |
+
if not messages:
|
| 32 |
+
formatted_content = "❌ 找不到對應的 Gmail,請確認您的搜尋條件是否合理,或者請重新執行!"
|
| 33 |
+
return formatted_content
|
| 34 |
+
else:
|
| 35 |
+
formatted_content = GMAIL_AGENT.format_messages(messages)
|
| 36 |
+
user_prompt += f"\n根據使用者的搜尋條件,以下是對應的信件內容(可能有多封信件):<信件內容開始>{formatted_content}<信件內容結束>. 請仔細閱讀以上的信件內容,並將資料整理統整給使用者!請以簡潔且新處的方式傳達內容!"
|
| 37 |
+
elif function_name == "send_email_tool":
|
| 38 |
+
formatted_content = GMAIL_AGENT.send_email(**function_args)
|
| 39 |
+
return formatted_content
|
| 40 |
+
elif function_name == "list_Calendar_events":
|
| 41 |
+
formatted_content = CALENDAR_Agent.list_events(**function_args)
|
| 42 |
+
user_prompt += f"\n根據使用者的搜尋條件,以下是從使用者的日曆中找到對應的事件內容(可能有多封信件): <事件內容開始>{formatted_content}<事件內容結束>. 請仔細閱讀以上的事件內容,並將資料統整給使用者!請以簡潔且新處的方式傳達內容!"
|
| 43 |
+
elif function_name == "create_Calendar_event":
|
| 44 |
+
formatted_content = CALENDAR_Agent.create_event(**function_args)
|
| 45 |
+
return formatted_content
|
| 46 |
+
elif function_name == "update_Calendar_event":
|
| 47 |
+
formatted_content = CALENDAR_Agent.update_event(**function_args)
|
| 48 |
+
return formatted_content
|
| 49 |
+
elif function_name == "delete_Calendar_event":
|
| 50 |
+
formatted_content = CALENDAR_Agent.delete_event(**function_args)
|
| 51 |
+
return formatted_content
|
| 52 |
+
elif function_name == "get_event_details":
|
| 53 |
+
formatted_content = CALENDAR_Agent.get_event_details(**function_args)
|
| 54 |
+
user_prompt += f"\n根據使用者的搜尋條件,以下是從使用者的日曆中找到對應的事件內容 <事件內容開始>{formatted_content}<事件內容結束>. 請仔細閱讀以上的事件內容,並且完成使用者的指示(例如:統整事件內容、寄送提醒信件給參與事件的人...等等)!"
|
| 55 |
+
else:
|
| 56 |
+
raise NotImplementedError(f"Function Call {function_name} is not supported yet.")
|
| 57 |
+
response = YOUR_AI_AGENT.generate_content(user_prompt)
|
| 58 |
+
return response.text
|
| 59 |
+
|
| 60 |
+
# It's better to get the email from the user's input
|
| 61 |
+
# than to hardcode it.
|
| 62 |
+
demo = gr.ChatInterface(
|
| 63 |
+
fn=generate_response,
|
| 64 |
+
chatbot=gr.Chatbot(
|
| 65 |
+
label="📬 AI 助理",
|
| 66 |
+
),
|
| 67 |
+
title="Gmail & Calendar 智慧助理",
|
| 68 |
+
description="輸入自然語言,讓 AI 幫你管理 Gmail 或 Google 行事曆",
|
| 69 |
+
examples=[
|
| 70 |
+
"找出2025/6月標註星號的信件",
|
| 71 |
+
"請幫我列出近期的事件",
|
| 72 |
+
"建立2025/8/10早上10點的網球活動",
|
| 73 |
+
]
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-api-python-client
|
| 2 |
+
google-auth-httplib2
|
| 3 |
+
google-auth-oauthlib
|
| 4 |
+
gradio
|
utils/__pycache__/auth.cpython-313.pyc
ADDED
|
Binary file (1.58 kB). View file
|
|
|
utils/__pycache__/llm.cpython-313.pyc
ADDED
|
Binary file (1.79 kB). View file
|
|
|
utils/auth.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os, pickle
|
| 3 |
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
| 4 |
+
from google.auth.transport.requests import Request
|
| 5 |
+
|
| 6 |
+
SCOPES = [
|
| 7 |
+
'https://www.googleapis.com/auth/calendar',
|
| 8 |
+
'https://www.googleapis.com/auth/gmail.readonly',
|
| 9 |
+
'https://www.googleapis.com/auth/gmail.send'
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
def authenticate_google_services():
|
| 13 |
+
creds = None
|
| 14 |
+
if os.path.exists('token.pkl'):
|
| 15 |
+
with open('token.pkl', 'rb') as token:
|
| 16 |
+
creds = pickle.load(token)
|
| 17 |
+
if not creds or not creds.valid:
|
| 18 |
+
if creds and creds.expired and creds.refresh_token:
|
| 19 |
+
creds.refresh(Request())
|
| 20 |
+
else:
|
| 21 |
+
flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES)
|
| 22 |
+
creds = flow.run_console()
|
| 23 |
+
with open('token.pkl', 'wb') as token:
|
| 24 |
+
pickle.dump(creds, token)
|
| 25 |
+
return creds
|
utils/llm.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import google.generativeai as genai
|
| 3 |
+
from google.generativeai import types
|
| 4 |
+
|
| 5 |
+
class Google_Gemini_LLM:
|
| 6 |
+
def __init__(self, api_key, model="gemini-1.5-flash"):
|
| 7 |
+
self.api_key = api_key
|
| 8 |
+
self.model = model
|
| 9 |
+
self.client = genai.Client(api_key=api_key)
|
| 10 |
+
self.LLM_tools = []
|
| 11 |
+
|
| 12 |
+
def generate_content(self, contents):
|
| 13 |
+
tools = types.Tool(function_declarations=self.LLM_tools)
|
| 14 |
+
config = types.GenerateContentConfig(tools=[tools])
|
| 15 |
+
response = self.client.models.generate_content(
|
| 16 |
+
model=self.model, contents=contents, config=config
|
| 17 |
+
)
|
| 18 |
+
return response.candidates[0].content.parts[0]
|
| 19 |
+
|
| 20 |
+
def append_function_tools(self, tools):
|
| 21 |
+
for tool in tools:
|
| 22 |
+
self.LLM_tools.append(tool)
|