ujalaarshad17 commited on
Commit
93dae80
·
verified ·
1 Parent(s): 3a9bd89

first commit

Browse files
Files changed (11) hide show
  1. .gitignore +4 -0
  2. DockerFile +16 -0
  3. all_tools.py +1678 -0
  4. app.py +1688 -0
  5. calendar_tools.py +118 -0
  6. config.py +199 -0
  7. db.py +81 -0
  8. prompt.py +698 -0
  9. requirements.txt +20 -0
  10. services.py +218 -0
  11. utils.py +75 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ ai-schedule
3
+ flask_session
4
+ credentials.json
DockerFile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["python", "app.py"]
all_tools.py ADDED
@@ -0,0 +1,1678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # import os
3
+ # import requests
4
+ # import base64
5
+ # import msal
6
+ # import time
7
+ # from typing import Type, Optional, List
8
+
9
+ # from dotenv import load_dotenv
10
+ # from pydantic.v1 import BaseModel, Field
11
+ # from msal import ConfidentialClientApplication
12
+
13
+ # from langchain_core.tools import BaseTool
14
+ # from slack_sdk.errors import SlackApiError
15
+ # from datetime import datetime
16
+ # from google_auth_oauthlib.flow import InstalledAppFlow
17
+ # from googleapiclient.discovery import build
18
+ # from google.oauth2.credentials import Credentials
19
+ # from google.auth.transport.requests import Request
20
+ # from services import construct_google_calendar_client
21
+ # from config import client
22
+ # from datetime import datetime, timedelta
23
+ # import pytz
24
+ # from typing import List, Dict, Optional
25
+ # from collections import defaultdict
26
+ # from slack_sdk.errors import SlackApiError
27
+ # from config import owner_id_pref, all_users_preload, GetAllUsers
28
+ # load_dotenv()
29
+ # calendar_service = None
30
+ # MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
31
+ # MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
32
+ # MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
33
+ # MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
34
+ # # Enhanced GetAllUsers function (not a tool) to fetch email as well
35
+ # MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
36
+
37
+
38
+ # # Pydantic models for tool arguments
39
+ # class DirectDMArgs(BaseModel):
40
+ # message: str = Field(description="The message to be sent to the Slack user")
41
+ # user_id: str = Field(description="The Slack user ID")
42
+
43
+ # class DateTimeTool(BaseTool):
44
+ # name: str = "current_date_time"
45
+ # description: str = "Provides the current date and time."
46
+
47
+ # def _run(self):
48
+ # return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
49
+
50
+ # # Tool to get a single user's Slack ID based on their name
51
+ # class GetSingleUserSlackIDArgs(BaseModel):
52
+ # name: str = Field(description="The real name of the user whose Slack ID is needed")
53
+
54
+ # class GetSingleUserSlackID(BaseTool):
55
+ # name: str = "gets_slack_id_single_user"
56
+ # description: str = "Gets the Slack ID of a user based on their real name"
57
+ # args_schema: Type[BaseModel] = GetSingleUserSlackIDArgs
58
+
59
+ # def _run(self, name: str):
60
+ # if not all_users_preload:
61
+ # print("Getting the users")
62
+ # all_users = GetAllUsers() # Fetch all users again
63
+ # else:
64
+ # print("Fetching the users")
65
+ # all_users = all_users_preload
66
+
67
+ # # Iterate through all_users to find a matching name
68
+ # for uid, info in all_users.items():
69
+ # if info["name"].lower() == name.lower():
70
+ # return uid, info['email']
71
+
72
+ # return "User not found"
73
+
74
+ # # Tool to get a single user's Slack name based on their ID
75
+ # class GetSingleUserSlackNameArgs(BaseModel):
76
+ # id: str = Field(description="The Slack user ID")
77
+
78
+ # class GetSingleUserSlackName(BaseTool):
79
+ # name: str = "gets_slack_name_single_user"
80
+ # description: str = "Gets the Slack real name of a user based on their slack ID"
81
+ # args_schema: Type[BaseModel] = GetSingleUserSlackNameArgs
82
+
83
+ # def _run(self, id: str):
84
+
85
+ # # Check if preload returns empty dict or "User not found"
86
+ # if not all_users_preload or all_users_preload == {}:
87
+ # all_users = GetAllUsers() # Fetch all users again
88
+ # else:
89
+ # all_users = all_users_preload
90
+
91
+ # user = all_users.get(id)
92
+ # print(all_users)
93
+
94
+ # if user:
95
+ # return user["name"], user['email']
96
+
97
+ # return "User not found"
98
+
99
+ # class MultiDMArgs(BaseModel):
100
+ # message: str
101
+ # user_ids: List[str]
102
+
103
+ # class MultiDirectDMTool(BaseTool):
104
+ # name: str = "send_multiple_dms"
105
+ # description: str = "Sends direct messages to multiple Slack users"
106
+ # args_schema: Type[BaseModel] = MultiDMArgs
107
+
108
+ # def _run(self, message: str, user_ids: List[str]):
109
+ # results = {}
110
+ # for user_id in user_ids:
111
+ # try:
112
+ # client.chat_postMessage(channel=user_id, text=message)
113
+ # results[user_id] = "Message sent successfully"
114
+ # except SlackApiError as e:
115
+ # results[user_id] = f"Error: {e.response['error']}"
116
+ # return results
117
+ # # Direct DM tool for sending messages within Slack
118
+ # class DirectDMTool(BaseTool):
119
+ # name: str = "send_direct_dm"
120
+ # description: str = "Sends direct messages to Slack users"
121
+ # args_schema: Type[BaseModel] = DirectDMArgs
122
+
123
+ # def _run(self, message: str, user_id: str):
124
+ # try:
125
+ # client.chat_postMessage(channel=user_id, text=message)
126
+ # return "Message sent successfully"
127
+ # except SlackApiError as e:
128
+ # return f"Error sending message: {e.response['error']}"
129
+ # def send_dm(user_id: str, message: str) -> bool:
130
+ # """Send a direct message to a user"""
131
+ # try:
132
+ # client.chat_postMessage(channel=user_id, text=message)
133
+ # return True
134
+ # except SlackApiError as e:
135
+ # print(f"Error sending DM: {e.response['error']}")
136
+ # return False
137
+
138
+ # def handle_event_modification(event_id: str, action: str) -> str:
139
+ # """Handle event modification (update/delete)"""
140
+ # # You'll need to implement the actual modification logic
141
+ # if action == "delete":
142
+ # result = GoogleDeleteCalendarEvent().run(event_id=event_id)
143
+ # else:
144
+ # result = GoogleUpdateCalendarEvent().run(event_id=event_id)
145
+
146
+ # if result.get("status") == "success":
147
+ # return f"Event {action}d successfully!"
148
+ # return f"Failed to {action} event: {result.get('message', 'Unknown error')}"
149
+ # # Google Calendar Tools
150
+ # PT = pytz.timezone('America/Los_Angeles')
151
+
152
+ # def convert_to_pt(dt: datetime) -> datetime:
153
+ # """Convert a datetime object to PT timezone"""
154
+ # if dt.tzinfo is None:
155
+ # dt = pytz.utc.localize(dt)
156
+ # return dt.astimezone(PT)
157
+ # class GoogleCalendarList(BaseTool):
158
+ # name: str = "list_calendar_list"
159
+ # description: str = "Lists available calendars in the user's Google Calendar account"
160
+
161
+ # def _run(self, user_id: str, max_capacity: int = 200):
162
+ # if not calendar_service:
163
+ # calendar_service = construct_google_calendar_client(user_id)
164
+ # else:
165
+ # return "Token should be refreshed in Google Calendar"
166
+
167
+ # all_calendars = []
168
+ # next_page_token = None
169
+ # capacity_tracker = 0
170
+
171
+ # while capacity_tracker < max_capacity:
172
+ # results = calendar_service.calendarList().list(
173
+ # maxResults=min(200, max_capacity - capacity_tracker),
174
+ # pageToken=next_page_token
175
+ # ).execute()
176
+ # calendars = results.get('items', [])
177
+ # all_calendars.extend(calendars)
178
+ # capacity_tracker += len(calendars)
179
+ # next_page_token = results.get('nextPageToken')
180
+ # if not next_page_token:
181
+ # break
182
+
183
+ # return [{
184
+ # 'id': cal['id'],
185
+ # 'name': cal['summary'],
186
+ # 'description': cal.get('description', '')
187
+ # } for cal in all_calendars]
188
+
189
+ # class GoogleCalendarEvents(BaseTool):
190
+ # name: str = "list_calendar_events"
191
+ # description: str = "Lists and gets events from a specific Google Calendar"
192
+
193
+ # def _run(self, user_id: str, calendar_id: str = "primary", max_capacity: int = 20):
194
+
195
+ # calendar_service = construct_google_calendar_client(user_id)
196
+
197
+ # all_events = []
198
+ # next_page_token = None
199
+ # capacity_tracker = 0
200
+
201
+ # while capacity_tracker < max_capacity:
202
+ # results = calendar_service.events().list(
203
+ # calendarId=calendar_id,
204
+ # maxResults=min(250, max_capacity - capacity_tracker),
205
+ # pageToken=next_page_token
206
+ # ).execute()
207
+ # events = results.get('items', [])
208
+ # all_events.extend(events)
209
+ # capacity_tracker += len(events)
210
+ # next_page_token = results.get('nextPageToken')
211
+ # if not next_page_token:
212
+ # break
213
+
214
+ # return all_events
215
+
216
+ # class GoogleCreateCalendar(BaseTool):
217
+ # name: str = "create_calendar_list"
218
+ # description: str = "Creates a new calendar in Google Calendar"
219
+
220
+ # def _run(self, user_id: str, calendar_name: str):
221
+
222
+ # calendar_service = construct_google_calendar_client(user_id)
223
+
224
+ # calendar_body = {'summary': calendar_name}
225
+ # created_calendar = calendar_service.calendars().insert(body=calendar_body).execute()
226
+ # return f"Created calendar: {created_calendar['id']}"
227
+
228
+ # # Updated Event Creation Tool with guest options, meeting agenda, and invite link support
229
+ # class GoogleAddCalendarEventArgs(BaseModel):
230
+ # calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
231
+ # summary: str = Field(description="Event title (should include meeting agenda if needed)")
232
+ # user_id: str = Field(default="user", description="User slack Id which should be matched to name")
233
+ # description: str = Field(default="", description="Event description or agenda")
234
+ # start_time: str = Field(description="Start time in ISO 8601 format")
235
+ # end_time: str = Field(description="End time in ISO 8601 format")
236
+ # location: str = Field(default="", description="Event location")
237
+ # invite_link: str = Field(default="", description="Invite link for the meeting")
238
+ # guests: List[str] = Field(default=None, description="List of guest emails to invite")
239
+
240
+ # class GoogleAddCalendarEvent(BaseTool):
241
+ # name: str = "google_add_calendar_event"
242
+ # description: str = "Creates an event in a Google Calendar with comprehensive meeting details and guest options"
243
+ # args_schema: Type[BaseModel] = GoogleAddCalendarEventArgs
244
+
245
+ # def _run(self, user_id: str, summary: str, start_time: str, end_time: str,
246
+ # description: str = "", calendar_id: str = 'primary', location: str = "",
247
+ # invite_link: str = "", guests: List[str] = None):
248
+ # calendar_service = construct_google_calendar_client(user_id)
249
+
250
+ # # Append invite link to description if provided
251
+ # if invite_link:
252
+ # description = f"{description}\nInvite Link: {invite_link}"
253
+
254
+ # event = {
255
+ # 'summary': summary,
256
+ # 'description': description,
257
+ # 'start': {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'},
258
+ # 'end': {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'},
259
+ # 'location': location,
260
+ # }
261
+
262
+ # # Add guests if provided
263
+ # if guests:
264
+ # event['attendees'] = [{'email': guest} for guest in guests]
265
+
266
+ # try:
267
+ # print("I am here registering the event")
268
+ # created_event = calendar_service.events().insert(
269
+ # calendarId=calendar_id,
270
+ # body=event,
271
+ # sendUpdates='all' # Send invitations to guests
272
+ # ).execute()
273
+ # return {
274
+ # "status": "success",
275
+ # "event_id": created_event['id'],
276
+ # "link": created_event.get('htmlLink', '')
277
+ # }
278
+ # except Exception as e:
279
+ # return {"status": "error", "message": str(e)}
280
+
281
+ # # Updated Tool: Update an existing calendar event including guest options
282
+ # class GoogleUpdateCalendarEventArgs(BaseModel):
283
+ # calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
284
+ # user_id: str = Field(default="user", description="User slack Id which should be matched to name")
285
+ # event_id: str = Field(description="The event ID to update")
286
+ # summary: str = Field(default=None, description="Updated event title or agenda")
287
+ # description: Optional[str] = Field(default=None, description="Updated event description or agenda")
288
+ # start_time: Optional[str] = Field(default=None, description="Updated start time in ISO 8601 format")
289
+ # end_time: Optional[str] = Field(default=None, description="Updated end time in ISO 8601 format")
290
+ # location: Optional[str] = Field(default=None, description="Updated event location")
291
+ # invite_link: str = Field(default=None, description="Updated invite link for the meeting")
292
+ # guests: List[str] = Field(default=None, description="Updated list of guest emails")
293
+
294
+ # class GoogleUpdateCalendarEvent(BaseTool):
295
+ # name: str = "google_update_calendar_event"
296
+ # description: str = "Updates an existing event in a Google Calendar, including guest options"
297
+ # args_schema: Type[BaseModel] = GoogleUpdateCalendarEventArgs
298
+
299
+ # def _run(self, user_id: str, event_id: str, calendar_id: str = "primary",
300
+ # summary: Optional[str] = None, description: Optional[str] = None,
301
+ # start_time: Optional[str] = None, end_time: Optional[str] = None,
302
+ # location: Optional[str] = None, invite_link: Optional[str] = None,
303
+ # guests: Optional[List[str]] = None):
304
+
305
+ # calendar_service = construct_google_calendar_client(user_id)
306
+
307
+ # # Retrieve the existing event
308
+ # try:
309
+ # event = calendar_service.events().get(calendarId=calendar_id, eventId=event_id).execute()
310
+ # except Exception as e:
311
+ # return {"status": "error", "message": f"Event retrieval failed: {str(e)}"}
312
+
313
+ # # Update fields if provided
314
+ # if summary:
315
+ # event['summary'] = summary
316
+ # if description:
317
+ # event['description'] = description
318
+ # if invite_link:
319
+ # # Append invite link to the description
320
+ # current_desc = event.get('description', '')
321
+ # event['description'] = f"{current_desc}\nInvite Link: {invite_link}"
322
+ # if start_time:
323
+ # event['start'] = {'dateTime': start_time, 'timeZone': 'UTC'}
324
+ # if end_time:
325
+ # event['end'] = {'dateTime': end_time, 'timeZone': 'UTC'}
326
+ # if location:
327
+ # event['location'] = location
328
+ # if guests is not None:
329
+ # event['attendees'] = [{'email': guest} for guest in guests]
330
+
331
+ # try:
332
+ # updated_event = calendar_service.events().update(
333
+ # calendarId=calendar_id,
334
+ # eventId=event_id,
335
+ # body=event
336
+ # ).execute()
337
+ # return {"status": "success", "event_id": updated_event['id']}
338
+ # except Exception as e:
339
+ # return {"status": "error", "message": f"Update failed: {str(e)}"}
340
+
341
+ # # New Tool: Delete a calendar event
342
+ # class GoogleDeleteCalendarEventArgs(BaseModel):
343
+ # calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
344
+ # event_id: str = Field(description="The event ID to delete")
345
+ # user_id: str = Field(default="user", description="User slack Id which should be matched to name")
346
+
347
+ # class GoogleDeleteCalendarEvent(BaseTool):
348
+ # name: str = "google_delete_calendar_event"
349
+ # description: str = "Deletes an event from a Google Calendar"
350
+ # args_schema: Type[BaseModel] = GoogleDeleteCalendarEventArgs
351
+
352
+ # def _run(self, user_id: str, event_id: str, calendar_id: str = "primary"):
353
+
354
+ # calendar_service = construct_google_calendar_client(user_id)
355
+ # try:
356
+ # calendar_service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
357
+ # return {"status": "success", "message": f"Deleted event {event_id}"}
358
+ # except Exception as e:
359
+ # return {"status": "error", "message": f"Deletion failed: {str(e)}"}
360
+
361
+
362
+ # # New Tool: Search Events by User
363
+ # class SearchUserEventsArgs(BaseModel):
364
+ # user_id: str
365
+ # lookback_days: int = Field(default=30)
366
+ # class SearchUserEventsTool(BaseTool):
367
+ # name: str = "search_events_by_user"
368
+ # description: str = "Finds calendar events associated with a specific user"
369
+ # args_schema: Type[BaseModel] = SearchUserEventsArgs
370
+
371
+ # def _run(self, user_id: str, lookback_days: int = 30):
372
+ # # Get user info which may be a tuple (name, email) or "User not found"
373
+ # user_info = GetSingleUserSlackName().run(user_id)
374
+
375
+ # # Handle case where user is not found
376
+ # if user_info == "User not found":
377
+ # return []
378
+
379
+ # # Extract user_name from tuple
380
+ # user_name, user_email = user_info
381
+
382
+ # # Fetch events for the user
383
+ # events = GoogleCalendarEvents().run(user_id)
384
+
385
+ # # Ensure 'now' is timezone-aware (using UTC)
386
+ # now = datetime.now(pytz.UTC)
387
+
388
+ # relevant_events = []
389
+ # for event in events:
390
+ # # Ensure event_time is timezone-aware
391
+ # event_time_str = event['start'].get('dateTime')
392
+ # if not event_time_str:
393
+ # continue # Skip events without a valid start time
394
+
395
+ # event_time = datetime.fromisoformat(event_time_str)
396
+ # if event_time.tzinfo is None:
397
+ # # If event_time is naive, make it timezone-aware (assuming UTC)
398
+ # event_time = pytz.UTC.localize(event_time)
399
+
400
+ # # Check if the event is within the lookback period
401
+ # if (now - event_time).days > lookback_days:
402
+ # continue
403
+
404
+ # # Check if the user's name is in the event summary or description
405
+ # if user_name in event.get('summary', '') or user_name in event.get('description', ''):
406
+ # relevant_events.append({
407
+ # 'id': event['id'],
408
+ # 'title': event['summary'],
409
+ # 'time': event_time.strftime("%Y-%m-%d %H:%M"),
410
+ # 'calendar_id': event['organizer']['email']
411
+ # })
412
+
413
+ # return relevant_events
414
+ # # Enhanced Agent Logic
415
+ # def handle_update_delete(user_id, text):
416
+ # events = SearchUserEventsTool().run(user_id)
417
+
418
+ # if not events:
419
+ # return "No recent events found for you."
420
+
421
+ # if len(events) > 1:
422
+ # options = [{"text": f"{e['title']} ({e['time']})", "value": e['id']} for e in events]
423
+ # return {
424
+ # "response_type": "ephemeral",
425
+ # "blocks": [{
426
+ # "type": "section",
427
+ # "text": {"type": "mrkdwn", "text": "Multiple events found:"},
428
+ # "accessory": {
429
+ # "type": "static_select",
430
+ # "options": options,
431
+ # "action_id": "select_event_to_modify"
432
+ # }
433
+ # }]
434
+ # }
435
+
436
+ # # If single event, proceed directly
437
+ # return handle_event_modification(events[0]['id'])
438
+
439
+
440
+ # # State Management
441
+ # from collections import defaultdict
442
+ # meeting_coordination = defaultdict(dict)
443
+
444
+ # class CreatePollArgs(BaseModel):
445
+ # time_slots: List[str]
446
+ # channel_id: str
447
+ # initiator_id: str
448
+
449
+ # class CoordinateDMsArgs(BaseModel):
450
+ # user_ids: List[str]
451
+ # time_slots: List[str]
452
+ # # Poll Creation Tool
453
+ # class CoordinateDMsTool(BaseTool):
454
+ # name:str = "coordinate_dm_responses"
455
+ # description:str = "Manages DM responses for meeting coordination"
456
+ # args_schema: Type[BaseModel] = CoordinateDMsArgs
457
+ # def _run(self, user_ids: List[str], time_slots: List[str]) -> Dict:
458
+ # session_id = f"dm_coord_{datetime.now().timestamp()}"
459
+ # message = "Please choose a time slot by replying with the number:\n" + \
460
+ # "\n".join([f"{i+1}. {slot}" for i, slot in enumerate(time_slots)])
461
+
462
+ # for uid in user_ids:
463
+ # if not send_dm(uid, message):
464
+ # return {"status": "error", "message": f"Failed to send DM to user {uid}"}
465
+
466
+ # meeting_coordination[session_id] = {
467
+ # "responses": {},
468
+ # "required": len(user_ids),
469
+ # "slots": time_slots,
470
+ # "participants": user_ids,
471
+ # "created_at": datetime.now()
472
+ # }
473
+
474
+ # return {
475
+ # "status": "success",
476
+ # "session_id": session_id,
477
+ # "message": "DMs sent to all participants"
478
+ # }
479
+
480
+ # # Poll Management
481
+
482
+
483
+
484
+ # # Zoom Meeting Tool
485
+ # # class ZoomCreateMeetingArgs(BaseModel):
486
+ # # topic: str = Field(description="Meeting topic")
487
+ # # start_time: str = Field(description="Start time in ISO 8601 format and PT timezone")
488
+ # # duration: int = Field(description="Duration in minutes")
489
+ # # agenda: Optional[str] = Field(default="", description="Meeting agenda")
490
+ # # timezone: str = Field(default="UTC", description="Timezone for the meeting")
491
+
492
+ # # class ZoomCreateMeetingTool(BaseTool):
493
+ # # name:str = "create_zoom_meeting"
494
+ # # description:str = "Creates a Zoom meeting using configured credentials"
495
+ # # args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
496
+
497
+ # # def _run(self, topic: str, start_time: str, duration: int = 30,
498
+ # # agenda: str = "", timezone: str = "UTC"):
499
+ # # # Get owner's credentials
500
+ # # owner_id = "owner_id_pref"
501
+ # # if not owner_id:
502
+ # # return "Workspace owner not found"
503
+
504
+ # # prefs_path = os.path.join('preferences', f'preferences_{owner_id}.json')
505
+ # # if not os.path.exists(prefs_path):
506
+ # # return "Zoom credentials not configured"
507
+
508
+ # # with open(prefs_path) as f:
509
+ # # prefs = json.load(f)
510
+
511
+ # # if prefs['zoom_config']['mode'] == 'manual':
512
+ # # return {"link": prefs.get('zoom_link'), "message": "Using manual Zoom link"}
513
+
514
+ # # # Automatic Zoom creation
515
+ # # auth_str = f"{prefs['zoom_config']['client_id']}:{prefs['zoom_config']['client_secret']}"
516
+ # # auth_bytes = base64.b64encode(auth_str.encode()).decode()
517
+
518
+ # # headers = {
519
+ # # "Authorization": f"Basic {auth_bytes}",
520
+ # # "Content-Type": "application/x-www-form-urlencoded"
521
+ # # }
522
+
523
+ # # data = {
524
+ # # "grant_type": "account_credentials",
525
+ # # "account_id": prefs['zoom_config']['account_id']
526
+ # # }
527
+
528
+ # # # Get access token
529
+ # # token_res = requests.post(
530
+ # # "https://zoom.us/oauth/token",
531
+ # # headers=headers,
532
+ # # data=data
533
+ # # )
534
+
535
+ # # if token_res.status_code != 200:
536
+ # # return "Zoom authentication failed"
537
+
538
+ # # access_token = token_res.json()["access_token"]
539
+
540
+ # # # Create meeting
541
+ # # meeting_data = {
542
+ # # "topic": topic,
543
+ # # "type": 2,
544
+ # # "start_time": start_time,
545
+ # # "duration": duration,
546
+ # # "timezone": timezone,
547
+ # # "agenda": agenda,
548
+ # # "settings": {
549
+ # # "host_video": True,
550
+ # # "participant_video": True,
551
+ # # "join_before_host": False
552
+ # # }
553
+ # # }
554
+
555
+ # # headers = {
556
+ # # "Authorization": f"Bearer {access_token}",
557
+ # # "Content-Type": "application/json"
558
+ # # }
559
+
560
+ # # meeting_res = requests.post(
561
+ # # "https://api.zoom.us/v2/users/me/meetings",
562
+ # # headers=headers,
563
+ # # json=meeting_data
564
+ # # )
565
+
566
+ # # if meeting_res.status_code == 201:
567
+ # # return {
568
+ # # "meeting_id": meeting_res.json()["id"],
569
+ # # "join_url": meeting_res.json()["join_url"],
570
+ # # 'passcode':meeting_res.json()["password"],
571
+ # # 'duration':duration
572
+ # # }
573
+ # # return f"Zoom meeting creation failed: {meeting_res.text}"
574
+ # from langchain.tools import BaseTool
575
+ # from pydantic import BaseModel, Field
576
+ # from typing import Optional, Type
577
+ # import os
578
+ # import json
579
+ # import time
580
+ # import requests
581
+ # import base64
582
+
583
+ # class ZoomCreateMeetingArgs(BaseModel):
584
+ # topic: str = Field(description="Meeting topic")
585
+ # start_time: str = Field(description="Start time in ISO 8601 format and PT timezone")
586
+ # duration: int = Field(description="Duration in minutes")
587
+ # agenda: Optional[str] = Field(default="", description="Meeting agenda")
588
+ # timezone: str = Field(default="UTC", description="Timezone for the meeting")
589
+
590
+ # from config import get_workspace_owner_id, load_preferences
591
+ # import os
592
+ # # ZOOM_REDIRECT_URI = os.environ['ZOOM_REDIRECT_URI']
593
+ # CLIENT_SECRET = os.environ['ZOOM_CLIENT_SECRET']
594
+ # CLIENT_ID = os.environ['ZOOM_CLIENT_ID']
595
+ # ZOOM_TOKEN_API = os.environ['ZOOM_TOKEN_API']
596
+ # # ZOOM_OAUTH_AUTHORIZE_API = os.environ['ZOOM_OAUTH_AUTHORIZE_API']
597
+ # class ZoomCreateMeetingTool(BaseTool):
598
+ # name: str = "create_zoom_meeting"
599
+ # description: str = "Creates a Zoom meeting using configured credentials"
600
+ # args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
601
+
602
+ # def _run(self, topic: str, start_time: str, duration: int = 30,
603
+ # agenda: str = "", timezone: str = "UTC"):
604
+ # # Get workspace owner's ID
605
+ # owner_id = get_workspace_owner_id()
606
+ # if not owner_id:
607
+ # return "Workspace owner not found"
608
+
609
+ # # Load preferences to check Zoom mode
610
+ # prefs = load_preferences(owner_id)
611
+ # zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
612
+
613
+ # # Handle manual mode
614
+ # if zoom_config["mode"] == "manual":
615
+ # link = zoom_config.get("link")
616
+ # if link:
617
+ # return f"Please join the meeting using this link: {link}"
618
+ # else:
619
+ # return "Manual Zoom link not configured"
620
+
621
+ # # Automatic mode
622
+ # token_path = f'token_files/zoom_{owner_id}.json'
623
+ # if not os.path.exists(token_path):
624
+ # return "Zoom not configured for automatic mode"
625
+
626
+ # with open(token_path) as f:
627
+ # token_data = json.load(f)
628
+
629
+ # access_token = token_data.get("access_token")
630
+ # if not access_token:
631
+ # return "Invalid Zoom token"
632
+
633
+ # # Check if token is expired and refresh if necessary
634
+ # expires_at = token_data.get("expires_at")
635
+ # if expires_at and time.time() > expires_at:
636
+ # refresh_token = token_data.get("refresh_token")
637
+ # if not refresh_token:
638
+ # return "Zoom token expired and no refresh token available"
639
+ # params = {
640
+ # "grant_type": "refresh_token",
641
+ # "refresh_token": refresh_token
642
+ # }
643
+ # auth_str = f"{CLIENT_ID}:{CLIENT_SECRET}"
644
+ # auth_bytes = base64.b64encode(auth_str.encode()).decode()
645
+ # headers = {
646
+ # "Authorization": f"Basic {auth_bytes}",
647
+ # "Content-Type": "application/x-www-form-urlencoded"
648
+ # }
649
+ # response = requests.post(ZOOM_TOKEN_API, data=params, headers=headers)
650
+ # if response.status_code == 200:
651
+ # new_token_data = response.json()
652
+ # token_data.update(new_token_data)
653
+ # token_data["expires_at"] = time.time() + new_token_data["expires_in"]
654
+ # with open(token_path, 'w') as f:
655
+ # json.dump(token_data, f)
656
+ # access_token = new_token_data["access_token"]
657
+ # else:
658
+ # return "Failed to refresh Zoom token"
659
+
660
+ # # Create Zoom meeting
661
+ # meeting_data = {
662
+ # "topic": topic,
663
+ # "type": 2, # Scheduled meeting
664
+ # "start_time": start_time,
665
+ # "duration": duration,
666
+ # "timezone": timezone,
667
+ # "agenda": agenda,
668
+ # "settings": {
669
+ # "host_video": True,
670
+ # "participant_video": True,
671
+ # "join_before_host": False
672
+ # }
673
+ # }
674
+ # headers = {
675
+ # "Authorization": f"Bearer {access_token}",
676
+ # "Content-Type": "application/json"
677
+ # }
678
+ # meeting_res = requests.post(
679
+ # "https://api.zoom.us/v2/users/me/meetings",
680
+ # headers=headers,
681
+ # json=meeting_data
682
+ # )
683
+ # if meeting_res.status_code == 201:
684
+ # meeting_info = meeting_res.json()
685
+
686
+ # # Extract meeting details
687
+ # meeting_id = meeting_info["id"]
688
+ # join_url = meeting_info["join_url"]
689
+ # password = meeting_info.get("password", "")
690
+ # dial_in_numbers = meeting_info["settings"].get("global_dial_in_numbers", [])
691
+ # print(meeting_info['settings'])
692
+ # print(dial_in_numbers)
693
+ # # Format one-tap mobile numbers (up to 2 US numbers)
694
+ # us_numbers = [num for num in dial_in_numbers if num["country"] == "US"]
695
+ # one_tap_strs = []
696
+ # for num in us_numbers[:2]:
697
+ # clean_number = ''.join(num["number"].split()) # Remove spaces, e.g., "+1 305 224 1968" -> "+13052241968"
698
+ # one_tap = f"{clean_number},,{meeting_id}#,,,,*{password}# US"
699
+ # one_tap_strs.append(one_tap)
700
+
701
+ # # Format dial-in numbers
702
+ # dial_in_strs = []
703
+ # for num in dial_in_numbers:
704
+ # country = num["country"]
705
+ # city = num.get("city", "")
706
+ # number = num["number"]
707
+ # if city:
708
+ # dial_in_strs.append(f"• {number} {country} ({city})")
709
+ # else:
710
+ # dial_in_strs.append(f"• {number} {country}")
711
+
712
+ # # Construct the invitation text
713
+ # invitation = "Citrusbug Technolabs is inviting you to a scheduled Zoom meeting.\n\n"
714
+ # invitation += f"Join Zoom Meeting\n{join_url}\n\n"
715
+ # invitation += f"Meeting ID: {meeting_id}\nPasscode: {password}\n\n"
716
+ # invitation += "---\n\n"
717
+ # if one_tap_strs:
718
+ # invitation += "One tap mobile\n"
719
+ # invitation += "\n".join(one_tap_strs) + "\n\n"
720
+ # invitation += "---\n\n"
721
+ # invitation += "Dial by your location\n"
722
+ # invitation += "\n".join(dial_in_strs) + "\n\n"
723
+ # invitation += f"Meeting ID: {meeting_id}\nPasscode: {password}\n"
724
+ # invitation += "Find your local number: https://zoom.us/zoomconference"
725
+
726
+ # return invitation
727
+ # else:
728
+ # return f"Zoom meeting creation failed: {meeting_res.text}"
729
+ # class MicrosoftBaseTool(BaseTool):
730
+ # """Base class for Microsoft tools with common auth handling"""
731
+
732
+ # def get_microsoft_client(self, user_id: str):
733
+ # """Get authenticated Microsoft client for a user"""
734
+ # token_path = os.path.join('token_files', f'microsoft_{user_id}.json')
735
+ # if not os.path.exists(token_path):
736
+ # return None, "Microsoft credentials not configured"
737
+
738
+ # with open(token_path) as f:
739
+ # token_data = json.load(f)
740
+
741
+ # if time.time() > token_data['expires_at']:
742
+ # # Handle token refresh
743
+ # app = ConfidentialClientApplication(
744
+ # MICROSOFT_CLIENT_ID,
745
+ # authority=MICROSOFT_AUTHORITY,
746
+ # client_credential=MICROSOFT_CLIENT_SECRET
747
+ # )
748
+ # result = app.acquire_token_by_refresh_token(
749
+ # token_data['refresh_token'],
750
+ # scopes=MICROSOFT_SCOPES
751
+ # )
752
+ # if "access_token" not in result:
753
+ # return None, "Token refresh failed"
754
+
755
+ # token_data.update(result)
756
+ # with open(token_path, 'w') as f:
757
+ # json.dump(token_data, f)
758
+
759
+ # headers = {
760
+ # "Authorization": f"Bearer {token_data['access_token']}",
761
+ # "Content-Type": "application/json"
762
+ # }
763
+ # return headers, None
764
+
765
+ # # Pydantic models for Microsoft tools
766
+ # class MicrosoftAddCalendarEventArgs(BaseModel):
767
+ # user_id: str = Field(description="Slack user ID of the calendar owner")
768
+ # subject: str = Field(description="Event title/subject")
769
+ # start_time: str = Field(description="Start time in ISO 8601 format")
770
+ # end_time: str = Field(description="End time in ISO 8601 format")
771
+ # content: str = Field(default="", description="Event description/content")
772
+ # location: str = Field(default="", description="Event location")
773
+ # attendees: List[str] = Field(default=[], description="List of attendee emails")
774
+
775
+ # class MicrosoftUpdateCalendarEventArgs(MicrosoftAddCalendarEventArgs):
776
+ # event_id: str = Field(description="Microsoft event ID to update")
777
+
778
+ # class MicrosoftDeleteCalendarEventArgs(BaseModel):
779
+ # user_id: str = Field(description="Slack user ID of the calendar owner")
780
+ # event_id: str = Field(description="Microsoft event ID to delete")
781
+
782
+ # # Microsoft Calendar Tools
783
+ # class MicrosoftListCalendarEvents(MicrosoftBaseTool):
784
+ # name:str = "microsoft_calendar_list_events"
785
+ # description:str = "Lists events from Microsoft Calendar"
786
+
787
+ # def _run(self, user_id: str, max_results: int = 10):
788
+ # headers, error = self.get_microsoft_client(user_id)
789
+ # if error:
790
+ # return error
791
+
792
+ # endpoint = "https://graph.microsoft.com/v1.0/me/events"
793
+ # params = {
794
+ # "$top": max_results,
795
+ # "$orderby": "start/dateTime desc"
796
+ # }
797
+
798
+ # response = requests.get(endpoint, headers=headers, params=params)
799
+ # if response.status_code != 200:
800
+ # return f"Error fetching events: {response.text}"
801
+
802
+ # events = response.json().get('value', [])
803
+ # return [{
804
+ # 'id': e['id'],
805
+ # 'subject': e.get('subject'),
806
+ # 'start': e['start'].get('dateTime'),
807
+ # 'end': e['end'].get('dateTime'),
808
+ # 'webLink': e.get('webUrl')
809
+ # } for e in events]
810
+
811
+ # class MicrosoftAddCalendarEvent(MicrosoftBaseTool):
812
+ # name:str = "microsoft_calendar_add_event"
813
+ # description:str = "Creates an event in Microsoft Calendar"
814
+ # args_schema: Type[BaseModel] = MicrosoftAddCalendarEventArgs
815
+
816
+ # def _run(self, user_id: str, subject: str, start_time: str, end_time: str,
817
+ # content: str = "", location: str = "", attendees: List[str] = []):
818
+ # headers, error = self.get_microsoft_client(user_id)
819
+ # if error:
820
+ # return error
821
+
822
+ # event_payload = {
823
+ # "subject": subject,
824
+ # "body": {
825
+ # "contentType": "HTML",
826
+ # "content": content
827
+ # },
828
+ # "start": {
829
+ # "dateTime": start_time,
830
+ # "timeZone": "America/Los_Angeles"
831
+ # },
832
+ # "end": {
833
+ # "dateTime": end_time,
834
+ # "timeZone": "America/Los_Angeles"
835
+ # },
836
+ # "location": {"displayName": location},
837
+ # "attendees": [{"emailAddress": {"address": email}} for email in attendees]
838
+ # }
839
+
840
+ # response = requests.post(
841
+ # "https://graph.microsoft.com/v1.0/me/events",
842
+ # headers=headers,
843
+ # json=event_payload
844
+ # )
845
+
846
+ # if response.status_code == 201:
847
+ # return {
848
+ # "status": "success",
849
+ # "event_id": response.json()['id'],
850
+ # "link": response.json().get('webUrl')
851
+ # }
852
+ # return f"Error creating event: {response.text}"
853
+
854
+ # class MicrosoftUpdateCalendarEvent(MicrosoftBaseTool):
855
+ # name:str = "microsoft_calendar_update_event"
856
+ # description:str = "Updates an existing Microsoft Calendar event"
857
+ # args_schema: Type[BaseModel] = MicrosoftUpdateCalendarEventArgs
858
+
859
+ # def _run(self, user_id: str, event_id: str, **kwargs):
860
+ # headers, error = self.get_microsoft_client(user_id)
861
+ # if error:
862
+ # return error
863
+
864
+ # get_response = requests.get(
865
+ # f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
866
+ # headers=headers
867
+ # )
868
+ # if get_response.status_code != 200:
869
+ # return f"Error finding event: {get_response.text}"
870
+
871
+ # existing_event = get_response.json()
872
+
873
+ # update_payload = {
874
+ # "subject": kwargs.get('subject', existing_event.get('subject')),
875
+ # "body": {
876
+ # "content": kwargs.get('content', existing_event.get('body', {}).get('content')),
877
+ # "contentType": "HTML"
878
+ # },
879
+ # "start": {
880
+ # "dateTime": kwargs.get('start_time', existing_event['start']['dateTime']),
881
+ # "timeZone": "UTC"
882
+ # },
883
+ # "end": {
884
+ # "dateTime": kwargs.get('end_time', existing_event['end']['dateTime']),
885
+ # "timeZone": "UTC"
886
+ # },
887
+ # "location": {"displayName": kwargs.get('location', existing_event.get('location', {}).get('displayName'))},
888
+ # "attendees": [{"emailAddress": {"address": email}} for email in
889
+ # kwargs.get('attendees', [a['emailAddress']['address'] for a in existing_event.get('attendees', [])])]
890
+ # }
891
+
892
+ # response = requests.patch(
893
+ # f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
894
+ # headers=headers,
895
+ # json=update_payload
896
+ # )
897
+
898
+ # if response.status_code == 200:
899
+ # return {"status": "success", "event_id": event_id}
900
+ # return f"Error updating event: {response.text}"
901
+
902
+ # class MicrosoftDeleteCalendarEvent(MicrosoftBaseTool):
903
+ # name:str = "microsoft_calendar_delete_event"
904
+ # description:str = "Deletes an event from Microsoft Calendar"
905
+ # args_schema: Type[BaseModel] = MicrosoftDeleteCalendarEventArgs
906
+
907
+ # def _run(self, user_id: str, event_id: str):
908
+ # headers, error = self.get_microsoft_client(user_id)
909
+ # if error:
910
+ # return error
911
+
912
+ # response = requests.delete(
913
+ # f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
914
+ # headers=headers
915
+ # )
916
+
917
+ # if response.status_code == 204:
918
+ # return {"status": "success", "message": f"Deleted event {event_id}"}
919
+ # return f"Error deleting event: {response.text}"
920
+
921
+
922
+
923
+
924
+ # tools = [
925
+ # DirectDMTool(),
926
+ # ZoomCreateMeetingTool(),
927
+ # # GetSingleUserSlackName(),
928
+ # # GetSingleUserSlackID(),
929
+ # # CoordinateDMsTool(),
930
+ # SearchUserEventsTool(),
931
+ # # DateTimeTool(),
932
+ # GoogleCalendarList(),
933
+ # GoogleCalendarEvents(),
934
+ # GoogleCreateCalendar(),
935
+ # GoogleAddCalendarEvent(),
936
+ # GoogleUpdateCalendarEvent(),
937
+ # GoogleDeleteCalendarEvent(),
938
+ # # MicrosoftListCalendarEvents(),
939
+ # MicrosoftAddCalendarEvent(),
940
+ # MicrosoftUpdateCalendarEvent(),
941
+ # MicrosoftDeleteCalendarEvent(),
942
+ # MultiDirectDMTool()
943
+ # ]
944
+ # calendar_prompt_tools = [
945
+ # MicrosoftListCalendarEvents(),
946
+ # GoogleCalendarEvents()
947
+
948
+ # ]
949
+ # dm_tools = [
950
+ # DirectDMTool(),
951
+ # ZoomCreateMeetingTool(),
952
+ # # CoordinateDMsTool(),
953
+ # SearchUserEventsTool(),
954
+ # GetSingleUserSlackName(),
955
+ # GetSingleUserSlackID(),
956
+ # # DateTimeTool(),
957
+ # GoogleCalendarList(),
958
+ # GoogleCalendarEvents(),
959
+ # GoogleCreateCalendar(),
960
+ # GoogleAddCalendarEvent(),
961
+ # GoogleUpdateCalendarEvent(),
962
+ # GoogleDeleteCalendarEvent(),
963
+ # MicrosoftListCalendarEvents(),
964
+ # MicrosoftAddCalendarEvent(),
965
+ # MicrosoftUpdateCalendarEvent(),
966
+ # MicrosoftDeleteCalendarEvent()
967
+ # ]
968
+
969
+ # dm_group_tools = [
970
+ # GoogleCalendarEvents(),
971
+ # MicrosoftListCalendarEvents(),
972
+ # DateTimeTool(),
973
+
974
+ # ]
975
+ import json
976
+ import os
977
+ import requests
978
+ import base64
979
+ import msal
980
+ import time
981
+ from typing import Type, Optional, List
982
+
983
+ from dotenv import load_dotenv
984
+ from pydantic.v1 import BaseModel, Field
985
+ from msal import ConfidentialClientApplication
986
+
987
+ from langchain_core.tools import BaseTool
988
+ from slack_sdk.errors import SlackApiError
989
+ from datetime import datetime
990
+ from google_auth_oauthlib.flow import InstalledAppFlow
991
+ from googleapiclient.discovery import build
992
+ from google.oauth2.credentials import Credentials
993
+ from google.auth.transport.requests import Request
994
+ from config import client, get_workspace_owner_id, load_preferences, load_token, save_token
995
+ from datetime import datetime, timedelta
996
+ import pytz
997
+ from collections import defaultdict
998
+ from config import all_users_preload, GetAllUsers
999
+
1000
+ load_dotenv()
1001
+
1002
+ # Load credentials from environment variables
1003
+ MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
1004
+ MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
1005
+ MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
1006
+ MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
1007
+ MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
1008
+ ZOOM_CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET")
1009
+ ZOOM_CLIENT_ID = os.getenv("ZOOM_CLIENT_ID")
1010
+ ZOOM_TOKEN_API = os.getenv("ZOOM_TOKEN_API", "https://zoom.us/oauth/token")
1011
+
1012
+ # Pydantic models for tool arguments
1013
+ class DirectDMArgs(BaseModel):
1014
+ message: str = Field(description="The message to be sent to the Slack user")
1015
+ user_id: str = Field(description="The Slack user ID")
1016
+
1017
+ class DateTimeTool(BaseTool):
1018
+ name: str = "current_date_time"
1019
+ description: str = "Provides the current date and time."
1020
+
1021
+ def _run(self):
1022
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1023
+
1024
+ # Slack Tools
1025
+ class GetSingleUserSlackIDArgs(BaseModel):
1026
+ name: str = Field(description="The real name of the user whose Slack ID is needed")
1027
+
1028
+ class GetSingleUserSlackID(BaseTool):
1029
+ name: str = "gets_slack_id_single_user"
1030
+ description: str = "Gets the Slack ID of a user based on their real name"
1031
+ args_schema: Type[BaseModel] = GetSingleUserSlackIDArgs
1032
+
1033
+ def _run(self, name: str):
1034
+ if not all_users_preload:
1035
+ all_users = GetAllUsers()
1036
+ else:
1037
+ all_users = all_users_preload
1038
+
1039
+ for uid, info in all_users.items():
1040
+ if info["name"].lower() == name.lower():
1041
+ return uid, info['email']
1042
+ return "User not found"
1043
+
1044
+ class GetSingleUserSlackNameArgs(BaseModel):
1045
+ id: str = Field(description="The Slack user ID")
1046
+
1047
+ class GetSingleUserSlackName(BaseTool):
1048
+ name: str = "gets_slack_name_single_user"
1049
+ description: str = "Gets the Slack real name of a user based on their Slack ID"
1050
+ args_schema: Type[BaseModel] = GetSingleUserSlackNameArgs
1051
+
1052
+ def _run(self, id: str):
1053
+ if not all_users_preload or all_users_preload == {}:
1054
+ all_users = GetAllUsers()
1055
+ else:
1056
+ all_users = all_users_preload
1057
+
1058
+ user = all_users.get(id)
1059
+ if user:
1060
+ return user["name"], user['email']
1061
+ return "User not found"
1062
+
1063
+ class MultiDMArgs(BaseModel):
1064
+ message: str
1065
+ user_ids: List[str]
1066
+
1067
+ class MultiDirectDMTool(BaseTool):
1068
+ name: str = "send_multiple_dms"
1069
+ description: str = "Sends direct messages to multiple Slack users"
1070
+ args_schema: Type[BaseModel] = MultiDMArgs
1071
+
1072
+ def _run(self, message: str, user_ids: List[str]):
1073
+ results = {}
1074
+ for user_id in user_ids:
1075
+ try:
1076
+ client.chat_postMessage(channel=user_id, text=message)
1077
+ results[user_id] = "Message sent successfully"
1078
+ except SlackApiError as e:
1079
+ results[user_id] = f"Error: {e.response['error']}"
1080
+ return results
1081
+
1082
+ class DirectDMTool(BaseTool):
1083
+ name: str = "send_direct_dm"
1084
+ description: str = "Sends direct messages to Slack users"
1085
+ args_schema: Type[BaseModel] = DirectDMArgs
1086
+
1087
+ def _run(self, message: str, user_id: str):
1088
+ try:
1089
+ client.chat_postMessage(channel=user_id, text=message)
1090
+ return "Message sent successfully"
1091
+ except SlackApiError as e:
1092
+ return f"Error sending message: {e.response['error']}"
1093
+
1094
+ def send_dm(user_id: str, message: str) -> bool:
1095
+ try:
1096
+ client.chat_postMessage(channel=user_id, text=message)
1097
+ return True
1098
+ except SlackApiError as e:
1099
+ print(f"Error sending DM: {e.response['error']}")
1100
+ return False
1101
+
1102
+ # Google Calendar Tools
1103
+ PT = pytz.timezone('America/Los_Angeles')
1104
+
1105
+ def construct_google_calendar_client(team_id: str, user_id: str):
1106
+ token_data = load_token(team_id, user_id, 'google')
1107
+ if not token_data:
1108
+ return None
1109
+ creds = Credentials(
1110
+ token=token_data.get('access_token'),
1111
+ refresh_token=token_data.get('refresh_token'),
1112
+ token_uri="https://oauth2.googleapis.com/token",
1113
+ client_id=os.getenv("GOOGLE_CLIENT_ID"),
1114
+ client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
1115
+ scopes=["https://www.googleapis.com/auth/calendar"]
1116
+ )
1117
+ if creds.expired and creds.refresh_token:
1118
+ creds.refresh(Request())
1119
+ token_data.update({
1120
+ "access_token": creds.token,
1121
+ "refresh_token": creds.refresh_token,
1122
+ "expires_at": creds.expiry.timestamp()
1123
+ })
1124
+ save_token(team_id, user_id, 'google', token_data)
1125
+ return build('calendar', 'v3', credentials=creds)
1126
+
1127
+ class GoogleCalendarList(BaseTool):
1128
+ name: str = "list_calendar_list"
1129
+ description: str = "Lists available calendars in the user's Google Calendar account"
1130
+
1131
+ def _run(self, team_id: str, user_id: str, max_capacity: int = 200):
1132
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1133
+ if not calendar_service:
1134
+ return "Google Calendar not configured or token invalid."
1135
+
1136
+ all_calendars = []
1137
+ next_page_token = None
1138
+ capacity_tracker = 0
1139
+
1140
+ while capacity_tracker < max_capacity:
1141
+ results = calendar_service.calendarList().list(
1142
+ maxResults=min(200, max_capacity - capacity_tracker),
1143
+ pageToken=next_page_token
1144
+ ).execute()
1145
+ calendars = results.get('items', [])
1146
+ all_calendars.extend(calendars)
1147
+ capacity_tracker += len(calendars)
1148
+ next_page_token = results.get('nextPageToken')
1149
+ if not next_page_token:
1150
+ break
1151
+
1152
+ return [{
1153
+ 'id': cal['id'],
1154
+ 'name': cal['summary'],
1155
+ 'description': cal.get('description', '')
1156
+ } for cal in all_calendars]
1157
+
1158
+ class GoogleCalendarEvents(BaseTool):
1159
+ name: str = "list_calendar_events"
1160
+ description: str = "Lists and gets events from a specific Google Calendar"
1161
+
1162
+ def _run(self, team_id: str, user_id: str, calendar_id: str = "primary", max_capacity: int = 20):
1163
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1164
+ if not calendar_service:
1165
+ return "Google Calendar not configured or token invalid."
1166
+
1167
+ all_events = []
1168
+ next_page_token = None
1169
+ capacity_tracker = 0
1170
+
1171
+ while capacity_tracker < max_capacity:
1172
+ results = calendar_service.events().list(
1173
+ calendarId=calendar_id,
1174
+ maxResults=min(250, max_capacity - capacity_tracker),
1175
+ pageToken=next_page_token
1176
+ ).execute()
1177
+ events = results.get('items', [])
1178
+ all_events.extend(events)
1179
+ capacity_tracker += len(events)
1180
+ next_page_token = results.get('nextPageToken')
1181
+ if not next_page_token:
1182
+ break
1183
+
1184
+ return all_events
1185
+
1186
+ class GoogleCreateCalendar(BaseTool):
1187
+ name: str = "create_calendar_list"
1188
+ description: str = "Creates a new calendar in Google Calendar"
1189
+
1190
+ def _run(self, team_id: str, user_id: str, calendar_name: str):
1191
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1192
+ if not calendar_service:
1193
+ return "Google Calendar not configured or token invalid."
1194
+
1195
+ calendar_body = {'summary': calendar_name}
1196
+ created_calendar = calendar_service.calendars().insert(body=calendar_body).execute()
1197
+ return f"Created calendar: {created_calendar['id']}"
1198
+
1199
+ class GoogleAddCalendarEventArgs(BaseModel):
1200
+ team_id: str = Field(description="Team id here")
1201
+ user_id: str = Field(description="User id here")
1202
+
1203
+ calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
1204
+ summary: str = Field(description="Event title")
1205
+ description: str = Field(default="", description="Event description or agenda")
1206
+ start_time: str = Field(description="Start time in ISO 8601 format")
1207
+ end_time: str = Field(description="End time in ISO 8601 format")
1208
+ location: str = Field(default="", description="Event location")
1209
+ invite_link: str = Field(default="", description="Invite link for the meeting")
1210
+ guests: List[str] = Field(default=None, description="List of guest emails to invite")
1211
+
1212
+ class GoogleAddCalendarEvent(BaseTool):
1213
+ name: str = "google_add_calendar_event"
1214
+ description: str = "Creates an event in a Google Calendar"
1215
+ args_schema: Type[BaseModel] = GoogleAddCalendarEventArgs
1216
+
1217
+ def _run(self, team_id: str, user_id: str, summary: str, start_time: str, end_time: str,
1218
+ description: str = "", calendar_id: str = 'primary', location: str = "",
1219
+ invite_link: str = "", guests: List[str] = None):
1220
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1221
+ if not calendar_service:
1222
+ return "Google Calendar not configured or token invalid."
1223
+
1224
+ if invite_link:
1225
+ description = f"{description}\nInvite Link: {invite_link}"
1226
+
1227
+ event = {
1228
+ 'summary': summary,
1229
+ 'description': description,
1230
+ 'start': {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'},
1231
+ 'end': {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'},
1232
+ 'location': location,
1233
+ }
1234
+
1235
+ if guests:
1236
+ event['attendees'] = [{'email': guest} for guest in guests]
1237
+
1238
+ try:
1239
+ created_event = calendar_service.events().insert(
1240
+ calendarId=calendar_id,
1241
+ body=event,
1242
+ sendUpdates='all'
1243
+ ).execute()
1244
+ return {
1245
+ "status": "success",
1246
+ "event_id": created_event['id'],
1247
+ "link": created_event.get('htmlLink', '')
1248
+ }
1249
+ except Exception as e:
1250
+ return {"status": "error", "message": str(e)}
1251
+
1252
+ class GoogleUpdateCalendarEventArgs(BaseModel):
1253
+ team_id: str = Field(description="Team id here")
1254
+ user_id: str = Field(description="User id here")
1255
+ calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
1256
+ event_id: str = Field(description="The event ID to update")
1257
+ summary: str = Field(default=None, description="Updated event title")
1258
+ description: Optional[str] = Field(default=None, description="Updated event description")
1259
+ start_time: Optional[str] = Field(default=None, description="Updated start time in ISO 8601 format")
1260
+ end_time: Optional[str] = Field(default=None, description="Updated end time in ISO 8601 format")
1261
+ location: Optional[str] = Field(default=None, description="Updated event location")
1262
+ invite_link: str = Field(default=None, description="Updated invite link")
1263
+ guests: List[str] = Field(default=None, description="Updated list of guest emails")
1264
+
1265
+ class GoogleUpdateCalendarEvent(BaseTool):
1266
+ name: str = "google_update_calendar_event"
1267
+ description: str = "Updates an existing event in a Google Calendar"
1268
+ args_schema: Type[BaseModel] = GoogleUpdateCalendarEventArgs
1269
+
1270
+ def _run(self, team_id: str, user_id: str, event_id: str, calendar_id: str = "primary",
1271
+ summary: Optional[str] = None, description: Optional[str] = None,
1272
+ start_time: Optional[str] = None, end_time: Optional[str] = None,
1273
+ location: Optional[str] = None, invite_link: Optional[str] = None,
1274
+ guests: Optional[List[str]] = None):
1275
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1276
+ if not calendar_service:
1277
+ return "Google Calendar not configured or token invalid."
1278
+
1279
+ try:
1280
+ event = calendar_service.events().get(calendarId=calendar_id, eventId=event_id).execute()
1281
+ except Exception as e:
1282
+ return {"status": "error", "message": f"Event retrieval failed: {str(e)}"}
1283
+
1284
+ if summary:
1285
+ event['summary'] = summary
1286
+ if description:
1287
+ event['description'] = description
1288
+ if invite_link:
1289
+ current_desc = event.get('description', '')
1290
+ event['description'] = f"{current_desc}\nInvite Link: {invite_link}"
1291
+ if start_time:
1292
+ event['start'] = {'dateTime': start_time, 'timeZone': 'America/Los_Angeles'}
1293
+ if end_time:
1294
+ event['end'] = {'dateTime': end_time, 'timeZone': 'America/Los_Angeles'}
1295
+ if location:
1296
+ event['location'] = location
1297
+ if guests is not None:
1298
+ event['attendees'] = [{'email': guest} for guest in guests]
1299
+
1300
+ try:
1301
+ updated_event = calendar_service.events().update(
1302
+ calendarId=calendar_id,
1303
+ eventId=event_id,
1304
+ body=event
1305
+ ).execute()
1306
+ return {"status": "success", "event_id": updated_event['id']}
1307
+ except Exception as e:
1308
+ return {"status": "error", "message": f"Update failed: {str(e)}"}
1309
+
1310
+ class GoogleDeleteCalendarEventArgs(BaseModel):
1311
+ team_id: str = Field(description="Team id here")
1312
+ user_id: str = Field(description="User id here")
1313
+ calendar_id: str = Field(default="primary", description="Calendar ID (default 'primary')")
1314
+ event_id: str = Field(description="The event ID to delete")
1315
+
1316
+ class GoogleDeleteCalendarEvent(BaseTool):
1317
+ name: str = "google_delete_calendar_event"
1318
+ description: str = "Deletes an event from a Google Calendar"
1319
+ args_schema: Type[BaseModel] = GoogleDeleteCalendarEventArgs
1320
+
1321
+ def _run(self, team_id: str, user_id: str, event_id: str, calendar_id: str = "primary"):
1322
+ calendar_service = construct_google_calendar_client(team_id, user_id)
1323
+ if not calendar_service:
1324
+ return "Google Calendar not configured or token invalid."
1325
+ try:
1326
+ calendar_service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
1327
+ return {"status": "success", "message": f"Deleted event {event_id}"}
1328
+ except Exception as e:
1329
+ return {"status": "error", "message": f"Deletion failed: {str(e)}"}
1330
+
1331
+ # Search Events Tool
1332
+ class SearchUserEventsArgs(BaseModel):
1333
+ user_id: str
1334
+ lookback_days: int = Field(default=30)
1335
+
1336
+ class SearchUserEventsTool(BaseTool):
1337
+ name: str = "search_events_by_user"
1338
+ description: str = "Finds calendar events associated with a specific user"
1339
+ args_schema: Type[BaseModel] = SearchUserEventsArgs
1340
+
1341
+ def _run(self, team_id: str, user_id: str, lookback_days: int = 30):
1342
+ user_info = GetSingleUserSlackName().run(user_id)
1343
+ if user_info == "User not found":
1344
+ return []
1345
+
1346
+ user_name, user_email = user_info
1347
+ events = GoogleCalendarEvents().run(team_id, user_id)
1348
+
1349
+ now = datetime.now(pytz.UTC)
1350
+ relevant_events = []
1351
+ for event in events:
1352
+ event_time_str = event['start'].get('dateTime')
1353
+ if not event_time_str:
1354
+ continue
1355
+
1356
+ event_time = datetime.fromisoformat(event_time_str)
1357
+ if event_time.tzinfo is None:
1358
+ event_time = pytz.UTC.localize(event_time)
1359
+
1360
+ if (now - event_time).days > lookback_days:
1361
+ continue
1362
+
1363
+ if user_name in event.get('summary', '') or user_name in event.get('description', ''):
1364
+ relevant_events.append({
1365
+ 'id': event['id'],
1366
+ 'title': event['summary'],
1367
+ 'time': event_time.strftime("%Y-%m-%d %H:%M"),
1368
+ 'calendar_id': event['organizer']['email']
1369
+ })
1370
+ return relevant_events
1371
+
1372
+ # Zoom Meeting Tool
1373
+ class ZoomCreateMeetingArgs(BaseModel):
1374
+ team_id:str = Field(description="Team Id here")
1375
+ topic: str = Field(description="Meeting topic with all names not slack ids starting with U--")
1376
+ start_time: str = Field(description="Start time in ISO 8601 format")
1377
+ duration: int = Field(description="Duration in minutes")
1378
+ agenda: Optional[str] = Field(default="", description="Meeting agenda")
1379
+ timezone: str = Field(default="UTC", description="Timezone for the meeting")
1380
+
1381
+ class ZoomCreateMeetingTool(BaseTool):
1382
+ name: str = "create_zoom_meeting"
1383
+ description: str = "Creates a Zoom meeting using configured credentials"
1384
+ args_schema: Type[BaseModel] = ZoomCreateMeetingArgs
1385
+
1386
+ def _run(self, team_id: str, topic: str, start_time: str, duration: int = 30,
1387
+ agenda: str = "", timezone: str = "UTC"):
1388
+ owner_id = get_workspace_owner_id(client,team_id)
1389
+ if not owner_id:
1390
+ return "Workspace owner not found"
1391
+
1392
+ prefs = load_preferences(team_id, owner_id)
1393
+ zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
1394
+
1395
+ if zoom_config["mode"] == "manual":
1396
+ link = zoom_config.get("link")
1397
+ if link:
1398
+ return f"Please join the meeting using this link: {link}"
1399
+ else:
1400
+ return "Manual Zoom link not configured"
1401
+
1402
+ token_data = load_token(team_id, owner_id, 'zoom')
1403
+ if not token_data:
1404
+ return "Zoom token not found in database"
1405
+
1406
+ access_token = token_data.get("access_token")
1407
+ if not access_token:
1408
+ return "Invalid Zoom token"
1409
+
1410
+ expires_at = token_data.get("expires_at")
1411
+ if expires_at and time.time() > expires_at:
1412
+ refresh_token = token_data.get("refresh_token")
1413
+ if not refresh_token:
1414
+ return "Zoom token expired and no refresh token available"
1415
+ params = {
1416
+ "grant_type": "refresh_token",
1417
+ "refresh_token": refresh_token
1418
+ }
1419
+ auth_str = f"{ZOOM_CLIENT_ID}:{ZOOM_CLIENT_SECRET}"
1420
+ auth_bytes = base64.b64encode(auth_str.encode()).decode()
1421
+ headers = {
1422
+ "Authorization": f"Basic {auth_bytes}",
1423
+ "Content-Type": "application/x-www-form-urlencoded"
1424
+ }
1425
+ response = requests.post(ZOOM_TOKEN_API, data=params, headers=headers)
1426
+ if response.status_code == 200:
1427
+ new_token_data = response.json()
1428
+ token_data.update(new_token_data)
1429
+ token_data["expires_at"] = time.time() + new_token_data["expires_in"]
1430
+ save_token(team_id, owner_id, 'zoom', token_data)
1431
+ access_token = new_token_data["access_token"]
1432
+ else:
1433
+ return "Failed to refresh Zoom token"
1434
+
1435
+ meeting_data = {
1436
+ "topic": topic,
1437
+ "type": 2,
1438
+ "start_time": start_time,
1439
+ "duration": duration,
1440
+ "timezone": timezone,
1441
+ "agenda": agenda,
1442
+ "settings": {
1443
+ "host_video": True,
1444
+ "participant_video": True,
1445
+ "join_before_host": False
1446
+ }
1447
+ }
1448
+ headers = {
1449
+ "Authorization": f"Bearer {access_token}",
1450
+ "Content-Type": "application/json"
1451
+ }
1452
+ meeting_res = requests.post(
1453
+ "https://api.zoom.us/v2/users/me/meetings",
1454
+ headers=headers,
1455
+ json=meeting_data
1456
+ )
1457
+ if meeting_res.status_code == 201:
1458
+ meeting_info = meeting_res.json()
1459
+ invitation = f"Join Zoom Meeting\n{meeting_info['join_url']}\nMeeting ID: {meeting_info['id']}\nPasscode: {meeting_info.get('password', '')}"
1460
+ return invitation
1461
+ else:
1462
+ return f"Zoom meeting creation failed: {meeting_res.text}"
1463
+
1464
+ # Microsoft Calendar Tools
1465
+ class MicrosoftBaseTool(BaseTool):
1466
+ def get_microsoft_client(self, team_id: str, user_id: str):
1467
+ token_data = load_token(team_id, user_id, 'microsoft')
1468
+ if not token_data:
1469
+ return None, "Microsoft token not found in database"
1470
+
1471
+ if time.time() > token_data['expires_at']:
1472
+ app = ConfidentialClientApplication(
1473
+ MICROSOFT_CLIENT_ID,
1474
+ authority=MICROSOFT_AUTHORITY,
1475
+ client_credential=MICROSOFT_CLIENT_SECRET
1476
+ )
1477
+ result = app.acquire_token_by_refresh_token(
1478
+ token_data['refresh_token'],
1479
+ scopes=MICROSOFT_SCOPES
1480
+ )
1481
+ if "access_token" not in result:
1482
+ return None, "Token refresh failed"
1483
+ token_data.update(result)
1484
+ save_token(team_id, user_id, 'microsoft', token_data)
1485
+
1486
+ headers = {
1487
+ "Authorization": f"Bearer {token_data['access_token']}",
1488
+ "Content-Type": "application/json"
1489
+ }
1490
+ return headers, None
1491
+
1492
+ class MicrosoftListCalendarEvents(MicrosoftBaseTool):
1493
+ name: str = "microsoft_calendar_list_events"
1494
+ description: str = "Lists events from Microsoft Calendar"
1495
+
1496
+ def _run(self, team_id: str, user_id: str, max_results: int = 10):
1497
+ headers, error = self.get_microsoft_client(team_id, user_id)
1498
+ if error:
1499
+ return error
1500
+
1501
+ endpoint = "https://graph.microsoft.com/v1.0/me/events"
1502
+ params = {"$top": max_results, "$orderby": "start/dateTime desc"}
1503
+ response = requests.get(endpoint, headers=headers, params=params)
1504
+ if response.status_code != 200:
1505
+ return f"Error fetching events: {response.text}"
1506
+
1507
+ events = response.json().get('value', [])
1508
+ return [{
1509
+ 'id': e['id'],
1510
+ 'subject': e.get('subject'),
1511
+ 'start': e['start'].get('dateTime'),
1512
+ 'end': e['end'].get('dateTime'),
1513
+ 'webLink': e.get('webUrl')
1514
+ } for e in events]
1515
+
1516
+ class MicrosoftAddCalendarEventArgs(BaseModel):
1517
+ team_id: str = Field(description="Team id here")
1518
+ user_id: str = Field(description="User id here")
1519
+ subject: str = Field(description="Event title/subject")
1520
+ start_time: str = Field(description="Start time in ISO 8601 format")
1521
+ end_time: str = Field(description="End time in ISO 8601 format")
1522
+ content: str = Field(default="", description="Event description/content")
1523
+ location: str = Field(default="", description="Event location")
1524
+ attendees: List[str] = Field(default=[], description="List of attendee emails")
1525
+
1526
+ class MicrosoftAddCalendarEvent(MicrosoftBaseTool):
1527
+ name: str = "microsoft_calendar_add_event"
1528
+ description: str = "Creates an event in Microsoft Calendar"
1529
+ args_schema: Type[BaseModel] = MicrosoftAddCalendarEventArgs
1530
+
1531
+ def _run(self, team_id: str, user_id: str, subject: str, start_time: str, end_time: str,
1532
+ content: str = "", location: str = "", attendees: List[str] = []):
1533
+ headers, error = self.get_microsoft_client(team_id, user_id)
1534
+ if error:
1535
+ return error
1536
+
1537
+ event_payload = {
1538
+ "subject": subject,
1539
+ "body": {"contentType": "HTML", "content": content},
1540
+ "start": {"dateTime": start_time, "timeZone": "America/Los_Angeles"},
1541
+ "end": {"dateTime": end_time, "timeZone": "America/Los_Angeles"},
1542
+ "location": {"displayName": location},
1543
+ "attendees": [{"emailAddress": {"address": email}} for email in attendees]
1544
+ }
1545
+
1546
+ response = requests.post(
1547
+ "https://graph.microsoft.com/v1.0/me/events",
1548
+ headers=headers,
1549
+ json=event_payload
1550
+ )
1551
+ if response.status_code == 201:
1552
+ return {
1553
+ "status": "success",
1554
+ "event_id": response.json()['id'],
1555
+ "link": response.json().get('webUrl')
1556
+ }
1557
+ return f"Error creating event: {response.text}"
1558
+
1559
+ class MicrosoftUpdateCalendarEventArgs(MicrosoftAddCalendarEventArgs):
1560
+ team_id: str = Field(description="Team id here")
1561
+ user_id: str = Field(description="User id here")
1562
+ event_id: str = Field(description="Microsoft event ID to update")
1563
+
1564
+ class MicrosoftUpdateCalendarEvent(MicrosoftBaseTool):
1565
+ name: str = "microsoft_calendar_update_event"
1566
+ description: str = "Updates an existing Microsoft Calendar event"
1567
+ args_schema: Type[BaseModel] = MicrosoftUpdateCalendarEventArgs
1568
+
1569
+ def _run(self, team_id: str, user_id: str, event_id: str, **kwargs):
1570
+ headers, error = self.get_microsoft_client(team_id, user_id)
1571
+ if error:
1572
+ return error
1573
+
1574
+ get_response = requests.get(
1575
+ f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
1576
+ headers=headers
1577
+ )
1578
+ if get_response.status_code != 200:
1579
+ return f"Error finding event: {get_response.text}"
1580
+
1581
+ existing_event = get_response.json()
1582
+ update_payload = {
1583
+ "subject": kwargs.get('subject', existing_event.get('subject')),
1584
+ "body": {
1585
+ "content": kwargs.get('content', existing_event.get('body', {}).get('content')),
1586
+ "contentType": "HTML"
1587
+ },
1588
+ "start": {
1589
+ "dateTime": kwargs.get('start_time', existing_event['start']['dateTime']),
1590
+ "timeZone": "America/Los_Angeles"
1591
+ },
1592
+ "end": {
1593
+ "dateTime": kwargs.get('end_time', existing_event['end']['dateTime']),
1594
+ "timeZone": "America/Los_Angeles"
1595
+ },
1596
+ "location": {"displayName": kwargs.get('location', existing_event.get('location', {}).get('displayName'))},
1597
+ "attendees": [{"emailAddress": {"address": email}} for email in
1598
+ kwargs.get('attendees', [a['emailAddress']['address'] for a in existing_event.get('attendees', [])])]
1599
+ }
1600
+
1601
+ response = requests.patch(
1602
+ f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
1603
+ headers=headers,
1604
+ json=update_payload
1605
+ )
1606
+ if response.status_code == 200:
1607
+ return {"status": "success", "event_id": event_id}
1608
+ return f"Error updating event: {response.text}"
1609
+
1610
+ class MicrosoftDeleteCalendarEventArgs(BaseModel):
1611
+ team_id: str = Field(description="Team id here")
1612
+ user_id: str = Field(description="User id here")
1613
+ event_id: str = Field(description="Microsoft event ID to delete")
1614
+
1615
+ class MicrosoftDeleteCalendarEvent(MicrosoftBaseTool):
1616
+ name: str = "microsoft_calendar_delete_event"
1617
+ description: str = "Deletes an event from Microsoft Calendar"
1618
+ args_schema: Type[BaseModel] = MicrosoftDeleteCalendarEventArgs
1619
+
1620
+ def _run(self, team_id: str, user_id: str, event_id: str):
1621
+ headers, error = self.get_microsoft_client(team_id, user_id)
1622
+ if error:
1623
+ return error
1624
+
1625
+ response = requests.delete(
1626
+ f"https://graph.microsoft.com/v1.0/me/events/{event_id}",
1627
+ headers=headers
1628
+ )
1629
+ if response.status_code == 204:
1630
+ return {"status": "success", "message": f"Deleted event {event_id}"}
1631
+ return f"Error deleting event: {response.text}"
1632
+
1633
+ # Tool Lists
1634
+ tools = [
1635
+ DirectDMTool(),
1636
+ ZoomCreateMeetingTool(),
1637
+ SearchUserEventsTool(),
1638
+ GoogleCalendarList(),
1639
+ GoogleCalendarEvents(),
1640
+ GoogleCreateCalendar(),
1641
+ GoogleAddCalendarEvent(),
1642
+ GoogleUpdateCalendarEvent(),
1643
+ GoogleDeleteCalendarEvent(),
1644
+ MicrosoftListCalendarEvents(),
1645
+ MicrosoftAddCalendarEvent(),
1646
+ MicrosoftUpdateCalendarEvent(),
1647
+ MicrosoftDeleteCalendarEvent(),
1648
+ MultiDirectDMTool()
1649
+ ]
1650
+
1651
+ calendar_prompt_tools = [
1652
+ MicrosoftListCalendarEvents(),
1653
+ GoogleCalendarEvents()
1654
+ ]
1655
+
1656
+ dm_tools = [
1657
+ DirectDMTool(),
1658
+ ZoomCreateMeetingTool(),
1659
+ SearchUserEventsTool(),
1660
+ GetSingleUserSlackName(),
1661
+ GetSingleUserSlackID(),
1662
+ GoogleCalendarList(),
1663
+ GoogleCalendarEvents(),
1664
+ GoogleCreateCalendar(),
1665
+ GoogleAddCalendarEvent(),
1666
+ GoogleUpdateCalendarEvent(),
1667
+ GoogleDeleteCalendarEvent(),
1668
+ MicrosoftListCalendarEvents(),
1669
+ MicrosoftAddCalendarEvent(),
1670
+ MicrosoftUpdateCalendarEvent(),
1671
+ MicrosoftDeleteCalendarEvent()
1672
+ ]
1673
+
1674
+ dm_group_tools = [
1675
+ GoogleCalendarEvents(),
1676
+ MicrosoftListCalendarEvents(),
1677
+ DateTimeTool(),
1678
+ ]
app.py ADDED
@@ -0,0 +1,1688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import secrets
3
+ import time
4
+ import json
5
+ import base64
6
+ from flask_talisman import Talisman
7
+ from flask import Flask, request, jsonify,render_template
8
+ from slack_sdk import WebClient
9
+ from slack_sdk.errors import SlackApiError
10
+ from slack_bolt import App
11
+ from slack_bolt.adapter.flask import SlackRequestHandler
12
+ from slack_bolt.oauth.oauth_settings import OAuthSettings
13
+ from dotenv import find_dotenv, load_dotenv
14
+ from google.auth.transport.requests import Request
15
+ from google.oauth2.credentials import Credentials
16
+ from google_auth_oauthlib.flow import Flow
17
+ from googleapiclient.discovery import build
18
+ from flask_session import Session
19
+ from msal import ConfidentialClientApplication
20
+ import psycopg2
21
+ from apscheduler.schedulers.background import BackgroundScheduler
22
+ from datetime import datetime, timedelta
23
+ from collections import defaultdict
24
+ import hashlib
25
+ import re
26
+ import logging
27
+ from threading import Lock
28
+
29
+ from urllib.parse import quote_plus
30
+ from langchain.chains import LLMChain
31
+ from langchain.prompts import ChatPromptTemplate
32
+ from agents.all_agents import (
33
+ create_schedule_agent, create_update_agent, create_delete_agent, llm,
34
+ create_schedule_group_agent, create_update_group_agent, create_schedule_channel_agent
35
+ )
36
+ from all_tools import tools, calendar_prompt_tools
37
+ from db import init_db
38
+
39
+ # Load environment variables
40
+ load_dotenv(find_dotenv())
41
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
42
+ os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
43
+ os.environ['OAUTHLIB_IGNORE_SCOPE_CHANGE'] = '1'
44
+
45
+
46
+ user_cache = {}
47
+ user_cache_lock = Lock() # Example threading lock for cache
48
+ preferences_cache = {}
49
+ preferences_cache_lock = Lock()
50
+ owner_id_cache = {}
51
+ owner_id_lock = Lock()
52
+ # Configuration
53
+ SLACK_CLIENT_ID = os.getenv('SLACK_CLIENT_ID','')
54
+ SLACK_CLIENT_SECRET = os.getenv('SLACK_CLIENT_SECRET','')
55
+ SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET','')
56
+ SLACK_SCOPES = [
57
+ "app_mentions:read",
58
+ "channels:history",
59
+ "chat:write",
60
+ "users:read",
61
+ "im:write",
62
+ "groups:write",
63
+ "mpim:write",
64
+ "commands",
65
+ "team:read",
66
+ "channels:read",
67
+ "groups:read",
68
+ "im:read",
69
+ "mpim:read",
70
+ "groups:history",
71
+ "im:history",
72
+ "mpim:history"
73
+ ]
74
+ import requests
75
+ SLACK_BOT_USER_ID = os.getenv("SLACK_BOT_USER_ID")
76
+ ZOOM_REDIRECT_URI = "https://clear-muskox-grand.ngrok-free.app/zoom_callback"
77
+ CLIENT_ID = "FiyFvBUSSeeXwjDv0tqg" # Zoom Client ID
78
+ CLIENT_SECRET = "tygAN91Xd7Wo1YAH056wtbrXQ8I6UieA" # Zoom Client Secret
79
+ ZOOM_TOKEN_API = "https://zoom.us/oauth/token"
80
+ ZOOM_OAUTH_AUTHORIZE_API = os.getenv("ZOOM_OAUTH_AUTHORIZE_API", "https://zoom.us/oauth/authorize")
81
+ OAUTH_REDIRECT_URI = os.getenv("OAUTH_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/oauth2callback")
82
+ MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
83
+ MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
84
+ MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
85
+ MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
86
+ MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
87
+
88
+ # Initialize Flask app
89
+ app = Flask(__name__)
90
+ app.secret_key = secrets.token_hex(16)
91
+ talisman = Talisman(
92
+ app,
93
+ content_security_policy={
94
+ 'default-src': "'self'",
95
+ 'script-src': "'self'",
96
+ 'object-src': "'none'"
97
+ },
98
+ force_https=True,
99
+ strict_transport_security=True,
100
+ strict_transport_security_max_age=31536000,
101
+ x_content_type_options=True,
102
+ referrer_policy='no-referrer-when-downgrade'
103
+ )
104
+ app.config['SESSION_TYPE'] = 'filesystem'
105
+ Session(app)
106
+
107
+ # Installation Store for OAuth
108
+ import json
109
+ import os
110
+ import psycopg2
111
+ from datetime import datetime
112
+
113
+ import json
114
+ import os
115
+ import psycopg2
116
+ from datetime import datetime
117
+ from slack_sdk.oauth import InstallationStore
118
+ # Custom JSON encoder to handle datetime objects
119
+ import json
120
+ import os
121
+ import psycopg2
122
+ from psycopg2.extras import Json
123
+ from datetime import datetime
124
+ import logging
125
+ from slack_sdk import WebClient
126
+ from slack_sdk.oauth import InstallationStore
127
+ from slack_sdk.oauth.installation_store.models.installation import Installation
128
+ from slack_bolt.authorization import AuthorizeResult
129
+
130
+ # Custom JSON encoder for datetime objects (used only if needed)
131
+ class DateTimeEncoder(json.JSONEncoder):
132
+ def default(self, obj):
133
+ if isinstance(obj, datetime):
134
+ return obj.isoformat()
135
+ return super().default(obj)
136
+
137
+ class DatabaseInstallationStore(InstallationStore):
138
+ """A database-backed installation store for Slack Bolt using PostgreSQL.
139
+
140
+ Assumes 'installation_data' is a jsonb column storing JSON data.
141
+ """
142
+
143
+ def __init__(self):
144
+ self._logger = logging.getLogger(__name__)
145
+
146
+ def save(self, installation):
147
+ try:
148
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
149
+ cur = conn.cursor()
150
+
151
+ workspace_id = installation.team_id
152
+ installed_at = datetime.fromtimestamp(installation.installed_at) if installation.installed_at else None
153
+
154
+ installation_data = {
155
+ "team_id": installation.team_id,
156
+ "enterprise_id": installation.enterprise_id,
157
+ "user_id": installation.user_id,
158
+ "bot_token": installation.bot_token,
159
+ "bot_id": installation.bot_id,
160
+ "bot_user_id": installation.bot_user_id,
161
+ "bot_scopes": installation.bot_scopes,
162
+ "user_token": installation.user_token,
163
+ "user_scopes": installation.user_scopes,
164
+ "incoming_webhook_url": installation.incoming_webhook_url,
165
+ "incoming_webhook_channel": installation.incoming_webhook_channel,
166
+ "incoming_webhook_channel_id": installation.incoming_webhook_channel_id,
167
+ "incoming_webhook_configuration_url": installation.incoming_webhook_configuration_url,
168
+ "app_id": installation.app_id,
169
+ "token_type": installation.token_type,
170
+ "installed_at": installed_at.isoformat() if installed_at else None
171
+ }
172
+
173
+ current_time = datetime.now()
174
+
175
+ cur.execute('''
176
+ INSERT INTO Installations (workspace_id, installation_data, updated_at)
177
+ VALUES (%s, %s, %s)
178
+ ON CONFLICT (workspace_id) DO UPDATE SET
179
+ installation_data = %s, updated_at = %s
180
+ ''', (workspace_id, Json(installation_data), current_time, Json(installation_data), current_time))
181
+
182
+ conn.commit()
183
+ self._logger.info(f"Saved installation for workspace {workspace_id}")
184
+
185
+ except Exception as e:
186
+ self._logger.error(f"Failed to save installation for workspace {workspace_id}: {e}")
187
+ raise
188
+ finally:
189
+ cur.close()
190
+ conn.close()
191
+
192
+ def find_installation(self, enterprise_id=None, team_id=None, user_id=None, is_enterprise_install=False):
193
+ if not team_id:
194
+ self._logger.warning("No team_id provided for find_installation")
195
+ return None
196
+
197
+ try:
198
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
199
+ cur = conn.cursor()
200
+ cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
201
+ row = cur.fetchone()
202
+
203
+ if row:
204
+ # For jsonb, row[0] is already a dict
205
+ installation_data = row[0]
206
+ installed_at = (datetime.fromisoformat(installation_data["installed_at"])
207
+ if installation_data.get("installed_at") else None)
208
+
209
+ return Installation(
210
+ app_id=installation_data["app_id"],
211
+ enterprise_id=installation_data.get("enterprise_id"),
212
+ team_id=installation_data["team_id"],
213
+ bot_token=installation_data["bot_token"],
214
+ bot_id=installation_data["bot_id"],
215
+ bot_user_id=installation_data["bot_user_id"],
216
+ bot_scopes=installation_data["bot_scopes"],
217
+ user_id=installation_data["user_id"],
218
+ user_token=installation_data.get("user_token"),
219
+ user_scopes=installation_data.get("user_scopes"),
220
+ incoming_webhook_url=installation_data.get("incoming_webhook_url"),
221
+ incoming_webhook_channel=installation_data.get("incoming_webhook_channel"),
222
+ incoming_webhook_channel_id=installation_data.get("incoming_webhook_channel_id"),
223
+ incoming_webhook_configuration_url=installation_data.get("incoming_webhook_configuration_url"),
224
+ token_type=installation_data["token_type"],
225
+ installed_at=installed_at
226
+ )
227
+ else:
228
+ self._logger.info(f"No installation found for team_id {team_id}")
229
+ return None
230
+
231
+ except Exception as e:
232
+ self._logger.error(f"Error retrieving installation for team_id {team_id}: {e}")
233
+ return None
234
+ finally:
235
+ cur.close()
236
+ conn.close()
237
+
238
+ def find_bot(self, enterprise_id=None, team_id=None, is_enterprise_install=False):
239
+ if not team_id:
240
+ self._logger.warning("No team_id provided for find_bot")
241
+ return None
242
+
243
+ try:
244
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
245
+ cur = conn.cursor()
246
+ cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
247
+ row = cur.fetchone()
248
+
249
+ if row:
250
+ installation_data = row[0]
251
+ return AuthorizeResult(
252
+ enterprise_id=installation_data.get("enterprise_id"),
253
+ team_id=installation_data["team_id"],
254
+ bot_token=installation_data["bot_token"],
255
+ bot_id=installation_data["bot_id"],
256
+ bot_user_id=installation_data["bot_user_id"]
257
+ )
258
+ else:
259
+ self._logger.info(f"No bot installation found for team_id {team_id}")
260
+ return None
261
+
262
+ except Exception as e:
263
+ self._logger.error(f"Error retrieving bot for team_id {team_id}: {e}")
264
+ return None
265
+ finally:
266
+ cur.close()
267
+ conn.close()
268
+
269
+ # Instantiate the store
270
+ installation_store = DatabaseInstallationStore()
271
+
272
+ def get_client_for_team(team_id):
273
+ """
274
+ Get a Slack WebClient for a given team ID using the stored bot token.
275
+
276
+ Args:
277
+ team_id (str): The team ID (workspace ID) to look up.
278
+
279
+ Returns:
280
+ WebClient: Slack client instance or None if not found.
281
+ """
282
+ installation = installation_store.find_installation(None, team_id)
283
+ if installation:
284
+ token = installation.bot_token # Use dot notation instead of subscripting
285
+ return WebClient(token=token)
286
+ return None
287
+
288
+
289
+
290
+ # Initialize Slack Bolt app with OAuth settings
291
+ oauth_settings = OAuthSettings(
292
+ client_id=SLACK_CLIENT_ID,
293
+ client_secret=SLACK_CLIENT_SECRET,
294
+ scopes=SLACK_SCOPES,
295
+ redirect_uri="https://clear-muskox-grand.ngrok-free.app/slack/oauth_redirect",
296
+ installation_store=installation_store
297
+ )
298
+ bolt_app = App(signing_secret=SLACK_SIGNING_SECRET, oauth_settings=oauth_settings)
299
+ slack_handler = SlackRequestHandler(bolt_app)
300
+
301
+ # Logging setup
302
+ logging.basicConfig(level=logging.INFO)
303
+ logger = logging.getLogger(__name__)
304
+
305
+ # Google Calendar API Scopes
306
+ SCOPES = [
307
+ 'https://www.googleapis.com/auth/calendar',
308
+ 'https://www.googleapis.com/auth/calendar.readonly',
309
+ 'https://www.googleapis.com/auth/userinfo.email',
310
+ 'https://www.googleapis.com/auth/userinfo.profile'
311
+ ]
312
+
313
+ # Initialize Neon Postgres database
314
+ init_db()
315
+
316
+ # State Management Classes
317
+ class StateManager:
318
+ def __init__(self):
319
+ self._states = {}
320
+ self._lock = Lock()
321
+
322
+ def create_state(self, user_id):
323
+ with self._lock:
324
+ state_token = secrets.token_urlsafe(32)
325
+ self._states[state_token] = {"user_id": user_id, "timestamp": datetime.now(), "used": False}
326
+ return state_token
327
+
328
+ def validate_and_consume_state(self, state_token):
329
+ with self._lock:
330
+ if state_token not in self._states:
331
+ return None
332
+ state_data = self._states[state_token]
333
+ if state_data["used"] or (datetime.now() - state_data["timestamp"]).total_seconds() > 600:
334
+ del self._states[state_token]
335
+ return None
336
+ state_data["used"] = True
337
+ return state_data["user_id"]
338
+
339
+ def cleanup_expired_states(self):
340
+ with self._lock:
341
+ current_time = datetime.now()
342
+ expired = [s for s, d in self._states.items() if (current_time - d["timestamp"]).total_seconds() > 600]
343
+ for state in expired:
344
+ del self._states[state]
345
+
346
+ state_manager = StateManager()
347
+
348
+ class EventDeduplicator:
349
+ def __init__(self, expiration_minutes=5):
350
+ self.processed_events = defaultdict(list)
351
+ self.expiration_minutes = expiration_minutes
352
+
353
+ def clean_expired_events(self):
354
+ current_time = datetime.now()
355
+ for event_id in list(self.processed_events.keys()):
356
+ events = [(t, h) for t, h in self.processed_events[event_id]
357
+ if current_time - t < timedelta(minutes=self.expiration_minutes)]
358
+ if events:
359
+ self.processed_events[event_id] = events
360
+ else:
361
+ del self.processed_events[event_id]
362
+
363
+ def is_duplicate_event(self, event_payload):
364
+ self.clean_expired_events()
365
+ event_id = event_payload.get('event_id', '')
366
+ payload_hash = hashlib.md5(str(event_payload).encode('utf-8')).hexdigest()
367
+ if 'challenge' in event_payload:
368
+ return False
369
+ if event_id in self.processed_events and payload_hash in [h for _, h in self.processed_events[event_id]]:
370
+ return True
371
+ self.processed_events[event_id].append((datetime.now(), payload_hash))
372
+ return False
373
+
374
+ event_deduplicator = EventDeduplicator()
375
+
376
+ class SessionStore:
377
+ def __init__(self):
378
+ self._store = {}
379
+ self._lock = Lock()
380
+
381
+ def set(self, user_id, key, value):
382
+ with self._lock:
383
+ if user_id not in self._store:
384
+ self._store[user_id] = {}
385
+ self._store[user_id][key] = {"value": value, "expires_at": datetime.now() + timedelta(hours=1)}
386
+
387
+ def get(self, user_id, key, default=None):
388
+ with self._lock:
389
+ if user_id not in self._store or key not in self._store[user_id]:
390
+ return default
391
+ session_data = self._store[user_id][key]
392
+ if datetime.now() > session_data["expires_at"]:
393
+ del self._store[user_id][key]
394
+ return default
395
+ return session_data["value"]
396
+
397
+ def clear(self, user_id, key):
398
+ with self._lock:
399
+ if user_id in self._store and key in self._store[user_id]:
400
+ del self._store[user_id][key]
401
+
402
+ session_store = SessionStore()
403
+
404
+ def store_in_session(user_id, key_type, data):
405
+ session_store.set(user_id, key_type, data)
406
+
407
+ def get_from_session(user_id, key_type, default=None):
408
+ return session_store.get(user_id, key_type, default)
409
+
410
+ # Global Caches (per workspace)
411
+ user_cache = {} # {team_id: {user_id: user_data}}
412
+ user_cache_lock = Lock()
413
+
414
+ owner_id_cache = {} # {team_id: owner_id}
415
+ owner_id_lock = Lock()
416
+
417
+ preferences_cache = {}
418
+ preferences_cache_lock = Lock()
419
+
420
+ # Database Helper Functions
421
+ def save_preference(team_id, user_id, zoom_config=None, calendar_tool=None):
422
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
423
+ cur = conn.cursor()
424
+ cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
425
+ existing = cur.fetchone()
426
+ if existing:
427
+ current_zoom_config, current_calendar_tool = existing
428
+ new_zoom_config = zoom_config if zoom_config is not None else current_zoom_config
429
+ new_calendar_tool = calendar_tool if calendar_tool is not None else current_calendar_tool
430
+ cur.execute('''
431
+ UPDATE Preferences
432
+ SET zoom_config = %s, calendar_tool = %s, updated_at = %s
433
+ WHERE team_id = %s AND user_id = %s
434
+ ''', (json.dumps(new_zoom_config) if new_zoom_config else None,
435
+ new_calendar_tool, datetime.now(), team_id, user_id))
436
+ else:
437
+ new_zoom_config = zoom_config or {"mode": "manual", "link": None}
438
+ new_calendar_tool = calendar_tool or "google"
439
+ cur.execute('''
440
+ INSERT INTO Preferences (team_id, user_id, zoom_config, calendar_tool, updated_at)
441
+ VALUES (%s, %s, %s, %s, %s)
442
+ ''', (team_id, user_id, json.dumps(new_zoom_config), new_calendar_tool, datetime.now()))
443
+ conn.commit()
444
+ cur.close()
445
+ conn.close()
446
+ with preferences_cache_lock:
447
+ preferences_cache[(team_id, user_id)] = {"zoom_config": new_zoom_config, "calendar_tool": new_calendar_tool}
448
+
449
+ def load_preferences(team_id, user_id):
450
+ with preferences_cache_lock:
451
+ if (team_id, user_id) in preferences_cache:
452
+ return preferences_cache[(team_id, user_id)]
453
+ try:
454
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
455
+ cur = conn.cursor()
456
+ cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
457
+ row = cur.fetchone()
458
+ if row:
459
+ zoom_config, calendar_tool = row
460
+ # For jsonb, zoom_config is already a dict; no json.loads needed
461
+ preferences = {
462
+ "zoom_config": zoom_config if zoom_config else {"mode": "manual", "link": None},
463
+ "calendar_tool": calendar_tool or "none"
464
+ }
465
+ else:
466
+ preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
467
+ cur.close()
468
+ conn.close()
469
+ except Exception as e:
470
+ logger.error(f"Failed to load preferences for team {team_id}, user {user_id}: {e}")
471
+ preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
472
+ with preferences_cache_lock:
473
+ preferences_cache[(team_id, user_id)] = preferences
474
+ return preferences
475
+
476
+ def save_token(team_id, user_id, service, token_data):
477
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
478
+ cur = conn.cursor()
479
+ cur.execute('''
480
+ INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
481
+ VALUES (%s, %s, %s, %s, %s)
482
+ ON CONFLICT (team_id, user_id, service) DO UPDATE SET token_data = %s, updated_at = %s
483
+ ''', (team_id, user_id, service, json.dumps(token_data), datetime.now(), json.dumps(token_data), datetime.now()))
484
+ conn.commit()
485
+ cur.close()
486
+ conn.close()
487
+
488
+ def load_token(team_id, user_id, service):
489
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
490
+ cur = conn.cursor()
491
+ cur.execute('SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s', (team_id, user_id, service))
492
+ row = cur.fetchone()
493
+ cur.close()
494
+ conn.close()
495
+ return row[0] if row else None
496
+
497
+ # Utility Functions
498
+ def initialize_workspace_cache(client, team_id):
499
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
500
+ cur = conn.cursor()
501
+ cur.execute('SELECT MAX(last_updated) FROM Users WHERE team_id = %s', (team_id,))
502
+ last_updated_row = cur.fetchone()
503
+ last_updated = last_updated_row[0] if last_updated_row and last_updated_row[0] else None
504
+
505
+ # Check if cache is fresh (e.g., less than 24 hours old)
506
+ if last_updated and (datetime.now() - last_updated).total_seconds() < 86400:
507
+ cur.execute('SELECT user_id, real_name, email, name, is_owner, workspace_name FROM Users WHERE team_id = %s', (team_id,))
508
+ rows = cur.fetchall()
509
+ new_cache = {row[0]: {"real_name": row[1], "email": row[2], "name": row[3], "is_owner": row[4], "workspace_name": row[5]} for row in rows}
510
+ with user_cache_lock:
511
+ user_cache[team_id] = new_cache
512
+ with owner_id_lock:
513
+ owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
514
+ else:
515
+ # Fetch user data from Slack and update database
516
+ response = client.users_list()
517
+ users = response["members"]
518
+ workspace_name = client.team_info()["team"]["name"] # Get workspace name from Slack API
519
+ new_cache = {}
520
+ for user in users:
521
+ user_id = user['id']
522
+ profile = user.get('profile', {})
523
+ real_name = profile.get('real_name', 'Unknown')
524
+ name = user.get('name', '')
525
+ email = f"{name}@gmail.com" # Placeholder; adjust as needed
526
+ is_owner = user.get('is_owner', False)
527
+ new_cache[user_id] = {"real_name": real_name, "email": email, "name": name, "is_owner": is_owner, "workspace_name": workspace_name}
528
+ cur.execute('''
529
+ INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
530
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
531
+ ON CONFLICT (team_id, user_id) DO UPDATE SET
532
+ workspace_name = %s, real_name = %s, email = %s, name = %s, is_owner = %s, last_updated = %s
533
+ ''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now(),
534
+ workspace_name, real_name, email, name, is_owner, datetime.now()))
535
+ conn.commit()
536
+ with user_cache_lock:
537
+ user_cache[team_id] = new_cache
538
+ with owner_id_lock:
539
+ owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
540
+ cur.close()
541
+ conn.close()
542
+
543
+ def get_all_users(team_id):
544
+ with user_cache_lock:
545
+ if team_id in user_cache:
546
+ return {k: {"Slack Id": k, "real_name": v["real_name"], "email": v["email"], "name": v["name"]}
547
+ for k, v in user_cache[team_id].items()}
548
+ return {}
549
+
550
+ def get_workspace_owner_id(client, team_id):
551
+ with owner_id_lock:
552
+ if team_id in owner_id_cache and owner_id_cache[team_id]:
553
+ return owner_id_cache[team_id]
554
+ initialize_workspace_cache(client, team_id)
555
+ with owner_id_lock:
556
+ return owner_id_cache.get(team_id)
557
+
558
+ def get_channel_owner_id(client, channel_id):
559
+ try:
560
+ response = client.conversations_info(channel=channel_id)
561
+ return response["channel"].get("creator")
562
+ except SlackApiError as e:
563
+ logger.error(f"Error fetching channel info: {e.response['error']}")
564
+ return None
565
+
566
+ def get_user_timezone(client, user_id):
567
+ try:
568
+ response = client.users_info(user=user_id)
569
+ return response["user"].get("tz", "UTC")
570
+ except SlackApiError as e:
571
+ logger.error(f"Timezone error: {e.response['error']}")
572
+ return "UTC"
573
+
574
+ def get_team_id_from_owner_id(owner_id):
575
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
576
+ cur = conn.cursor()
577
+ cur.execute("SELECT workspace_id FROM Installations WHERE installation_data->>'user_id' = %s", (owner_id,))
578
+ row = cur.fetchone()
579
+ cur.close()
580
+ conn.close()
581
+ return row[0] if row else None
582
+
583
+ # def get_client_for_team(team_id):
584
+ # installation = installation_store.find_installation(None, team_id)
585
+ # if installation:
586
+ # print(installation)
587
+ # token = installation['bot_token']
588
+ # return WebClient(token=token)
589
+ # return None
590
+
591
+ def get_owner_selected_calendar(client, team_id):
592
+ owner_id = get_workspace_owner_id(client, team_id)
593
+ if not owner_id:
594
+ return None
595
+ # Fixed: Pass both team_id and owner_id to load_preferences
596
+ prefs = load_preferences(team_id, owner_id)
597
+ return prefs.get("calendar_tool", "none")
598
+
599
+ def get_zoom_link(client, team_id):
600
+ owner_id = get_workspace_owner_id(client, team_id)
601
+ if not owner_id:
602
+ return None
603
+ prefs = load_preferences(team_id,owner_id)
604
+ return prefs.get('zoom_config', {}).get('link')
605
+
606
+ def create_home_tab(client, team_id, user_id):
607
+ logger.info(f"Creating home tab for user {user_id}, team {team_id}")
608
+
609
+ # Get workspace owner ID
610
+ workspace_owner_id = get_workspace_owner_id(client, team_id)
611
+ if not workspace_owner_id:
612
+ logger.warning(f"No workspace owner for team {team_id}")
613
+ blocks = [
614
+ {"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}},
615
+ {"type": "section", "text": {"type": "mrkdwn", "text": "Unable to determine workspace owner. Please contact support."}},
616
+ ]
617
+ return {"type": "home", "blocks": blocks}
618
+
619
+ # Determine if the user is the workspace owner
620
+ is_owner = user_id == workspace_owner_id
621
+
622
+ # Base blocks for all users
623
+ blocks = [
624
+ {"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}}
625
+ ]
626
+
627
+ # Non-owner view
628
+ if not is_owner:
629
+ blocks.extend([
630
+ {"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Please wait for the workspace owner to configure the settings."}},
631
+ {"type": "section", "text": {"type": "mrkdwn", "text": "Only the workspace owner can configure the calendar and Zoom settings."}}
632
+ ])
633
+ return {"type": "home", "blocks": blocks}
634
+
635
+ # Owner view: Add configuration options
636
+ blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Your settings are below."}})
637
+ blocks.append({"type": "divider"})
638
+
639
+ # Load preferences and tokens
640
+ prefs = load_preferences(team_id, workspace_owner_id)
641
+ selected_provider = prefs.get("calendar_tool", "none")
642
+ zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
643
+ mode = zoom_config["mode"]
644
+ calendar_token = load_token(team_id, workspace_owner_id, selected_provider) if selected_provider != "none" else None
645
+ zoom_token = load_token(team_id, workspace_owner_id, "zoom") if mode == "automatic" else None
646
+ logger.info(f"Preferences loaded: {prefs}, Calendar token: {calendar_token}, Zoom token: {zoom_token}")
647
+
648
+ # Check Zoom token expiration
649
+ zoom_token_expired = False
650
+ if zoom_token and mode == "automatic":
651
+ expires_at = zoom_token.get("expires_at", 0)
652
+ current_time = time.time()
653
+ zoom_token_expired = current_time >= expires_at
654
+
655
+ # Configuration status
656
+ calendar_provider_set = selected_provider != "none"
657
+ calendar_configured = calendar_token is not None if calendar_provider_set else False
658
+ zoom_configured = (zoom_token is not None and not zoom_token_expired) if mode == "automatic" else True
659
+
660
+ # Setup prompt if configurations are incomplete
661
+ if not calendar_provider_set or not calendar_configured or not zoom_configured:
662
+ prompt_text = "To start using the app, please complete the following setups:"
663
+ if not calendar_provider_set:
664
+ prompt_text += "\n- Select a calendar provider."
665
+ if calendar_provider_set and not calendar_configured:
666
+ prompt_text += f"\n- Configure your {selected_provider.capitalize()} calendar."
667
+ if mode == "automatic" and not zoom_configured:
668
+ if zoom_token_expired:
669
+ prompt_text += "\n- Your Zoom token has expired. Please refresh it."
670
+ else:
671
+ prompt_text += "\n- Authenticate with Zoom for automatic mode."
672
+ blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": prompt_text}})
673
+
674
+ # Calendar Configuration Section
675
+ blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "*🗓️ Calendar Configuration*"}})
676
+ blocks.append({
677
+ "type": "section",
678
+ "block_id": "calendar_provider_block",
679
+ "text": {"type": "mrkdwn", "text": "Select your calendar provider:"},
680
+ "accessory": {
681
+ "type": "static_select",
682
+ "action_id": "calendar_provider_dropdown",
683
+ "placeholder": {"type": "plain_text", "text": "Select provider"},
684
+ "options": [
685
+ {"text": {"type": "plain_text", "text": "Select calendar"}, "value": "none"},
686
+ {"text": {"type": "plain_text", "text": "Google Calendar"}, "value": "google"},
687
+ {"text": {"type": "plain_text", "text": "Microsoft Calendar"}, "value": "microsoft"}
688
+ ],
689
+ "initial_option": {
690
+ "text": {"type": "plain_text", "text": "Select calendar" if selected_provider == "none" else
691
+ "Google Calendar" if selected_provider == "google" else "Microsoft Calendar"},
692
+ "value": selected_provider
693
+ }
694
+ }
695
+ })
696
+
697
+ # Calendar configuration prompts
698
+ if selected_provider == "none":
699
+ blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": "Please select a calendar provider to begin configuration."}]})
700
+ elif not calendar_configured:
701
+ blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": f"Please configure your {selected_provider.capitalize()} calendar."}]})
702
+
703
+ # Calendar configure button and status
704
+ if selected_provider != "none":
705
+ status = "⚠️ Not Configured" if not calendar_configured else (
706
+ f":white_check_mark: Connected ({calendar_token.get('google_email', 'unknown')})" if selected_provider == "google" else (
707
+ f":white_check_mark: Connected (expires: {datetime.fromtimestamp(int(calendar_token.get('expires_at', 0))).strftime('%Y-%m-%d %H:%M')})" if calendar_token and calendar_token.get('expires_at') else ":white_check_mark: Connected"
708
+ )
709
+ )
710
+ blocks.extend([
711
+ {
712
+ "type": "actions",
713
+ "elements": [
714
+ {
715
+ "type": "button",
716
+ "text": {
717
+ "type": "plain_text",
718
+ "text": f"✨ Configure {selected_provider.capitalize()}" if not calendar_configured else f"✅ Reconfigure {selected_provider.capitalize()}",
719
+ "emoji": True
720
+ },
721
+ "action_id": "configure_gcal" if selected_provider == "google" else "configure_mscal"
722
+ }
723
+ ]
724
+ },
725
+ {"type": "context", "elements": [{"type": "mrkdwn", "text": status}]}
726
+ ])
727
+
728
+ # Zoom Configuration Section
729
+ status = ("⌛ Token Expired" if zoom_token_expired else
730
+ "⚠️ Not Configured" if mode == "automatic" and not zoom_configured else
731
+ "✅ Configured")
732
+ blocks.extend([
733
+ {"type": "divider"},
734
+ {"type": "section", "text": {"type": "mrkdwn", "text": f"*🔗 Zoom Configuration*\nCurrent mode: {mode}\n{status}"}},
735
+ {
736
+ "type": "actions",
737
+ "elements": [
738
+ {"type": "button", "text": {"type": "plain_text", "text": "Configure Zoom Settings", "emoji": True}, "action_id": "open_zoom_config_modal"}
739
+ ]
740
+ }
741
+ ])
742
+
743
+ # Zoom authentication/refresh button
744
+ if mode == "automatic":
745
+ if not zoom_configured and not zoom_token_expired:
746
+ blocks[-1]["elements"].append({
747
+ "type": "button",
748
+ "text": {"type": "plain_text", "text": "Authenticate with Zoom", "emoji": True},
749
+ "action_id": "configure_zoom"
750
+ })
751
+ elif zoom_token_expired:
752
+ blocks[-1]["elements"].append({
753
+ "type": "button",
754
+ "text": {"type": "plain_text", "text": "Refresh Zoom Token", "emoji": True},
755
+ "action_id": "configure_zoom" # Same action_id for refresh
756
+ })
757
+
758
+ return {"type": "home", "blocks": blocks}
759
+
760
+ # Intent Classification
761
+ intent_prompt = ChatPromptTemplate.from_template("""
762
+ You are an intent classification assistant. Based on the user's message and the conversation history, determine the intent of the user's request. The possible intents are: "schedule meeting", "update event", "delete event", or "other". Provide only the intent as your response.
763
+ - By looking at the history if someone is confirming or denying the schedule , also categorize it as a "schedule meeting"
764
+ - If someone is asking about update the schedule then its "update event"
765
+ - If someone is asking about delete the schedule then its "delete event"
766
+ Conversation History:
767
+ {history}
768
+
769
+ User's Message:
770
+ {input}
771
+ """)
772
+ from prompt import calender_prompt, general_prompt
773
+ intent_chain = LLMChain(llm=llm, prompt=intent_prompt)
774
+
775
+ mentioned_users_prompt = ChatPromptTemplate.from_template("""
776
+ Given the following chat history, identify the Slack user IDs, Names and emails of the users who are mentioned. Mentions can be in the form of <@user_id> (e.g., <@U12345>) or by their names (e.g., "Alice" or "Alice Smith").
777
+ - Do not give 'Bob'<@{bob_id}> in mentions
778
+ - Exclude the {bob_id}.
779
+ # See the history if there is a request for new meeting or request for new schedule just ignore the mentions in the old messages and consider the new mentions in the new request.
780
+ All users in the channel:
781
+ {user_information}
782
+ Format: Slack Id: U3234234 , Name: Alice , Email: alice@gmail.com (map slack ids to the names)
783
+ Chat history:
784
+ {chat_history}
785
+ # Only output the users which are mentioned not all the users from the user-information.
786
+ # Only see the latest message for mention information ignore previous ones.
787
+ Please output the user slack IDs of the mentioned users , their names and emails . If no users are mentioned, output "None".
788
+ CURRENT_INPUT: {current_input}
789
+ Example: [[SlackId1 , Name1 , Email@gmal.com], [SlackId2, Name2, Email@gmail.com]...]
790
+ """)
791
+ mentioned_users_chain = LLMChain(llm=llm, prompt=mentioned_users_prompt)
792
+
793
+ # Slack Event Handlers
794
+ @bolt_app.event("app_home_opened")
795
+ def handle_app_home_opened(event, client, context):
796
+ user_id = event.get("user")
797
+ team_id = context['team_id']
798
+ if not user_id:
799
+ return
800
+ try:
801
+ client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
802
+ except Exception as e:
803
+ logger.error(f"Error publishing home tab: {e}")
804
+
805
+ @bolt_app.action("calendar_provider_dropdown")
806
+ def handle_calendar_provider(ack, body, client, logger):
807
+ ack()
808
+ selected_provider = body["actions"][0]["selected_option"]["value"]
809
+ user_id = body["user"]["id"]
810
+ team_id = body["team"]["id"]
811
+ owner_id = get_workspace_owner_id(client, team_id)
812
+
813
+ if user_id != owner_id:
814
+ client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
815
+ return
816
+
817
+ # Corrected line: pass both team_id and owner_id (user_id) parameters
818
+ save_preference(team_id, owner_id, calendar_tool=selected_provider)
819
+
820
+ client.views_publish(user_id=owner_id, view=create_home_tab(client, team_id, owner_id))
821
+ if selected_provider != "none":
822
+ client.chat_postMessage(channel=owner_id, text=f"Calendar provider updated to {selected_provider.capitalize()}.")
823
+ else:
824
+ client.chat_postMessage(channel=owner_id, text="Calendar provider reset.")
825
+ logger.info(f"Calendar provider updated to {selected_provider} for owner {owner_id}")
826
+
827
+ @bolt_app.action("configure_gcal")
828
+ def handle_gcal_config(ack, body, client, logger):
829
+ ack()
830
+ user_id = body["user"]["id"]
831
+ team_id = body["team"]["id"]
832
+ owner_id = get_workspace_owner_id(client, team_id)
833
+ if user_id != owner_id:
834
+ client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
835
+ return
836
+
837
+ # Generate and store the state using StateManager
838
+ state = state_manager.create_state(owner_id)
839
+ print(f"state stored: {state}")
840
+ store_in_session(owner_id, "gcal_state", state) # Optional: for additional validation
841
+
842
+ # Set up the OAuth flow and pass the state
843
+ flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
844
+ auth_url, _ = flow.authorization_url(
845
+ access_type='offline',
846
+ prompt='consent',
847
+ include_granted_scopes='true',
848
+ state=state # Use the state from StateManager
849
+ )
850
+
851
+ # Open the modal with the auth URL
852
+ try:
853
+ client.views_open(
854
+ trigger_id=body["trigger_id"],
855
+ view={
856
+ "type": "modal",
857
+ "title": {"type": "plain_text", "text": "Google Calendar Auth"},
858
+ "close": {"type": "plain_text", "text": "Cancel"},
859
+ "blocks": [
860
+ {"type": "section", "text": {"type": "mrkdwn", "text": "Click below to connect Google Calendar:"}},
861
+ {"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Google Calendar"}, "url": auth_url, "action_id": "launch_auth"}]}
862
+ ]
863
+ }
864
+ )
865
+ except Exception as e:
866
+ logger.error(f"Error opening modal: {e}")
867
+
868
+ @bolt_app.action("configure_mscal")
869
+ def handle_mscal_config(ack, body, client, logger):
870
+ ack()
871
+ user_id = body["user"]["id"]
872
+ team_id = body["team"]["id"]
873
+ owner_id = get_workspace_owner_id(client, team_id)
874
+ if user_id != owner_id:
875
+ client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
876
+ return
877
+ msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
878
+ state = state_manager.create_state(owner_id)
879
+ auth_url = msal_app.get_authorization_request_url(scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI, state=state)
880
+ try:
881
+ client.views_open(
882
+ trigger_id=body["trigger_id"],
883
+ view={
884
+ "type": "modal",
885
+ "title": {"type": "plain_text", "text": "Microsoft Calendar Auth"},
886
+ "close": {"type": "plain_text", "text": "Close"},
887
+ "blocks": [
888
+ {"type": "section", "text": {"type": "mrkdwn", "text": "Click below to authenticate with Microsoft:"}},
889
+ {"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Microsoft Calendar"}, "url": auth_url, "action_id": "ms_auth_button"}]}
890
+ ]
891
+ }
892
+ )
893
+ except Exception as e:
894
+ logger.error(f"Error opening Microsoft auth modal: {e}")
895
+ @bolt_app.event("app_mention")
896
+ def handle_mentions(event, say, client, context):
897
+ if event_deduplicator.is_duplicate_event(event):
898
+ logger.info("Duplicate event detected, skipping processing")
899
+ return
900
+
901
+ # Ignore messages from bots
902
+ if event.get("bot_id"):
903
+ logger.info("Ignoring message from bot")
904
+ return
905
+
906
+ user_id = event.get("user")
907
+ channel_id = event.get("channel")
908
+ text = event.get("text", "").strip()
909
+ thread_ts = event.get("thread_ts")
910
+ team_id = context['team_id']
911
+ calendar_tool = get_owner_selected_calendar(client, team_id)
912
+ if not calendar_tool or calendar_tool == "none":
913
+ say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
914
+ return
915
+
916
+ # Fetch bot_user_id dynamically from installation
917
+ installation = installation_store.find_installation(team_id=team_id)
918
+ if not installation or not installation.bot_user_id:
919
+ logger.error(f"No bot_user_id found for team {team_id}")
920
+ say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
921
+ return
922
+ print(f"App mention events")
923
+
924
+ bot_user_id = installation.bot_user_id
925
+ print(f"Bot user id: {bot_user_id}")
926
+ mention = f"<@{bot_user_id}>"
927
+ mentions = list(set(re.findall(r'<@(\w+)>', text)))
928
+ if bot_user_id in mentions:
929
+ mentions.remove(bot_user_id)
930
+ text = text.replace(mention, "").strip()
931
+
932
+ workspace_owner_id = get_workspace_owner_id(client, team_id)
933
+ timezone = get_user_timezone(client, user_id)
934
+ zoom_link = get_zoom_link(client, team_id)
935
+ zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
936
+
937
+ channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
938
+ channel_history = format_channel_history(channel_history)
939
+ intent = intent_chain.run({"history": channel_history, "input": text})
940
+
941
+ relevant_user_ids = get_relevant_user_ids(client, channel_id)
942
+ all_users = get_all_users(team_id)
943
+ relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
944
+ for uid in relevant_user_ids}
945
+ user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
946
+ for uid, info in relevant_users.items() if uid != bot_user_id])
947
+
948
+ print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
949
+ mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
950
+
951
+ import pytz
952
+ pst = pytz.timezone('America/Los_Angeles')
953
+ current_time_pst = datetime.now(pst)
954
+ formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
955
+
956
+ from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
957
+ if calendar_tool == "google":
958
+ calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
959
+ schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
960
+ update_tools = [tools[i] for i in [0, 7, 12]]
961
+ delete_tools = [tools[i] for i in [0, 8, 12]]
962
+ elif calendar_tool == "microsoft":
963
+ calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
964
+ schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
965
+ update_tools = [tools[i] for i in [0, 10, 12]]
966
+ delete_tools = [tools[i] for i in [0, 11, 12]]
967
+ else:
968
+ say("Invalid calendar tool configured.", thread_ts=thread_ts)
969
+ return
970
+
971
+ calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
972
+ output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
973
+ print(f"MENTIONED USERS:{mentioned_users_output}")
974
+
975
+ agent_input = {
976
+ 'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
977
+ 'event_details': str(event),
978
+ 'target_user_id': user_id,
979
+ 'timezone': timezone,
980
+ 'user_id': user_id,
981
+ 'admin': workspace_owner_id,
982
+ 'zoom_link': zoom_link,
983
+ 'zoom_mode': zoom_mode,
984
+ 'channel_history': channel_history,
985
+ 'user_information': user_information,
986
+ 'calendar_tool': calendar_tool,
987
+ 'date_time': formatted_time,
988
+ 'formatted_calendar': output,
989
+ 'team_id': team_id
990
+ }
991
+
992
+ mentions = list(set(re.findall(r'<@(\w+)>', text)))
993
+ if bot_user_id in mentions:
994
+ mentions.remove(bot_user_id)
995
+
996
+ schedule_group_exec = create_schedule_channel_agent(schedule_tools)
997
+ update_group_exec = create_update_group_agent(update_tools)
998
+ delete_exec = create_delete_agent(delete_tools)
999
+
1000
+ if intent == "schedule meeting":
1001
+ group_agent_input = agent_input.copy()
1002
+ group_agent_input['mentioned_users'] = mentioned_users_output
1003
+ response = schedule_group_exec.invoke(group_agent_input)
1004
+ say(response['output'])
1005
+ return
1006
+ elif intent == "update event":
1007
+ group_agent_input = agent_input.copy()
1008
+ group_agent_input['mentioned_users'] = mentioned_users_output
1009
+ response = update_group_exec.invoke(group_agent_input)
1010
+ say(response['output'])
1011
+ return
1012
+ elif intent == "delete event":
1013
+ response = delete_exec.invoke(agent_input)
1014
+ say(response['output'])
1015
+ return
1016
+ elif intent == "other":
1017
+ response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
1018
+ say(response)
1019
+ return
1020
+ else:
1021
+ say("I'm not sure how to handle that request.")
1022
+ # @bolt_app.event("app_mention")
1023
+ # def handle_mentions(event, say, client, context):
1024
+ # if event_deduplicator.is_duplicate_event(event):
1025
+ # logger.info("Duplicate event detected, skipping processing")
1026
+ # return
1027
+
1028
+ # # Ignore messages from bots
1029
+ # if event.get("bot_id"):
1030
+ # logger.info("Ignoring message from bot")
1031
+ # return
1032
+
1033
+
1034
+ # user_id = event.get("user")
1035
+ # channel_id = event.get("channel")
1036
+ # text = event.get("text", "").strip()
1037
+ # thread_ts = event.get("thread_ts")
1038
+ # team_id = context['team_id']
1039
+ # calendar_tool = get_owner_selected_calendar(client, team_id)
1040
+ # if not calendar_tool or calendar_tool == "none":
1041
+ # say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
1042
+ # return
1043
+
1044
+ # # Fetch bot_user_id dynamically from installation
1045
+ # installation = installation_store.find_installation(team_id=team_id)
1046
+ # if not installation or not installation.bot_user_id:
1047
+ # logger.error(f"No bot_user_id found for team {team_id}")
1048
+ # say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
1049
+ # return
1050
+ # print(f"App mention events")
1051
+
1052
+ # bot_user_id = installation.bot_user_id
1053
+ # print(f"Bot user id: {bot_user_id}")
1054
+ # mention = f"<@{bot_user_id}>"
1055
+ # mentions = list(set(re.findall(r'<@(\w+)>', text)))
1056
+ # # Use dynamic bot_user_id instead of SLACK_BOT_USER_ID
1057
+ # if bot_user_id in mentions:
1058
+ # mentions.remove(bot_user_id)
1059
+ # text = text.replace(mention, "").strip()
1060
+
1061
+ # workspace_owner_id = get_workspace_owner_id(client, team_id)
1062
+ # timezone = get_user_timezone(client, user_id)
1063
+ # zoom_link = get_zoom_link(client, team_id)
1064
+ # zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
1065
+
1066
+ # channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
1067
+ # channel_history = format_channel_history(channel_history)
1068
+ # intent = intent_chain.run({"history": channel_history, "input": text})
1069
+
1070
+ # relevant_user_ids = get_relevant_user_ids(client, channel_id)
1071
+ # all_users = get_all_users(team_id)
1072
+ # relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
1073
+ # for uid in relevant_user_ids}
1074
+ # user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
1075
+ # for uid, info in relevant_users.items() if uid != bot_user_id])
1076
+
1077
+ # print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
1078
+ # mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history,"current_input":text, 'bob_id':bot_user_id})
1079
+
1080
+ # import pytz
1081
+ # pst = pytz.timezone('America/Los_Angeles')
1082
+ # current_time_pst = datetime.now(pst)
1083
+ # formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
1084
+
1085
+ # from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
1086
+ # if calendar_tool == "google":
1087
+ # calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
1088
+ # schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
1089
+ # update_tools = [tools[i] for i in [0, 7, 12]]
1090
+ # delete_tools = [tools[i] for i in [0, 8, 12]]
1091
+ # elif calendar_tool == "microsoft":
1092
+ # calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
1093
+ # schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
1094
+ # update_tools = [tools[i] for i in [0, 10, 12]]
1095
+ # delete_tools = [tools[i] for i in [0, 11, 12]]
1096
+ # else:
1097
+ # say("Invalid calendar tool configured.", thread_ts=thread_ts)
1098
+ # return
1099
+
1100
+ # calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
1101
+ # output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
1102
+ # print(f"MENTIONED USERS:{mentioned_users_output}")
1103
+
1104
+ # agent_input = {
1105
+ # 'input': text,
1106
+ # 'event_details': str(event),
1107
+ # 'target_user_id': user_id,
1108
+ # 'timezone': timezone,
1109
+ # 'user_id': user_id,
1110
+ # 'admin': workspace_owner_id,
1111
+ # 'zoom_link': zoom_link,
1112
+ # 'zoom_mode': zoom_mode,
1113
+ # 'channel_history': channel_history,
1114
+ # 'user_information': mentioned_users_output,
1115
+ # 'calendar_tool': calendar_tool,
1116
+ # 'date_time': formatted_time,
1117
+ # 'formatted_calendar': output,
1118
+ # 'team_id': team_id # Added
1119
+ # }
1120
+
1121
+ # mentions = list(set(re.findall(r'<@(\w+)>', text)))
1122
+ # if bot_user_id in mentions:
1123
+ # mentions.remove(bot_user_id)
1124
+
1125
+ # schedule_group_exec = create_schedule_channel_agent(schedule_tools)
1126
+ # update_group_exec = create_update_group_agent(update_tools)
1127
+ # delete_exec = create_delete_agent(delete_tools)
1128
+
1129
+ # if intent == "schedule meeting":
1130
+ # group_agent_input = agent_input.copy()
1131
+ # group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
1132
+ # response = schedule_group_exec.invoke(group_agent_input)
1133
+ # say(response['output'])
1134
+ # return
1135
+ # elif intent == "update event":
1136
+ # group_agent_input = agent_input.copy()
1137
+ # group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
1138
+ # response = update_group_exec.invoke(group_agent_input)
1139
+ # say(response['output'])
1140
+ # return
1141
+ # elif intent == "delete event":
1142
+ # response = delete_exec.invoke(agent_input)
1143
+ # say(response['output'])
1144
+ # return
1145
+ # elif intent == "other":
1146
+ # response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
1147
+ # say(response)
1148
+ # return
1149
+ # else:
1150
+ # say("I'm not sure how to handle that request.")
1151
+
1152
+ def format_channel_history(raw_history):
1153
+ cleaned_history = []
1154
+ for msg in raw_history:
1155
+ if 'bot_id' in msg and 'Calendar provider updated' in msg.get('text', ''):
1156
+ continue
1157
+ sender = msg.get('user', 'Unknown') if 'bot_id' not in msg else msg.get('bot_profile', {}).get('name', 'Bot')
1158
+ message_text = msg.get('text', '').strip()
1159
+ timestamp = float(msg.get('ts', 0))
1160
+ readable_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %I:%M %p')
1161
+ user_id = msg.get('user', 'N/A')
1162
+ team_id = msg.get('team', 'N/A')
1163
+ cleaned_history.append({
1164
+ 'message': message_text,
1165
+ 'from': sender,
1166
+ 'timestamp': readable_time,
1167
+ 'user_team': f"{user_id}/{team_id}"
1168
+ })
1169
+ formatted_output = ""
1170
+ for i, entry in enumerate(cleaned_history, 1):
1171
+ formatted_output += f"Message {i}: {entry['message']}\nFrom: {entry['from']}\nTimestamp: {entry['timestamp']}\nUserId/TeamId: {entry['user_team']}\n\n"
1172
+ return formatted_output.strip()
1173
+
1174
+ def get_relevant_user_ids(client, channel_id):
1175
+ try:
1176
+ members = []
1177
+ cursor = None
1178
+ while True:
1179
+ response = client.conversations_members(channel=channel_id, limit=10, cursor=cursor)
1180
+ if not response["ok"]:
1181
+ logger.error(f"Failed to get members for channel {channel_id}: {response['error']}")
1182
+ break
1183
+ members.extend(response["members"])
1184
+ cursor = response.get("response_metadata", {}).get("next_cursor")
1185
+ if not cursor:
1186
+ break
1187
+ return members
1188
+ except SlackApiError as e:
1189
+ logger.error(f"Error getting conversation members: {e}")
1190
+ return []
1191
+
1192
+ calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
1193
+
1194
+ @bolt_app.event("message")
1195
+ def handle_messages(body, say, client, context):
1196
+ if event_deduplicator.is_duplicate_event(body):
1197
+ logger.info("Duplicate event detected, skipping processing")
1198
+ return
1199
+ event = body.get("event", {})
1200
+ if event.get("bot_id"):
1201
+ logger.info("Ignoring message from bot")
1202
+ return
1203
+
1204
+ user_id = event.get("user")
1205
+ text = event.get("text", "").strip()
1206
+ channel_id = event.get("channel")
1207
+ thread_ts = event.get("thread_ts")
1208
+ team_id = context['team_id']
1209
+ calendar_tool = get_owner_selected_calendar(client, team_id)
1210
+ channel_info = client.conversations_info(channel=channel_id)
1211
+ channel = channel_info["channel"]
1212
+
1213
+ # Fetch bot_user_id dynamically from installation
1214
+ installation = installation_store.find_installation(team_id=team_id)
1215
+ if not installation or not installation.bot_user_id:
1216
+ logger.error(f"No bot_user_id found for team {team_id}")
1217
+ say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
1218
+ return
1219
+ bot_user_id = installation.bot_user_id
1220
+
1221
+ if not channel.get("is_im") and f"<@{bot_user_id}>" in text:
1222
+ return
1223
+ if not channel.get("is_im") and "thread_ts" not in event:
1224
+ return
1225
+ if not calendar_tool or calendar_tool == "none":
1226
+ say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
1227
+ return
1228
+ print(f"Message events")
1229
+ workspace_owner_id = get_workspace_owner_id(client, team_id)
1230
+ is_owner = user_id == workspace_owner_id
1231
+ timezone = get_user_timezone(client, user_id)
1232
+ zoom_link = get_zoom_link(client, team_id)
1233
+ zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
1234
+ channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
1235
+ channel_history = format_channel_history(channel_history)
1236
+ intent = intent_chain.run({"history": channel_history, "input": text})
1237
+
1238
+ if intent == "schedule meeting" and not is_owner and not channel.get("is_group") and not channel.get("is_mpim") and 'thread_ts' not in event:
1239
+ admin_dm = client.conversations_open(users=workspace_owner_id)
1240
+ prompt = ChatPromptTemplate.from_template("""
1241
+ You have this text: {text} and your job is to mention @{workspace_owner_id} and say following in 2 scenarios:
1242
+ if message history confirms about scheduling meeting then format below text and return only that response with no other explanation
1243
+ "Hi {workspace_owner_id} you wanted to schedule a meeting with {user_id}, {user_id} has proposed these slots [Time slots from the text] , Are you comfortable with these slots ? Confirm so I can fix the meeting."
1244
+ else:
1245
+ Format the text : {text}
1246
+ MESSAGE HISTORY: {channel_history}
1247
+ """)
1248
+ response = LLMChain(llm=llm, prompt=prompt)
1249
+ client.chat_postMessage(channel=admin_dm["channel"]["id"],
1250
+ text=response.run({'text': text, 'workspace_owner_id': workspace_owner_id, 'user_id': user_id, 'channel_history': channel_history}))
1251
+ say(f"<@{user_id}> I've notified the workspace owner about your meeting request.", thread_ts=thread_ts)
1252
+ return
1253
+
1254
+ mentions = list(set(re.findall(r'<@(\w+)>', text)))
1255
+ if bot_user_id in mentions:
1256
+ mentions.remove(bot_user_id)
1257
+
1258
+ import pytz
1259
+ pst = pytz.timezone('America/Los_Angeles')
1260
+ current_time_pst = datetime.now(pst)
1261
+ formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
1262
+
1263
+ from all_tools import MicrosoftListCalendarEvents, GoogleCalendarEvents
1264
+ if calendar_tool == "google":
1265
+ schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
1266
+ update_tools = [tools[i] for i in [0, 7, 12]]
1267
+ delete_tools = [tools[i] for i in [0, 8, 12]]
1268
+ output = calendar_formatting_chain.run({'input': GoogleCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
1269
+ elif calendar_tool == "microsoft":
1270
+ schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
1271
+ update_tools = [tools[i] for i in [0, 10, 12]]
1272
+ delete_tools = [tools[i] for i in [0, 11, 12]]
1273
+ output = calendar_formatting_chain.run({'input': MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
1274
+ else:
1275
+ say("Invalid calendar tool configured.", thread_ts=thread_ts)
1276
+ return
1277
+
1278
+ relevant_user_ids = get_relevant_user_ids(client, channel_id)
1279
+ all_users = get_all_users(team_id)
1280
+ relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
1281
+ for uid in relevant_user_ids}
1282
+ user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
1283
+ for uid, info in relevant_users.items() if uid != bot_user_id])
1284
+ print(f"All users: {all_users}\n\n Relevant users: {relevant_user_ids}")
1285
+ mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
1286
+ schedule_exec = create_schedule_agent(schedule_tools)
1287
+ update_exec = create_update_agent(update_tools)
1288
+ delete_exec = create_delete_agent(delete_tools)
1289
+ schedule_group_exec = create_schedule_group_agent(schedule_tools)
1290
+ update_group_exec = create_update_group_agent(update_tools)
1291
+ print(f"MENTIONED USERS:{mentioned_users_output}")
1292
+ channel_type = channel.get("is_group", False) or channel.get("is_mpim", False)
1293
+ agent_input = {
1294
+ 'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
1295
+ 'event_details': str(event),
1296
+ 'target_user_id': user_id,
1297
+ 'timezone': timezone,
1298
+ 'user_id': user_id,
1299
+ 'admin': workspace_owner_id,
1300
+ 'zoom_link': zoom_link,
1301
+ 'zoom_mode': zoom_mode,
1302
+ 'channel_history': channel_history,
1303
+ 'user_information': user_information,
1304
+ 'calendar_tool': calendar_tool,
1305
+ 'date_time': formatted_time,
1306
+ 'formatted_calendar': output,
1307
+ 'team_id': team_id
1308
+ }
1309
+
1310
+ if intent == "schedule meeting":
1311
+ if not channel_type and len(mentions) > 1:
1312
+ mentions.append(user_id)
1313
+ dm_channel_id, error = open_group_dm(client, mentions)
1314
+ if dm_channel_id:
1315
+ group_agent_input = agent_input.copy()
1316
+ group_agent_input['mentioned_users'] = mentioned_users_output
1317
+ group_agent_input['channel_history'] = channel_history
1318
+ group_agent_input['formatted_calendar'] = output
1319
+ response = schedule_group_exec.invoke(group_agent_input)
1320
+ client.chat_postMessage(channel=dm_channel_id, text=f"Group conversation started by <@{user_id}>\n\n{response['output']}")
1321
+ elif error:
1322
+ say(f"Sorry, I couldn't create the group conversation: {error}", thread_ts=thread_ts)
1323
+ else:
1324
+ if channel_type or 'thread_ts' in event:
1325
+ group_agent_input = agent_input.copy()
1326
+ if 'thread_ts' in event:
1327
+ schedule_group_exec = create_schedule_channel_agent(schedule_tools)
1328
+ history_response = client.conversations_replies(channel=channel_id, ts=thread_ts, limit=2)
1329
+ channel_history = format_channel_history(history_response.get("messages", []))
1330
+ else:
1331
+ channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=3).get("messages", []))
1332
+ group_agent_input['mentioned_users'] = mentioned_users_output
1333
+ group_agent_input['channel_history'] = channel_history
1334
+ group_agent_input['formatted_calendar'] = output
1335
+ response = schedule_group_exec.invoke(group_agent_input)
1336
+ say(response['output'], thread_ts=thread_ts)
1337
+ return
1338
+ response = schedule_exec.invoke(agent_input)
1339
+ say(response['output'], thread_ts=thread_ts)
1340
+ elif intent == "update event":
1341
+ agent_input['current_date'] = formatted_time
1342
+ agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
1343
+ if channel_type or 'thread_ts' in event:
1344
+ group_agent_input = agent_input.copy()
1345
+ channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=2).get("messages", []))
1346
+ group_agent_input['mentioned_users'] = mentioned_users_output
1347
+ group_agent_input['channel_history'] = channel_history
1348
+ group_agent_input['formatted_calendar'] = output
1349
+ response = update_group_exec.invoke(group_agent_input)
1350
+ say(response['output'], thread_ts=thread_ts)
1351
+ return
1352
+ response = update_exec.invoke(agent_input)
1353
+ say(response['output'], thread_ts=thread_ts)
1354
+ elif intent == "delete event":
1355
+ agent_input['current_date'] = formatted_time
1356
+ agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
1357
+ response = delete_exec.invoke(agent_input)
1358
+ say(response['output'], thread_ts=thread_ts)
1359
+ elif intent == "other":
1360
+ response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
1361
+ say(response, thread_ts=thread_ts)
1362
+ else:
1363
+ say("I'm not sure how to handle that request.", thread_ts=thread_ts)
1364
+ @bolt_app.event("team_join")
1365
+ def handle_team_join(event, client, context, logger):
1366
+ try:
1367
+ user_info = event['user']
1368
+ team_id = context.team_id
1369
+
1370
+ # Fetch workspace name from Slack API
1371
+ try:
1372
+ team_info = client.team_info()
1373
+ workspace_name = team_info['team']['name']
1374
+ except SlackApiError as e:
1375
+ logger.error(f"Error fetching team info: {e.response['error']}")
1376
+ workspace_name = "Unknown Workspace"
1377
+
1378
+ # Extract user details
1379
+ user_id = user_info['id']
1380
+ real_name = user_info.get('real_name', 'Unknown')
1381
+ profile = user_info.get('profile', {})
1382
+ email = profile.get('email', f"{user_info.get('name', 'user')}@example.com") # Fallback email
1383
+ name = user_info.get('name', '')
1384
+ is_owner = user_info.get('is_owner', False)
1385
+
1386
+ # Connect to database
1387
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
1388
+ cur = conn.cursor()
1389
+
1390
+ # Insert/update user in Users table
1391
+ cur.execute('''
1392
+ INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
1393
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
1394
+ ON CONFLICT (team_id, user_id) DO UPDATE SET
1395
+ workspace_name = EXCLUDED.workspace_name,
1396
+ real_name = EXCLUDED.real_name,
1397
+ email = EXCLUDED.email,
1398
+ name = EXCLUDED.name,
1399
+ is_owner = EXCLUDED.is_owner,
1400
+ last_updated = EXCLUDED.last_updated
1401
+ ''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now()))
1402
+
1403
+ conn.commit()
1404
+ cur.close()
1405
+ conn.close()
1406
+
1407
+ # Update user cache
1408
+ with user_cache_lock:
1409
+ if team_id not in user_cache:
1410
+ user_cache[team_id] = {}
1411
+ user_cache[team_id][user_id] = {
1412
+ "real_name": real_name,
1413
+ "email": f"{name}@gmail.com",
1414
+ "name": name,
1415
+ "is_owner": is_owner,
1416
+ "workspace_name": workspace_name
1417
+ }
1418
+
1419
+ # Update owner_id_cache if user is owner
1420
+ if is_owner:
1421
+ with owner_id_lock:
1422
+ owner_id_cache[team_id] = user_id
1423
+
1424
+ logger.info(f"Processed team_join event for user {user_id} in team {team_id}")
1425
+
1426
+ except KeyError as e:
1427
+ logger.error(f"Missing key in event data: {e}")
1428
+ except psycopg2.Error as e:
1429
+ logger.error(f"Database error: {e}")
1430
+ except Exception as e:
1431
+ logger.error(f"Unexpected error handling team_join: {e}")
1432
+ def open_group_dm(client, users):
1433
+ try:
1434
+ response = client.conversations_open(users=",".join(users))
1435
+ return response["channel"]["id"] if response["ok"] else (None, "Failed to create group DM")
1436
+ except SlackApiError as e:
1437
+ return None, f"Error creating group DM: {e.response['error']}"
1438
+
1439
+ # Flask Routes
1440
+ @app.route("/slack/events", methods=["POST"])
1441
+ def slack_events():
1442
+ return slack_handler.handle(request)
1443
+
1444
+ @app.route("/slack/install", methods=["GET"])
1445
+ def slack_install():
1446
+ return slack_handler.handle(request)
1447
+
1448
+ @app.route("/slack/oauth_redirect", methods=["GET"])
1449
+ def slack_oauth_redirect():
1450
+ return slack_handler.handle(request)
1451
+
1452
+ @app.route("/oauth2callback")
1453
+ def oauth2callback():
1454
+ state = request.args.get('state', '')
1455
+ print(f"STATE: {state}")
1456
+ print(f"STATs: {state_manager._states}")
1457
+ user_id = state_manager.validate_and_consume_state(state)
1458
+ stored_state = get_from_session(user_id, "gcal_state") if user_id else None
1459
+
1460
+ if not user_id or stored_state != state:
1461
+ return "Invalid state", 400
1462
+
1463
+ team_id = get_team_id_from_owner_id(user_id)
1464
+ if not team_id:
1465
+ return "Workspace not found", 404
1466
+
1467
+ client = get_client_for_team(team_id)
1468
+ if not client:
1469
+ return "Client not found", 500
1470
+
1471
+ flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
1472
+ flow.fetch_token(authorization_response=request.url)
1473
+ credentials = flow.credentials
1474
+ service = build('oauth2', 'v2', credentials=credentials)
1475
+ user_info = service.userinfo().get().execute()
1476
+ google_email = user_info.get('email', 'unknown@example.com')
1477
+ token_data = json.loads(credentials.to_json())
1478
+ token_data['google_email'] = google_email
1479
+
1480
+ save_token(team_id, user_id, 'google', token_data) # Adjusted to use team_id and user_id
1481
+ client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
1482
+
1483
+ return "Google Calendar connected successfully! You can close this window."
1484
+ @bolt_app.action("launch_auth")
1485
+ def handle_launch_auth(ack, body, logger):
1486
+ ack() # Acknowledge the action
1487
+ logger.info(f"Launch auth triggered by user {body['user']['id']}")
1488
+ # No further action needed since the URL redirect handles the OAuth flow
1489
+ @app.route("/microsoft_callback")
1490
+ def microsoft_callback():
1491
+ code = request.args.get("code")
1492
+ state = request.args.get("state")
1493
+ if not code or not state:
1494
+ return "Missing parameters", 400
1495
+ user_id = state_manager.validate_and_consume_state(state)
1496
+ if not user_id:
1497
+ return "Invalid or expired state parameter", 403
1498
+ team_id = get_team_id_from_owner_id(user_id)
1499
+ if not team_id:
1500
+ return "Workspace not found", 404
1501
+ client = get_client_for_team(team_id)
1502
+ if not client:
1503
+ return "Client not found", 500
1504
+ if user_id != get_workspace_owner_id(client, team_id):
1505
+ return "Unauthorized", 403
1506
+ msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
1507
+ result = msal_app.acquire_token_by_authorization_code(code, scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI)
1508
+ if "access_token" not in result:
1509
+ return "Authentication failed", 400
1510
+ token_data = {"access_token": result["access_token"], "refresh_token": result.get("refresh_token", ""), "expires_at": result["expires_in"] + time.time()}
1511
+ save_token(user_id, 'microsoft', token_data)
1512
+ client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
1513
+ return "Microsoft Calendar connected successfully! You can close this window."
1514
+
1515
+ @app.route("/zoom_callback")
1516
+ def zoom_callback():
1517
+ code = request.args.get("code")
1518
+ state = request.args.get("state")
1519
+ user_id = state_manager.validate_and_consume_state(state)
1520
+ if not user_id:
1521
+ return "Invalid or expired state", 403
1522
+ team_id = get_team_id_from_owner_id(user_id)
1523
+ if not team_id:
1524
+ return "Workspace not found", 404
1525
+ client = get_client_for_team(team_id)
1526
+ if not client:
1527
+ return "Client not found", 500
1528
+ params = {"grant_type": "authorization_code", "code": code, "redirect_uri": ZOOM_REDIRECT_URI}
1529
+ try:
1530
+ response = requests.post(ZOOM_TOKEN_API, params=params, auth=(CLIENT_ID, CLIENT_SECRET))
1531
+ except Exception as e:
1532
+ return jsonify({"error": f"Token request failed: {str(e)}"}), 500
1533
+ if response.status_code == 200:
1534
+ token_data = response.json()
1535
+ token_data["expires_at"] = time.time() + token_data["expires_in"]
1536
+ # Fixed: Pass all required arguments in correct order
1537
+ save_token(team_id, user_id, 'zoom', token_data)
1538
+ client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
1539
+ return "Zoom connected successfully! You can close this window."
1540
+ return "Failed to retrieve token", 400
1541
+
1542
+ @bolt_app.action("open_zoom_config_modal")
1543
+ def handle_open_zoom_config_modal(ack, body, client, logger):
1544
+ ack()
1545
+ user_id = body["user"]["id"]
1546
+ team_id = body["team"]["id"]
1547
+ owner_id = get_workspace_owner_id(client, team_id)
1548
+ if user_id != owner_id:
1549
+ client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
1550
+ return
1551
+
1552
+ # Fixed: Pass both team_id and user_id to load_preferences
1553
+ prefs = load_preferences(team_id, user_id)
1554
+ zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
1555
+ mode = zoom_config["mode"]
1556
+ link = zoom_config.get("link", "")
1557
+
1558
+ try:
1559
+ client.views_open(
1560
+ trigger_id=body["trigger_id"],
1561
+ view={
1562
+ "type": "modal",
1563
+ "callback_id": "zoom_config_submit",
1564
+ "title": {"type": "plain_text", "text": "Configure Zoom"},
1565
+ "submit": {"type": "plain_text", "text": "Save"},
1566
+ "close": {"type": "plain_text", "text": "Cancel"},
1567
+ "blocks": [
1568
+ {
1569
+ "type": "input",
1570
+ "block_id": "zoom_mode",
1571
+ "label": {"type": "plain_text", "text": "Zoom Mode"},
1572
+ "element": {
1573
+ "type": "static_select",
1574
+ "action_id": "mode_select",
1575
+ "placeholder": {"type": "plain_text", "text": "Select mode"},
1576
+ "options": [
1577
+ {"text": {"type": "plain_text", "text": "Automatic"}, "value": "automatic"},
1578
+ {"text": {"type": "plain_text", "text": "Manual"}, "value": "manual"}
1579
+ ],
1580
+ "initial_option": {"text": {"type": "plain_text", "text": "Automatic" if mode == "automatic" else "Manual"}, "value": mode} if mode else None
1581
+ }
1582
+ },
1583
+ {
1584
+ "type": "input",
1585
+ "block_id": "zoom_link",
1586
+ "label": {"type": "plain_text", "text": "Manual Zoom Link"},
1587
+ "element": {
1588
+ "type": "plain_text_input",
1589
+ "action_id": "link_input",
1590
+ "placeholder": {"type": "plain_text", "text": "Enter Zoom link"},
1591
+ "initial_value": link if isinstance(link, str) else ""
1592
+ },
1593
+ "optional": True
1594
+ }
1595
+ ]
1596
+ }
1597
+ )
1598
+ except Exception as e:
1599
+ logger.error(f"Error opening Zoom config modal: {e}")
1600
+
1601
+ @bolt_app.action("configure_zoom")
1602
+ def handle_zoom_config(ack, body, client, logger):
1603
+ ack() # Acknowledge the action
1604
+ user_id = body["user"]["id"]
1605
+ team_id = body["team"]["id"]
1606
+
1607
+ # Ensure only the workspace owner can configure Zoom
1608
+ owner_id = get_workspace_owner_id(client, team_id)
1609
+ if user_id != owner_id:
1610
+ client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
1611
+ return
1612
+
1613
+ # Check if this is a refresh or initial authentication
1614
+ zoom_token = load_token(team_id, owner_id, "zoom")
1615
+ is_refresh = zoom_token is not None
1616
+
1617
+ # Generate the Zoom OAuth URL
1618
+ state = state_manager.create_state(owner_id) # Assume this generates a unique state
1619
+ auth_url = f"{ZOOM_OAUTH_AUTHORIZE_API}?response_type=code&client_id={CLIENT_ID}&redirect_uri={quote_plus(ZOOM_REDIRECT_URI)}&state={state}"
1620
+
1621
+ # Set modal text based on the scenario
1622
+ modal_title = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
1623
+ button_text = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
1624
+
1625
+ # Open a modal with the appropriate text
1626
+ try:
1627
+ client.views_open(
1628
+ trigger_id=body["trigger_id"],
1629
+ view={
1630
+ "type": "modal",
1631
+ "title": {"type": "plain_text", "text": modal_title},
1632
+ "close": {"type": "plain_text", "text": "Cancel"},
1633
+ "blocks": [
1634
+ {
1635
+ "type": "section",
1636
+ "text": {"type": "mrkdwn", "text": f"Click below to {button_text.lower()}:"}
1637
+ },
1638
+ {
1639
+ "type": "actions",
1640
+ "elements": [
1641
+ {
1642
+ "type": "button",
1643
+ "text": {"type": "plain_text", "text": button_text},
1644
+ "url": auth_url,
1645
+ "action_id": "launch_zoom_auth"
1646
+ }
1647
+ ]
1648
+ }
1649
+ ]
1650
+ }
1651
+ )
1652
+ except Exception as e:
1653
+ logger.error(f"Error opening Zoom auth modal: {e}")
1654
+
1655
+ @bolt_app.view("zoom_config_submit")
1656
+ def handle_zoom_config_submit(ack, body, client, logger):
1657
+ ack() # Ensure ack is called before any processing to avoid warnings
1658
+ user_id = body["user"]["id"]
1659
+ team_id = body["team"]["id"]
1660
+ owner_id = get_workspace_owner_id(client, team_id)
1661
+ if user_id != owner_id:
1662
+ return # Early return if not owner; no need to proceed
1663
+
1664
+ values = body["view"]["state"]["values"]
1665
+ mode = values["zoom_mode"]["mode_select"]["selected_option"]["value"]
1666
+ link = values["zoom_link"]["link_input"]["value"] if "zoom_link" in values and "link_input" in values["zoom_link"] else None
1667
+ zoom_config = {"mode": mode, "link": link if mode == "manual" else None}
1668
+
1669
+
1670
+ save_preference(team_id, user_id, zoom_config=zoom_config)
1671
+
1672
+ client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
1673
+ @bolt_app.action("launch_zoom_auth")
1674
+ def handle_some_action(ack, body, logger):
1675
+ ack()
1676
+
1677
+ scheduler = BackgroundScheduler()
1678
+ scheduler.add_job(state_manager.cleanup_expired_states, 'interval', minutes=5)
1679
+ scheduler.start()
1680
+
1681
+ @app.route('/')
1682
+ def home():
1683
+ return render_template('index.html')
1684
+ # @app.route('/ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
1685
+ # def home():
1686
+ # return render_template('ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
1687
+ if __name__ == "__main__":
1688
+ app.run(port=3000)
calendar_tools.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from services import create_service
3
+ client_secret = 'credentials.json'
4
+
5
+ def construct_google_calendar_client(client_secret):
6
+ """
7
+ Constructs a Google Calendar API client.
8
+
9
+ Parameters:
10
+ - client_secret (str): The path to the client secret JSON file.
11
+
12
+ Returns:
13
+ - service: The Google Calendar API service instance.
14
+ """
15
+ API_NAME = 'calendar'
16
+ API_VERSION = 'v3'
17
+ SCOPES = ['https://www.googleapis.com/auth/calendar']
18
+ service = create_service(client_secret, API_NAME, API_VERSION, SCOPES)
19
+ return service
20
+
21
+
22
+ calendar_service = construct_google_calendar_client(client_secret=client_secret)
23
+
24
+ def create_calendar_list(calendar_name):
25
+ """
26
+ Creates a new calendar list.
27
+
28
+ Parameters:
29
+ - calendar_name (str): The name of the new calendar list.
30
+
31
+ Returns:
32
+ - dict: A dictionary containing the ID of the new calendar list.
33
+ """
34
+ calendar_list = {
35
+ 'summary': calendar_name
36
+ }
37
+ created_calendar_list = calendar_service.calendarList().insert(body=calendar_list).execute()
38
+ return created_calendar_list
39
+
40
+
41
+ def list_calendar_list(max_capacity=200):
42
+ """
43
+ Lists calendar lists until the total number of items reaches max_capacity.
44
+
45
+ Parameters:
46
+ - max_capacity (int or str, optional): The maximum number of calendar lists to retrieve. Defaults to 200.
47
+ If a string is provided, it will be converted to an integer.
48
+
49
+ Returns:
50
+ - list: A list of dictionaries containing cleaned calendar list information with 'id', 'name', and 'description'.
51
+ """
52
+ if isinstance(max_capacity, str):
53
+ max_capacity = int(max_capacity)
54
+
55
+ all_calendars = []
56
+ all_calendars_cleaned = []
57
+ next_page_token = None
58
+ capacity_tracker = 0
59
+
60
+ while True:
61
+ calendar_list = calendar_service.calendarList().list(
62
+ maxResults=min(200, max_capacity - capacity_tracker),
63
+ pageToken=next_page_token
64
+ ).execute()
65
+ calendars = calendar_list.get('items', [])
66
+ all_calendars.extend(calendars)
67
+ capacity_tracker += len(calendars)
68
+ if capacity_tracker >= max_capacity:
69
+ break
70
+ next_page_token = calendar_list.get('nextPageToken')
71
+ if not next_page_token:
72
+ break
73
+
74
+ for calendar in all_calendars:
75
+ all_calendars_cleaned.append(
76
+ {
77
+ 'id': calendar['id'],
78
+ 'name': calendar['summary'],
79
+ 'description': calendar.get('description', '')
80
+ })
81
+
82
+ return all_calendars_cleaned
83
+
84
+ def list_calendar_events(calendar_id, max_capacity=20):
85
+ """
86
+ Lists events from a specified calendar until the total number of events reaches max_capacity.
87
+
88
+ Parameters:
89
+ - calendar_id (str): The ID of the calendar from which to list events.
90
+ - max_capacity (int or str, optional): The maximum number of events to retrieve. Defaults to 20.
91
+ If a string is provided, it will be converted to an integer.
92
+
93
+ Returns:
94
+ - list: A list of events from the specified calendar.
95
+ """
96
+ if isinstance(max_capacity, str):
97
+ max_capacity = int(max_capacity)
98
+
99
+ all_events = []
100
+ next_page_token = None
101
+ capacity_tracker = 0
102
+
103
+ while True:
104
+ events_list = calendar_service.events().list(
105
+ calendarId=calendar_id,
106
+ maxResults=min(250, max_capacity - capacity_tracker),
107
+ pageToken=next_page_token
108
+ ).execute()
109
+ events = events_list.get('items', [])
110
+ all_events.extend(events)
111
+ capacity_tracker += len(events)
112
+ if capacity_tracker >= max_capacity:
113
+ break
114
+ next_page_token = events_list.get('nextPageToken')
115
+ if not next_page_token:
116
+ break
117
+
118
+ return all_events
config.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from slack_sdk.errors import SlackApiError
2
+ import os
3
+ from dotenv import load_dotenv
4
+ from slack_sdk import WebClient
5
+ import sqlite3
6
+ import json
7
+ import psycopg2
8
+ import logging
9
+ from datetime import datetime
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ # Slack credentials
14
+ SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
15
+
16
+ # Initialize the Slack client
17
+ client = WebClient(token=SLACK_BOT_TOKEN)
18
+ logger = logging.getLogger(__name__)
19
+ def load_preferences(team_id, user_id):
20
+ with preferences_cache_lock:
21
+ if (team_id, user_id) in preferences_cache:
22
+ return preferences_cache[(team_id, user_id)]
23
+ try:
24
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
25
+ cur = conn.cursor()
26
+ cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
27
+ row = cur.fetchone()
28
+ if row:
29
+ zoom_config, calendar_tool = row
30
+ # For jsonb, zoom_config is already a dict; no json.loads needed
31
+ preferences = {
32
+ "zoom_config": zoom_config if zoom_config else {"mode": "manual", "link": None},
33
+ "calendar_tool": calendar_tool or "none"
34
+ }
35
+ else:
36
+ preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
37
+ cur.close()
38
+ conn.close()
39
+ except Exception as e:
40
+ logger.error(f"Failed to load preferences for team {team_id}, user {user_id}: {e}")
41
+ preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
42
+ with preferences_cache_lock:
43
+ preferences_cache[(team_id, user_id)] = preferences
44
+ return preferences
45
+ from threading import Lock
46
+ user_cache = {}
47
+ preferences_cache = {}
48
+ preferences_cache_lock = Lock()
49
+ user_cache_lock = Lock() # Example threading lock for cache
50
+
51
+ owner_id_cache = {}
52
+ owner_id_lock = Lock()
53
+ def initialize_workspace_cache(client, team_id):
54
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
55
+ cur = conn.cursor()
56
+ cur.execute('SELECT MAX(last_updated) FROM Users WHERE team_id = %s', (team_id,))
57
+ last_updated_row = cur.fetchone()
58
+ last_updated = last_updated_row[0] if last_updated_row and last_updated_row[0] else None
59
+
60
+ # Check if cache is fresh (e.g., less than 24 hours old)
61
+ if last_updated and (datetime.now() - last_updated).total_seconds() < 86400:
62
+ cur.execute('SELECT user_id, real_name, email, name, is_owner, workspace_name FROM Users WHERE team_id = %s', (team_id,))
63
+ rows = cur.fetchall()
64
+ new_cache = {row[0]: {"real_name": row[1], "email": row[2], "name": row[3], "is_owner": row[4], "workspace_name": row[5]} for row in rows}
65
+ with user_cache_lock:
66
+ user_cache[team_id] = new_cache
67
+ with owner_id_lock:
68
+ owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
69
+ else:
70
+ # Fetch user data from Slack and update database
71
+ response = client.users_list()
72
+ users = response["members"]
73
+ workspace_name = client.team_info()["team"]["name"] # Get workspace name from Slack API
74
+ new_cache = {}
75
+ for user in users:
76
+ user_id = user['id']
77
+ profile = user.get('profile', {})
78
+ real_name = profile.get('real_name', 'Unknown')
79
+ name = user.get('name', '')
80
+ email = f"{name}@gmail.com" # Placeholder; adjust as needed
81
+ is_owner = user.get('is_owner', False)
82
+ new_cache[user_id] = {"real_name": real_name, "email": email, "name": name, "is_owner": is_owner, "workspace_name": workspace_name}
83
+ cur.execute('''
84
+ INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
85
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
86
+ ON CONFLICT (team_id, user_id) DO UPDATE SET
87
+ workspace_name = %s, real_name = %s, email = %s, name = %s, is_owner = %s, last_updated = %s
88
+ ''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now(),
89
+ workspace_name, real_name, email, name, is_owner, datetime.now()))
90
+ conn.commit()
91
+ with user_cache_lock:
92
+ user_cache[team_id] = new_cache
93
+ with owner_id_lock:
94
+ owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
95
+ cur.close()
96
+ conn.close()
97
+ def get_workspace_owner_id_client(client ):
98
+ """Get the workspace owner's user ID."""
99
+ try:
100
+ response = client.users_list()
101
+ members = response["members"]
102
+ for member in members:
103
+ if member.get("is_owner"):
104
+ return member["id"]
105
+ except SlackApiError as e:
106
+ print(f"Error fetching users: {e.response['error']}")
107
+ return None
108
+
109
+ def get_workspace_owner_id(client, team_id):
110
+ with owner_id_lock:
111
+ if team_id in owner_id_cache and owner_id_cache[team_id]:
112
+ return owner_id_cache[team_id]
113
+ initialize_workspace_cache(client, team_id)
114
+ with owner_id_lock:
115
+ return owner_id_cache.get(team_id)
116
+ # def get_workspace_owner_id():
117
+ # conn = sqlite3.connect('workspace_cache.db')
118
+ # c = conn.cursor()
119
+ # c.execute('SELECT user_id FROM workspace_cache WHERE is_owner = 1')
120
+ # owner_id = c.fetchone()
121
+ # conn.close()
122
+ # return owner_id[0] if owner_id else None
123
+
124
+ owner_id_pref = get_workspace_owner_id_client(client)
125
+ def GetAllUsers():
126
+ all_users = {}
127
+ try:
128
+ response = client.users_list()
129
+ print(response)
130
+ members = response['members']
131
+ for member in members:
132
+ user_id = member['id']
133
+ profile = member.get('profile', {})
134
+ user_name = profile.get('real_name', '') # Use real_name from profile
135
+ # Use actual email if available, otherwise construct one from member['name']
136
+ email = profile.get('email', f"{member.get('name', '')}@gmail.com")
137
+ print(f"User ID: {user_id}, Name: {user_name}, Email: {email}")
138
+ all_users[user_id] = {"Slack Id": user_id, "name": user_name, "email": email}
139
+ return all_users
140
+ except SlackApiError as e:
141
+ print(f"Error fetching users: {e.response['error']}")
142
+ return {}
143
+ def load_token(team_id, user_id, service):
144
+ """
145
+ Load token data from the database for a specific team, user, and service.
146
+
147
+ Parameters:
148
+ team_id (str): The Slack team ID.
149
+ user_id (str): The Slack user ID.
150
+ service (str): The service name (e.g., 'google').
151
+
152
+ Returns:
153
+ dict: The token data as a dictionary, or None if not found.
154
+ """
155
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
156
+ cur = conn.cursor()
157
+ cur.execute(
158
+ 'SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s',
159
+ (team_id, user_id, service)
160
+ )
161
+ row = cur.fetchone()
162
+ cur.close()
163
+ conn.close()
164
+ return row[0] if row else None
165
+
166
+ def save_token(team_id, user_id, service, token_data):
167
+ """
168
+ Save token data to the database for a specific team, user, and service.
169
+
170
+ Parameters:
171
+ team_id (str): The Slack team ID.
172
+ user_id (str): The Slack user ID.
173
+ service (str): The service name (e.g., 'google').
174
+ token_data (dict): The token data to save (e.g., {'access_token', 'refresh_token', 'expires_at'}).
175
+ """
176
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
177
+ cur = conn.cursor()
178
+ cur.execute(
179
+ '''
180
+ INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
181
+ VALUES (%s, %s, %s, %s, %s)
182
+ ON CONFLICT (team_id, user_id, service)
183
+ DO UPDATE SET token_data = %s, updated_at = %s
184
+ ''',
185
+ (
186
+ team_id, user_id, service, json.dumps(token_data), datetime.now(),
187
+ json.dumps(token_data), datetime.now()
188
+ )
189
+ )
190
+ conn.commit()
191
+ cur.close()
192
+ conn.close()
193
+ all_users_preload = GetAllUsers()
194
+ if all_users_preload:
195
+ print("Users Prefection enabled")
196
+ # def GetAllUsers():
197
+ # return ""
198
+ # all_users_preload = ""
199
+ # owner_id_pref = ""
db.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import psycopg2
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ def init_db():
8
+ """
9
+ Initialize the Neon Postgres database by creating the necessary tables for multi-workspace support:
10
+ - Installations: Stores Slack workspace installation data.
11
+ - Users: Stores Slack user information with team_id, user_id, and workspace_name.
12
+ - Preferences: Stores user-specific preferences with team_id and user_id.
13
+ - Tokens: Stores authentication tokens with team_id, user_id, and service.
14
+ Prerequisites: 'DATABASE_URL' environment variable must be set with Neon Postgres connection string.
15
+ """
16
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
17
+ cur = conn.cursor()
18
+
19
+ # Create Installations table for OAuth installation data
20
+ cur.execute('''
21
+ CREATE TABLE IF NOT EXISTS Installations (
22
+ workspace_id TEXT PRIMARY KEY, -- Slack workspace ID (team_id)
23
+ installation_data JSONB, -- Installation data stored as JSON
24
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Last update timestamp
25
+ )
26
+ ''')
27
+
28
+ # Create Users table with composite primary key (team_id, user_id) and workspace_name
29
+ cur.execute('''
30
+ CREATE TABLE IF NOT EXISTS Users (
31
+ team_id TEXT, -- Slack workspace ID
32
+ user_id TEXT, -- Slack user ID
33
+ workspace_name TEXT, -- Name of the workspace
34
+ real_name TEXT, -- User's real name from Slack
35
+ email TEXT, -- User's email from Slack
36
+ name TEXT, -- User's Slack handle
37
+ is_owner BOOLEAN, -- Indicates if user is workspace owner
38
+ last_updated TIMESTAMP, -- Last time user data was updated
39
+ PRIMARY KEY (team_id, user_id) -- Composite key for uniqueness across workspaces
40
+ )
41
+ ''')
42
+
43
+ # Create Preferences table with composite primary key and foreign key
44
+ cur.execute('''
45
+ CREATE TABLE IF NOT EXISTS Preferences (
46
+ team_id TEXT, -- Slack workspace ID
47
+ user_id TEXT, -- Slack user ID
48
+ zoom_config JSONB, -- Zoom configuration stored as JSON
49
+ calendar_tool TEXT, -- Selected calendar tool (e.g., google, microsoft)
50
+ updated_at TIMESTAMP, -- Last update timestamp
51
+ PRIMARY KEY (team_id, user_id), -- Composite key for uniqueness
52
+ CONSTRAINT fk_user
53
+ FOREIGN KEY(team_id, user_id) -- References Users table
54
+ REFERENCES Users(team_id, user_id)
55
+ ON DELETE CASCADE -- Delete preferences if user is deleted
56
+ )
57
+ ''')
58
+
59
+ # Create Tokens table with composite primary key and foreign key
60
+ cur.execute('''
61
+ CREATE TABLE IF NOT EXISTS Tokens (
62
+ team_id TEXT, -- Slack workspace ID
63
+ user_id TEXT, -- Slack user ID
64
+ service TEXT, -- Service name (google, microsoft, zoom)
65
+ token_data JSONB, -- Token data stored as JSON
66
+ updated_at TIMESTAMP, -- Last update timestamp
67
+ PRIMARY KEY (team_id, user_id, service), -- Composite key ensures one token per service per user per workspace
68
+ CONSTRAINT fk_user
69
+ FOREIGN KEY(team_id, user_id) -- References Users table
70
+ REFERENCES Users(team_id, user_id)
71
+ ON DELETE CASCADE -- Delete tokens if user is deleted
72
+ )
73
+ ''')
74
+
75
+ conn.commit()
76
+ cur.close()
77
+ conn.close()
78
+
79
+ if __name__ == '__main__':
80
+ init_db()
81
+ print('Neon Postgres database initialized successfully.')
prompt.py ADDED
@@ -0,0 +1,698 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.prompts import ChatPromptTemplate
2
+
3
+ # Intent Classification Prompt
4
+ intent_prompt = ChatPromptTemplate.from_template("""
5
+ You are an intent classification assistant. Based on the user's message and the conversation history, determine the intent of the user's request. The possible intents are: "schedule meeting", "update event", "delete event", or "other". Provide only the intent as your response.
6
+ - By looking at the history if someone is confirming or denying the schedule , also categorize it as a schedule
7
+ Conversation History:
8
+ {history}
9
+
10
+ User's Message:
11
+ {input}
12
+ """)
13
+
14
+ # Schedule Meeting Agent Prompt
15
+
16
+
17
+
18
+ calender_prompt = ChatPromptTemplate.from_template("""
19
+ <SYSTEM>
20
+ You are an intelligent agent and your job is to design the timeslots for the meetings
21
+ You will be given with raw calendar events and you have the following job
22
+
23
+ <CURRENT DATE AND TIME>
24
+ {date_time}
25
+
26
+ <WORKSPACE ADMIN ID>
27
+ Use workspace admin slack id for the calendar event: {admin_id}
28
+ <JOB STEPS>
29
+ STEP 1. Fetch current date once
30
+ STEP 2. Filter all past events (events on dates which are behind the current date) and also the future event timeslots
31
+ STEP 3. Generate the 7 day timeslots omitting the past events and future time slots which have events registered
32
+ STEP 4. Prepare those slots in this reference format
33
+ "
34
+ Date | Day | Slots | Timezone
35
+ 01-12-2024 | Friday | All Day | PT
36
+ 02-12-2024 | Saturday | 9am - 11am, 2pm - 3pm | PT
37
+ 03-12-2024 | Sunday | All Day | PT
38
+ 04-12-2024 | Monday | 10am - 12pm | PT
39
+ 05-12-2024 | Tuesday | 1pm - 3pm | PT
40
+ 06-12-2024 | Wednesday | All Day | PT
41
+ 07-12-2024 | Thursday | 9am - 10am | PT
42
+
43
+ "
44
+ <UNFORMATTED EVENTS>
45
+ {input}
46
+ FINAL OUTPUT: Formatted slots in given format and dont include any step details or preprocessing details
47
+ """)
48
+
49
+
50
+
51
+ # schedule_prompt = ChatPromptTemplate.from_template("""
52
+ # <SYSTEM>:
53
+ # You are a meeting scheduling assistant. Your task is to help the user schedule a meeting by finding available time slots, coordinating with participants, and creating the meeting event in the calendar.
54
+ # <TOOLS>: Be Precise in using the tools and use it only once
55
+ # {{tools}}
56
+ # <CURRENT DATE AND TIME>
57
+ # {date_time}
58
+ # <TASK>:
59
+ # Based on the user's request, use the appropriate tools to schedule the meeting. Check calendar availability, send direct messages to coordinate, and create the event.
60
+ # <STEPS>
61
+ # 1. You will be given the formatted current events of the calendar.
62
+ # 2. Send the schedule by mentioning all the mentioned user s and ask for their preferences.
63
+ # 3. Note that there can be multiple users mentioned in the meeting text and if one user replies about a timing then also mention other users and ask if they are comfortable with this time or not.
64
+ # 3.1: If all user agrees then schedule the meeting in the calendar
65
+ # 3.2 If even one of them disagrees then go to step 2.1 and send the sames slots mentioning the disagreement and keep performing this step until the consensus is reached and all mentioned user are agreed on one time
66
+ # 3.3 You can keep track for yourself, lets say we have 3 users U1 , U2 and U3, you can check the users U1 has agreed , U2 has agreed but U3 didnt so again send the timetable but already prepared one so dont use the tool again just the history and mention that U3 have the clash so pick another time from the slots.
67
+
68
+ # 4. After resolving the conflict you must call the tool to register the event in the calendar.
69
+ # 4.1: You should include the summary of the event like who are included in the meeting
70
+ # 4.2: You should include the email addresses of the mentioned users which you can access from the <USERS INFORMATION>.
71
+
72
+ # 5. Finally if meeting is registered in the calendar then send the direct message to each of the attendee/ mentioned users about confirming the meeting schedule and again you can access the user information from the <USERS INFORMATION>
73
+ # - To send the dm to single mentioned user: send_direct_dm and pass the slack user id of the receiver i.e user_id = 'UC3472938'
74
+ # - To send the dm to multiple mentioned users use this tool: send_multiple_dms and user slacks id will be passed as list to this tool i.e user_ids = ['UA263487', 'UB8984234']
75
+
76
+ # ------------------------------------ YOUR WORK STARTS FROM HERE.
77
+ # <INPUT>:
78
+ # {input}
79
+ # <CHANNEL HISTORY(PREVIOUS MESSAGES FOR CONTEXT)>:
80
+ # {channel_history}
81
+ # - If a new message is sent or received related to new schedule of meeting then ignore old responses
82
+ # - Always analyze the latest history and in context to new messages
83
+ # <EVENTS FROM THE CALENDAR>
84
+ # "Hello <@mentioned_users>,
85
+ # <@{admin}> wants to schedule a meeting with you. Here are their available time slots:
86
+ # {formatted_calendar} Which slot suits for you best "
87
+ # <USERS INFORMATION>:
88
+ # {user_information}
89
+
90
+ # <CALENDAR TOOL>:
91
+ # {calendar_tool}
92
+ # <EVENT DETAILS TO REGISTER IN THE CALENDAR>:
93
+ # {event_details}
94
+
95
+ # <TARGET USER ID>:
96
+ # {target_user_id}
97
+
98
+ # <TIMEZONE>:
99
+ # {timezone}
100
+
101
+ # <USER ID>:
102
+ # {user_id}
103
+
104
+ # <ADMIN SLACK ID>:
105
+ # {admin}
106
+
107
+ # <ZOOM LINK>:
108
+ # {zoom_link}
109
+
110
+ # <ZOOM MODE>:
111
+ # {zoom_mode}
112
+
113
+
114
+ # <AGENT SCRATCHPAD>:
115
+ # {agent_scratchpad}
116
+
117
+ # <OUTPUT>:
118
+ # Provide a confirmation message after scheduling, e.g., "Meeting scheduled successfully."
119
+ # """)
120
+ from langchain.prompts import ChatPromptTemplate
121
+
122
+ schedule_prompt = ChatPromptTemplate.from_template("""
123
+ ## System Message
124
+ You are a meeting scheduling assistant. You task is following.
125
+ 1. Resolve conflicts when multiple users are proposing their timeslot
126
+ 2. Schedule meetings and send the direct message only once to users.
127
+ 3. You are not allowed to use any tool twice if a tool is used once then dont use it again
128
+ ## User Information:
129
+ - Email addresses of all participants, found in {user_information}.
130
+ - Store name in calendar not ids
131
+ - You can match ids with names and emails.
132
+ - Pass {admin} to calendar events as user
133
+ id
134
+ - Also add admin in calendar attendees as well. - You can ignore previous respones if in those responses some meeting is already set up.
135
+ ## Tools
136
+ - **Available Tools:** {{tools}}
137
+ *(Placeholder for the list of tools the assistant can use, e.g., calendar tools, messaging functions.)*
138
+ - **Tool Usage Guidelines:**
139
+ - **Messaging Tools:** Use `send_direct_dm` or `send_multiple_dms` each time you need to send a message (e.g., proposing slots, collecting responses, or confirming the meeting). Call these only when explicitly required by the workflow.
140
+
141
+ ## Current Date and Time
142
+ {date_time}
143
+ *(Placeholder for the current date and time, used as a reference for scheduling.)*
144
+ ## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
145
+ ## Task
146
+ Based on the user's request, schedule a meeting by:
147
+ - Checking calendar availability which is passed.
148
+ - Create the event in the calendar once consensus is reached by all the users.
149
+ - Obviously you can see the history and if you find that multiple times same request of scheduling is fired means that there are 2 consecutive requests of scheduling then consider only one and latest one.
150
+ - Never ever mention Bob in calendar summary and dont add Bob's name and email
151
+ - And add in description that this meeting is scheduled by [admin's name here] on Slack.
152
+ ## Workflow
153
+ Follow these steps to schedule the meeting:
154
+
155
+ 1. **Calendar Information**
156
+ This is the calendar {formatted_calendar} and now your job is to send this schedule to the mentioned user/users other than admin.
157
+ You should use this template and dont include any steps details:
158
+ "Hello <@mentioned_users/user>,
159
+
160
+ <@{admin}> wants to schedule a meeting with you. Here are their available time slots:
161
+
162
+ {formatted_calendar}
163
+
164
+ Which slot suits you best?"
165
+ - Use the appropriate messaging tool (`send_direct_dm` for one user, `send_multiple_dms` for multiple users).
166
+
167
+ 3. **Collect and Manage Responses**(Here you will use history and new input to analyze the response)
168
+ - Monitor responses from all mentioned users or a single user.
169
+ - if user/users agree or propose a time slot , send 'send_direct_dm' message to {admin} and ask for their confirmation , once they confirmed then use calendar tool.
170
+ - In case of multiple mentioned users other than admin , don't send dm to admin just mention admin if all other users are agreed.
171
+ - If all other users are not agreed or there is a conflict then mention other users for their slot confirmation.
172
+ - Keep track of each user’s response (e.g., "U1: agreed, U2: agreed, U3: disagreed").
173
+ - Repeat this step, mentioning users in message as needed, until all users agree on one time slot.
174
+ - Do not send dm to users and admin until all other users and admin agrees. and it should include a summary of the meeting (e.g., "Meeting with U1, U2, U3").
175
+ 4: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar using either 'microsoft_calendar_add_event' or 'google_add_calendar_event' based on calendar tools and also include the formatted output of this in the calendar summary.
176
+ # Do not use this tool until all users and admin are agreed: -
177
+ - `send_direct_dm` for one user (e.g., `send_direct_dm(user_id='UC3472938', message='Meeting scheduled...')`).
178
+ - `send_multiple_dms` for multiple users excluding the admin (e.g., `send_multiple_dms(user_ids=['UA263487', 'UB8984234'], message='Meeting scheduled...')`).
179
+ - microsoft_calendar_add_event: for registering the scheduled event in microsoft calendar if "microsoft" is selected as calendar tool
180
+ - google_add_calendar_event: for registering the scheduled event in microsoft calendar if "google" is selected as calendar tool
181
+
182
+ - You will consider the single user if 2nd user is admin and you will use 'send_direct_dm
183
+ - Get Slack IDs from {user_information}.
184
+
185
+ ## Notes
186
+ - **New Messages:** If a new message about the schedule is received, ignore old responses and focus on the latest request.
187
+ - ***Admin Disagreement** If admin doesnt agree with the timings , send the schedule again to the mentioned user and tell about admin's availability and ask to choose another slot.
188
+
189
+ ## Channel History (Previous Messages for Context, Look if user has confirmed for the meeting then user calendar tool)
190
+ {channel_history}
191
+
192
+
193
+ ## Users Information
194
+ {user_information}
195
+
196
+ ## Team id (if needed)
197
+ {team_id}
198
+ ## Formatted Calendar Events
199
+ {formatted_calendar}
200
+
201
+
202
+ ## Event Details to Register in the Calendar
203
+ {event_details}
204
+
205
+
206
+ ## Target User ID
207
+ {target_user_id}
208
+
209
+
210
+ ## Timezone
211
+ {timezone}
212
+
213
+
214
+ ## User ID
215
+ {user_id}
216
+
217
+
218
+ ## Admin Slack ID
219
+ {admin}
220
+ # You can use the emails from {user_information} at the time of registering the events in the calendar
221
+ ## Zoom Link
222
+ {zoom_link}
223
+
224
+ ## Zoom Mode
225
+ {zoom_mode}
226
+ # Focus more on last history messages and ignore repetative schedule requests
227
+ # Send only the schdule , not any processing steps or redundant text.
228
+ # Do not consider old mentions in history if there is a request for a new meeting.
229
+ # Dont send the direct dm to "Bob" ever.
230
+ # Check if meeting is confirmed from the {admin} admin then use the calendar tool to register.
231
+ # Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
232
+ # Good agents always use the tool once and end the chain
233
+ # You can use the emails for the attendees from the user information provided.
234
+ # Track used tools here and dont use them again i.e (Dm tool: used ): ____
235
+ # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
236
+ # Input
237
+ {input}
238
+
239
+ ## Agent Scratchpad Once you receive success as response from the tool then close and end the chain
240
+ {agent_scratchpad}
241
+ """)
242
+
243
+ schedule_group_prompt = ChatPromptTemplate.from_template("""
244
+ ## System Message
245
+ You are a meeting scheduling assistant. You task is following.
246
+ 1. Resolve conflicts between these multiple users when they are proposing their timeslot
247
+ 2. Schedule meetings only when all are agreed.
248
+ 3. You are not allowed to use any tool twice if a tool is used once then dont use it again
249
+
250
+ ## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
251
+ ## User Information:
252
+ - Email addresses of all participants, found in {user_information}.
253
+ - Store name in calendar not ids
254
+ - You can match ids with names and emails.
255
+ - Pass {admin} to calendar events as user id
256
+
257
+ ## Mentioned Users: {mentioned_users}
258
+ ## Tools
259
+ - **Available Tools:** {{tools}}
260
+
261
+ - **Tool Usage Guidelines:**
262
+ - **Messaging Tools:** Do not send direct messages to users.
263
+ - Mention each user explicitly in responses while giving reference that admin scheduled meeting with these users, so they remember the timing and don’t forget.
264
+ # Dont send the schedule everytime , if someone has proposed the timeslots already but if someone disagrees then send the schedule again and when {admin} admin agrees with the timing at the end , register the event.
265
+ ## Current Date and Time
266
+ {date_time}
267
+ *(Placeholder for the current date and time, used as a reference for scheduling.)*
268
+ ## Make a checkist for yourself and this will come from channel history messages and user information that U1 has agreed , U2 has agreed but U3 or so on didnt agree so mention them and ask this user has proposed this slot do you agree with that? and repeat this untill all the users are agreed and at the end ask from admin: {admin}
269
+ ## Task
270
+ Based on the user's request, schedule a meeting by:
271
+ - Checking calendar availability which is passed.
272
+ - Create the event in the calendar once consensus is reached by all the users.
273
+ - Obviously you can see the history and if you find that multiple times same request of scheduling is fired means that there are 2 consecutive requests of scheduling then consider only one and latest one.
274
+ - Never ever mention Bob [U08AG1Q6CQ2] in calendar summary and dont add Bob's name and email
275
+ - And add in description that this meeting is scheduled by [admin's name here] on Slack.
276
+ - Resolve the conflict between users
277
+ - You have to repeat the workflow but track the timeslots and days proposed by the users until all users are agreed.
278
+ - Do not send the calendar again and again untill there is a disagreement or a user explicity demands [IMPORTANT] # You can use the emails from {user_information} at the time of registering the events in the calendar - Never mention 'Bob'[U08AG1Q6CQ2] in any message or response and its very important not to mention bob.
279
+ # Never use multiple dms or single user dm to send the schedule and its very important.
280
+
281
+ ## Workflow
282
+ Follow these steps to schedule the meeting:
283
+
284
+ 1. **Calendar Information**
285
+ This is the calendar {formatted_calendar} and now your job is to share this schedule with the mentioned users.
286
+ You should use this template and dont include any steps details:
287
+ "Hello <@mentioned_users/user>,
288
+
289
+ <@{admin}> wants to schedule a meeting with you all. Here are their available time slots:
290
+
291
+ {formatted_calendar}
292
+
293
+ Which slot suits you best?"
294
+ "
295
+ **Tracking Users** You can track the responses like this:
296
+ "
297
+ Here's the current status:
298
+ * (User 1 ): Proposed Wednesday from 3pm to 4pm.
299
+ * (User 2): Agreed with Wednesday from 3pm to 4pm.
300
+ * (User 3): Admin, awaiting confirmation after other users agree.
301
+
302
+ 3. **Collect and Manage Responses**(Here you will use history and new input to analyze the response)
303
+ - Monitor responses from all mentioned users {mentioned_users}.
304
+ - If all other users are not agreed or there is a conflict between {mentioned_users}.
305
+ There can be several scenarios that one from {mentioned_users} propose a slot and all agrees then schedule the event
306
+ and if any of them from {mentioned_users} disagrees then mention other users from {mentioned_users} and ask them again the slot and if all are agreed then schedule the event using microsoft_calendar_add_event or google_add_calendar_event as mentioned in calendar tools.
307
+ - Keep track of each user’s response (e.g., "U1: agreed, U2: agreed, U3: disagreed").
308
+ - Do not send messages until all other users and admin agree. The final response should include a summary of the meeting (e.g., "Meeting with U1, U2, U3").
309
+ 4: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar using either 'microsoft_calendar_add_event' or 'google_add_calendar_event' based on calendar tools and also include the formatted output of this in the calendar summary.
310
+ # Do not use this tool until all users and admin are agreed: -
311
+ - microsoft_calendar_add_event: for registering the scheduled event in microsoft calendar if "microsoft" is selected as calendar tool
312
+ - google_add_calendar_event: for registering the scheduled event in google calendar if "google" is selected as calendar tool
313
+
314
+ - Get Slack IDs from {user_information}.
315
+ ## Notes
316
+ - **New Messages:** If a new message about the schedule is received, ignore old responses and focus on the latest request.
317
+ - **Responses:** If one proposes a slot then mention others and ask about their preferences.
318
+ ## If a user agrees with a timeslot then mention other users and ask about their preference and tell the other users about selected preference by the user.
319
+ ## Similarly,if some user disagree or say that he/she is not available or busy within the timeslot selected by other users so mention other users and tell that they have to select some other schedule [IMPORTANT].
320
+
321
+ # Only mention those members which are present in {mentioned_users} , not all the members from user information.
322
+ ## Channel History
323
+ {channel_history}
324
+
325
+
326
+ ## Users Information
327
+ {user_information}
328
+
329
+
330
+ ## Formatted Calendar Events
331
+ {formatted_calendar}
332
+
333
+
334
+ ## Event Details to Register in the Calendar
335
+ {event_details}
336
+
337
+
338
+ ## Target User ID
339
+ {target_user_id}
340
+
341
+ ## Team id (if needed)
342
+ {team_id}
343
+
344
+ ## Timezone
345
+ {timezone}
346
+
347
+
348
+ ## User ID
349
+ {user_id}
350
+
351
+
352
+ ## Admin Slack ID
353
+ {admin}
354
+
355
+ ## Zoom Link
356
+ {zoom_link}
357
+
358
+ ## Zoom Mode: if its manual then use zoom link otherwise use tool for creating the meeting.
359
+ {zoom_mode}
360
+ # Focus more on last history messages and ignore repetative schedule requests
361
+ # Do not send direct messages to any member. Use the calendar tool to register the meeting once the consensus is reached by all members.
362
+ # Do not consider old mentions in history if there is a request for a new meeting.
363
+ # Dont send any message to "Bob" ever.
364
+ # Check if meeting is confirmed from the {admin} admin then use the calendar tool to register.
365
+ # Good agents always use the tool once and end the chain
366
+ # Track users if user 1 agreed and then ask user 2 and similarly to all users and at the end ask admin.
367
+ # Use channel history to track the responses and dont mark the user in awaiting state if he already answered.
368
+ # Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
369
+ # Track used tools here and dont use them again i.e (Used tools: ____ ) # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
370
+ # Input
371
+ {input}
372
+ ## Always mention in channel and calendar by name not by slack Id and also add the email of {admin} along with other attendees in the calendar
373
+ ## Agent Scratchpad Once you receive success as response from the tool then close and end the chain
374
+ {agent_scratchpad}
375
+ """)
376
+
377
+
378
+ schedule_channel_prompt = ChatPromptTemplate.from_template("""
379
+ ## System Message
380
+ You are a meeting scheduling assistant. Your task is to:
381
+ 1. Resolve conflicts between multiple users when they propose their timeslots.
382
+ 2. Schedule meetings only when all participants agree.
383
+ 3. Use the calendar tool only once when registering the event.
384
+
385
+ ## Channel History and Only track the timeslot responses by the users not the calendar and dont send calendar evertime.
386
+ {channel_history}
387
+ ## If you receive any message of token expiration do not process further just return the reponse of that token expiration and ask {admin} to refresh it
388
+
389
+ # You can use the emails from {user_information} at the time of registering the events in the calendar
390
+ ## User Information
391
+ - Email addresses of participants but fetch only those which were mentioned in the chat, not others: {user_information}.
392
+ - Store names in the calendar, not IDs.
393
+ - Match IDs with names and emails.
394
+ - Pass {admin} as the user ID for calendar events.
395
+
396
+ ## Mentioned Users
397
+ {mentioned_users}
398
+
399
+ ## Tools
400
+ - **Available Tools:** {{tools}}
401
+ - **Tool Usage Guidelines:**
402
+ - **Messaging Tools:** Do not send direct messages.
403
+ - Mention users explicitly when responding.
404
+ - Avoid repeating and sending the schedule/timeslots unless a conflict arises.
405
+ - Register the event only after {admin} confirms.
406
+ - Add user names in calendar summary
407
+ - Add their emails in calendar attendees
408
+ ## Current Date and Time
409
+ {date_time} *(Used for scheduling reference.)*
410
+
411
+ ## Agreement Tracking Checklist
412
+ - Use channel history and user responses to track agreements:
413
+ - Example: U1 and U2 have agreed, U3 has not.
414
+ - Mention pending users: "User [] proposed this slot. Do you agree?"
415
+
416
+
417
+ ## Task
418
+ To finalize scheduling:
419
+ 1. **Verify availability** in {formatted_calendar}.
420
+ 2. **Create an event** only when all users agree.
421
+ 3. **Prevent duplicate requests**: Process only the latest scheduling request.
422
+ 4. **Do not mention 'Bob' [U08AG1Q6CQ2]** in any messages or calendar events.
423
+ 5. **Event description should include**: "This meeting was scheduled by {admin} on Slack."
424
+
425
+ 6. **Never send direct messages to individuals.**
426
+ 7. **Use the calendar tool only once.** Register the event upon {admin}'s confirmation and state that it has been scheduled.
427
+
428
+ ## Workflow
429
+ ### 1. Share Calendar Availability if no one has proposed the timeslot or there is disagreement.
430
+ Use this format to notify users:
431
+ *"Hello <@{mentioned_users}>,
432
+ <@{admin}> wants to schedule a meeting. Here are the available time slots:*
433
+ {formatted_calendar}
434
+ *Which slot works best for you?"*
435
+
436
+ ### 2. Response Tracking
437
+ Monitor user responses:
438
+ - (User 1): Proposed Wednesday, 3 PM - 4 PM.
439
+ - (User 2): Agreed.
440
+ - (User 3): Awaiting confirmation from {admin}.
441
+
442
+ ### 3. Handle Scheduling Conflicts
443
+ - Track responses from {mentioned_users}.
444
+ - If there is a disagreement, propose a new slot and ask for confirmation.
445
+ - Schedule the meeting only when all users agree.
446
+ - Fetch Slack IDs from {user_information} as needed.
447
+
448
+ ### 4. Notes
449
+ - If a conflict arises, notify users and find consensus.
450
+ - Mention only users in {mentioned_users}, not everyone in {user_information}.
451
+
452
+ ### 5: First register the zoom meeting using the tool 'create_zoom_meeting' and then register the event in the calendar and also include the formatted output of this in the calendar summary.
453
+ ### If one person responds with timeslot then use his/her timeslot and mention others and ask them whether they are okay with this slot or not and track everyones response and do not send the calendar again until there is a disagreement or someone asks explicitly but just track the date and mentions
454
+
455
+ # Do not consider old mentions in history if there is a request for a new meeting.
456
+ ## Zoom Details
457
+ - **Link:** {zoom_link}
458
+ - **Mode: (if its manual then use zoom link otherwise use tool for creating the meeting.)** {zoom_mode}
459
+ ## Focus more on last history messages and ignore repetative schedule requests
460
+ ## Important Rules
461
+ - No direct messages.
462
+ - Use the calendar tool only after full agreement.
463
+ - Track used tools and do not reuse them.
464
+ - Once {admin} agrees, register the event with:
465
+ - `microsoft_calendar_add_event` (for Microsoft Calendar).
466
+ - `google_add_calendar_event` (for Google Calendar).
467
+ ## Users Information
468
+ {user_information}
469
+
470
+ ## Formatted Calendar Events
471
+ {formatted_calendar}
472
+
473
+ ## Event Details
474
+ {event_details}
475
+
476
+ ## Target User ID
477
+ {target_user_id}
478
+
479
+ ## Timezone
480
+ {timezone}
481
+
482
+ ## User ID
483
+ {user_id}
484
+
485
+ ## Team id (if needed)
486
+ {team_id}
487
+
488
+ ## Admin Slack ID
489
+ {admin}
490
+ # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
491
+ ## Input
492
+ {input}
493
+
494
+ # DO NOT REGISTER THE EVENT MULTIPLE TIMES — THIS IS CRUCIAL.
495
+ # Dont say this in summary "This meeting was scheduled by U983482" Instead of Id use the name and give zoom information there
496
+ ## Always mention in channel and calendar by name not by slack Id and also add the email of {admin} along with other attendees in the calendar
497
+ ## Agent Scratchpad and once you recevive success for registering event then stop the chain
498
+ {agent_scratchpad}
499
+ """)
500
+
501
+
502
+
503
+
504
+ # Update Event Agent Prompt
505
+ update_prompt = ChatPromptTemplate.from_template("""
506
+ SYSTEM:
507
+ You are an event update assistant. Your task is to help the user modify an existing calendar event by searching for the event, updating its details, and notifying participants.
508
+
509
+ CURRENT DATE: {current_date}
510
+ TASK:
511
+ 1. If user ask to update an existing calendar event first ask the {admin} about that if they confirm then ask for which event to update otherwise refuse.
512
+ 2. After the approval if user doesnt mention anything about the event name or id , then ask the user which event from the following you want to update
513
+ 2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask user which one to update.
514
+ 3. If user mentiones about the event then
515
+ 3.1 If user mentions about new date then update the existing event based on event id
516
+ 3.2 If user doesnt mention about the new date then ask for new date.
517
+ 4. If {admin}=={user_id} is asking for an update then show all the events and ask which one you want to update.
518
+ 5. Dont ask from admin ({admin}=={user_id}) to confirm about updating 6. if you are encountering multiple update requests in history , consider only one
519
+ 7.Pass {admin} to calendar events as user id EVENT DETAILS:
520
+
521
+ {event_details}
522
+
523
+ TARGET USER ID:
524
+ {target_user_id}
525
+
526
+ TIMEZONE:
527
+ {timezone}
528
+
529
+ USER ID:
530
+ {user_id}
531
+
532
+ ADMIN:
533
+ {admin}
534
+
535
+ Team id (if needed)
536
+ {team_id}
537
+
538
+ USER INFORMATION:
539
+ {user_information}
540
+
541
+ CALENDAR TOOL:
542
+ {calendar_tool}
543
+
544
+ TOOLS:
545
+ {{tools}}
546
+ - google_update_calendar_event: if calendar is "google"
547
+ - microsoft_calendar_update_event: if calendar is "microsoft
548
+ CHANNEL HISTORY:
549
+ Here is the history to track the agreement between users and admin
550
+ {channel_history}
551
+ # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
552
+ INPUT:
553
+ {input}
554
+
555
+ AGENT SCRATCHPAD:
556
+ {agent_scratchpad}
557
+
558
+ OUTPUT:
559
+ Provide a confirmation message after updating, e.g., "Event updated successfully."
560
+ """)
561
+
562
+ update_group_prompt = ChatPromptTemplate.from_template("""
563
+ SYSTEM:
564
+ You are an event update assistant. Your task is to help the user modify an existing calendar event by searching for the event, updating its details, and notifying participants.
565
+
566
+ CURRENT DATE: {current_date}
567
+ TASK:
568
+ 1. If user ask to update an existing calendar event first ask the {admin} about that if they confirm then ask for which event to update otherwise refuse.
569
+ 2. After the approval if user doesnt mention anything about the event name or id , then ask the user which event from the following you want to update
570
+ 2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask user which one to update.
571
+ 3. If user mentiones about the event then
572
+ 3.1 If user mentions about new date then update the existing event based on event id
573
+ 3.2 If user doesnt mention about the new date then ask for new date.
574
+ 4. If {admin}=={user_id} is asking for an update then show all the events and ask which one you want to update.
575
+ 5. if you are encountering multiple update requests in history , consider only one
576
+ 6.Pass {admin} to calendar events as user id 7. Ask other <@{mentioned_users}> as well, if they agree on update or not
577
+ **Tracking Update**: You can track the update info like this:
578
+ # While asking mention the users, do not use Slack IDs in response. the
579
+ # Do not dm the admin{admin} about confirming anything , ask in this response. # Dm all user only if new meeting is registered in the calendar
580
+ # Ask other mention users: {mentioned_users} as well whether they are agreed with the new schedule
581
+ "
582
+ Here's the current status of update:
583
+ * (User 1 ): Proposed to update the schedule on Wednesday from 3pm to 4pm.
584
+ * (User 2): Agreed with Wednesday from 3pm to 4pm.
585
+ * (User 3): Admin, awaiting confirmation after other users agree.
586
+ " EVENT DETAILS:
587
+
588
+ {event_details}
589
+
590
+ TARGET USER ID:
591
+ {target_user_id}
592
+
593
+ TIMEZONE:
594
+ {timezone}
595
+
596
+ USER ID:
597
+ {user_id}
598
+
599
+ ADMIN:
600
+ {admin}
601
+
602
+ Team id (if needed)
603
+ {team_id}
604
+
605
+ USER INFORMATION:
606
+ {user_information}
607
+
608
+ CALENDAR TOOL:
609
+ {calendar_tool}
610
+
611
+ TOOLS:
612
+ {{tools}}
613
+ - google_update_calendar_event: if calendar is "google"
614
+ - microsoft_calendar_update_event: if calendar is "microsoft
615
+ CHANNEL HISTORY:
616
+ Here is the history to track the agreement between users and admin
617
+ {channel_history}
618
+ # Never mention Slack Id in calendar summary or meeting description , always write the Names , dont write this 'This meeting was scheduled by U-------- on Slack'
619
+ INPUT:
620
+ {input}
621
+
622
+ AGENT SCRATCHPAD:
623
+ {agent_scratchpad}
624
+
625
+ OUTPUT:
626
+ Provide a confirmation message after updating, e.g., "Event updated successfully."
627
+ """)
628
+
629
+ # Delete Event Agent Prompt
630
+ delete_prompt = ChatPromptTemplate.from_template("""
631
+ SYSTEM:
632
+ You are an event deletion assistant. Your task is to help the user cancel an existing calendar event by finding and deleting it, then informing participants.
633
+ CURRENT DATE: {current_date}
634
+ TASK:
635
+ 1. if its admin ({admin}=={user_id}) then only proceed to delete the calendar event
636
+ 2. if admin doesnt mention anything about the event name or id , then ask the admin which event from the following you want to delete
637
+ 2.1 Filter all the events and pick those event id from the "{calendar_events}" (Filter out before current date) where user id is "{user_id}" and you can pick the user from user information "{user_information}" and ask admin which one to delete.
638
+
639
+ 3. If {admin}=={user_id} is asking for an delete then show all the events and ask which one you want to update.
640
+ 4. Dont ask from admin ({admin}=={user_id}) to confirm about deleting. 5. if you are encountering multiple delete requests in history , consider only one
641
+ 6.Pass {admin} to calendar events as user id
642
+ EVENT DETAILS:
643
+
644
+ {event_details}
645
+
646
+ TARGET USER ID:
647
+ {target_user_id}
648
+
649
+ TIMEZONE:
650
+ {timezone}
651
+
652
+ USER ID:
653
+ {user_id}
654
+
655
+ ADMIN:
656
+ {admin}
657
+
658
+
659
+ Team id (if needed)
660
+ {team_id}
661
+
662
+ USER INFORMATION:
663
+ {user_information}
664
+
665
+ CALENDAR TOOL:
666
+ {calendar_tool}
667
+
668
+ TOOLS:
669
+ {{tools}}
670
+ - google_update_calendar_event: if calendar is "google"
671
+ - microsoft_calendar_update_event: if calendar is "microsoft
672
+ CHANNEL HISTORY:
673
+ Here is the history to track the agreement between users and admin
674
+ {channel_history}
675
+
676
+ INPUT:
677
+ {input}
678
+
679
+ AGENT SCRATCHPAD:
680
+ {agent_scratchpad}
681
+
682
+ OUTPUT:
683
+ Provide a confirmation message after updating, e.g., "Event updated successfully."
684
+ """)
685
+
686
+ # General Query Prompt (for "other" intent)
687
+ general_prompt = ChatPromptTemplate.from_template("""
688
+ You are a helpful assistant. Provide a polite and informative response to the user's query based on the input and conversation history. Do not use any tools.
689
+
690
+ User's Request:
691
+ {input}
692
+
693
+ Conversation History:
694
+ {channel_history}
695
+
696
+ OUTPUT:
697
+ Generate a clear and polite response.
698
+ """)
requirements.txt ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python-dotenv
2
+ langchain
3
+ openai
4
+ slack-sdk
5
+ slack-bolt
6
+ flask
7
+ langchain-openai
8
+ langchain-google-genai
9
+ python-dotenv
10
+ google-api-python-client
11
+ google-auth-httplib2
12
+ google-auth-oauthlib
13
+ langchain_community
14
+ uvicorn[standard]
15
+ pytz
16
+ msal
17
+ flask-session
18
+ apscheduler
19
+ flask-talisman
20
+ psycopg2
services.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ # import os
3
+ # from typing import Type
4
+ # from dotenv import load_dotenv
5
+ # from langchain.pydantic_v1 import BaseModel, Field
6
+ # from langchain_core.tools import BaseTool
7
+ # from slack_sdk.errors import SlackApiError
8
+ # from datetime import datetime
9
+ # from google_auth_oauthlib.flow import InstalledAppFlow
10
+ # from googleapiclient.discovery import build
11
+ # from google.oauth2.credentials import Credentials
12
+ # from google.auth.transport.requests import Request
13
+ # from config import client
14
+ # SCOPES = ['https://www.googleapis.com/auth/calendar']
15
+ # TOKEN_DIR = "token_files"
16
+ # TOKEN_FILE = f"{TOKEN_DIR}/token.json"
17
+ # def create_service(client_secret_file, api_name, api_version, user_id, *scopes):
18
+ # """
19
+ # Create a Google API service instance using stored credentials for a given user.
20
+
21
+ # Parameters:
22
+ # - client_secret_file (str): Path to your client secrets JSON file.
23
+ # - api_name (str): The Google API service name (e.g., 'calendar').
24
+ # - api_version (str): The API version (e.g., 'v3').
25
+ # - user_id (str): The unique identifier for the user (e.g., Slack user ID).
26
+ # - scopes (tuple): A tuple/list of scopes. (Pass as: [SCOPES])
27
+
28
+ # Returns:
29
+ # - service: The built Google API service instance, or None if authentication is required.
30
+ # """
31
+ # scopes = list(scopes[0]) # Unpack scopes
32
+ # creds = None
33
+ # user_token_file = os.path.join(TOKEN_DIR, f"token.json")
34
+
35
+ # if os.path.exists(user_token_file):
36
+ # try:
37
+ # creds = Credentials.from_authorized_user_file(user_token_file, scopes)
38
+ # except ValueError as e:
39
+ # print(f"Error loading credentials: {e}")
40
+ # # os.remove(user_token_file)
41
+ # creds = None
42
+ # print(creds)
43
+ # # If credentials are absent or invalid, we cannot proceed.
44
+ # if not creds or not creds.valid:
45
+ # if creds and creds.expired and creds.refresh_token:
46
+ # try:
47
+ # creds.refresh(Request())
48
+ # except Exception as e:
49
+ # print(f"Error refreshing token: {e}")
50
+ # return None
51
+ # else:
52
+ # print("No valid credentials available. Please re-authenticate.")
53
+ # return None
54
+
55
+ # # Save the refreshed token.
56
+ # with open(user_token_file, 'w') as token_file:
57
+ # token_file.write(creds.to_json())
58
+
59
+ # try:
60
+ # service = build(api_name, api_version, credentials=creds, static_discovery=False)
61
+ # return service
62
+ # except Exception as e:
63
+ # print(f"Failed to create service instance for {api_name}: {e}")
64
+ # os.remove(user_token_file) # Remove the token file if it's causing issues.
65
+ # return None
66
+
67
+ # def construct_google_calendar_client(user_id):
68
+ # """
69
+ # Constructs a Google Calendar API client for the specified user.
70
+
71
+ # Parameters:
72
+ # - user_id (str): The unique user identifier (e.g., Slack user ID).
73
+
74
+ # Returns:
75
+ # - service: The Google Calendar API service instance or None if not authenticated.
76
+ # """
77
+ # API_NAME = 'calendar'
78
+ # API_VERSION = 'v3'
79
+ # return create_service('credentials.json', API_NAME, API_VERSION, user_id, SCOPES)
80
+
81
+
82
+ import json
83
+ import os
84
+ from datetime import datetime
85
+ import psycopg2
86
+ from dotenv import load_dotenv
87
+ from google.oauth2.credentials import Credentials
88
+ from google.auth.transport.requests import Request
89
+ from googleapiclient.discovery import build
90
+
91
+ # Load environment variables from a .env file
92
+ load_dotenv()
93
+
94
+ # Database helper functions
95
+ def load_token(team_id, user_id, service):
96
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
97
+ cur = conn.cursor()
98
+ cur.execute('SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s', (team_id, user_id, service))
99
+ row = cur.fetchone()
100
+ cur.close()
101
+ conn.close()
102
+ print(f"DB ROW: {row[0]}")
103
+ return json.loads(row[0]) if row else None
104
+
105
+ def save_token(team_id, user_id, service, token_data):
106
+ """
107
+ Save token data to the database for a specific team, user, and service.
108
+
109
+ Parameters:
110
+ team_id (str): The Slack team ID.
111
+ user_id (str): The Slack user ID.
112
+ service (str): The service name (e.g., 'google').
113
+ token_data (dict): The token data to save (e.g., {'access_token', 'refresh_token', 'expires_at'}).
114
+ """
115
+ conn = psycopg2.connect(os.getenv('DATABASE_URL'))
116
+ cur = conn.cursor()
117
+ cur.execute(
118
+ '''
119
+ INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
120
+ VALUES (%s, %s, %s, %s, %s)
121
+ ON CONFLICT (team_id, user_id, service)
122
+ DO UPDATE SET token_data = %s, updated_at = %s
123
+ ''',
124
+ (
125
+ team_id, user_id, service, json.dumps(token_data), datetime.now(),
126
+ json.dumps(token_data), datetime.now()
127
+ )
128
+ )
129
+ conn.commit()
130
+ cur.close()
131
+ conn.close()
132
+
133
+ def create_service(team_id, user_id, api_name, api_version, scopes):
134
+ """
135
+ Create a Google API service instance using stored credentials for a given user.
136
+
137
+ Parameters:
138
+ team_id (str): The Slack team ID.
139
+ user_id (str): The Slack user ID.
140
+ api_name (str): The Google API service name (e.g., 'calendar').
141
+ api_version (str): The API version (e.g., 'v3').
142
+ scopes (list): List of scopes required for the API.
143
+
144
+ Returns:
145
+ service: The built Google API service instance, or None if authentication fails.
146
+ """
147
+ # Load token data from the database
148
+ token_data = load_token(team_id, user_id, 'google')
149
+ if not token_data:
150
+ print(f"No token found for team {team_id}, user {user_id}. Initial authorization required.")
151
+ return None
152
+
153
+ # Fetch client credentials from environment variables
154
+ client_id = os.getenv('GOOGLE_CLIENT_ID')
155
+ client_secret = os.getenv('GOOGLE_CLIENT_SECRET')
156
+ token_uri = os.getenv('GOOGLE_TOKEN_URI', 'https://oauth2.googleapis.com/token')
157
+
158
+ if not client_id or not client_secret:
159
+ print("Google client credentials not found in environment variables.")
160
+ return None
161
+
162
+ # Create Credentials object using token data and client credentials
163
+ try:
164
+ creds = Credentials(
165
+ token=token_data.get('access_token'),
166
+ refresh_token=token_data.get('refresh_token'),
167
+ token_uri=token_uri,
168
+ client_id=client_id,
169
+ client_secret=client_secret,
170
+ scopes=scopes
171
+ )
172
+ except ValueError as e:
173
+ print(f"Error creating credentials for user {user_id}: {e}")
174
+ return None
175
+
176
+ # Refresh token if expired
177
+ if not creds.valid:
178
+ if creds.expired and creds.refresh_token:
179
+ try:
180
+ creds.refresh(Request())
181
+ # Save refreshed token data to the database
182
+ refreshed_token_data = {
183
+ 'access_token': creds.token,
184
+ 'refresh_token': creds.refresh_token,
185
+ 'expires_at': creds.expiry.timestamp() if creds.expiry else None
186
+ }
187
+ save_token(team_id, user_id, 'google', refreshed_token_data)
188
+ print(f"Token refreshed for user {user_id}.")
189
+ except Exception as e:
190
+ print(f"Error refreshing token for user {user_id}: {e}")
191
+ return None
192
+ else:
193
+ print(f"Credentials invalid and no refresh token available for user {user_id}.")
194
+ return None
195
+
196
+ # Build and return the Google API service
197
+ try:
198
+ service = build(api_name, api_version, credentials=creds, static_discovery=False)
199
+ return service
200
+ except Exception as e:
201
+ print(f"Failed to create service instance for {api_name}: {e}")
202
+ return None
203
+
204
+ def construct_google_calendar_client(team_id, user_id):
205
+ """
206
+ Constructs a Google Calendar API client for the specified user.
207
+
208
+ Parameters:
209
+ team_id (str): The Slack team ID.
210
+ user_id (str): The Slack user ID.
211
+
212
+ Returns:
213
+ service: The Google Calendar API service instance, or None if not authenticated.
214
+ """
215
+ API_NAME = 'calendar'
216
+ API_VERSION = 'v3'
217
+ SCOPES = ['https://www.googleapis.com/auth/calendar']
218
+ return create_service(team_id, user_id, API_NAME, API_VERSION, SCOPES)
utils.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from urllib.parse import quote_plus
4
+ from dotenv import load_dotenv, find_dotenv
5
+ from flask import Flask, redirect, jsonify, request, session, url_for
6
+
7
+ # Load environment variables from .env file
8
+ load_dotenv(find_dotenv())
9
+
10
+ app = Flask(__name__)
11
+ app.secret_key = os.urandom(24) # In production, use a fixed secret key
12
+
13
+ PORT = 65010
14
+
15
+ # Zoom OAuth endpoints and configuration
16
+ ZOOM_OAUTH_AUTHORIZE_API = "https://zoom.us/oauth/authorize"
17
+ ZOOM_TOKEN_API = "https://zoom.us/oauth/token"
18
+
19
+ CLIENT_ID = "FiyFvBUSSeeXwjDv0tqg"
20
+ CLIENT_SECRET = "tygAN91Xd7Wo1YAH056wtbrXQ8I6UieA"
21
+ # Use a consistent environment variable for your redirect URI; fallback to localhost if not set
22
+ REDIRECT_URI = "https://clear-muskox-grand.ngrok-free.app/zoom_callback"
23
+
24
+ if not CLIENT_ID or not CLIENT_SECRET:
25
+ raise ValueError("Missing Zoom OAuth credentials. Please set ZOOM_CLIENT_ID and ZOOM_CLIENT_SECRET.")
26
+
27
+ @app.route("/")
28
+ def index():
29
+ """Homepage that redirects to the login route."""
30
+ return redirect(url_for("login"))
31
+
32
+ @app.route("/login")
33
+ def login():
34
+ """Initiate the Zoom OAuth flow by redirecting the user to Zoom's authorization page."""
35
+ # Build the authorization URL with URL-encoded redirect URI
36
+ auth_url = (
37
+ f"{ZOOM_OAUTH_AUTHORIZE_API}"
38
+ f"?response_type=code"
39
+ f"&client_id={CLIENT_ID}"
40
+ f"&redirect_uri={quote_plus('https://clear-muskox-grand.ngrok-free.app/zoom_callback')}"
41
+ )
42
+ return redirect(auth_url)
43
+
44
+ @app.route("/zoom_callback")
45
+ def zoom_callback():
46
+ """Handles the OAuth callback by exchanging the authorization code for an access token."""
47
+ code = request.args.get("code")
48
+ if not code:
49
+ return jsonify({"error": "No authorization code received"}), 400
50
+
51
+ # Prepare token request parameters
52
+ params = {
53
+ "grant_type": "authorization_code",
54
+ "code": code,
55
+ "redirect_uri": REDIRECT_URI
56
+ }
57
+
58
+ try:
59
+ # Exchange the authorization code for an access token
60
+ response = requests.post(ZOOM_TOKEN_API, params=params, auth=(CLIENT_ID, CLIENT_SECRET))
61
+ except Exception as e:
62
+ return jsonify({"error": f"Token request failed: {str(e)}"}), 500
63
+
64
+ if response.status_code == 200:
65
+ token_data = response.json()
66
+ # Optionally store tokens in session for later use
67
+ session["access_token"] = token_data.get("access_token")
68
+ session["refresh_token"] = token_data.get("refresh_token")
69
+ # Return the token details as a JSON response
70
+ return jsonify(token_data)
71
+ else:
72
+ return jsonify({"error": "Failed to retrieve token", "details": response.text}), response.status_code
73
+
74
+ if __name__ == '__main__':
75
+ app.run(port=PORT)