NeerajCodz commited on
Commit
da3bac8
·
verified ·
1 Parent(s): d1168e3
Files changed (1) hide show
  1. app.py +179 -120
app.py CHANGED
@@ -2,17 +2,20 @@ import os
2
  import io
3
  import re
4
  import logging
 
5
  from typing import List, Dict, Any
6
 
7
  # Core Web Framework and Async Handlers
8
  from fastapi import FastAPI, Request
 
9
  from uvicorn import run as uvicorn_run
10
 
11
- # Slack Libraries
12
- from slack_bolt import App
13
- from slack_bolt.adapter.fastapi.async_handler import SlackRequestHandler
14
  from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
15
  from slack_sdk.oauth import AuthorizeUrlGenerator
 
16
 
17
  # RAG/ML Libraries
18
  from sentence_transformers import SentenceTransformer
@@ -41,6 +44,12 @@ SLACK_CLIENT_ID = os.environ.get("SLACK_CLIENT_ID")
41
  SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET")
42
  HF_TOKEN = os.environ.get("HF_TOKEN")
43
 
 
 
 
 
 
 
44
  # Set HF_TOKEN if provided (helps with authentication for Hub access)
45
  if HF_TOKEN:
46
  try:
@@ -49,27 +58,16 @@ if HF_TOKEN:
49
  except Exception as e:
50
  logger.warning(f"Failed to log into Hugging Face Hub: {e}")
51
 
52
- # Check for required environment variables
53
- if not all([SUPABASE_URL, SUPABASE_KEY, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET]):
54
- logger.error("Missing required environment variables (SUPABASE, SLACK Client/Secret/Signing). Exiting.")
55
- # In a real app, you might raise an error here
56
- # raise EnvironmentError("Missing required environment variables.")
57
-
58
-
59
  # Initialize Supabase client
60
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
61
 
62
- # --- Supabase Installation Store (Updated to synchronous for simplicity, but can be Async if needed) ---
63
- # NOTE: Bolt's `SlackRequestHandler` is often easier to use with synchronous handlers for FastAPI
64
- # unless you specifically need async in the middleware.
65
-
66
- class SupabaseInstallationStore:
67
  def __init__(self, supabase_client):
68
  self.supabase = supabase_client
69
- # Import Installation here to avoid circular dependency
70
- from slack_bolt.models.installation import Installation
71
 
72
- def save(self, installation):
 
73
  data = {
74
  "team_id": installation.team_id,
75
  "bot_token": installation.bot_token,
@@ -77,43 +75,63 @@ class SupabaseInstallationStore:
77
  "bot_scopes": ",".join(installation.bot_scopes) if installation.bot_scopes else None,
78
  "app_id": installation.app_id,
79
  }
80
- self.supabase.table("installations").upsert(data, on_conflict="team_id").execute()
81
- logger.info(f"Saved installation for team: {installation.team_id}")
82
-
83
- def fetch_installation(self, team_id):
84
- result = self.supabase.table("installations").select("*").eq("team_id", team_id).execute()
85
- if not result.data:
86
- return None
87
-
88
- # Reconstruct Installation object
89
- from slack_bolt.models.installation import Installation
90
- data = result.data[0]
91
- return Installation(
92
- app_id=data.get("app_id"),
93
- enterprise_id=None,
94
- team_id=team_id,
95
- user_id=None,
96
- bot_token=data.get("bot_token"),
97
- bot_user_id=data.get("bot_user_id"),
98
- bot_scopes=data.get("bot_scopes", "").split(',') if data.get("bot_scopes") else [],
99
- incoming_webhook=None,
100
- )
101
 
102
- def delete_installation(self, team_id):
103
- self.supabase.table("installations").delete().eq("team_id", team_id).execute()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  # Initialize installation store
107
- installation_store = SupabaseInstallationStore(supabase)
108
 
109
- # Initialize Bolt App (Synchronous App with FastAPI Async Handler)
110
- app = App(
111
  client_id=SLACK_CLIENT_ID,
112
  client_secret=SLACK_CLIENT_SECRET,
113
  signing_secret=SLACK_SIGNING_SECRET,
114
  installation_store=installation_store,
115
- # NOTE: It's crucial to use the synchronous Bolt App unless all your handlers are async
116
- token=SLACK_BOT_TOKEN # Use this for single-workspace setup as a fallback
117
  )
