NeerajCodz commited on
Commit
a12349c
·
verified ·
1 Parent(s): cd14cb2
Files changed (1) hide show
  1. app.py +64 -49
app.py CHANGED
@@ -17,7 +17,7 @@ from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
17
  from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
18
  from slack_sdk.oauth import AuthorizeUrlGenerator
19
  from slack_sdk.oauth.installation_store.models import Installation
20
- from slack_bolt.authorization import AuthorizeResult # Fixed: Use AuthorizeResult instead of AsyncAuthorizeResult
21
 
22
  # RAG/ML Libraries
23
  from sentence_transformers import SentenceTransformer
@@ -32,8 +32,6 @@ from docx import Document
32
  import requests
33
 
34
  # --- Configuration and Initialization ---
35
-
36
- # Set up logging
37
  logging.basicConfig(level=logging.INFO)
38
  logger = logging.getLogger(__name__)
39
 
@@ -56,7 +54,7 @@ if not all(required_vars):
56
  if not SLACK_BOT_TOKEN:
57
  raise ValueError("SLACK_BOT_TOKEN is required for single-team mode.")
58
 
59
- # Set HF_TOKEN if provided (helps with authentication for Hub access)
60
  if HF_TOKEN:
61
  try:
62
  login(token=HF_TOKEN, add_to_git_credential=False)
@@ -67,7 +65,7 @@ if HF_TOKEN:
67
  # Initialize Supabase client
68
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
69
 
70
- # --- Supabase Async Installation Store (for multi-team; optional) ---
71
  class SupabaseAsyncInstallationStore(AsyncInstallationStore):
72
  def __init__(self, supabase_client):
73
  self.supabase = supabase_client
@@ -125,67 +123,84 @@ class SupabaseAsyncInstallationStore(AsyncInstallationStore):
125
  logger.error(f"Failed to delete installation for team {team_id}: {e}")
126
  raise
127
 
128
- # Initialize installation store (only if multi-team)
129
  installation_store = None
130
- USE_MULTI_TEAM = os.environ.get("USE_MULTI_TEAM", "false").lower() == "true" # Set env to 'true' for multi-team
 
131
  if USE_MULTI_TEAM:
132
  installation_store = SupabaseAsyncInstallationStore(supabase)
133
  logger.info("Using multi-team mode with Supabase store.")
134
  else:
135
  logger.info("Using single-team mode with SLACK_BOT_TOKEN.")
136
 
137
- # Custom authorize for multi-team (falls back to store)
138
  async def async_authorize(enterprise_id, team_id, user_id):
 
 
 
 
 
139
  logger.info(f"Custom authorize called for enterprise: {enterprise_id}, team: {team_id}, user: {user_id}")
 
140
  if not USE_MULTI_TEAM:
141
- return None # Single-team doesn't need this
 
 
 
 
 
 
 
 
 
 
142
  installation = await installation_store.fetch_installation(team_id=team_id, enterprise_id=enterprise_id)
143
  if installation:
144
- return AuthorizeResult( # Fixed: Use AuthorizeResult
145
  enterprise_id=enterprise_id or installation.enterprise_id,
146
  team_id=team_id,
147
  user_id=user_id or installation.user_id,
148
  bot_token=installation.bot_token,
149
  bot_user_id=installation.bot_user_id,
150
  )
 
151
  logger.error(f"No authorization found for team {team_id}")
152
  return None
153
 
154
- # Initialize Bolt Async App
155
  app = AsyncApp(
156
  signing_secret=SLACK_SIGNING_SECRET,
157
- token=SLACK_BOT_TOKEN if not USE_MULTI_TEAM else None, # Single-team: use token
158
- installation_store=installation_store,
159
- authorize=async_authorize if USE_MULTI_TEAM else None, # Multi-team: custom authorize
160
  oauth_settings=AsyncOAuthSettings(
161
  client_id=SLACK_CLIENT_ID,
162
  client_secret=SLACK_CLIENT_SECRET,
163
  scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"],
164
  redirect_uri_path="/slack/oauth/callback",
165
- ),
166
  )
167
 
168
  api = FastAPI()
169
 
170
  # --- RAG Model Loading ---
171
- # Check for GPU and set device
172
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
173
  logger.info(f"Using device: {device}")
174
 
175
  print("Loading embedding model...")
176
  embedding_model = None
177
  qa_pipeline = None
 
178
  try:
179
  embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
180
  print("Loading QA model...")
181
- qa_pipeline = pipeline("question-answering", model="deepset/roberta-base-squad2", device=device)
182
  print("Models loaded successfully!")
183
  except Exception as e:
184
  logger.error(f"Error loading models: {e}")
185
  raise RuntimeError("Failed to load required ML models. Check dependencies and hardware.")
186
 
187
  # --- Utility Functions ---
