siddhartharya commited on
Commit
7391b3d
·
verified ·
1 Parent(s): f4e6753

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +183 -241
app.py CHANGED
@@ -8,13 +8,13 @@ import numpy as np
8
  import requests
9
  import time
10
  import re
 
11
  import logging
12
  import os
13
  import sys
14
- import threading
15
- from queue import Queue, Empty
16
- import json
17
  from concurrent.futures import ThreadPoolExecutor
 
18
 
19
  # Import OpenAI library
20
  import openai
@@ -74,91 +74,18 @@ CATEGORIES = [
74
  "Uncategorized",
75
  ]
76
 
77
- # Set up OpenAI API key and base URL
78
- OPENAI_API_KEY = os.getenv('GROQ_API_KEY') # Ensure this environment variable is set correctly
79
 
80
- if not OPENAI_API_KEY:
81
  logger.error("GROQ_API_KEY environment variable not set.")
82
 
83
- openai.api_key = OPENAI_API_KEY
84
- openai.api_base = "https://api.groq.com/openai/v1" # Ensure this is the correct base URL for your API
85
-
86
- # Rate Limiter Configuration
87
- RPM_LIMIT = 60 # Requests per minute (adjust based on your API's limit)
88
- TPM_LIMIT = 60000 # Tokens per minute (adjust based on your API's limit)
89
- BATCH_SIZE = 5 # Number of bookmarks per batch
90
-
91
- # Implementing a Token Bucket Rate Limiter
92
- class TokenBucket:
93
- def __init__(self, rate, capacity):
94
- self.rate = rate # tokens per second
95
- self.capacity = capacity
96
- self.tokens = capacity
97
- self.timestamp = time.time()
98
- self.lock = threading.Lock()
99
-
100
- def consume(self, tokens=1):
101
- with self.lock:
102
- now = time.time()
103
- elapsed = now - self.timestamp
104
- refill = elapsed * self.rate
105
- self.tokens = min(self.capacity, self.tokens + refill)
106
- self.timestamp = now
107
- if self.tokens >= tokens:
108
- self.tokens -= tokens
109
- return True
110
- else:
111
- return False
112
-
113
- def wait_for_token(self, tokens=1):
114
- while not self.consume(tokens):
115
- time.sleep(0.05)
116
 
117
- # Initialize rate limiters
118
- rpm_rate = RPM_LIMIT / 60 # tokens per second
119
- tpm_rate = TPM_LIMIT / 60 # tokens per second
120
-
121
- rpm_bucket = TokenBucket(rate=rpm_rate, capacity=RPM_LIMIT)
122
- tpm_bucket = TokenBucket(rate=tpm_rate, capacity=TPM_LIMIT)
123
-
124
- # Queue for LLM tasks
125
- llm_queue = Queue()
126
-
127
- def categorize_based_on_summary(summary, url):
128
- """
129
- Assign category based on keywords in the summary or URL.
130
- """
131
- summary_lower = summary.lower()
132
- url_lower = url.lower()
133
- if 'social media' in summary_lower or 'twitter' in summary_lower or 'x.com' in url_lower:
134
- return 'Social Media'
135
- elif 'wikipedia' in url_lower:
136
- return 'Reference and Knowledge Bases'
137
- elif 'cloud computing' in summary_lower or 'aws' in summary_lower:
138
- return 'Technology'
139
- elif 'news' in summary_lower or 'media' in summary_lower:
140
- return 'News and Media'
141
- elif 'education' in summary_lower or 'learning' in summary_lower:
142
- return 'Education and Learning'
143
- # Add more conditions as needed
144
- else:
145
- return 'Uncategorized'
146
-
147
- def validate_category(bookmark):
148
- """
149
- Further validate and adjust the category if needed.
150
- """
151
- # Example: Specific cases based on URL
152
- url_lower = bookmark['url'].lower()
153
- if 'facebook' in url_lower or 'x.com' in url_lower:
154
- return 'Social Media'
155
- elif 'wikipedia' in url_lower:
156
- return 'Reference and Knowledge Bases'
157
- elif 'aws.amazon.com' in url_lower:
158
- return 'Technology'
159
- # Add more specific cases as needed
160
- else:
161
- return bookmark['category']
162
 