118
 
119
  api = FastAPI()
@@ -124,6 +142,8 @@ device = 'cuda' if torch.cuda.is_available() else 'cpu'
124
  logger.info(f"Using device: {device}")
125
 
126
  print("Loading embedding model...")
 
 
127
  try:
128
  embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
129
  print("Loading QA model...")
@@ -131,42 +151,51 @@ try:
131
  print("Models loaded successfully!")
132
  except Exception as e:
133
  logger.error(f"Error loading models: {e}")
134
- # Consider graceful shutdown or using default models if this fails
135
- raise
136
 
137
  # --- Utility Functions ---
138
 
139
  def download_slack_file(url: str, token: str) -> bytes:
140
  """Downloads a file from Slack using the private download URL and bot token."""
141
- headers = {"Authorization": f"Bearer {token}"}
142
- response = requests.get(url, headers=headers, stream=True)
143
- response.raise_for_status()
144
- return response.content
 
 
 
 
145
 
146
  def extract_text_from_pdf(file_content: bytes) -> str:
147
  """Extracts text from PDF bytes."""
148
- pdf_reader = pypdf.PdfReader(io.BytesIO(file_content))
149
- text = ""
150
- for page in pdf_reader.pages:
151
- # Using .extract_text() is standard
152
- extracted = page.extract_text()
153
- if extracted:
154
- text += extracted + "\n"
155
- return text
 
 
 
156
 
157
  def extract_text_from_docx(file_content: bytes) -> str:
158
  """Extracts text from DOCX bytes."""
159
- doc = Document(io.BytesIO(file_content))
160
- text = ""
161
- for paragraph in doc.paragraphs:
162
- text += paragraph.text + "\n"
163
- return text
 
 
 
 
164
 
165
  def chunk_text(text: str, chunk_size: int = 300) -> List[str]:
166
  """Chunks text by word count to create manageable RAG chunks."""
167
  words = text.split()
168
  chunks = []
169
- # Using a simple word-split chunking
170
  for i in range(0, len(words), chunk_size):
171
  chunk = " ".join(words[i:i + chunk_size])
172
  if chunk.strip():
@@ -175,28 +204,35 @@ def chunk_text(text: str, chunk_size: int = 300) -> List[str]:
175
 
176
  def embed_text(text: str) -> List[float]:
177
  """Generates an embedding for a piece of text."""
178
- # Use convert_to_tensor=True for better performance, then convert back for Supabase
 
179
  embedding = embedding_model.encode(text, convert_to_tensor=False)
180
  return embedding.tolist()
181
 
182
  def store_embeddings(chunks: List[str]):
183
  """Stores text chunks and their embeddings in Supabase."""
 
184
  for chunk in chunks:
185
  embedding = embed_text(chunk)
186
  try:
187
- # Using the upsert method for idempotent inserts (though a unique ID per chunk is better)
188
- supabase.table("documents").insert({
189
- "content": chunk,
190
- "embedding": embedding
191
- }).execute()
 
 
192
  except Exception as e:
193
  logger.error(f"Failed to insert chunk into Supabase: {str(e)}")
194
 
195
  def is_table_empty() -> bool:
196
  """Checks if the documents table has any records."""
197
  try:
198
- # Use simple count query
199
- result = supabase.table("documents").select("id", count="exact").limit(1).execute()
 
 
 
200
  return result.count == 0
201
  except Exception as e:
202
  logger.error(f"Error checking table emptiness: {str(e)}")
@@ -204,13 +240,18 @@ def is_table_empty() -> bool:
204
 
205
  def search_documents(query: str, match_count: int = 5) -> List[Dict[str, Any]]:
206
  """Searches Supabase for documents matching the query using vector similarity."""
 
 
207
  query_embedding = embed_text(query)
208
  try:
209
- # Assumes you have a 'match_documents' function in your Supabase database
210
- result = supabase.rpc("match_documents", {
211
- "query_embedding": query_embedding,
212
- "match_count": match_count
213
- }).execute()
 
 
 
214
  return result.data
215
  except Exception as e:
216
  logger.error(f"Error searching documents for query '{query}': {str(e)}")
@@ -218,10 +259,11 @@ def search_documents(query: str, match_count: int = 5) -> List[Dict[str, Any]]:
218
 
