Slack_App / app.py
ujalaarshad17's picture
first commit
93dae80 verified
import os
import secrets
import time
import json
import base64
from flask_talisman import Talisman
from flask import Flask, request, jsonify,render_template
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from slack_bolt.oauth.oauth_settings import OAuthSettings
from dotenv import find_dotenv, load_dotenv
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from flask_session import Session
from msal import ConfidentialClientApplication
import psycopg2
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime, timedelta
from collections import defaultdict
import hashlib
import re
import logging
from threading import Lock
from urllib.parse import quote_plus
from langchain.chains import LLMChain
from langchain.prompts import ChatPromptTemplate
from agents.all_agents import (
create_schedule_agent, create_update_agent, create_delete_agent, llm,
create_schedule_group_agent, create_update_group_agent, create_schedule_channel_agent
)
from all_tools import tools, calendar_prompt_tools
from db import init_db
# Load environment variables
load_dotenv(find_dotenv())
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
os.environ['OAUTHLIB_IGNORE_SCOPE_CHANGE'] = '1'
user_cache = {}
user_cache_lock = Lock() # Example threading lock for cache
preferences_cache = {}
preferences_cache_lock = Lock()
owner_id_cache = {}
owner_id_lock = Lock()
# Configuration
SLACK_CLIENT_ID = os.getenv('SLACK_CLIENT_ID','')
SLACK_CLIENT_SECRET = os.getenv('SLACK_CLIENT_SECRET','')
SLACK_SIGNING_SECRET = os.getenv('SLACK_SIGNING_SECRET','')
SLACK_SCOPES = [
"app_mentions:read",
"channels:history",
"chat:write",
"users:read",
"im:write",
"groups:write",
"mpim:write",
"commands",
"team:read",
"channels:read",
"groups:read",
"im:read",
"mpim:read",
"groups:history",
"im:history",
"mpim:history"
]
import requests
SLACK_BOT_USER_ID = os.getenv("SLACK_BOT_USER_ID")
ZOOM_REDIRECT_URI = "https://clear-muskox-grand.ngrok-free.app/zoom_callback"
CLIENT_ID = "FiyFvBUSSeeXwjDv0tqg" # Zoom Client ID
CLIENT_SECRET = "tygAN91Xd7Wo1YAH056wtbrXQ8I6UieA" # Zoom Client Secret
ZOOM_TOKEN_API = "https://zoom.us/oauth/token"
ZOOM_OAUTH_AUTHORIZE_API = os.getenv("ZOOM_OAUTH_AUTHORIZE_API", "https://zoom.us/oauth/authorize")
OAUTH_REDIRECT_URI = os.getenv("OAUTH_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/oauth2callback")
MICROSOFT_CLIENT_ID = "855e4571-d92a-4d51-802e-e712a879c00b"
MICROSOFT_CLIENT_SECRET = os.getenv("MICROSOFT_CLIENT_SECRET")
MICROSOFT_AUTHORITY = "https://login.microsoftonline.com/common"
MICROSOFT_SCOPES = ["User.Read", "Calendars.ReadWrite"]
MICROSOFT_REDIRECT_URI = os.getenv("MICROSOFT_REDIRECT_URI", "https://clear-muskox-grand.ngrok-free.app/microsoft_callback")
# Initialize Flask app
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
talisman = Talisman(
app,
content_security_policy={
'default-src': "'self'",
'script-src': "'self'",
'object-src': "'none'"
},
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
x_content_type_options=True,
referrer_policy='no-referrer-when-downgrade'
)
app.config['SESSION_TYPE'] = 'filesystem'
Session(app)
# Installation Store for OAuth
import json
import os
import psycopg2
from datetime import datetime
import json
import os
import psycopg2
from datetime import datetime
from slack_sdk.oauth import InstallationStore
# Custom JSON encoder to handle datetime objects
import json
import os
import psycopg2
from psycopg2.extras import Json
from datetime import datetime
import logging
from slack_sdk import WebClient
from slack_sdk.oauth import InstallationStore
from slack_sdk.oauth.installation_store.models.installation import Installation
from slack_bolt.authorization import AuthorizeResult
# Custom JSON encoder for datetime objects (used only if needed)
class DateTimeEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
class DatabaseInstallationStore(InstallationStore):
"""A database-backed installation store for Slack Bolt using PostgreSQL.
Assumes 'installation_data' is a jsonb column storing JSON data.
"""
def __init__(self):
self._logger = logging.getLogger(__name__)
def save(self, installation):
try:
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
workspace_id = installation.team_id
installed_at = datetime.fromtimestamp(installation.installed_at) if installation.installed_at else None
installation_data = {
"team_id": installation.team_id,
"enterprise_id": installation.enterprise_id,
"user_id": installation.user_id,
"bot_token": installation.bot_token,
"bot_id": installation.bot_id,
"bot_user_id": installation.bot_user_id,
"bot_scopes": installation.bot_scopes,
"user_token": installation.user_token,
"user_scopes": installation.user_scopes,
"incoming_webhook_url": installation.incoming_webhook_url,
"incoming_webhook_channel": installation.incoming_webhook_channel,
"incoming_webhook_channel_id": installation.incoming_webhook_channel_id,
"incoming_webhook_configuration_url": installation.incoming_webhook_configuration_url,
"app_id": installation.app_id,
"token_type": installation.token_type,
"installed_at": installed_at.isoformat() if installed_at else None
}
current_time = datetime.now()
cur.execute('''
INSERT INTO Installations (workspace_id, installation_data, updated_at)
VALUES (%s, %s, %s)
ON CONFLICT (workspace_id) DO UPDATE SET
installation_data = %s, updated_at = %s
''', (workspace_id, Json(installation_data), current_time, Json(installation_data), current_time))
conn.commit()
self._logger.info(f"Saved installation for workspace {workspace_id}")
except Exception as e:
self._logger.error(f"Failed to save installation for workspace {workspace_id}: {e}")
raise
finally:
cur.close()
conn.close()
def find_installation(self, enterprise_id=None, team_id=None, user_id=None, is_enterprise_install=False):
if not team_id:
self._logger.warning("No team_id provided for find_installation")
return None
try:
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
row = cur.fetchone()
if row:
# For jsonb, row[0] is already a dict
installation_data = row[0]
installed_at = (datetime.fromisoformat(installation_data["installed_at"])
if installation_data.get("installed_at") else None)
return Installation(
app_id=installation_data["app_id"],
enterprise_id=installation_data.get("enterprise_id"),
team_id=installation_data["team_id"],
bot_token=installation_data["bot_token"],
bot_id=installation_data["bot_id"],
bot_user_id=installation_data["bot_user_id"],
bot_scopes=installation_data["bot_scopes"],
user_id=installation_data["user_id"],
user_token=installation_data.get("user_token"),
user_scopes=installation_data.get("user_scopes"),
incoming_webhook_url=installation_data.get("incoming_webhook_url"),
incoming_webhook_channel=installation_data.get("incoming_webhook_channel"),
incoming_webhook_channel_id=installation_data.get("incoming_webhook_channel_id"),
incoming_webhook_configuration_url=installation_data.get("incoming_webhook_configuration_url"),
token_type=installation_data["token_type"],
installed_at=installed_at
)
else:
self._logger.info(f"No installation found for team_id {team_id}")
return None
except Exception as e:
self._logger.error(f"Error retrieving installation for team_id {team_id}: {e}")
return None
finally:
cur.close()
conn.close()
def find_bot(self, enterprise_id=None, team_id=None, is_enterprise_install=False):
if not team_id:
self._logger.warning("No team_id provided for find_bot")
return None
try:
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT installation_data FROM Installations WHERE workspace_id = %s', (team_id,))
row = cur.fetchone()
if row:
installation_data = row[0]
return AuthorizeResult(
enterprise_id=installation_data.get("enterprise_id"),
team_id=installation_data["team_id"],
bot_token=installation_data["bot_token"],
bot_id=installation_data["bot_id"],
bot_user_id=installation_data["bot_user_id"]
)
else:
self._logger.info(f"No bot installation found for team_id {team_id}")
return None
except Exception as e:
self._logger.error(f"Error retrieving bot for team_id {team_id}: {e}")
return None
finally:
cur.close()
conn.close()
# Instantiate the store
installation_store = DatabaseInstallationStore()
def get_client_for_team(team_id):
"""
Get a Slack WebClient for a given team ID using the stored bot token.
Args:
team_id (str): The team ID (workspace ID) to look up.
Returns:
WebClient: Slack client instance or None if not found.
"""
installation = installation_store.find_installation(None, team_id)
if installation:
token = installation.bot_token # Use dot notation instead of subscripting
return WebClient(token=token)
return None
# Initialize Slack Bolt app with OAuth settings
oauth_settings = OAuthSettings(
client_id=SLACK_CLIENT_ID,
client_secret=SLACK_CLIENT_SECRET,
scopes=SLACK_SCOPES,
redirect_uri="https://clear-muskox-grand.ngrok-free.app/slack/oauth_redirect",
installation_store=installation_store
)
bolt_app = App(signing_secret=SLACK_SIGNING_SECRET, oauth_settings=oauth_settings)
slack_handler = SlackRequestHandler(bolt_app)
# Logging setup
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Google Calendar API Scopes
SCOPES = [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile'
]
# Initialize Neon Postgres database
init_db()
# State Management Classes
class StateManager:
def __init__(self):
self._states = {}
self._lock = Lock()
def create_state(self, user_id):
with self._lock:
state_token = secrets.token_urlsafe(32)
self._states[state_token] = {"user_id": user_id, "timestamp": datetime.now(), "used": False}
return state_token
def validate_and_consume_state(self, state_token):
with self._lock:
if state_token not in self._states:
return None
state_data = self._states[state_token]
if state_data["used"] or (datetime.now() - state_data["timestamp"]).total_seconds() > 600:
del self._states[state_token]
return None
state_data["used"] = True
return state_data["user_id"]
def cleanup_expired_states(self):
with self._lock:
current_time = datetime.now()
expired = [s for s, d in self._states.items() if (current_time - d["timestamp"]).total_seconds() > 600]
for state in expired:
del self._states[state]
state_manager = StateManager()
class EventDeduplicator:
def __init__(self, expiration_minutes=5):
self.processed_events = defaultdict(list)
self.expiration_minutes = expiration_minutes
def clean_expired_events(self):
current_time = datetime.now()
for event_id in list(self.processed_events.keys()):
events = [(t, h) for t, h in self.processed_events[event_id]
if current_time - t < timedelta(minutes=self.expiration_minutes)]
if events:
self.processed_events[event_id] = events
else:
del self.processed_events[event_id]
def is_duplicate_event(self, event_payload):
self.clean_expired_events()
event_id = event_payload.get('event_id', '')
payload_hash = hashlib.md5(str(event_payload).encode('utf-8')).hexdigest()
if 'challenge' in event_payload:
return False
if event_id in self.processed_events and payload_hash in [h for _, h in self.processed_events[event_id]]:
return True
self.processed_events[event_id].append((datetime.now(), payload_hash))
return False
event_deduplicator = EventDeduplicator()
class SessionStore:
def __init__(self):
self._store = {}
self._lock = Lock()
def set(self, user_id, key, value):
with self._lock:
if user_id not in self._store:
self._store[user_id] = {}
self._store[user_id][key] = {"value": value, "expires_at": datetime.now() + timedelta(hours=1)}
def get(self, user_id, key, default=None):
with self._lock:
if user_id not in self._store or key not in self._store[user_id]:
return default
session_data = self._store[user_id][key]
if datetime.now() > session_data["expires_at"]:
del self._store[user_id][key]
return default
return session_data["value"]
def clear(self, user_id, key):
with self._lock:
if user_id in self._store and key in self._store[user_id]:
del self._store[user_id][key]
session_store = SessionStore()
def store_in_session(user_id, key_type, data):
session_store.set(user_id, key_type, data)
def get_from_session(user_id, key_type, default=None):
return session_store.get(user_id, key_type, default)
# Global Caches (per workspace)
user_cache = {} # {team_id: {user_id: user_data}}
user_cache_lock = Lock()
owner_id_cache = {} # {team_id: owner_id}
owner_id_lock = Lock()
preferences_cache = {}
preferences_cache_lock = Lock()
# Database Helper Functions
def save_preference(team_id, user_id, zoom_config=None, calendar_tool=None):
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
existing = cur.fetchone()
if existing:
current_zoom_config, current_calendar_tool = existing
new_zoom_config = zoom_config if zoom_config is not None else current_zoom_config
new_calendar_tool = calendar_tool if calendar_tool is not None else current_calendar_tool
cur.execute('''
UPDATE Preferences
SET zoom_config = %s, calendar_tool = %s, updated_at = %s
WHERE team_id = %s AND user_id = %s
''', (json.dumps(new_zoom_config) if new_zoom_config else None,
new_calendar_tool, datetime.now(), team_id, user_id))
else:
new_zoom_config = zoom_config or {"mode": "manual", "link": None}
new_calendar_tool = calendar_tool or "google"
cur.execute('''
INSERT INTO Preferences (team_id, user_id, zoom_config, calendar_tool, updated_at)
VALUES (%s, %s, %s, %s, %s)
''', (team_id, user_id, json.dumps(new_zoom_config), new_calendar_tool, datetime.now()))
conn.commit()
cur.close()
conn.close()
with preferences_cache_lock:
preferences_cache[(team_id, user_id)] = {"zoom_config": new_zoom_config, "calendar_tool": new_calendar_tool}
def load_preferences(team_id, user_id):
with preferences_cache_lock:
if (team_id, user_id) in preferences_cache:
return preferences_cache[(team_id, user_id)]
try:
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT zoom_config, calendar_tool FROM Preferences WHERE team_id = %s AND user_id = %s', (team_id, user_id))
row = cur.fetchone()
if row:
zoom_config, calendar_tool = row
# For jsonb, zoom_config is already a dict; no json.loads needed
preferences = {
"zoom_config": zoom_config if zoom_config else {"mode": "manual", "link": None},
"calendar_tool": calendar_tool or "none"
}
else:
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
cur.close()
conn.close()
except Exception as e:
logger.error(f"Failed to load preferences for team {team_id}, user {user_id}: {e}")
preferences = {"zoom_config": {"mode": "manual", "link": None}, "calendar_tool": "none"}
with preferences_cache_lock:
preferences_cache[(team_id, user_id)] = preferences
return preferences
def save_token(team_id, user_id, service, token_data):
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('''
INSERT INTO Tokens (team_id, user_id, service, token_data, updated_at)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (team_id, user_id, service) DO UPDATE SET token_data = %s, updated_at = %s
''', (team_id, user_id, service, json.dumps(token_data), datetime.now(), json.dumps(token_data), datetime.now()))
conn.commit()
cur.close()
conn.close()
def load_token(team_id, user_id, service):
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT token_data FROM Tokens WHERE team_id = %s AND user_id = %s AND service = %s', (team_id, user_id, service))
row = cur.fetchone()
cur.close()
conn.close()
return row[0] if row else None
# Utility Functions
def initialize_workspace_cache(client, team_id):
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute('SELECT MAX(last_updated) FROM Users WHERE team_id = %s', (team_id,))
last_updated_row = cur.fetchone()
last_updated = last_updated_row[0] if last_updated_row and last_updated_row[0] else None
# Check if cache is fresh (e.g., less than 24 hours old)
if last_updated and (datetime.now() - last_updated).total_seconds() < 86400:
cur.execute('SELECT user_id, real_name, email, name, is_owner, workspace_name FROM Users WHERE team_id = %s', (team_id,))
rows = cur.fetchall()
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}
with user_cache_lock:
user_cache[team_id] = new_cache
with owner_id_lock:
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
else:
# Fetch user data from Slack and update database
response = client.users_list()
users = response["members"]
workspace_name = client.team_info()["team"]["name"] # Get workspace name from Slack API
new_cache = {}
for user in users:
user_id = user['id']
profile = user.get('profile', {})
real_name = profile.get('real_name', 'Unknown')
name = user.get('name', '')
email = f"{name}@gmail.com" # Placeholder; adjust as needed
is_owner = user.get('is_owner', False)
new_cache[user_id] = {"real_name": real_name, "email": email, "name": name, "is_owner": is_owner, "workspace_name": workspace_name}
cur.execute('''
INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (team_id, user_id) DO UPDATE SET
workspace_name = %s, real_name = %s, email = %s, name = %s, is_owner = %s, last_updated = %s
''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now(),
workspace_name, real_name, email, name, is_owner, datetime.now()))
conn.commit()
with user_cache_lock:
user_cache[team_id] = new_cache
with owner_id_lock:
owner_id_cache[team_id] = next((user_id for user_id, data in new_cache.items() if data['is_owner']), None)
cur.close()
conn.close()
def get_all_users(team_id):
with user_cache_lock:
if team_id in user_cache:
return {k: {"Slack Id": k, "real_name": v["real_name"], "email": v["email"], "name": v["name"]}
for k, v in user_cache[team_id].items()}
return {}
def get_workspace_owner_id(client, team_id):
with owner_id_lock:
if team_id in owner_id_cache and owner_id_cache[team_id]:
return owner_id_cache[team_id]
initialize_workspace_cache(client, team_id)
with owner_id_lock:
return owner_id_cache.get(team_id)
def get_channel_owner_id(client, channel_id):
try:
response = client.conversations_info(channel=channel_id)
return response["channel"].get("creator")
except SlackApiError as e:
logger.error(f"Error fetching channel info: {e.response['error']}")
return None
def get_user_timezone(client, user_id):
try:
response = client.users_info(user=user_id)
return response["user"].get("tz", "UTC")
except SlackApiError as e:
logger.error(f"Timezone error: {e.response['error']}")
return "UTC"
def get_team_id_from_owner_id(owner_id):
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
cur.execute("SELECT workspace_id FROM Installations WHERE installation_data->>'user_id' = %s", (owner_id,))
row = cur.fetchone()
cur.close()
conn.close()
return row[0] if row else None
# def get_client_for_team(team_id):
# installation = installation_store.find_installation(None, team_id)
# if installation:
# print(installation)
# token = installation['bot_token']
# return WebClient(token=token)
# return None
def get_owner_selected_calendar(client, team_id):
owner_id = get_workspace_owner_id(client, team_id)
if not owner_id:
return None
# Fixed: Pass both team_id and owner_id to load_preferences
prefs = load_preferences(team_id, owner_id)
return prefs.get("calendar_tool", "none")
def get_zoom_link(client, team_id):
owner_id = get_workspace_owner_id(client, team_id)
if not owner_id:
return None
prefs = load_preferences(team_id,owner_id)
return prefs.get('zoom_config', {}).get('link')
def create_home_tab(client, team_id, user_id):
logger.info(f"Creating home tab for user {user_id}, team {team_id}")
# Get workspace owner ID
workspace_owner_id = get_workspace_owner_id(client, team_id)
if not workspace_owner_id:
logger.warning(f"No workspace owner for team {team_id}")
blocks = [
{"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}},
{"type": "section", "text": {"type": "mrkdwn", "text": "Unable to determine workspace owner. Please contact support."}},
]
return {"type": "home", "blocks": blocks}
# Determine if the user is the workspace owner
is_owner = user_id == workspace_owner_id
# Base blocks for all users
blocks = [
{"type": "header", "text": {"type": "plain_text", "text": "🤖 Welcome to AI Assistant!", "emoji": True}}
]
# Non-owner view
if not is_owner:
blocks.extend([
{"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Please wait for the workspace owner to configure the settings."}},
{"type": "section", "text": {"type": "mrkdwn", "text": "Only the workspace owner can configure the calendar and Zoom settings."}}
])
return {"type": "home", "blocks": blocks}
# Owner view: Add configuration options
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "I help manage schedules and meetings! Your settings are below."}})
blocks.append({"type": "divider"})
# Load preferences and tokens
prefs = load_preferences(team_id, workspace_owner_id)
selected_provider = prefs.get("calendar_tool", "none")
zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
mode = zoom_config["mode"]
calendar_token = load_token(team_id, workspace_owner_id, selected_provider) if selected_provider != "none" else None
zoom_token = load_token(team_id, workspace_owner_id, "zoom") if mode == "automatic" else None
logger.info(f"Preferences loaded: {prefs}, Calendar token: {calendar_token}, Zoom token: {zoom_token}")
# Check Zoom token expiration
zoom_token_expired = False
if zoom_token and mode == "automatic":
expires_at = zoom_token.get("expires_at", 0)
current_time = time.time()
zoom_token_expired = current_time >= expires_at
# Configuration status
calendar_provider_set = selected_provider != "none"
calendar_configured = calendar_token is not None if calendar_provider_set else False
zoom_configured = (zoom_token is not None and not zoom_token_expired) if mode == "automatic" else True
# Setup prompt if configurations are incomplete
if not calendar_provider_set or not calendar_configured or not zoom_configured:
prompt_text = "To start using the app, please complete the following setups:"
if not calendar_provider_set:
prompt_text += "\n- Select a calendar provider."
if calendar_provider_set and not calendar_configured:
prompt_text += f"\n- Configure your {selected_provider.capitalize()} calendar."
if mode == "automatic" and not zoom_configured:
if zoom_token_expired:
prompt_text += "\n- Your Zoom token has expired. Please refresh it."
else:
prompt_text += "\n- Authenticate with Zoom for automatic mode."
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": prompt_text}})
# Calendar Configuration Section
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": "*🗓️ Calendar Configuration*"}})
blocks.append({
"type": "section",
"block_id": "calendar_provider_block",
"text": {"type": "mrkdwn", "text": "Select your calendar provider:"},
"accessory": {
"type": "static_select",
"action_id": "calendar_provider_dropdown",
"placeholder": {"type": "plain_text", "text": "Select provider"},
"options": [
{"text": {"type": "plain_text", "text": "Select calendar"}, "value": "none"},
{"text": {"type": "plain_text", "text": "Google Calendar"}, "value": "google"},
{"text": {"type": "plain_text", "text": "Microsoft Calendar"}, "value": "microsoft"}
],
"initial_option": {
"text": {"type": "plain_text", "text": "Select calendar" if selected_provider == "none" else
"Google Calendar" if selected_provider == "google" else "Microsoft Calendar"},
"value": selected_provider
}
}
})
# Calendar configuration prompts
if selected_provider == "none":
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": "Please select a calendar provider to begin configuration."}]})
elif not calendar_configured:
blocks.append({"type": "context", "elements": [{"type": "mrkdwn", "text": f"Please configure your {selected_provider.capitalize()} calendar."}]})
# Calendar configure button and status
if selected_provider != "none":
status = "⚠️ Not Configured" if not calendar_configured else (
f":white_check_mark: Connected ({calendar_token.get('google_email', 'unknown')})" if selected_provider == "google" else (
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"
)
)
blocks.extend([
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": f"✨ Configure {selected_provider.capitalize()}" if not calendar_configured else f"✅ Reconfigure {selected_provider.capitalize()}",
"emoji": True
},
"action_id": "configure_gcal" if selected_provider == "google" else "configure_mscal"
}
]
},
{"type": "context", "elements": [{"type": "mrkdwn", "text": status}]}
])
# Zoom Configuration Section
status = ("⌛ Token Expired" if zoom_token_expired else
"⚠️ Not Configured" if mode == "automatic" and not zoom_configured else
"✅ Configured")
blocks.extend([
{"type": "divider"},
{"type": "section", "text": {"type": "mrkdwn", "text": f"*🔗 Zoom Configuration*\nCurrent mode: {mode}\n{status}"}},
{
"type": "actions",
"elements": [
{"type": "button", "text": {"type": "plain_text", "text": "Configure Zoom Settings", "emoji": True}, "action_id": "open_zoom_config_modal"}
]
}
])
# Zoom authentication/refresh button
if mode == "automatic":
if not zoom_configured and not zoom_token_expired:
blocks[-1]["elements"].append({
"type": "button",
"text": {"type": "plain_text", "text": "Authenticate with Zoom", "emoji": True},
"action_id": "configure_zoom"
})
elif zoom_token_expired:
blocks[-1]["elements"].append({
"type": "button",
"text": {"type": "plain_text", "text": "Refresh Zoom Token", "emoji": True},
"action_id": "configure_zoom" # Same action_id for refresh
})
return {"type": "home", "blocks": blocks}
# Intent Classification
intent_prompt = ChatPromptTemplate.from_template("""
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.
- By looking at the history if someone is confirming or denying the schedule , also categorize it as a "schedule meeting"
- If someone is asking about update the schedule then its "update event"
- If someone is asking about delete the schedule then its "delete event"
Conversation History:
{history}
User's Message:
{input}
""")
from prompt import calender_prompt, general_prompt
intent_chain = LLMChain(llm=llm, prompt=intent_prompt)
mentioned_users_prompt = ChatPromptTemplate.from_template("""
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").
- Do not give 'Bob'<@{bob_id}> in mentions
- Exclude the {bob_id}.
# 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.
All users in the channel:
{user_information}
Format: Slack Id: U3234234 , Name: Alice , Email: alice@gmail.com (map slack ids to the names)
Chat history:
{chat_history}
# Only output the users which are mentioned not all the users from the user-information.
# Only see the latest message for mention information ignore previous ones.
Please output the user slack IDs of the mentioned users , their names and emails . If no users are mentioned, output "None".
CURRENT_INPUT: {current_input}
Example: [[SlackId1 , Name1 , Email@gmal.com], [SlackId2, Name2, Email@gmail.com]...]
""")
mentioned_users_chain = LLMChain(llm=llm, prompt=mentioned_users_prompt)
# Slack Event Handlers
@bolt_app.event("app_home_opened")
def handle_app_home_opened(event, client, context):
user_id = event.get("user")
team_id = context['team_id']
if not user_id:
return
try:
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
except Exception as e:
logger.error(f"Error publishing home tab: {e}")
@bolt_app.action("calendar_provider_dropdown")
def handle_calendar_provider(ack, body, client, logger):
ack()
selected_provider = body["actions"][0]["selected_option"]["value"]
user_id = body["user"]["id"]
team_id = body["team"]["id"]
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
return
# Corrected line: pass both team_id and owner_id (user_id) parameters
save_preference(team_id, owner_id, calendar_tool=selected_provider)
client.views_publish(user_id=owner_id, view=create_home_tab(client, team_id, owner_id))
if selected_provider != "none":
client.chat_postMessage(channel=owner_id, text=f"Calendar provider updated to {selected_provider.capitalize()}.")
else:
client.chat_postMessage(channel=owner_id, text="Calendar provider reset.")
logger.info(f"Calendar provider updated to {selected_provider} for owner {owner_id}")
@bolt_app.action("configure_gcal")
def handle_gcal_config(ack, body, client, logger):
ack()
user_id = body["user"]["id"]
team_id = body["team"]["id"]
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
return
# Generate and store the state using StateManager
state = state_manager.create_state(owner_id)
print(f"state stored: {state}")
store_in_session(owner_id, "gcal_state", state) # Optional: for additional validation
# Set up the OAuth flow and pass the state
flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
auth_url, _ = flow.authorization_url(
access_type='offline',
prompt='consent',
include_granted_scopes='true',
state=state # Use the state from StateManager
)
# Open the modal with the auth URL
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"title": {"type": "plain_text", "text": "Google Calendar Auth"},
"close": {"type": "plain_text", "text": "Cancel"},
"blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": "Click below to connect Google Calendar:"}},
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Google Calendar"}, "url": auth_url, "action_id": "launch_auth"}]}
]
}
)
except Exception as e:
logger.error(f"Error opening modal: {e}")
@bolt_app.action("configure_mscal")
def handle_mscal_config(ack, body, client, logger):
ack()
user_id = body["user"]["id"]
team_id = body["team"]["id"]
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure the calendar.")
return
msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
state = state_manager.create_state(owner_id)
auth_url = msal_app.get_authorization_request_url(scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI, state=state)
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"title": {"type": "plain_text", "text": "Microsoft Calendar Auth"},
"close": {"type": "plain_text", "text": "Close"},
"blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": "Click below to authenticate with Microsoft:"}},
{"type": "actions", "elements": [{"type": "button", "text": {"type": "plain_text", "text": "Connect Microsoft Calendar"}, "url": auth_url, "action_id": "ms_auth_button"}]}
]
}
)
except Exception as e:
logger.error(f"Error opening Microsoft auth modal: {e}")
@bolt_app.event("app_mention")
def handle_mentions(event, say, client, context):
if event_deduplicator.is_duplicate_event(event):
logger.info("Duplicate event detected, skipping processing")
return
# Ignore messages from bots
if event.get("bot_id"):
logger.info("Ignoring message from bot")
return
user_id = event.get("user")
channel_id = event.get("channel")
text = event.get("text", "").strip()
thread_ts = event.get("thread_ts")
team_id = context['team_id']
calendar_tool = get_owner_selected_calendar(client, team_id)
if not calendar_tool or calendar_tool == "none":
say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
return
# Fetch bot_user_id dynamically from installation
installation = installation_store.find_installation(team_id=team_id)
if not installation or not installation.bot_user_id:
logger.error(f"No bot_user_id found for team {team_id}")
say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
return
print(f"App mention events")
bot_user_id = installation.bot_user_id
print(f"Bot user id: {bot_user_id}")
mention = f"<@{bot_user_id}>"
mentions = list(set(re.findall(r'<@(\w+)>', text)))
if bot_user_id in mentions:
mentions.remove(bot_user_id)
text = text.replace(mention, "").strip()
workspace_owner_id = get_workspace_owner_id(client, team_id)
timezone = get_user_timezone(client, user_id)
zoom_link = get_zoom_link(client, team_id)
zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
channel_history = format_channel_history(channel_history)
intent = intent_chain.run({"history": channel_history, "input": text})
relevant_user_ids = get_relevant_user_ids(client, channel_id)
all_users = get_all_users(team_id)
relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
for uid in relevant_user_ids}
user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
for uid, info in relevant_users.items() if uid != bot_user_id])
print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
import pytz
pst = pytz.timezone('America/Los_Angeles')
current_time_pst = datetime.now(pst)
formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
if calendar_tool == "google":
calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
update_tools = [tools[i] for i in [0, 7, 12]]
delete_tools = [tools[i] for i in [0, 8, 12]]
elif calendar_tool == "microsoft":
calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
update_tools = [tools[i] for i in [0, 10, 12]]
delete_tools = [tools[i] for i in [0, 11, 12]]
else:
say("Invalid calendar tool configured.", thread_ts=thread_ts)
return
calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
print(f"MENTIONED USERS:{mentioned_users_output}")
agent_input = {
'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
'event_details': str(event),
'target_user_id': user_id,
'timezone': timezone,
'user_id': user_id,
'admin': workspace_owner_id,
'zoom_link': zoom_link,
'zoom_mode': zoom_mode,
'channel_history': channel_history,
'user_information': user_information,
'calendar_tool': calendar_tool,
'date_time': formatted_time,
'formatted_calendar': output,
'team_id': team_id
}
mentions = list(set(re.findall(r'<@(\w+)>', text)))
if bot_user_id in mentions:
mentions.remove(bot_user_id)
schedule_group_exec = create_schedule_channel_agent(schedule_tools)
update_group_exec = create_update_group_agent(update_tools)
delete_exec = create_delete_agent(delete_tools)
if intent == "schedule meeting":
group_agent_input = agent_input.copy()
group_agent_input['mentioned_users'] = mentioned_users_output
response = schedule_group_exec.invoke(group_agent_input)
say(response['output'])
return
elif intent == "update event":
group_agent_input = agent_input.copy()
group_agent_input['mentioned_users'] = mentioned_users_output
response = update_group_exec.invoke(group_agent_input)
say(response['output'])
return
elif intent == "delete event":
response = delete_exec.invoke(agent_input)
say(response['output'])
return
elif intent == "other":
response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
say(response)
return
else:
say("I'm not sure how to handle that request.")
# @bolt_app.event("app_mention")
# def handle_mentions(event, say, client, context):
# if event_deduplicator.is_duplicate_event(event):
# logger.info("Duplicate event detected, skipping processing")
# return
# # Ignore messages from bots
# if event.get("bot_id"):
# logger.info("Ignoring message from bot")
# return
# user_id = event.get("user")
# channel_id = event.get("channel")
# text = event.get("text", "").strip()
# thread_ts = event.get("thread_ts")
# team_id = context['team_id']
# calendar_tool = get_owner_selected_calendar(client, team_id)
# if not calendar_tool or calendar_tool == "none":
# say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
# return
# # Fetch bot_user_id dynamically from installation
# installation = installation_store.find_installation(team_id=team_id)
# if not installation or not installation.bot_user_id:
# logger.error(f"No bot_user_id found for team {team_id}")
# say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
# return
# print(f"App mention events")
# bot_user_id = installation.bot_user_id
# print(f"Bot user id: {bot_user_id}")
# mention = f"<@{bot_user_id}>"
# mentions = list(set(re.findall(r'<@(\w+)>', text)))
# # Use dynamic bot_user_id instead of SLACK_BOT_USER_ID
# if bot_user_id in mentions:
# mentions.remove(bot_user_id)
# text = text.replace(mention, "").strip()
# workspace_owner_id = get_workspace_owner_id(client, team_id)
# timezone = get_user_timezone(client, user_id)
# zoom_link = get_zoom_link(client, team_id)
# zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
# channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
# channel_history = format_channel_history(channel_history)
# intent = intent_chain.run({"history": channel_history, "input": text})
# relevant_user_ids = get_relevant_user_ids(client, channel_id)
# all_users = get_all_users(team_id)
# relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
# for uid in relevant_user_ids}
# user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
# for uid, info in relevant_users.items() if uid != bot_user_id])
# print(f"User Information: {user_information}\n\nRelevant Users: {relevant_user_ids}\n\n All users: {all_users}")
# mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history,"current_input":text, 'bob_id':bot_user_id})
# import pytz
# pst = pytz.timezone('America/Los_Angeles')
# current_time_pst = datetime.now(pst)
# formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
# from all_tools import GoogleCalendarEvents, MicrosoftListCalendarEvents
# if calendar_tool == "google":
# calendar_events = GoogleCalendarEvents()._run(team_id, workspace_owner_id)
# schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
# update_tools = [tools[i] for i in [0, 7, 12]]
# delete_tools = [tools[i] for i in [0, 8, 12]]
# elif calendar_tool == "microsoft":
# calendar_events = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id)
# schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
# update_tools = [tools[i] for i in [0, 10, 12]]
# delete_tools = [tools[i] for i in [0, 11, 12]]
# else:
# say("Invalid calendar tool configured.", thread_ts=thread_ts)
# return
# calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
# output = calendar_formatting_chain.run({'input': calendar_events, 'admin_id': workspace_owner_id, 'date_time': formatted_time})
# print(f"MENTIONED USERS:{mentioned_users_output}")
# agent_input = {
# 'input': text,
# 'event_details': str(event),
# 'target_user_id': user_id,
# 'timezone': timezone,
# 'user_id': user_id,
# 'admin': workspace_owner_id,
# 'zoom_link': zoom_link,
# 'zoom_mode': zoom_mode,
# 'channel_history': channel_history,
# 'user_information': mentioned_users_output,
# 'calendar_tool': calendar_tool,
# 'date_time': formatted_time,
# 'formatted_calendar': output,
# 'team_id': team_id # Added
# }
# mentions = list(set(re.findall(r'<@(\w+)>', text)))
# if bot_user_id in mentions:
# mentions.remove(bot_user_id)
# schedule_group_exec = create_schedule_channel_agent(schedule_tools)
# update_group_exec = create_update_group_agent(update_tools)
# delete_exec = create_delete_agent(delete_tools)
# if intent == "schedule meeting":
# group_agent_input = agent_input.copy()
# group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
# response = schedule_group_exec.invoke(group_agent_input)
# say(response['output'])
# return
# elif intent == "update event":
# group_agent_input = agent_input.copy()
# group_agent_input['mentioned_users'] = "See from the history except 'Bob'"
# response = update_group_exec.invoke(group_agent_input)
# say(response['output'])
# return
# elif intent == "delete event":
# response = delete_exec.invoke(agent_input)
# say(response['output'])
# return
# elif intent == "other":
# response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
# say(response)
# return
# else:
# say("I'm not sure how to handle that request.")
def format_channel_history(raw_history):
cleaned_history = []
for msg in raw_history:
if 'bot_id' in msg and 'Calendar provider updated' in msg.get('text', ''):
continue
sender = msg.get('user', 'Unknown') if 'bot_id' not in msg else msg.get('bot_profile', {}).get('name', 'Bot')
message_text = msg.get('text', '').strip()
timestamp = float(msg.get('ts', 0))
readable_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %I:%M %p')
user_id = msg.get('user', 'N/A')
team_id = msg.get('team', 'N/A')
cleaned_history.append({
'message': message_text,
'from': sender,
'timestamp': readable_time,
'user_team': f"{user_id}/{team_id}"
})
formatted_output = ""
for i, entry in enumerate(cleaned_history, 1):
formatted_output += f"Message {i}: {entry['message']}\nFrom: {entry['from']}\nTimestamp: {entry['timestamp']}\nUserId/TeamId: {entry['user_team']}\n\n"
return formatted_output.strip()
def get_relevant_user_ids(client, channel_id):
try:
members = []
cursor = None
while True:
response = client.conversations_members(channel=channel_id, limit=10, cursor=cursor)
if not response["ok"]:
logger.error(f"Failed to get members for channel {channel_id}: {response['error']}")
break
members.extend(response["members"])
cursor = response.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
return members
except SlackApiError as e:
logger.error(f"Error getting conversation members: {e}")
return []
calendar_formatting_chain = LLMChain(llm=llm, prompt=calender_prompt)
@bolt_app.event("message")
def handle_messages(body, say, client, context):
if event_deduplicator.is_duplicate_event(body):
logger.info("Duplicate event detected, skipping processing")
return
event = body.get("event", {})
if event.get("bot_id"):
logger.info("Ignoring message from bot")
return
user_id = event.get("user")
text = event.get("text", "").strip()
channel_id = event.get("channel")
thread_ts = event.get("thread_ts")
team_id = context['team_id']
calendar_tool = get_owner_selected_calendar(client, team_id)
channel_info = client.conversations_info(channel=channel_id)
channel = channel_info["channel"]
# Fetch bot_user_id dynamically from installation
installation = installation_store.find_installation(team_id=team_id)
if not installation or not installation.bot_user_id:
logger.error(f"No bot_user_id found for team {team_id}")
say("Error: Could not determine bot user ID.", thread_ts=thread_ts)
return
bot_user_id = installation.bot_user_id
if not channel.get("is_im") and f"<@{bot_user_id}>" in text:
return
if not channel.get("is_im") and "thread_ts" not in event:
return
if not calendar_tool or calendar_tool == "none":
say("The workspace owner has not configured a calendar yet.", thread_ts=thread_ts)
return
print(f"Message events")
workspace_owner_id = get_workspace_owner_id(client, team_id)
is_owner = user_id == workspace_owner_id
timezone = get_user_timezone(client, user_id)
zoom_link = get_zoom_link(client, team_id)
zoom_mode = load_preferences(team_id, workspace_owner_id).get("zoom_config", {}).get("mode", "manual")
channel_history = client.conversations_history(channel=channel_id, limit=2).get("messages", [])
channel_history = format_channel_history(channel_history)
intent = intent_chain.run({"history": channel_history, "input": text})
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:
admin_dm = client.conversations_open(users=workspace_owner_id)
prompt = ChatPromptTemplate.from_template("""
You have this text: {text} and your job is to mention @{workspace_owner_id} and say following in 2 scenarios:
if message history confirms about scheduling meeting then format below text and return only that response with no other explanation
"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."
else:
Format the text : {text}
MESSAGE HISTORY: {channel_history}
""")
response = LLMChain(llm=llm, prompt=prompt)
client.chat_postMessage(channel=admin_dm["channel"]["id"],
text=response.run({'text': text, 'workspace_owner_id': workspace_owner_id, 'user_id': user_id, 'channel_history': channel_history}))
say(f"<@{user_id}> I've notified the workspace owner about your meeting request.", thread_ts=thread_ts)
return
mentions = list(set(re.findall(r'<@(\w+)>', text)))
if bot_user_id in mentions:
mentions.remove(bot_user_id)
import pytz
pst = pytz.timezone('America/Los_Angeles')
current_time_pst = datetime.now(pst)
formatted_time = current_time_pst.strftime("%Y-%m-%d | %A | %I:%M %p | %Z")
from all_tools import MicrosoftListCalendarEvents, GoogleCalendarEvents
if calendar_tool == "google":
schedule_tools = [tools[i] for i in [0, 1, 4, 6, 12]]
update_tools = [tools[i] for i in [0, 7, 12]]
delete_tools = [tools[i] for i in [0, 8, 12]]
output = calendar_formatting_chain.run({'input': GoogleCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
elif calendar_tool == "microsoft":
schedule_tools = [tools[i] for i in [0, 1, 9, 12]]
update_tools = [tools[i] for i in [0, 10, 12]]
delete_tools = [tools[i] for i in [0, 11, 12]]
output = calendar_formatting_chain.run({'input': MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id), 'admin_id': workspace_owner_id, 'date_time': formatted_time})
else:
say("Invalid calendar tool configured.", thread_ts=thread_ts)
return
relevant_user_ids = get_relevant_user_ids(client, channel_id)
all_users = get_all_users(team_id)
relevant_users = {uid: all_users.get(uid, {"real_name": "Unknown", "email": "unknown@example.com", "name": "Unknown"})
for uid in relevant_user_ids}
user_information = "\n".join([f"{uid}: Name={info['real_name']}, Email={info['email']}, Slack Name={info['name']}"
for uid, info in relevant_users.items() if uid != bot_user_id])
print(f"All users: {all_users}\n\n Relevant users: {relevant_user_ids}")
mentioned_users_output = mentioned_users_chain.run({"user_information": user_information, "chat_history": channel_history, "current_input": text, 'bob_id': bot_user_id})
schedule_exec = create_schedule_agent(schedule_tools)
update_exec = create_update_agent(update_tools)
delete_exec = create_delete_agent(delete_tools)
schedule_group_exec = create_schedule_group_agent(schedule_tools)
update_group_exec = create_update_group_agent(update_tools)
print(f"MENTIONED USERS:{mentioned_users_output}")
channel_type = channel.get("is_group", False) or channel.get("is_mpim", False)
agent_input = {
'input': f"Here is the input by user: {text} and do not mention <@{bot_user_id}> even tho mentioned in history",
'event_details': str(event),
'target_user_id': user_id,
'timezone': timezone,
'user_id': user_id,
'admin': workspace_owner_id,
'zoom_link': zoom_link,
'zoom_mode': zoom_mode,
'channel_history': channel_history,
'user_information': user_information,
'calendar_tool': calendar_tool,
'date_time': formatted_time,
'formatted_calendar': output,
'team_id': team_id
}
if intent == "schedule meeting":
if not channel_type and len(mentions) > 1:
mentions.append(user_id)
dm_channel_id, error = open_group_dm(client, mentions)
if dm_channel_id:
group_agent_input = agent_input.copy()
group_agent_input['mentioned_users'] = mentioned_users_output
group_agent_input['channel_history'] = channel_history
group_agent_input['formatted_calendar'] = output
response = schedule_group_exec.invoke(group_agent_input)
client.chat_postMessage(channel=dm_channel_id, text=f"Group conversation started by <@{user_id}>\n\n{response['output']}")
elif error:
say(f"Sorry, I couldn't create the group conversation: {error}", thread_ts=thread_ts)
else:
if channel_type or 'thread_ts' in event:
group_agent_input = agent_input.copy()
if 'thread_ts' in event:
schedule_group_exec = create_schedule_channel_agent(schedule_tools)
history_response = client.conversations_replies(channel=channel_id, ts=thread_ts, limit=2)
channel_history = format_channel_history(history_response.get("messages", []))
else:
channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=3).get("messages", []))
group_agent_input['mentioned_users'] = mentioned_users_output
group_agent_input['channel_history'] = channel_history
group_agent_input['formatted_calendar'] = output
response = schedule_group_exec.invoke(group_agent_input)
say(response['output'], thread_ts=thread_ts)
return
response = schedule_exec.invoke(agent_input)
say(response['output'], thread_ts=thread_ts)
elif intent == "update event":
agent_input['current_date'] = formatted_time
agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
if channel_type or 'thread_ts' in event:
group_agent_input = agent_input.copy()
channel_history = format_channel_history(client.conversations_history(channel=channel_id, limit=2).get("messages", []))
group_agent_input['mentioned_users'] = mentioned_users_output
group_agent_input['channel_history'] = channel_history
group_agent_input['formatted_calendar'] = output
response = update_group_exec.invoke(group_agent_input)
say(response['output'], thread_ts=thread_ts)
return
response = update_exec.invoke(agent_input)
say(response['output'], thread_ts=thread_ts)
elif intent == "delete event":
agent_input['current_date'] = formatted_time
agent_input['calendar_events'] = MicrosoftListCalendarEvents()._run(team_id, workspace_owner_id) if calendar_tool == "microsoft" else GoogleCalendarEvents()._run(team_id, workspace_owner_id)
response = delete_exec.invoke(agent_input)
say(response['output'], thread_ts=thread_ts)
elif intent == "other":
response = llm.predict(general_prompt.format(input=text, channel_history=channel_history))
say(response, thread_ts=thread_ts)
else:
say("I'm not sure how to handle that request.", thread_ts=thread_ts)
@bolt_app.event("team_join")
def handle_team_join(event, client, context, logger):
try:
user_info = event['user']
team_id = context.team_id
# Fetch workspace name from Slack API
try:
team_info = client.team_info()
workspace_name = team_info['team']['name']
except SlackApiError as e:
logger.error(f"Error fetching team info: {e.response['error']}")
workspace_name = "Unknown Workspace"
# Extract user details
user_id = user_info['id']
real_name = user_info.get('real_name', 'Unknown')
profile = user_info.get('profile', {})
email = profile.get('email', f"{user_info.get('name', 'user')}@example.com") # Fallback email
name = user_info.get('name', '')
is_owner = user_info.get('is_owner', False)
# Connect to database
conn = psycopg2.connect(os.getenv('DATABASE_URL'))
cur = conn.cursor()
# Insert/update user in Users table
cur.execute('''
INSERT INTO Users (team_id, user_id, workspace_name, real_name, email, name, is_owner, last_updated)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (team_id, user_id) DO UPDATE SET
workspace_name = EXCLUDED.workspace_name,
real_name = EXCLUDED.real_name,
email = EXCLUDED.email,
name = EXCLUDED.name,
is_owner = EXCLUDED.is_owner,
last_updated = EXCLUDED.last_updated
''', (team_id, user_id, workspace_name, real_name, email, name, is_owner, datetime.now()))
conn.commit()
cur.close()
conn.close()
# Update user cache
with user_cache_lock:
if team_id not in user_cache:
user_cache[team_id] = {}
user_cache[team_id][user_id] = {
"real_name": real_name,
"email": f"{name}@gmail.com",
"name": name,
"is_owner": is_owner,
"workspace_name": workspace_name
}
# Update owner_id_cache if user is owner
if is_owner:
with owner_id_lock:
owner_id_cache[team_id] = user_id
logger.info(f"Processed team_join event for user {user_id} in team {team_id}")
except KeyError as e:
logger.error(f"Missing key in event data: {e}")
except psycopg2.Error as e:
logger.error(f"Database error: {e}")
except Exception as e:
logger.error(f"Unexpected error handling team_join: {e}")
def open_group_dm(client, users):
try:
response = client.conversations_open(users=",".join(users))
return response["channel"]["id"] if response["ok"] else (None, "Failed to create group DM")
except SlackApiError as e:
return None, f"Error creating group DM: {e.response['error']}"
# Flask Routes
@app.route("/slack/events", methods=["POST"])
def slack_events():
return slack_handler.handle(request)
@app.route("/slack/install", methods=["GET"])
def slack_install():
return slack_handler.handle(request)
@app.route("/slack/oauth_redirect", methods=["GET"])
def slack_oauth_redirect():
return slack_handler.handle(request)
@app.route("/oauth2callback")
def oauth2callback():
state = request.args.get('state', '')
print(f"STATE: {state}")
print(f"STATs: {state_manager._states}")
user_id = state_manager.validate_and_consume_state(state)
stored_state = get_from_session(user_id, "gcal_state") if user_id else None
if not user_id or stored_state != state:
return "Invalid state", 400
team_id = get_team_id_from_owner_id(user_id)
if not team_id:
return "Workspace not found", 404
client = get_client_for_team(team_id)
if not client:
return "Client not found", 500
flow = Flow.from_client_secrets_file('credentials.json', scopes=SCOPES, redirect_uri=OAUTH_REDIRECT_URI)
flow.fetch_token(authorization_response=request.url)
credentials = flow.credentials
service = build('oauth2', 'v2', credentials=credentials)
user_info = service.userinfo().get().execute()
google_email = user_info.get('email', 'unknown@example.com')
token_data = json.loads(credentials.to_json())
token_data['google_email'] = google_email
save_token(team_id, user_id, 'google', token_data) # Adjusted to use team_id and user_id
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
return "Google Calendar connected successfully! You can close this window."
@bolt_app.action("launch_auth")
def handle_launch_auth(ack, body, logger):
ack() # Acknowledge the action
logger.info(f"Launch auth triggered by user {body['user']['id']}")
# No further action needed since the URL redirect handles the OAuth flow
@app.route("/microsoft_callback")
def microsoft_callback():
code = request.args.get("code")
state = request.args.get("state")
if not code or not state:
return "Missing parameters", 400
user_id = state_manager.validate_and_consume_state(state)
if not user_id:
return "Invalid or expired state parameter", 403
team_id = get_team_id_from_owner_id(user_id)
if not team_id:
return "Workspace not found", 404
client = get_client_for_team(team_id)
if not client:
return "Client not found", 500
if user_id != get_workspace_owner_id(client, team_id):
return "Unauthorized", 403
msal_app = ConfidentialClientApplication(MICROSOFT_CLIENT_ID, authority=MICROSOFT_AUTHORITY, client_credential=MICROSOFT_CLIENT_SECRET)
result = msal_app.acquire_token_by_authorization_code(code, scopes=MICROSOFT_SCOPES, redirect_uri=MICROSOFT_REDIRECT_URI)
if "access_token" not in result:
return "Authentication failed", 400
token_data = {"access_token": result["access_token"], "refresh_token": result.get("refresh_token", ""), "expires_at": result["expires_in"] + time.time()}
save_token(user_id, 'microsoft', token_data)
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
return "Microsoft Calendar connected successfully! You can close this window."
@app.route("/zoom_callback")
def zoom_callback():
code = request.args.get("code")
state = request.args.get("state")
user_id = state_manager.validate_and_consume_state(state)
if not user_id:
return "Invalid or expired state", 403
team_id = get_team_id_from_owner_id(user_id)
if not team_id:
return "Workspace not found", 404
client = get_client_for_team(team_id)
if not client:
return "Client not found", 500
params = {"grant_type": "authorization_code", "code": code, "redirect_uri": ZOOM_REDIRECT_URI}
try:
response = requests.post(ZOOM_TOKEN_API, params=params, auth=(CLIENT_ID, CLIENT_SECRET))
except Exception as e:
return jsonify({"error": f"Token request failed: {str(e)}"}), 500
if response.status_code == 200:
token_data = response.json()
token_data["expires_at"] = time.time() + token_data["expires_in"]
# Fixed: Pass all required arguments in correct order
save_token(team_id, user_id, 'zoom', token_data)
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
return "Zoom connected successfully! You can close this window."
return "Failed to retrieve token", 400
@bolt_app.action("open_zoom_config_modal")
def handle_open_zoom_config_modal(ack, body, client, logger):
ack()
user_id = body["user"]["id"]
team_id = body["team"]["id"]
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
return
# Fixed: Pass both team_id and user_id to load_preferences
prefs = load_preferences(team_id, user_id)
zoom_config = prefs.get("zoom_config", {"mode": "manual", "link": None})
mode = zoom_config["mode"]
link = zoom_config.get("link", "")
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "zoom_config_submit",
"title": {"type": "plain_text", "text": "Configure Zoom"},
"submit": {"type": "plain_text", "text": "Save"},
"close": {"type": "plain_text", "text": "Cancel"},
"blocks": [
{
"type": "input",
"block_id": "zoom_mode",
"label": {"type": "plain_text", "text": "Zoom Mode"},
"element": {
"type": "static_select",
"action_id": "mode_select",
"placeholder": {"type": "plain_text", "text": "Select mode"},
"options": [
{"text": {"type": "plain_text", "text": "Automatic"}, "value": "automatic"},
{"text": {"type": "plain_text", "text": "Manual"}, "value": "manual"}
],
"initial_option": {"text": {"type": "plain_text", "text": "Automatic" if mode == "automatic" else "Manual"}, "value": mode} if mode else None
}
},
{
"type": "input",
"block_id": "zoom_link",
"label": {"type": "plain_text", "text": "Manual Zoom Link"},
"element": {
"type": "plain_text_input",
"action_id": "link_input",
"placeholder": {"type": "plain_text", "text": "Enter Zoom link"},
"initial_value": link if isinstance(link, str) else ""
},
"optional": True
}
]
}
)
except Exception as e:
logger.error(f"Error opening Zoom config modal: {e}")
@bolt_app.action("configure_zoom")
def handle_zoom_config(ack, body, client, logger):
ack() # Acknowledge the action
user_id = body["user"]["id"]
team_id = body["team"]["id"]
# Ensure only the workspace owner can configure Zoom
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
client.chat_postMessage(channel=user_id, text="Only the workspace owner can configure Zoom.")
return
# Check if this is a refresh or initial authentication
zoom_token = load_token(team_id, owner_id, "zoom")
is_refresh = zoom_token is not None
# Generate the Zoom OAuth URL
state = state_manager.create_state(owner_id) # Assume this generates a unique state
auth_url = f"{ZOOM_OAUTH_AUTHORIZE_API}?response_type=code&client_id={CLIENT_ID}&redirect_uri={quote_plus(ZOOM_REDIRECT_URI)}&state={state}"
# Set modal text based on the scenario
modal_title = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
button_text = "Refresh Zoom Token" if is_refresh else "Authenticate with Zoom"
# Open a modal with the appropriate text
try:
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"title": {"type": "plain_text", "text": modal_title},
"close": {"type": "plain_text", "text": "Cancel"},
"blocks": [
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"Click below to {button_text.lower()}:"}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": button_text},
"url": auth_url,
"action_id": "launch_zoom_auth"
}
]
}
]
}
)
except Exception as e:
logger.error(f"Error opening Zoom auth modal: {e}")
@bolt_app.view("zoom_config_submit")
def handle_zoom_config_submit(ack, body, client, logger):
ack() # Ensure ack is called before any processing to avoid warnings
user_id = body["user"]["id"]
team_id = body["team"]["id"]
owner_id = get_workspace_owner_id(client, team_id)
if user_id != owner_id:
return # Early return if not owner; no need to proceed
values = body["view"]["state"]["values"]
mode = values["zoom_mode"]["mode_select"]["selected_option"]["value"]
link = values["zoom_link"]["link_input"]["value"] if "zoom_link" in values and "link_input" in values["zoom_link"] else None
zoom_config = {"mode": mode, "link": link if mode == "manual" else None}
save_preference(team_id, user_id, zoom_config=zoom_config)
client.views_publish(user_id=user_id, view=create_home_tab(client, team_id, user_id))
@bolt_app.action("launch_zoom_auth")
def handle_some_action(ack, body, logger):
ack()
scheduler = BackgroundScheduler()
scheduler.add_job(state_manager.cleanup_expired_states, 'interval', minutes=5)
scheduler.start()
@app.route('/')
def home():
return render_template('index.html')
# @app.route('/ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
# def home():
# return render_template('ZOOM_verify_a12f2ccf48a647aa8ebc987a249133f8.html')
if __name__ == "__main__":
app.run(port=3000)