163
  def extract_main_content(soup):
164
  """
@@ -229,140 +156,149 @@ def get_page_metadata(soup):
229
 
230
  return metadata
231
 
232
- def llm_worker():
233
  """
234
- Worker thread to process LLM tasks from the queue while respecting rate limits.
235
  """
236
- logger.info("LLM worker started.")
237
- while True:
238
- batch = []
 
 
 
239
  try:
240
- # Collect bookmarks up to BATCH_SIZE
241
- while len(batch) < BATCH_SIZE:
242
- bookmark = llm_queue.get(timeout=1)
243
- if bookmark is None:
244
- # Shutdown signal
245
- logger.info("LLM worker shutting down.")
246
- return
247
- if not bookmark.get('dead_link') and not bookmark.get('slow_link'):
248
- batch.append(bookmark)
249
- else:
250
- # Skip processing for dead or slow links
251
- bookmark['summary'] = 'No summary available.'
252
- bookmark['category'] = 'Uncategorized'
253
- llm_queue.task_done()
254
-
255
- except Empty:
256
- pass # No more bookmarks at the moment
257
-
258
- if batch:
259
- try:
260
- # Rate Limiting
261
- rpm_bucket.wait_for_token()
262
- # Estimate tokens: prompt + max_tokens
263
- # Here, we assume max_tokens=150 per bookmark
264
- total_tokens = 150 * len(batch)
265
- tpm_bucket.wait_for_token(tokens=total_tokens)
266
-
267
- # Prepare prompt
268
- prompt = "You are an assistant that creates concise webpage summaries and assigns categories.\n\n"
269
- prompt += "Provide summaries and categories for the following bookmarks:\n\n"
270
-
271
- for idx, bookmark in enumerate(batch, 1):
272
- prompt += f"Bookmark {idx}:\nURL: {bookmark['url']}\nTitle: {bookmark['title']}\n\n"
273
-
274
- # Corrected f-string without backslashes
275
- prompt += f"Categories:\n{', '.join([f'\"{cat}\"' for cat in CATEGORIES])}\n\n"
276
-
277
- prompt += "Format your response as a JSON object where each key is the bookmark URL and the value is another JSON object containing 'summary' and 'category'.\n\n"
278
- prompt += "Example:\n"
279
- prompt += "{\n"
280
- prompt += " \"https://example.com\": {\n"
281
- prompt += " \"summary\": \"This is an example summary.\",\n"
282
- prompt += " \"category\": \"Technology\"\n"
283
- prompt += " }\n"
284
- prompt += "}\n\n"
285
- prompt += "Now, provide the summaries and categories for the bookmarks listed above."
286
-
287
- response = openai.ChatCompletion.create(
288
- model='llama-3.1-70b-versatile', # Ensure this model is correct and available
289
- messages=[
290
- {"role": "user", "content": prompt}
291
- ],
292
- max_tokens=150 * len(batch),
293
- temperature=0.5,
294
- )
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
- content = response['choices'][0]['message']['content'].strip()
297
- if not content:
298
- raise ValueError("Empty response received from the model.")
299
-
300
- # Parse JSON response
301
- try:
302
- json_response = json.loads(content)
303
- for bookmark in batch:
304
- url = bookmark['url']
305
- if url in json_response:
306
- summary = json_response[url].get('summary', '').strip()
307
- category = json_response[url].get('category', '').strip()
308
-
309
- if not summary:
310
- summary = 'No summary available.'
311
- bookmark['summary'] = summary
312
-
313
- if category in CATEGORIES:
314
- bookmark['category'] = category
315
- else:
316
- # Fallback to keyword-based categorization
317
- bookmark['category'] = categorize_based_on_summary(summary, url)
318
- else:
319
- logger.warning(f"No data returned for {url}. Using fallback methods.")
320
- bookmark['summary'] = 'No summary available.'
321
- bookmark['category'] = 'Uncategorized'
322
-
323
- # Additional keyword-based validation
324
- bookmark['category'] = validate_category(bookmark)
325
-
326
- logger.info(f"Processed bookmark: {url}")
327
-
328
- except json.JSONDecodeError:
329
- logger.error("Failed to parse JSON response from LLM. Using fallback methods.")
330
- for bookmark in batch:
331
- bookmark['summary'] = 'No summary available.'
332
- bookmark['category'] = categorize_based_on_summary(bookmark.get('summary', ''), bookmark['url'])
333
- bookmark['category'] = validate_category(bookmark)
334
-
335
- except Exception as e:
336
- logger.error(f"Error processing LLM response: {e}", exc_info=True)
337
- for bookmark in batch:
338
- bookmark['summary'] = 'No summary available.'
339
- bookmark['category'] = 'Uncategorized'
340
-
341
- except openai.error.RateLimitError as e:
342
- logger.warning(f"LLM Rate limit reached. Retrying after 60 seconds.")
343
- # Re-enqueue the entire batch for retry
344
- for bookmark in batch:
345
- llm_queue.put(bookmark)
346
- time.sleep(60) # Wait before retrying
347
- continue # Skip the rest and retry
348
-
349
- except Exception as e:
350
- logger.error(f"Error during LLM processing: {e}", exc_info=True)
351
- for bookmark in batch:
352
- bookmark['summary'] = 'No summary available.'
353
- bookmark['category'] = 'Uncategorized'
354
 
355
- finally:
356
- # Mark all bookmarks in the batch as done
357
- for _ in batch:
358
- llm_queue.task_done()
359
 
360
- def generate_summary_and_assign_category(bookmark):
361
- """
362
- Enqueue bookmarks for LLM processing.
363
- """
364
- logger.info(f"Enqueuing bookmark for LLM processing: {bookmark.get('url')}")
365
- llm_queue.put(bookmark)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
  def parse_bookmarks(file_content):
368
  """