219
  def answer_question(question: str, context: str) -> str:
220
  """Answers a question based on the provided context using the QA pipeline."""
 
 
221
  if not context.strip():
222
  return "No relevant documents found."
223
  try:
224
- # QA models have a context token limit (e.g., 512 for roberta-base); we limit to be safe.
225
  MAX_CONTEXT_LEN = 4096
226
  context_slice = context[:MAX_CONTEXT_LEN]
227
 
@@ -231,24 +273,23 @@ def answer_question(question: str, context: str) -> str:
231
  logger.error(f"Error in QA pipeline: {str(e)}")
232
  return "Error generating answer from context."
233
 
234
- # --- Slack Handlers ---
235
 
236
  @app.event("file_shared")
237
- def handle_file_shared(event, say, client):
238
  """Processes files shared in a channel and adds their content to the RAG knowledge base."""
239
  file_id = event["file_id"]
240
  try:
241
- file_info = client.files_info(file=file_id)
242
  file_data = file_info["file"]
243
 
244
  file_type = file_data.get("mimetype", "")
245
  file_url = file_data.get("url_private_download")
246
 
247
  if not file_url:
248
- say("No download URL available for the file.")
249
  return
250
 
251
- # Get the bot token dynamically for the workspace
252
  token = client.token
253
  file_content = download_slack_file(file_url, token)
254
 
@@ -258,23 +299,23 @@ def handle_file_shared(event, say, client):
258
  elif "wordprocessingml" in file_type or "msword" in file_type:
259
  text = extract_text_from_docx(file_content)
260
  else:
261
- say("Unsupported file type. Please upload PDF or DOCX files.")
262
  return
263
 
264
  if not text.strip():
265
- say("No text could be extracted from the file.")
266
  return
267
 
268
  chunks = chunk_text(text)
269
  store_embeddings(chunks)
270
 
271
- say(f"✅ File processed successfully! Added **{len(chunks)}** chunks to the knowledge base.")
272
  except Exception as e:
273
  logger.error(f"Error in file_shared handler for file_id {file_id}: {str(e)}")
274
- say(f"❌ Error processing file: {str(e)}")
275
 
276
  @app.event("app_mention")
277
- def handle_mention(event, say, client):
278
  """Handles mentions (@bot) for answering questions using RAG."""
279
  text = event["text"]
280
  user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
@@ -282,7 +323,7 @@ def handle_mention(event, say, client):
282
  # Simple check for the presence of a file in the message for combined upload/query
283
  files = event.get("files", [])
284
  if files:
285
- say("I see a file attached. I'll process it first, and then answer your question.")
286
  # NOTE: Handling files in app_mention is complex due to timing/race conditions.
287
  # For simplicity, we delegate file processing primarily to file_shared event.
288
  # This loop is kept as a fallback/redundancy but the file_shared event is more reliable.
@@ -291,19 +332,19 @@ def handle_mention(event, say, client):
291
  pass # The file processing logic from the original code would go here
292
 
293
  if not user_query:
294
- say("Please ask me a question after mentioning me!")
295
  return
296
 
297
  try:
298
  # Check if table is empty
299
- if is_table_empty():
300
- say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
301
  return
302
 
303
  results = search_documents(user_query, match_count=5)
304
 
305
  if not results:
306
- say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
307
  return
308
 
309
  # Combine the content of the top documents into a single context string
@@ -311,21 +352,24 @@ def handle_mention(event, say, client):
311
  answer = answer_question(user_query, context)
312
 
313
  # Format the final response
314
- say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
315
  except Exception as e:
316
  logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
317
- say(f"❌ Error answering question: {str(e)}")
318
 
319
  # --- FastAPI Integration ---
320
 
321
- # Initialize the Slack Request Handler with the Bolt App
322
- # We use the synchronous handler as the App is synchronous
323
- handler = SlackRequestHandler(app)
324
 
325
  @api.post("/slack/events")
326
  async def slack_events(request: Request):
327
  """Endpoint for all Slack event subscriptions."""
328
- return await handler.handle(request)
 
 
 
 
329
 
330
  @api.get("/")
331
  async def root():
@@ -335,40 +379,55 @@ async def root():
335
  @api.get("/health")
336
  async def health():
337
  """Health check endpoint."""
