Spaces:
No application file
No application file
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 | |
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}") | |
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}") | |
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}") | |
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}") | |
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) | |
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) | |
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 | |
def slack_events(): | |
return slack_handler.handle(request) | |
def slack_install(): | |
return slack_handler.handle(request) | |
def slack_oauth_redirect(): | |
return slack_handler.handle(request) | |
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." | |
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 | |
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." | |
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 | |
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}") | |
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}") | |
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)) | |
def handle_some_action(ack, body, logger): | |
ack() | |
scheduler = BackgroundScheduler() | |
scheduler.add_job(state_manager.cleanup_expired_states, 'interval', minutes=5) | |
scheduler.start() | |
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) |