@@ -411,17 +347,15 @@ def fetch_url_info(bookmark):
411
 
412
  if response.status_code >= 500:
413
  bookmark['dead_link'] = True
414
- bookmark['html_content'] = ''
415
  bookmark['description'] = ''
 
416
  logger.warning(f"Dead link detected: {url} with status {response.status_code}")
417
  else:
418
  bookmark['dead_link'] = False
419
  bookmark['html_content'] = content
420
- # Extract description from metadata
421
- soup = BeautifulSoup(content, 'html.parser')
422
- metadata = get_page_metadata(soup)
423
- bookmark['description'] = metadata.get('description', '')
424
  logger.info(f"Fetched information for {url}")
 
425
  except requests.exceptions.Timeout:
426
  bookmark['dead_link'] = False
427
  bookmark['etag'] = 'N/A'
@@ -555,14 +489,10 @@ def process_uploaded_file(file, state_bookmarks):
555
  with ThreadPoolExecutor(max_workers=10) as executor:
556
  executor.map(fetch_url_info, bookmarks)
557
 
558
- # Enqueue bookmarks for LLM processing
559
- logger.info("Enqueuing bookmarks for LLM processing")
560
- for bookmark in bookmarks:
561
- generate_summary_and_assign_category(bookmark)
562
-
563
- # Wait until all LLM tasks are completed
564
- llm_queue.join()
565
- logger.info("All LLM tasks have been processed")
566
 
567
  try:
568
  faiss_index = vectorize_and_index(bookmarks)
@@ -689,11 +619,15 @@ def chatbot_response(user_query, chat_history):
689
  try:
690
  chat_history.append({"role": "user", "content": user_query})
691
 
692
- # Rate Limiting
693
- rpm_bucket.wait_for_token()
694
- # Estimate tokens: prompt + max_tokens
695
- # Here, we assume max_tokens=300 per chatbot response
696
- tpm_bucket.wait_for_token(tokens=300)
 
 
 
 
697
 
698
  query_vector = embedding_model.encode([user_query]).astype('float32')
699
  k = 5
@@ -701,8 +635,7 @@ def chatbot_response(user_query, chat_history):
701
  ids = ids.flatten()
702
 
703
  id_to_bookmark = {bookmark['id']: bookmark for bookmark in bookmarks}
704
- # Filter out bookmarks without summaries
705
- matching_bookmarks = [id_to_bookmark.get(id) for id in ids if id in id_to_bookmark and id_to_bookmark.get(id).get('summary')]
706
 
707
  if not matching_bookmarks:
708
  answer = "No relevant bookmarks found for your query."
@@ -722,17 +655,30 @@ Bookmarks:
722
  Provide a concise and helpful response.
723
  """
724
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  response = openai.ChatCompletion.create(
726
- model='llama-3.1-70b-versatile', # Ensure this model is correct and available
727
  messages=[
728
  {"role": "user", "content": prompt}
729
  ],
730
- max_tokens=300,
731
  temperature=0.7,
732
  )
733
 
734
  answer = response['choices'][0]['message']['content'].strip()
735
  logger.info("Chatbot response generated")
 
736
 
737
  chat_history.append({"role": "assistant", "content": answer})
738
  return chat_history
@@ -863,7 +809,7 @@ Navigate through the tabs to explore each feature in detail.
863
  """)
864
 
865
  manage_output = gr.Textbox(label="🔄 Status", interactive=False)
866
-
867
  # CheckboxGroup for selecting bookmarks
868
  bookmark_selector = gr.CheckboxGroup(
869
  label="✅ Select Bookmarks",
@@ -924,12 +870,8 @@ Navigate through the tabs to explore each feature in detail.
924
  logger.info("Launching Gradio app")
925
  demo.launch(debug=True)
926
  except Exception as e:
927
- logger.error(f"Error building Gradio app: {e}", exc_info=True)
928
- print(f"Error building Gradio app: {e}")
929
 
930
  if __name__ == "__main__":
931
- # Start the LLM worker thread before launching the app
932
- llm_thread = threading.Thread(target=llm_worker, daemon=True)
933
- llm_thread.start()
934
-
935
  build_app()
 
8
  import requests
9
  import time
10
  import re
11
+ import base64
12
  import logging
13
  import os
14
  import sys
15
+ import concurrent.futures
 
 
16
  from concurrent.futures import ThreadPoolExecutor
17
+ import threading
18
 
19
  # Import OpenAI library
20
  import openai
 
74
  "Uncategorized",
75
  ]
76
 
77
+ # Set up Groq Cloud API key and base URL
78
+ GROQ_API_KEY = os.getenv('GROQ_API_KEY')
79
 
80
+ if not GROQ_API_KEY:
81
  logger.error("GROQ_API_KEY environment variable not set.")
82
 
83
+ openai.api_key = GROQ_API_KEY
84
+ openai.api_base = "https://api.groq.com/openai/v1"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ # Initialize global variables for rate limiting
87
+ api_lock = threading.Lock()
88
+ last_api_call_time = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  def extract_main_content(soup):
91
  """
 
156
 
157
  return metadata
158
 
159
+ def generate_summary_and_assign_category(bookmark):
160
  """