338
- # Add a check for Supabase connectivity here for a robust health check
339
  try:
340
- supabase.table("installations").select("team_id").limit(1).execute()
 
 
 
 
341
  db_status = "ok"
342
- except Exception:
343
- db_status = "error"
344
-
345
- return {"status": "ok", "database": db_status}
 
346
 
347
  @api.get("/slack/install")
348
  async def install_url():
349
  """Generates the Slack installation URL."""
350
- generator = AuthorizeUrlGenerator(
351
- client_id=SLACK_CLIENT_ID,
352
- scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"]
353
- )
354
- # NOTE: The redirect_uri should match your Slack App config
355
- return {"install_url": generator.generate(state="state")}
 
 
 
 
 
356
 
357
  @api.get("/slack/oauth/callback")
358
  async def oauth_callback(request: Request):
359
  """Handles the OAuth callback from Slack to complete installation."""
360
  try:
361
- # Use Bolt's handler to process the OAuth flow
362
- # The handler automatically uses the `installation_store` defined in the App object
363
- response = await handler.handle(request)
364
- # Simple HTML response indicating success
365
- return f"<html><body><h1>Installation successful!</h1><p>You can now use the bot in your Slack workspace.</p></body></html>"
 
 
 
366
  except Exception as e:
367
  logger.error(f"OAuth Callback Error: {e}")
368
- return f"<html><body><h1>Installation Failed!</h1><p>Error: {str(e)}</p></body></html>"
 
 
 
369
 
370
 
371
  if __name__ == "__main__":
372
- # Use uvicorn_run for proper program execution
373
  port = int(os.environ.get("PORT", 7860))
374
  uvicorn_run(api, host="0.0.0.0", port=port)
 
2
  import io
3
  import re
4
  import logging
5
+ import asyncio
6
  from typing import List, Dict, Any
7
 
8
  # Core Web Framework and Async Handlers
9
  from fastapi import FastAPI, Request
10
+ from fastapi.responses import HTMLResponse
11
  from uvicorn import run as uvicorn_run
12
 
13
+ # Slack Libraries (Async versions)
14
+ from slack_bolt import AsyncApp
15
+ from slack_bolt.adapter.fastapi import AsyncSlackRequestHandler
16
  from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
17
  from slack_sdk.oauth import AuthorizeUrlGenerator
18
+ from slack_bolt.models.installation import Installation
19
 
20
  # RAG/ML Libraries
21
  from sentence_transformers import SentenceTransformer
 
44
  SLACK_CLIENT_SECRET = os.environ.get("SLACK_CLIENT_SECRET")
45
  HF_TOKEN = os.environ.get("HF_TOKEN")
46
 
47
+ # Check for required environment variables
48
+ required_vars = [SUPABASE_URL, SUPABASE_KEY, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET]
49
+ if not all(required_vars):
50
+ missing = [var for var in required_vars if not var]
51
+ raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
52
+
53
  # Set HF_TOKEN if provided (helps with authentication for Hub access)
54
  if HF_TOKEN:
55
  try:
 
58
  except Exception as e:
59
  logger.warning(f"Failed to log into Hugging Face Hub: {e}")
60
 
 
 
 
 
 
 
 
61
  # Initialize Supabase client
62
  supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
63
 
64
+ # --- Supabase Async Installation Store ---
65
+ class SupabaseAsyncInstallationStore(AsyncInstallationStore):
 
 
 
66
  def __init__(self, supabase_client):
67
  self.supabase = supabase_client
 
 
68
 
69
+ async def save(self, installation: Installation) -> None:
70
+ loop = asyncio.get_event_loop()
71
  data = {
72
  "team_id": installation.team_id,
73
  "bot_token": installation.bot_token,
 
75
  "bot_scopes": ",".join(installation.bot_scopes) if installation.bot_scopes else None,
76
  "app_id": installation.app_id,
77
  }