188
-
189
  def download_slack_file(url: str, token: str) -> bytes:
190
  """Downloads a file from Slack using the private download URL and bot token."""
191
  try:
@@ -289,7 +304,7 @@ async def answer_question(question: str, context: str) -> str:
289
  if not context.strip():
290
  return "No relevant documents found."
291
  try:
292
- MAX_CONTEXT_LEN = 4096
293
  context_slice = context[:MAX_CONTEXT_LEN]
294
 
295
  result = await asyncio.to_thread(
@@ -301,7 +316,6 @@ async def answer_question(question: str, context: str) -> str:
301
  return "Error generating answer from context."
302
 
303
  # --- Slack Handlers (Async) ---
304
-
305
  @app.event("file_shared")
306
  async def handle_file_shared(event, say, client):
307
  """Processes files shared in a channel and adds their content to the RAG knowledge base."""
@@ -317,7 +331,7 @@ async def handle_file_shared(event, say, client):
317
  await say("No download URL available for the file.")
318
  return
319
 
320
- token = client.token
321
  file_content = await asyncio.to_thread(download_slack_file, file_url, token)
322
 
323
  text = ""
@@ -347,23 +361,11 @@ async def handle_mention(event, say, client):
347
  text = event["text"]
348
  user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
349
 
350
- # Simple check for the presence of a file in the message for combined upload/query
351
- files = event.get("files", [])
352
- if files:
353
- await say("I see a file attached. I'll process it first, and then answer your question.")
354
- # NOTE: Handling files in app_mention is complex due to timing/race conditions.
355
- # For simplicity, we delegate file processing primarily to file_shared event.
356
- # This loop is kept as a fallback/redundancy but the file_shared event is more reliable.
357
- # The logic here is *identical* to file_shared and is omitted for brevity but should
358
- # be included if you expect users to combine uploads and queries in one message.
359
- pass # The file processing logic from the original code would go here
360
-
361
  if not user_query:
362
  await say("Please ask me a question after mentioning me!")
363
  return
364
 
365
  try:
366
- # Check if table is empty
367
  if await is_table_empty():
368
  await say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
369
  return
@@ -374,19 +376,15 @@ async def handle_mention(event, say, client):
374
  await say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
375
  return
376
 
377
- # Combine the content of the top documents into a single context string
378
  context = " ".join([doc["content"] for doc in results])
379
  answer = await answer_question(user_query, context)
380
 
381
- # Format the final response
382
  await say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
383
  except Exception as e:
384
  logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
385
  await say(f"❌ Error answering question: {str(e)}")
386
 
387
  # --- FastAPI Integration ---
388
-
389
- # Initialize the Async Slack Request Handler
390
  handler = AsyncSlackRequestHandler(app)
391
 
392
  @api.post("/slack/events")
@@ -410,14 +408,19 @@ async def health():
410
  db_status = "error"
411
  try:
412
  await asyncio.to_thread(
413
- lambda: supabase.table("installations").select("team_id").limit(1).execute()
414
  )
415
  db_status = "ok"
416
  except Exception as e:
417
  logger.error(f"Database health check failed: {e}")
418
 
419
  models_status = "ok" if embedding_model and qa_pipeline else "error"
420
- return {"status": "ok" if db_status == "ok" and models_status == "ok" else "error", "database": db_status, "models": models_status}
 
 
 
 
 
421
 
422
  @api.get("/slack/install")
423
  async def install_url():
@@ -440,36 +443,48 @@ async def oauth_callback(request: Request):
440
  try:
441
  logger.info("OAuth callback received")
442
  response = await handler.handle(request)
443
- logger.info(f"OAuth callback response: {response.status_code if hasattr(response, 'status_code') else 'No status'}")
 
444
  if hasattr(response, 'status_code') and response.status_code == 200:
445
- # In single-team, no need to save; for multi, it should auto-save via store
446
- if USE_MULTI_TEAM:
447
- logger.info("Multi-team install: Check Supabase for new data.")
448
  return HTMLResponse(
449
- content=f"<html><body><h1>Installation successful!</h1><p>You can now use the bot in your Slack workspace.</p><p>Mode: {'Single-team (no store needed)' if not USE_MULTI_TEAM else 'Multi-team (check Supabase)'}</p><p><a href='/debug/installations'>Debug Installations</a></p></body></html>",
 
 
 
 
 
 
450
  status_code=200
451
  )
452
  return response
453
  except Exception as e:
454
  logger.error(f"OAuth Callback Error: {e}")
455
  return HTMLResponse(
456
- content=f"<html><body><h1>Installation Failed!</h1><p>Error: {str(e)}</p><p>Check logs. If multi-team, ensure Supabase table exists.</p></body></html>",
 
 
 
 
 
 
457
  status_code=500
458
  )
459
 
460
- # Optional: Add a debug endpoint to check installation
461
  @api.get("/debug/installations")
462
  async def debug_installations():
463
- """Debug: List all installations (remove in prod)."""
464
  try:
465
  result = await asyncio.to_thread(
466
  lambda: supabase.table("installations").select("*").execute()
467
  )
468
- return {"installations": result.data, "mode": "multi-team" if USE_MULTI_TEAM else "single-team"}
 
 
 
 
469
  except Exception as e:
470
  return {"error": str(e)}
471
 
472
-
473
  if __name__ == "__main__":
474
  port = int(os.environ.get("PORT", 7860))
475
  uvicorn_run(api, host="0.0.0.0", port=port)
 
17
  from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
18
  from slack_sdk.oauth import AuthorizeUrlGenerator
19
  from slack_sdk.oauth.installation_store.models import Installation
20
+ from slack_bolt.authorization import AuthorizeResult
21
 
22
  # RAG/ML Libraries
23
  from sentence_transformers import SentenceTransformer
 
32
  import requests
33
 
34
  # --- Configuration and Initialization ---
 
 
35
  logging.basicConfig(level=logging.INFO)
36
  logger = logging.getLogger(__name__)
37
 
 
54
  if not SLACK_BOT_TOKEN:
55
  raise ValueError("SLACK_BOT_TOKEN is required for single-team mode.")
56
 
57
+ # Set HF_TOKEN if provided
58
  if HF_TOKEN:
59
  try:
60
  login(token=HF_TOKEN, add_to_git_credential=False)
 
65
  # Initialize Supabase client
66
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
67
 
68
+ # --- Supabase Async Installation Store ---
69
  class SupabaseAsyncInstallationStore(AsyncInstallationStore):
70
  def __init__(self, supabase_client):
71
  self.supabase = supabase_client
 
123
  logger.error(f"Failed to delete installation for team {team_id}: {e}")
124
  raise
125
 
126
+ # Initialize installation store
127
  installation_store = None
128
+ USE_MULTI_TEAM = os.environ.get("USE_MULTI_TEAM", "false").lower() == "true"
129
+
130
  if USE_MULTI_TEAM:
131
  installation_store = SupabaseAsyncInstallationStore(supabase)
132
  logger.info("Using multi-team mode with Supabase store.")
133
  else:
134
  logger.info("Using single-team mode with SLACK_BOT_TOKEN.")
135
 
136
+ # FIXED: Custom authorize function that works for both single and multi-team
137
  async def async_authorize(enterprise_id, team_id, user_id):
138
+ """
139
+ Custom authorization function.
140
+ For single-team: Returns a static AuthorizeResult using SLACK_BOT_TOKEN.
141
+ For multi-team: Fetches from installation store.
142
+ """
143
  logger.info(f"Custom authorize called for enterprise: {enterprise_id}, team: {team_id}, user: {user_id}")
144
+
145
  if not USE_MULTI_TEAM:
146
+ # FIXED: Return AuthorizeResult for single-team mode instead of None
147
+ logger.info("Single-team mode: Using SLACK_BOT_TOKEN")
148
+ return AuthorizeResult(
149
+ enterprise_id=enterprise_id,
150
+ team_id=team_id,
151
+ bot_user_id=None, # Will be populated automatically by Slack SDK
152
+ bot_token=SLACK_BOT_TOKEN,
153
+ user_id=user_id,
154
+ )
155
+
156
+ # Multi-team mode: fetch from store
157
  installation = await installation_store.fetch_installation(team_id=team_id, enterprise_id=enterprise_id)
158
  if installation:
159
+ return AuthorizeResult(
160
  enterprise_id=enterprise_id or installation.enterprise_id,
161
  team_id=team_id,
162
  user_id=user_id or installation.user_id,
163
  bot_token=installation.bot_token,
164
  bot_user_id=installation.bot_user_id,
165
  )
166
+
167
  logger.error(f"No authorization found for team {team_id}")
168
  return None
169
 
170
+ # FIXED: Initialize Bolt Async App with proper configuration
171
  app = AsyncApp(
172
  signing_secret=SLACK_SIGNING_SECRET,
173
+ token=None, # Always use None when using authorize function
174
+ installation_store=installation_store if USE_MULTI_TEAM else None,
175
+ authorize=async_authorize, # Always use custom authorize
176
  oauth_settings=AsyncOAuthSettings(
177
  client_id=SLACK_CLIENT_ID,
178
  client_secret=SLACK_CLIENT_SECRET,
179
  scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"],
180
  redirect_uri_path="/slack/oauth/callback",
181
+ ) if USE_MULTI_TEAM else None,
182
  )
183
 
184
  api = FastAPI()
185
 
186
  # --- RAG Model Loading ---
 
187
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
188
  logger.info(f"Using device: {device}")
189
 
190
  print("Loading embedding model...")
191
  embedding_model = None
192
  qa_pipeline = None
193
+
194
  try:
195
  embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
196
  print("Loading QA model...")
197
+ qa_pipeline = pipeline("question-answering", model="deepset/roberta-base-squad2", device=0 if device == 'cuda' else -1)
198
  print("Models loaded successfully!")
199
  except Exception as e:
200
  logger.error(f"Error loading models: {e}")
201
  raise RuntimeError("Failed to load required ML models. Check dependencies and hardware.")
202
 
203
  # --- Utility Functions ---
 
204
  def download_slack_file(url: str, token: str) -> bytes:
205
  """Downloads a file from Slack using the private download URL and bot token."""
206
  try:
 
304
  if not context.strip():
305
  return "No relevant documents found."
306
  try:
307
+ MAX_CONTEXT_LEN = 4096
308
  context_slice = context[:MAX_CONTEXT_LEN]
309
 
310
  result = await asyncio.to_thread(
 
316
  return "Error generating answer from context."
317
 
318
  # --- Slack Handlers (Async) ---
 
319
  @app.event("file_shared")
320
  async def handle_file_shared(event, say, client):
321
  """Processes files shared in a channel and adds their content to the RAG knowledge base."""
 
331
  await say("No download URL available for the file.")
332
  return
333
 
334
+ token = client.token
335
  file_content = await asyncio.to_thread(download_slack_file, file_url, token)
336
 
337
  text = ""
 
361
  text = event["text"]
362
  user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
363
 
 
 
 
 
 
 
 
 
 
 
 
364
  if not user_query:
365
  await say("Please ask me a question after mentioning me!")
366
  return
367
 
368
  try:
 
369
  if await is_table_empty():
370
  await say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
371
  return
 
376
  await say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
377
  return
378
 
 
379
  context = " ".join([doc["content"] for doc in results])
380
  answer = await answer_question(user_query, context)
381
 
 
382
  await say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
383
  except Exception as e:
384
  logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
385
  await say(f"❌ Error answering question: {str(e)}")
386
 
387
  # --- FastAPI Integration ---
 
 
388
  handler = AsyncSlackRequestHandler(app)
389
 
390
  @api.post("/slack/events")
 
408
  db_status = "error"
409
  try:
410
  await asyncio.to_thread(
411
+ lambda: supabase.table("documents").select("id").limit(1).execute()
412
  )
413
  db_status = "ok"
414
  except Exception as e:
415
  logger.error(f"Database health check failed: {e}")
416
 
417
  models_status = "ok" if embedding_model and qa_pipeline else "error"
418
+ return {
419
+ "status": "ok" if db_status == "ok" and models_status == "ok" else "error",
420
+ "database": db_status,
421
+ "models": models_status,
422
+ "mode": "single-team" if not USE_MULTI_TEAM else "multi-team"
423
+ }
424
 
425
  @api.get("/slack/install")
426
  async def install_url():
 
443
  try:
444
  logger.info("OAuth callback received")
445
  response = await handler.handle(request)
446
+ logger.info(f"OAuth callback handled")
447
+
448
  if hasattr(response, 'status_code') and response.status_code == 200:
 
 
 
449
  return HTMLResponse(
450
+ content=f"""<html>
451
+ <body>
452
+ <h1>Installation successful!</h1>
453
+ <p>You can now use the bot in your Slack workspace.</p>
454
+ <p>Mode: {'Single-team' if not USE_MULTI_TEAM else 'Multi-team'}</p>
455
+ </body>
456
+ </html>""",
457
  status_code=200
458
  )
459
  return response
460
  except Exception as e:
461
  logger.error(f"OAuth Callback Error: {e}")
462
  return HTMLResponse(
463
+ content=f"""<html>
464
+ <body>
465
+ <h1>Installation Failed!</h1>
466
+ <p>Error: {str(e)}</p>
467
+ <p>Check logs for details.</p>
468
+ </body>
469
+ </html>""",
470
  status_code=500
471
  )
472
 
 
473
  @api.get("/debug/installations")
474
  async def debug_installations():
475
+ """Debug: List all installations (remove in production)."""
476
  try:
477
  result = await asyncio.to_thread(
478
  lambda: supabase.table("installations").select("*").execute()
479
  )
480
+ return {
481
+ "installations": result.data,
482
+ "mode": "multi-team" if USE_MULTI_TEAM else "single-team",
483
+ "count": len(result.data)
484
+ }
485
  except Exception as e:
486
  return {"error": str(e)}
487
 
 
488
  if __name__ == "__main__":
489
  port = int(os.environ.get("PORT", 7860))
490
  uvicorn_run(api, host="0.0.0.0", port=port)