161
+ Generate a concise summary and assign a category using a single LLM call.
162
  """
163
+ logger.info(f"Generating summary and assigning category for bookmark: {bookmark.get('url')}")
164
+
165
+ max_retries = 3
166
+ retry_count = 0
167
+
168
+ while retry_count < max_retries:
169
  try:
170
+ # Rate Limiting Logic
171
+ with api_lock:
172
+ global last_api_call_time
173
+ current_time = time.time()
174
+ elapsed = current_time - last_api_call_time
175
+ if elapsed < 2:
176
+ sleep_duration = 2 - elapsed
177
+ logger.info(f"Sleeping for {sleep_duration:.2f} seconds to respect rate limits.")
178
+ time.sleep(sleep_duration)
179
+ last_api_call_time = time.time()
180
+
181
+ html_content = bookmark.get('html_content', '')
182
+ soup = BeautifulSoup(html_content, 'html.parser')
183
+ metadata = get_page_metadata(soup)
184
+ main_content = extract_main_content(soup)
185
+
186
+ # Prepare content for the prompt
187
+ content_parts = []
188
+ if metadata['title']:
189
+ content_parts.append(f"Title: {metadata['title']}")
190
+ if metadata['description']:
191
+ content_parts.append(f"Description: {metadata['description']}")
192
+ if metadata['keywords']:
193
+ content_parts.append(f"Keywords: {metadata['keywords']}")
194
+ if main_content:
195
+ content_parts.append(f"Main Content: {main_content}")
196
+
197
+ content_text = '\n'.join(content_parts)
198
+
199
+ # Detect insufficient or erroneous content
200
+ error_keywords = ['Access Denied', 'Security Check', 'Cloudflare', 'captcha', 'unusual traffic']
201
+ if not content_text or len(content_text.split()) < 50:
202
+ use_prior_knowledge = True
203
+ logger.info(f"Content for {bookmark.get('url')} is insufficient. Instructing LLM to use prior knowledge.")
204
+ elif any(keyword.lower() in content_text.lower() for keyword in error_keywords):
205
+ use_prior_knowledge = True
206
+ logger.info(f"Content for {bookmark.get('url')} contains error messages. Instructing LLM to use prior knowledge.")
207
+ else:
208
+ use_prior_knowledge = False
209
+
210
+ if use_prior_knowledge:
211
+ prompt = f"""
212
+ You are a knowledgeable assistant with up-to-date information as of 2023.
213
+ URL: {bookmark.get('url')}
214
+ Provide:
215
+ 1. A concise summary (max two sentences) about this website.
216
+ 2. Assign the most appropriate category from the list below.
217
+ Categories:
218
+ {', '.join([f'"{cat}"' for cat in CATEGORIES])}
219
+ Format:
220
+ Summary: [Your summary]
221
+ Category: [One category]
222
+ """
223
+ else:
224
+ prompt = f"""
225
+ You are an assistant that creates concise webpage summaries and assigns categories.
226
+ Content:
227
+ {content_text}
228
+ Provide:
229
+ 1. A concise summary (max two sentences) focusing on the main topic.
230
+ 2. Assign the most appropriate category from the list below.
231
+ Categories:
232
+ {', '.join([f'"{cat}"' for cat in CATEGORIES])}
233
+ Format:
234
+ Summary: [Your summary]
235
+ Category: [One category]
236
+ """
237
 
238
+ def estimate_tokens(text):
239
+ return len(text) / 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ prompt_tokens = estimate_tokens(prompt)
242
+ max_tokens = 150
243
+ total_tokens = prompt_tokens + max_tokens
 
244
 
245
+ tokens_per_minute = 40000
246
+ tokens_per_second = tokens_per_minute / 60
247
+ required_delay = total_tokens / tokens_per_second
248
+ sleep_time = max(required_delay, 2)
249
+
250
+ response = openai.ChatCompletion.create(
251
+ model='llama-3.1-70b-versatile',
252
+ messages=[
253
+ {"role": "user", "content": prompt}
254
+ ],
255
+ max_tokens=int(max_tokens),
256
+ temperature=0.5,
257
+ )
258
+
259
+ content = response['choices'][0]['message']['content'].strip()
260
+ if not content:
261
+ raise ValueError("Empty response received from the model.")
262
+
263
+ summary_match = re.search(r"Summary:\s*(.*)", content)
264
+ category_match = re.search(r"Category:\s*(.*)", content)
265
+
266
+ if summary_match:
267
+ bookmark['summary'] = summary_match.group(1).strip()
268
+ else:
269
+ bookmark['summary'] = 'No summary available.'
270
+
271
+ if category_match:
272
+ category = category_match.group(1).strip().strip('"')
273
+ if category in CATEGORIES:
274
+ bookmark['category'] = category
275
+ else:
276
+ bookmark['category'] = 'Uncategorized'
277
+ else:
278
+ bookmark['category'] = 'Uncategorized'
279
+
280
+ # Simple keyword-based validation
281
+ summary_lower = bookmark['summary'].lower()
282
+ url_lower = bookmark['url'].lower()
283
+ if 'social media' in summary_lower or 'twitter' in summary_lower or 'x.com' in url_lower:
284
+ bookmark['category'] = 'Social Media'
285
+ elif 'wikipedia' in url_lower:
286
+ bookmark['category'] = 'Reference and Knowledge Bases'
287
+
288
+ logger.info("Successfully generated summary and assigned category")
289
+ time.sleep(sleep_time)
290
+ break
291
+
292
+ except openai.error.RateLimitError as e:
293
+ retry_count += 1
294
+ wait_time = int(e.headers.get("Retry-After", 5))
295
+ logger.warning(f"Rate limit reached. Waiting for {wait_time} seconds before retrying... (Attempt {retry_count}/{max_retries})")
296
+ time.sleep(wait_time)
297
+ except Exception as e:
298
+ logger.error(f"Error generating summary and assigning category: {e}", exc_info=True)
299
+ bookmark['summary'] = 'No summary available.'
300
+ bookmark['category'] = 'Uncategorized'
301
+ break
302
 
303
  def parse_bookmarks(file_content):
304
  """
 