78
+ try:
79
+ await loop.run_in_executor(
80
+ None,
81
+ lambda: self.supabase.table("installations").upsert(data, on_conflict="team_id").execute()
82
+ )
83
+ logger.info(f"Saved installation for team: {installation.team_id}")
84
+ except Exception as e:
85
+ logger.error(f"Failed to save installation for team {installation.team_id}: {e}")
86
+ raise
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ async def fetch_installation(self, team_id: str, *, enterprise_id: str | None = None, user_id: str | None = None) -> Installation | None:
89
+ loop = asyncio.get_event_loop()
90
+ try:
91
+ result = await loop.run_in_executor(
92
+ None,
93
+ lambda: self.supabase.table("installations").select("*").eq("team_id", team_id).execute()
94
+ )
95
+ if not result.data:
96
+ return None
97
+
98
+ data = result.data[0]
99
+ return Installation(
100
+ app_id=data.get("app_id"),
101
+ enterprise_id=enterprise_id,
102
+ team_id=team_id,
103
+ user_id=user_id,
104
+ bot_token=data.get("bot_token"),
105
+ bot_user_id=data.get("bot_user_id"),
106
+ bot_scopes=data.get("bot_scopes", "").split(',') if data.get("bot_scopes") else [],
107
+ incoming_webhook=None,
108
+ )
109
+ except Exception as e:
110
+ logger.error(f"Failed to fetch installation for team {team_id}: {e}")
111
+ raise
112
 
113
+ async def delete(self, team_id: str, *, enterprise_id: str | None = None, user_id: str | None = None) -> None:
114
+ loop = asyncio.get_event_loop()
115
+ try:
116
+ await loop.run_in_executor(
117
+ None,
118
+ lambda: self.supabase.table("installations").delete().eq("team_id", team_id).execute()
119
+ )
120
+ logger.info(f"Deleted installation for team: {team_id}")
121
+ except Exception as e:
122
+ logger.error(f"Failed to delete installation for team {team_id}: {e}")
123
+ raise
124
 
125
  # Initialize installation store
126
+ installation_store = SupabaseAsyncInstallationStore(supabase)
127
 
128
+ # Initialize Bolt Async App
129
+ app = AsyncApp(
130
  client_id=SLACK_CLIENT_ID,
131
  client_secret=SLACK_CLIENT_SECRET,
132
  signing_secret=SLACK_SIGNING_SECRET,
133
  installation_store=installation_store,
134
+ # Removed token for multi-workspace support; add back if single-workspace only
 
135
  )
136
 
137
  api = FastAPI()
 
142
  logger.info(f"Using device: {device}")
143
 
144
  print("Loading embedding model...")
145
+ embedding_model = None
146
+ qa_pipeline = None
147
  try:
148
  embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
149
  print("Loading QA model...")
 
151
  print("Models loaded successfully!")
152
  except Exception as e:
153
  logger.error(f"Error loading models: {e}")
154
+ raise RuntimeError("Failed to load required ML models. Check dependencies and hardware.")
 
155
 
156
  # --- Utility Functions ---
157
 
158
  def download_slack_file(url: str, token: str) -> bytes:
159
  """Downloads a file from Slack using the private download URL and bot token."""
160
+ try:
161
+ headers = {"Authorization": f"Bearer {token}"}
162
+ response = requests.get(url, headers=headers, stream=True, timeout=30)
163
+ response.raise_for_status()
164
+ return response.content
165
+ except requests.RequestException as e:
166
+ logger.error(f"Failed to download Slack file from {url}: {e}")
167
+ raise
168
 
169
  def extract_text_from_pdf(file_content: bytes) -> str:
170
  """Extracts text from PDF bytes."""
171
+ try:
172
+ pdf_reader = pypdf.PdfReader(io.BytesIO(file_content))
173
+ text = ""
174
+ for page in pdf_reader.pages:
175
+ extracted = page.extract_text()
176
+ if extracted:
177
+ text += extracted + "\n"
178
+ return text
179
+ except Exception as e:
180
+ logger.error(f"Failed to extract text from PDF: {e}")
181
+ raise
182
 
183
  def extract_text_from_docx(file_content: bytes) -> str:
184
  """Extracts text from DOCX bytes."""
185
+ try:
186
+ doc = Document(io.BytesIO(file_content))
187
+ text = ""
188
+ for paragraph in doc.paragraphs:
189
+ text += paragraph.text + "\n"
190
+ return text
191
+ except Exception as e:
192
+ logger.error(f"Failed to extract text from DOCX: {e}")
193
+ raise
194
 
195
  def chunk_text(text: str, chunk_size: int = 300) -> List[str]:
196
  """Chunks text by word count to create manageable RAG chunks."""
197
  words = text.split()
198
  chunks = []
 
199
  for i in range(0, len(words), chunk_size):
200
  chunk = " ".join(words[i:i + chunk_size])
201
  if chunk.strip():
 
204
 
205
  def embed_text(text: str) -> List[float]:
206
  """Generates an embedding for a piece of text."""
207
+ if embedding_model is None:
208
+ raise RuntimeError("Embedding model not loaded.")
209
  embedding = embedding_model.encode(text, convert_to_tensor=False)
210
  return embedding.tolist()
211
 
212
  def store_embeddings(chunks: List[str]):
213
  """Stores text chunks and their embeddings in Supabase."""
214
+ loop = asyncio.get_event_loop()
215
  for chunk in chunks:
216
  embedding = embed_text(chunk)
217
  try:
218
+ loop.run_in_executor(
219
+ None,
220
+ lambda c=chunk, e=embedding: supabase.table("documents").insert({
221
+ "content": c,
222
+ "embedding": e
223
+ }).execute()
224
+ )
225
  except Exception as e:
226
  logger.error(f"Failed to insert chunk into Supabase: {str(e)}")
227
 
228
  def is_table_empty() -> bool:
229
  """Checks if the documents table has any records."""
230
  try:
231
+ loop = asyncio.get_event_loop()
232
+ result = loop.run_in_executor(
233
+ None,
234
+ lambda: supabase.table("documents").select("id", count="exact").limit(1).execute()
235
+ )
236
  return result.count == 0
237
  except Exception as e:
238
  logger.error(f"Error checking table emptiness: {str(e)}")
 
240
 
241
  def search_documents(query: str, match_count: int = 5) -> List[Dict[str, Any]]:
242
  """Searches Supabase for documents matching the query using vector similarity."""
243
+ if embedding_model is None:
244
+ raise RuntimeError("Embedding model not loaded.")
245
  query_embedding = embed_text(query)
246
  try:
247
+ loop = asyncio.get_event_loop()
248
+ result = loop.run_in_executor(
249
+ None,
250
+ lambda: supabase.rpc("match_documents", {
251
+ "query_embedding": query_embedding,
252
+ "match_count": match_count
253
+ }).execute()
254
+ )
255
  return result.data
256
  except Exception as e:
257
  logger.error(f"Error searching documents for query '{query}': {str(e)}")
 
259
 
260
  def answer_question(question: str, context: str) -> str:
261
  """Answers a question based on the provided context using the QA pipeline."""
262
+ if qa_pipeline is None:
263
+ raise RuntimeError("QA pipeline not loaded.")
264
  if not context.strip():
265
  return "No relevant documents found."
266
  try:
 
267
  MAX_CONTEXT_LEN = 4096
268
  context_slice = context[:MAX_CONTEXT_LEN]
269
 
 
273
  logger.error(f"Error in QA pipeline: {str(e)}")
274
  return "Error generating answer from context."
275
 
276
+ # --- Slack Handlers (Async) ---
277
 
278
  @app.event("file_shared")
279
+ async def handle_file_shared(event, say, client):
280
  """Processes files shared in a channel and adds their content to the RAG knowledge base."""
281
  file_id = event["file_id"]
282
  try:
283
+ file_info = await client.files_info(file=file_id)
284
  file_data = file_info["file"]
285
 
286
  file_type = file_data.get("mimetype", "")
287
  file_url = file_data.get("url_private_download")
288
 
289
  if not file_url:
290
+ await say("No download URL available for the file.")
291
  return
292
 
 
293
  token = client.token
294
  file_content = download_slack_file(file_url, token)
295
 
 
299
  elif "wordprocessingml" in file_type or "msword" in file_type:
300
  text = extract_text_from_docx(file_content)
301
  else:
302
+ await say("Unsupported file type. Please upload PDF or DOCX files.")
303
  return
304
 
305
  if not text.strip():
306
+ await say("No text could be extracted from the file.")
307
  return
308
 
309
  chunks = chunk_text(text)
310
  store_embeddings(chunks)
311
 
312
+ await say(f"✅ File processed successfully! Added **{len(chunks)}** chunks to the knowledge base.")
313
  except Exception as e:
314
  logger.error(f"Error in file_shared handler for file_id {file_id}: {str(e)}")