347
 
348
  if response.status_code >= 500:
349
  bookmark['dead_link'] = True
 
350
  bookmark['description'] = ''
351
+ bookmark['html_content'] = ''
352
  logger.warning(f"Dead link detected: {url} with status {response.status_code}")
353
  else:
354
  bookmark['dead_link'] = False
355
  bookmark['html_content'] = content
356
+ bookmark['description'] = ''
 
 
 
357
  logger.info(f"Fetched information for {url}")
358
+
359
  except requests.exceptions.Timeout:
360
  bookmark['dead_link'] = False
361
  bookmark['etag'] = 'N/A'
 
489
  with ThreadPoolExecutor(max_workers=10) as executor:
490
  executor.map(fetch_url_info, bookmarks)
491
 
492
+ # Process bookmarks concurrently with LLM calls
493
+ logger.info("Processing bookmarks with LLM concurrently")
494
+ with ThreadPoolExecutor(max_workers=1) as executor:
495
+ executor.map(generate_summary_and_assign_category, bookmarks)
 
 
 
 
496
 
497
  try:
498
  faiss_index = vectorize_and_index(bookmarks)
 
619
  try:
620
  chat_history.append({"role": "user", "content": user_query})
621
 
622
+ with api_lock:
623
+ global last_api_call_time
624
+ current_time = time.time()
625
+ elapsed = current_time - last_api_call_time
626
+ if elapsed < 2:
627
+ sleep_duration = 2 - elapsed
628
+ logger.info(f"Sleeping for {sleep_duration:.2f} seconds to respect rate limits.")
629
+ time.sleep(sleep_duration)
630
+ last_api_call_time = time.time()
631
 
632
  query_vector = embedding_model.encode([user_query]).astype('float32')
633
  k = 5
 
635
  ids = ids.flatten()
636
 
637
  id_to_bookmark = {bookmark['id']: bookmark for bookmark in bookmarks}
638
+ matching_bookmarks = [id_to_bookmark.get(id) for id in ids if id in id_to_bookmark]
 
639
 
640
  if not matching_bookmarks:
641
  answer = "No relevant bookmarks found for your query."
 
655
  Provide a concise and helpful response.
656
  """
657
 
658
+ def estimate_tokens(text):
659
+ return len(text) / 4
660
+
661
+ prompt_tokens = estimate_tokens(prompt)
662
+ max_tokens = 300
663
+ total_tokens = prompt_tokens + max_tokens
664
+
665
+ tokens_per_minute = 40000
666
+ tokens_per_second = tokens_per_minute / 60
667
+ required_delay = total_tokens / tokens_per_second
668
+ sleep_time = max(required_delay, 2)
669
+
670
  response = openai.ChatCompletion.create(
671
+ model='llama-3.1-70b-versatile',
672
  messages=[
673
  {"role": "user", "content": prompt}
674
  ],
675
+ max_tokens=int(max_tokens),
676
  temperature=0.7,
677
  )
678
 
679
  answer = response['choices'][0]['message']['content'].strip()
680
  logger.info("Chatbot response generated")
681
+ time.sleep(sleep_time)
682
 
683
  chat_history.append({"role": "assistant", "content": answer})
684
  return chat_history
 
809
  """)
810
 
811
  manage_output = gr.Textbox(label="🔄 Status", interactive=False)
812
+
813
  # CheckboxGroup for selecting bookmarks
814
  bookmark_selector = gr.CheckboxGroup(
815
  label="✅ Select Bookmarks",
 
870
  logger.info("Launching Gradio app")
871
  demo.launch(debug=True)
872
  except Exception as e:
873
+ logger.error(f"Error building the app: {e}", exc_info=True)
874
+ print(f"Error building the app: {e}")
875
 
876
  if __name__ == "__main__":
 
 
 
 
877
  build_app()