315
+ await say(f"❌ Error processing file: {str(e)}")
316
 
317
  @app.event("app_mention")
318
+ async def handle_mention(event, say, client):
319
  """Handles mentions (@bot) for answering questions using RAG."""
320
  text = event["text"]
321
  user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
 
323
  # Simple check for the presence of a file in the message for combined upload/query
324
  files = event.get("files", [])
325
  if files:
326
+ await say("I see a file attached. I'll process it first, and then answer your question.")
327
  # NOTE: Handling files in app_mention is complex due to timing/race conditions.
328
  # For simplicity, we delegate file processing primarily to file_shared event.
329
  # This loop is kept as a fallback/redundancy but the file_shared event is more reliable.
 
332
  pass # The file processing logic from the original code would go here
333
 
334
  if not user_query:
335
+ await say("Please ask me a question after mentioning me!")
336
  return
337
 
338
  try:
339
  # Check if table is empty
340
+ if await asyncio.to_thread(is_table_empty):
341
+ await say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
342
  return
343
 
344
  results = search_documents(user_query, match_count=5)
345
 
346
  if not results:
347
+ await say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
348
  return
349
 
350
  # Combine the content of the top documents into a single context string
 
352
  answer = answer_question(user_query, context)
353
 
354
  # Format the final response
355
+ await say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
356
  except Exception as e:
357
  logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
358
+ await say(f"❌ Error answering question: {str(e)}")
359
 
360
  # --- FastAPI Integration ---
361
 
362
+ # Initialize the Async Slack Request Handler
363
+ handler = AsyncSlackRequestHandler(app)
 
364
 
365
  @api.post("/slack/events")
366
  async def slack_events(request: Request):
367
  """Endpoint for all Slack event subscriptions."""
368
+ try:
369
+ return await handler.handle_async(request)
370
+ except Exception as e:
371
+ logger.error(f"Error handling Slack events: {e}")
372
+ raise
373
 
374
  @api.get("/")
375
  async def root():
 
379
  @api.get("/health")
380
  async def health():
381
  """Health check endpoint."""
382
+ db_status = "error"
383
  try:
384
+ loop = asyncio.get_event_loop()
385
+ await loop.run_in_executor(
386
+ None,
387
+ lambda: supabase.table("installations").select("team_id").limit(1).execute()
388
+ )
389
  db_status = "ok"
390
+ except Exception as e:
391
+ logger.error(f"Database health check failed: {e}")
392
+
393
+ models_status = "ok" if embedding_model and qa_pipeline else "error"
394
+ return {"status": "ok" if db_status == "ok" and models_status == "ok" else "error", "database": db_status, "models": models_status}
395
 
396
  @api.get("/slack/install")
397
  async def install_url():
398
  """Generates the Slack installation URL."""
399
+ try:
400
+ generator = AuthorizeUrlGenerator(
401
+ client_id=SLACK_CLIENT_ID,
402
+ scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"]
403
+ )
404
+ # NOTE: The redirect_uri should match your Slack App config (e.g., https://yourspace.hf.space/slack/oauth/callback)
405
+ url = generator.generate(state="state")
406
+ return {"install_url": url}
407
+ except Exception as e:
408
+ logger.error(f"Error generating install URL: {e}")
409
+ raise
410
 
411
  @api.get("/slack/oauth/callback")
412
  async def oauth_callback(request: Request):
413
  """Handles the OAuth callback from Slack to complete installation."""
414
  try:
415
+ response = await handler.handle_async(request)
416
+ # For successful OAuth, return a simple HTML response
417
+ if response.status_code == 200:
418
+ return HTMLResponse(
419
+ content="<html><body><h1>Installation successful!</h1><p>You can now use the bot in your Slack workspace.</p></body></html>",
420
+ status_code=200
421
+ )
422
+ return response
423
  except Exception as e:
424
  logger.error(f"OAuth Callback Error: {e}")
425
+ return HTMLResponse(
426
+ content=f"<html><body><h1>Installation Failed!</h1><p>Error: {str(e)}</p></body></html>",
427
+ status_code=500
428
+ )
429
 
430
 
431
  if __name__ == "__main__":
 
432
  port = int(os.environ.get("PORT", 7860))
433
  uvicorn_run(api, host="0.0.0.0", port=port)