KURUPRASATH-J commited on
Commit
83515d2
·
verified ·
1 Parent(s): 2a5bda7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -226
app.py CHANGED
@@ -12,6 +12,14 @@ try:
12
  except ImportError:
13
  print("pysqlite3 not found, using standard sqlite3 library.")
14
 
 
 
 
 
 
 
 
 
15
  import json
16
  import uuid
17
  import time
@@ -23,13 +31,13 @@ from dotenv import load_dotenv
23
  import google.generativeai as genai
24
  from google.api_core.exceptions import ResourceExhausted, GoogleAPIError
25
  from langchain.text_splitter import RecursiveCharacterTextSplitter
26
- # MODIFIED: LangChain imports updated to use langchain_community for better compatibility
27
  from langchain_community.vectorstores import Chroma
28
- from langchain_community.embeddings import HuggingFaceEmbeddings
 
29
  from langchain.schema import Document
30
  import PyPDF2
31
  import io
32
- import base64
33
  from typing import List, Dict, Any
34
  import requests
35
  from bs4 import BeautifulSoup
@@ -50,15 +58,13 @@ app = Flask(__name__)
50
  CORS(app)
51
 
52
  # --- Configuration ---
53
- # ADDED: Moved model names to environment variables for easier configuration
54
  GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
55
- GENERATIVE_MODEL = os.getenv('GENERATIVE_MODEL', 'gemini-1.5-flash')
56
  EMBEDDING_MODEL = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
57
 
58
  # Configure Gemini
59
  if not GEMINI_API_KEY:
60
  logging.error("GEMINI_API_KEY environment variable not set.")
61
- # Exit or handle the error appropriately
62
  else:
63
  genai.configure(api_key=GEMINI_API_KEY)
64
 
@@ -71,102 +77,85 @@ class ChatbotWithMemoryAndRAG:
71
  def __init__(self):
72
  """Initializes the chatbot instance."""
73
  logging.info("Initializing Juno AI...")
74
- self.embeddings = HuggingFaceEmbeddings(
75
- model_name=EMBEDDING_MODEL
76
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  self.text_splitter = RecursiveCharacterTextSplitter(
79
  chunk_size=1000,
80
  chunk_overlap=200,
81
  length_function=len
82
  )
83
-
84
  self.vectorstore = None
85
  self.chat_history = []
86
  self.memory = {}
87
  self.session_id = str(uuid.uuid4())
88
  self.last_rate_limit = None
89
  self.consecutive_rate_limits = 0
90
-
91
- # Initialize Juno AI Prompts System
92
  self.prompts = juno_prompts
93
-
94
  logging.info(f"🤖 Juno AI initialized with session ID: {self.session_id}")
95
 
96
  def _retry_with_backoff(self, func, max_retries=5, base_delay=2):
97
  """Improved retry function with progressive backoff for rate limit handling"""
98
-
99
- # If we recently hit rate limits, wait longer before trying
100
  if self.last_rate_limit and datetime.now() - self.last_rate_limit < timedelta(seconds=30):
101
- additional_wait = min(self.consecutive_rate_limits * 5, 30) # Up to 30 seconds
102
  logging.warning(f"Recent rate limits detected, waiting additional {additional_wait}s")
103
  time.sleep(additional_wait)
104
-
105
  for attempt in range(max_retries):
106
  try:
107
  result = func()
108
- # Reset rate limit tracking on success
109
  self.consecutive_rate_limits = 0
110
  self.last_rate_limit = None
111
  return result
112
-
113
  except ResourceExhausted as e:
114
  self.last_rate_limit = datetime.now()
115
  self.consecutive_rate_limits += 1
116
-
117
  if attempt == max_retries - 1:
118
  logging.error(f"Max retries ({max_retries}) exceeded for rate limit.")
119
  raise e
120
-
121
- # Progressive backoff with jitter: 2s, 6s, 14s, 30s, 62s
122
- delay = base_delay * (2 ** attempt) + random.uniform(1, 3) # Add jitter
123
- delay = min(delay, 60) # Cap at 60 seconds
124
-
125
  logging.warning(f"Rate limit hit (attempt {attempt + 1}/{max_retries}), waiting {delay:.1f}s...")
126
  time.sleep(delay)
127
-
128
  except GoogleAPIError as e:
129
  logging.error(f"Google API Error: {e}")
130
  if "quota" in str(e).lower() or "rate" in str(e).lower():
131
- # Treat as rate limit
132
  self.last_rate_limit = datetime.now()
133
  self.consecutive_rate_limits += 1
134
-
135
  if attempt == max_retries - 1:
136
  raise ResourceExhausted("API quota exceeded")
137
-
138
  delay = base_delay * (2 ** attempt) + random.uniform(1, 3)
139
  delay = min(delay, 60)
140
  logging.warning(f"API quota issue, waiting {delay:.1f}s...")
141
  time.sleep(delay)
142
  else:
143
  raise e
144
-
145
  except Exception as e:
146
- # For non-rate-limit errors, don't retry
147
  logging.error(f"Non-retryable error: {e}", exc_info=True)
148
  raise e
149
 
150
  def _fallback_response(self, user_message):
151
- """Generate a fallback response when API is unavailable using Juno AI prompts"""
152
  logging.warning(f"Generating fallback response for message: '{user_message[:50]}...'")
153
- # Use Juno AI fallback response templates
154
  fallback_templates = get_fallback_responses()
155
-
156
- # Select a template and personalize it
157
  template = random.choice(fallback_templates)
158
- response = template.format(
159
- user_message_preview=user_message[:50]
160
- )
161
-
162
- # Add to chat history so conversation continues
163
- self.chat_history.append({
164
- "user": user_message,
165
- "bot": response,
166
- "timestamp": datetime.now().isoformat(),
167
- "fallback": True
168
- })
169
-
170
  return response
171
 
172
  def extract_text_from_pdf(self, pdf_content):
@@ -174,29 +163,20 @@ class ChatbotWithMemoryAndRAG:
174
  try:
175
  pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_content))
176
  text = ""
177
-
178
  for i, page in enumerate(pdf_reader.pages):
179
  page_text = page.extract_text()
180
- # Check if extracted text is substantial
181
- if page_text and len(page_text.strip()) > 10: # Heuristic to check for actual content
182
  text += page_text + "\n"
183
  else:
184
- # Attempt OCR if text extraction is poor
185
  logging.info(f"Poor text extraction on page {i+1}. Attempting OCR fallback.")
186
  try:
187
- # Iterate through images on the page for OCR
188
  for image_file_object in page.images:
189
  img = Image.open(io.BytesIO(image_file_object.data))
190
- # ADDED: Specify language for better OCR accuracy if needed
191
- # ocr_text = pytesseract.image_to_string(img, lang='eng')
192
  ocr_text = pytesseract.image_to_string(img)
193
  if ocr_text:
194
  text += ocr_text + "\n"
195
  except Exception as ocr_error:
196
- # OCR can fail if no images, etc. Silently pass.
197
  logging.warning(f"OCR fallback failed for a page: {ocr_error}")
198
- pass
199
-
200
  return text
201
  except Exception as e:
202
  logging.error(f"Error extracting PDF: {e}", exc_info=True)
@@ -204,30 +184,18 @@ class ChatbotWithMemoryAndRAG:
204
 
205
  def process_document(self, text_content, filename="document"):
206
  """Process document text and create vector store"""
 
 
 
 
207
  try:
208
  logging.info(f"Processing document: {filename}")
209
- # Split text into chunks
210
  chunks = self.text_splitter.split_text(text_content)
211
-
212
- # Create documents
213
- documents = [
214
- Document(
215
- page_content=chunk,
216
- metadata={"source": filename, "chunk_id": i}
217
- )
218
- for i, chunk in enumerate(chunks)
219
- ]
220
-
221
- # Create or update vector store
222
  if self.vectorstore is None:
223
- self.vectorstore = Chroma.from_documents(
224
- documents=documents,
225
- embedding=self.embeddings,
226
- collection_name=f"collection_{self.session_id}"
227
- )
228
  else:
229
  self.vectorstore.add_documents(documents)
230
-
231
  logging.info(f"Successfully processed {len(chunks)} chunks from {filename}")
232
  return f"Successfully processed {len(chunks)} chunks from {filename}"
233
  except Exception as e:
@@ -238,27 +206,19 @@ class ChatbotWithMemoryAndRAG:
238
  """Retrieve relevant context from vector store"""
239
  if self.vectorstore is None:
240
  return ""
241
-
242
  try:
243
  docs = self.vectorstore.similarity_search(query, k=k)
244
- context = "\n".join([doc.page_content for doc in docs])
245
- return context
246
  except Exception as e:
247
  logging.error(f"Error retrieving context: {e}", exc_info=True)
248
  return ""
249
 
250
  def summarize_text(self, text, max_length=500):
251
- """Summarize long text using Juno AI prompts with improved rate limit handling"""
252
  def _summarize():
253
- # MODIFIED: Use configured generative model
254
  model = genai.GenerativeModel(GENERATIVE_MODEL)
255
-
256
- # Use Juno AI document summarization prompt
257
  prompt = self.prompts.get_document_summarization_prompt(text, max_length)
258
-
259
- response = model.generate_content(prompt)
260
- return response.text
261
-
262
  try:
263
  return self._retry_with_backoff(_summarize)
264
  except (ResourceExhausted, GoogleAPIError):
@@ -269,49 +229,21 @@ class ChatbotWithMemoryAndRAG:
269
  return f"Error summarizing text: {str(e)}"
270
 
271
  def generate_response(self, user_message, context=""):
272
- """Generate response using Juno AI prompts with improved rate limit handling"""
273
  def _generate():
274
- # MODIFIED: Use configured generative model
275
  model = genai.GenerativeModel(GENERATIVE_MODEL)
276
-
277
- # Build conversation context for Juno AI
278
  conversation_history = []
279
  if self.chat_history:
280
- recent_history = self.chat_history[-3:] # Last 3 exchanges
281
- for exchange in recent_history:
282
- if not exchange.get('fallback', False): # Skip fallback responses
283
- conversation_history.append({
284
- 'user': exchange['user'],
285
- 'bot': exchange['bot'],
286
- 'timestamp': exchange.get('timestamp', '')
287
- })
288
-
289
- # Use Juno AI conversation prompt with full context
290
- prompt = self.prompts.get_conversation_prompt(
291
- user_message=user_message,
292
- context=context,
293
- conversation_history=conversation_history,
294
- memory_context=self.memory
295
- )
296
-
297
- response = model.generate_content(prompt)
298
- return response.text
299
-
300
  try:
301
  bot_response = self._retry_with_backoff(_generate)
302
-
303
- # Update chat history
304
- self.chat_history.append({
305
- "user": user_message,
306
- "bot": bot_response,
307
- "timestamp": datetime.now().isoformat()
308
- })
309
-
310
- # Update memory with important information
311
  self.update_memory(user_message, bot_response)
312
-
313
  return bot_response
314
-
315
  except (ResourceExhausted, GoogleAPIError):
316
  return self._fallback_response(user_message)
317
  except Exception as e:
@@ -320,47 +252,23 @@ class ChatbotWithMemoryAndRAG:
320
 
321
  def update_memory(self, user_message, bot_response):
322
  """Update session memory with important information"""
323
- current_time = datetime.now().isoformat()
324
-
325
- if "memory" not in self.memory:
326
- self.memory["memory"] = []
327
-
328
- self.memory["memory"].append({
329
- "user": user_message,
330
- "bot": bot_response,
331
- "timestamp": current_time
332
- })
333
-
334
- # Keep only last 10 interactions in memory
335
- if len(self.memory["memory"]) > 10:
336
- self.memory["memory"] = self.memory["memory"][-10:]
337
 
338
  def scrape_web_content(self, url):
339
  """Scrape content from a web URL"""
340
  try:
341
  logging.info(f"Scraping web content from: {url}")
342
- headers = {
343
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
344
- }
345
-
346
  response = requests.get(url, headers=headers, timeout=10)
347
  response.raise_for_status()
348
-
349
  soup = BeautifulSoup(response.content, 'html.parser')
350
-
351
- # Remove script and style elements
352
- for script in soup(["script", "style"]):
353
- script.decompose()
354
-
355
- # Get text content
356
  text = soup.get_text()
357
-
358
- # Clean up text
359
  lines = (line.strip() for line in text.splitlines())
360
  chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
361
- text = ' '.join(chunk for chunk in chunks if chunk)
362
-
363
- return text[:10000] # Limit to 10000 characters
364
  except Exception as e:
365
  logging.error(f"Error scraping URL '{url}': {e}", exc_info=True)
366
  return f"Error scraping URL: {str(e)}"
@@ -368,15 +276,9 @@ class ChatbotWithMemoryAndRAG:
368
  def analyze_web_content(self, url, content):
369
  """Analyze scraped web content using Juno AI prompts"""
370
  def _analyze():
371
- # MODIFIED: Use configured generative model
372
  model = genai.GenerativeModel(GENERATIVE_MODEL)
373
-
374
- # Use Juno AI web content analysis prompt
375
  prompt = self.prompts.get_web_content_analysis_prompt(url, content)
376
-
377
- response = model.generate_content(prompt)
378
- return response.text
379
-
380
  try:
381
  return self._retry_with_backoff(_analyze)
382
  except (ResourceExhausted, GoogleAPIError):
@@ -389,22 +291,10 @@ class ChatbotWithMemoryAndRAG:
389
  def generate_rag_response(self, user_query, context, sources=None):
390
  """Generate RAG response using Juno AI prompts"""
391
  def _generate_rag():
392
- # MODIFIED: Use configured generative model
393
  model = genai.GenerativeModel(GENERATIVE_MODEL)
394
-
395
- # Split context into chunks for better handling
396
  context_chunks = [context[i:i+2000] for i in range(0, len(context), 2000)]
397
-
398
- # Use Juno AI RAG prompt
399
- prompt = self.prompts.get_rag_response_prompt(
400
- user_query=user_query,
401
- retrieved_chunks=context_chunks[:3], # Top 3 chunks
402
- source_info=sources
403
- )
404
-
405
- response = model.generate_content(prompt)
406
- return response.text
407
-
408
  try:
409
  return self._retry_with_backoff(_generate_rag)
410
  except (ResourceExhausted, GoogleAPIError):
@@ -415,20 +305,9 @@ class ChatbotWithMemoryAndRAG:
415
 
416
  def save_conversation(self, conversation_id, title=""):
417
  """Save current conversation to memory"""
418
- if not title:
419
- title = f"Chat {datetime.now().strftime('%Y-%m-%d %H:%M')}"
420
-
421
- conversation_data = {
422
- "id": conversation_id,
423
- "title": title,
424
- "messages": self.chat_history,
425
- "created_at": datetime.now().isoformat(),
426
- "last_updated": datetime.now().isoformat()
427
- }
428
-
429
- if "conversations" not in self.memory:
430
- self.memory["conversations"] = {}
431
-
432
  self.memory["conversations"][conversation_id] = conversation_data
433
  logging.info(f"Conversation '{conversation_id}' saved with title '{title}'.")
434
  return conversation_data
@@ -463,17 +342,11 @@ class ChatbotWithMemoryAndRAG:
463
  return False
464
 
465
  def generate_streaming_response(self, user_message, context=""):
466
- """Generate streaming response using Juno AI prompts with improved rate limit handling"""
467
  def _generate_stream():
468
- # MODIFIED: Use configured generative model
469
  model = genai.GenerativeModel(GENERATIVE_MODEL)
470
-
471
- # Use Juno AI streaming prompt (optimized for speed)
472
  prompt = self.prompts.get_streaming_response_prompt(user_message, context)
473
-
474
- response = model.generate_content(prompt, stream=True)
475
- return response
476
-
477
  try:
478
  return self._retry_with_backoff(_generate_stream, max_retries=3, base_delay=1)
479
  except (ResourceExhausted, GoogleAPIError):
@@ -532,8 +405,7 @@ def upload_document():
532
  return jsonify({'error': 'No file selected'}), 400
533
 
534
  if file and file.filename.lower().endswith('.pdf'):
535
- pdf_content = file.read()
536
- text_content = chatbot.extract_text_from_pdf(pdf_content)
537
 
538
  if text_content.startswith("Error"):
539
  return jsonify({'error': text_content}), 400
@@ -644,9 +516,7 @@ def chat_stream():
644
  'streaming': False
645
  })
646
 
647
- full_response = ""
648
- response_chunks = []
649
-
650
  try:
651
  for chunk in streaming_response:
652
  if chunk.text:
@@ -664,13 +534,8 @@ def chat_stream():
664
  'streaming': False
665
  })
666
 
667
- chatbot.chat_history.append({
668
- "user": user_message,
669
- "bot": full_response,
670
- "timestamp": datetime.now().isoformat()
671
- })
672
  chatbot.update_memory(user_message, full_response)
673
-
674
  return jsonify({
675
  'response': full_response,
676
  'chunks': response_chunks,
@@ -688,13 +553,7 @@ def get_conversations():
688
  conversations = []
689
  if "conversations" in chatbot.memory:
690
  for conv_id, conv_data in chatbot.memory["conversations"].items():
691
- conversations.append({
692
- 'id': conv_id,
693
- 'title': conv_data['title'],
694
- 'created_at': conv_data['created_at'],
695
- 'last_updated': conv_data['last_updated'],
696
- 'message_count': len(conv_data['messages'])
697
- })
698
  conversations.sort(key=lambda x: x['last_updated'], reverse=True)
699
  return jsonify({'conversations': conversations})
700
  except Exception as e:
@@ -742,10 +601,8 @@ def rename_conversation(conversation_id):
742
  try:
743
  data = request.json
744
  new_title = data.get('title', '')
745
-
746
  if not new_title:
747
  return jsonify({'error': 'No title provided'}), 400
748
-
749
  success = chatbot.rename_conversation(conversation_id, new_title)
750
  if success:
751
  return jsonify({'message': 'Conversation renamed successfully'})
@@ -760,16 +617,13 @@ def edit_message(message_index):
760
  try:
761
  data = request.json
762
  new_message = data.get('message', '')
763
-
764
  if not new_message:
765
  return jsonify({'error': 'No message provided'}), 400
766
-
767
- if 0 <= message_index < len(chatbot.chat_history):
768
  chatbot.chat_history[message_index]['user'] = new_message
769
  chatbot.chat_history[message_index]['edited'] = True
770
  chatbot.chat_history[message_index]['edited_at'] = datetime.now().isoformat()
771
  chatbot.chat_history = chatbot.chat_history[:message_index + 1]
772
-
773
  return jsonify({'message': 'Message edited successfully', 'updated_history': chatbot.chat_history})
774
  else:
775
  return jsonify({'error': 'Invalid message index'}), 400
@@ -778,11 +632,8 @@ def edit_message(message_index):
778
  return jsonify({'error': 'An internal server error occurred.'}), 500
779
 
780
  if __name__ == '__main__':
781
- print("🚀 Starting Juno AI Server...")
782
- print("🤖 Advanced AI Assistant with Document Processing, Web Scraping, and Memory")
783
- print("🌟 Powered by Juno AI Prompts System")
784
-
785
- # Get port from environment variable (Hugging Face Spaces uses PORT env var)
786
- port = int(os.environ.get('PORT', 7860))
787
-
788
- app.run(debug=False, host='0.0.0.0', port=port)
 
12
  except ImportError:
13
  print("pysqlite3 not found, using standard sqlite3 library.")
14
 
15
+ # NEWLY ADDED: Set up proper cache directories for deployment environments
16
+ os.environ['TRANSFORMERS_CACHE'] = '/code/.cache/huggingface'
17
+ os.environ['HF_HOME'] = '/code/.cache/huggingface'
18
+ os.environ['TORCH_HOME'] = '/code/.cache/torch'
19
+ os.environ['HF_HUB_CACHE'] = '/code/.cache/huggingface'
20
+ os.environ['SENTENCE_TRANSFORMERS_HOME'] = '/code/.cache/sentence_transformers'
21
+
22
+
23
  import json
24
  import uuid
25
  import time
 
31
  import google.generativeai as genai
32
  from google.api_core.exceptions import ResourceExhausted, GoogleAPIError
33
  from langchain.text_splitter import RecursiveCharacterTextSplitter
34
+ # MODIFIED: LangChain imports updated for compatibility
35
  from langchain_community.vectorstores import Chroma
36
+ # NEWLY MODIFIED: Use the dedicated langchain-huggingface package for embeddings
37
+ from langchain_huggingface import HuggingFaceEmbeddings
38
  from langchain.schema import Document
39
  import PyPDF2
40
  import io
 
41
  from typing import List, Dict, Any
42
  import requests
43
  from bs4 import BeautifulSoup
 
58
  CORS(app)
59
 
60
  # --- Configuration ---
 
61
  GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
62
+ GENERATIVE_MODEL = os.getenv('GENERATIVE_MODEL', 'gemini-2.5-flash')
63
  EMBEDDING_MODEL = os.getenv('EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2')
64
 
65
  # Configure Gemini
66
  if not GEMINI_API_KEY:
67
  logging.error("GEMINI_API_KEY environment variable not set.")
 
68
  else:
69
  genai.configure(api_key=GEMINI_API_KEY)
70
 
 
77
  def __init__(self):
78
  """Initializes the chatbot instance."""
79
  logging.info("Initializing Juno AI...")
80
+
81
+ # NEWLY MODIFIED: More robust embedding model initialization
82
+ try:
83
+ cache_dir = os.environ.get('SENTENCE_TRANSFORMERS_HOME', '/code/.cache/sentence_transformers')
84
+ os.makedirs(cache_dir, exist_ok=True)
85
+
86
+ logging.info(f"Initializing embeddings with model: {EMBEDDING_MODEL}")
87
+ self.embeddings = HuggingFaceEmbeddings(
88
+ model_name=EMBEDDING_MODEL,
89
+ cache_folder=cache_dir,
90
+ model_kwargs={'device': 'cpu'},
91
+ encode_kwargs={'normalize_embeddings': True}
92
+ )
93
+ logging.info("HuggingFace Embeddings initialized successfully.")
94
+ except Exception as e:
95
+ logging.error(f"CRITICAL: Could not initialize embeddings: {e}", exc_info=True)
96
+ logging.warning("Continuing without embeddings - RAG features will be disabled.")
97
+ self.embeddings = None
98
 
99
  self.text_splitter = RecursiveCharacterTextSplitter(
100
  chunk_size=1000,
101
  chunk_overlap=200,
102
  length_function=len
103
  )
 
104
  self.vectorstore = None
105
  self.chat_history = []
106
  self.memory = {}
107
  self.session_id = str(uuid.uuid4())
108
  self.last_rate_limit = None
109
  self.consecutive_rate_limits = 0
 
 
110
  self.prompts = juno_prompts
 
111
  logging.info(f"🤖 Juno AI initialized with session ID: {self.session_id}")
112
 
113
  def _retry_with_backoff(self, func, max_retries=5, base_delay=2):
114
  """Improved retry function with progressive backoff for rate limit handling"""
 
 
115
  if self.last_rate_limit and datetime.now() - self.last_rate_limit < timedelta(seconds=30):
116
+ additional_wait = min(self.consecutive_rate_limits * 5, 30)
117
  logging.warning(f"Recent rate limits detected, waiting additional {additional_wait}s")
118
  time.sleep(additional_wait)
 
119
  for attempt in range(max_retries):
120
  try:
121
  result = func()
 
122
  self.consecutive_rate_limits = 0
123
  self.last_rate_limit = None
124
  return result
 
125
  except ResourceExhausted as e:
126
  self.last_rate_limit = datetime.now()
127
  self.consecutive_rate_limits += 1
 
128
  if attempt == max_retries - 1:
129
  logging.error(f"Max retries ({max_retries}) exceeded for rate limit.")
130
  raise e
131
+ delay = base_delay * (2 ** attempt) + random.uniform(1, 3)
132
+ delay = min(delay, 60)
 
 
 
133
  logging.warning(f"Rate limit hit (attempt {attempt + 1}/{max_retries}), waiting {delay:.1f}s...")
134
  time.sleep(delay)
 
135
  except GoogleAPIError as e:
136
  logging.error(f"Google API Error: {e}")
137
  if "quota" in str(e).lower() or "rate" in str(e).lower():
 
138
  self.last_rate_limit = datetime.now()
139
  self.consecutive_rate_limits += 1
 
140
  if attempt == max_retries - 1:
141
  raise ResourceExhausted("API quota exceeded")
 
142
  delay = base_delay * (2 ** attempt) + random.uniform(1, 3)
143
  delay = min(delay, 60)
144
  logging.warning(f"API quota issue, waiting {delay:.1f}s...")
145
  time.sleep(delay)
146
  else:
147
  raise e
 
148
  except Exception as e:
 
149
  logging.error(f"Non-retryable error: {e}", exc_info=True)
150
  raise e
151
 
152
  def _fallback_response(self, user_message):
153
+ """Generate a fallback response when API is unavailable"""
154
  logging.warning(f"Generating fallback response for message: '{user_message[:50]}...'")
 
155
  fallback_templates = get_fallback_responses()
 
 
156
  template = random.choice(fallback_templates)
157
+ response = template.format(user_message_preview=user_message[:50])
158
+ self.chat_history.append({"user": user_message, "bot": response, "timestamp": datetime.now().isoformat(), "fallback": True})
 
 
 
 
 
 
 
 
 
 
159
  return response
160
 
161
  def extract_text_from_pdf(self, pdf_content):
 
163
  try:
164
  pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_content))
165
  text = ""
 
166
  for i, page in enumerate(pdf_reader.pages):
167
  page_text = page.extract_text()
168
+ if page_text and len(page_text.strip()) > 10:
 
169
  text += page_text + "\n"
170
  else:
 
171
  logging.info(f"Poor text extraction on page {i+1}. Attempting OCR fallback.")
172
  try:
 
173
  for image_file_object in page.images:
174
  img = Image.open(io.BytesIO(image_file_object.data))
 
 
175
  ocr_text = pytesseract.image_to_string(img)
176
  if ocr_text:
177
  text += ocr_text + "\n"
178
  except Exception as ocr_error:
 
179
  logging.warning(f"OCR fallback failed for a page: {ocr_error}")
 
 
180
  return text
181
  except Exception as e:
182
  logging.error(f"Error extracting PDF: {e}", exc_info=True)
 
184
 
185
  def process_document(self, text_content, filename="document"):
186
  """Process document text and create vector store"""
187
+ # NEWLY ADDED: Graceful handling if embeddings failed to initialize
188
+ if self.embeddings is None:
189
+ logging.error("Embeddings are not available. Cannot process document.")
190
+ return "Error: Document processing is disabled because the embedding model could not be loaded."
191
  try:
192
  logging.info(f"Processing document: {filename}")
 
193
  chunks = self.text_splitter.split_text(text_content)
194
+ documents = [Document(page_content=chunk, metadata={"source": filename, "chunk_id": i}) for i, chunk in enumerate(chunks)]
 
 
 
 
 
 
 
 
 
 
195
  if self.vectorstore is None:
196
+ self.vectorstore = Chroma.from_documents(documents=documents, embedding=self.embeddings, collection_name=f"collection_{self.session_id}")
 
 
 
 
197
  else:
198
  self.vectorstore.add_documents(documents)
 
199
  logging.info(f"Successfully processed {len(chunks)} chunks from {filename}")
200
  return f"Successfully processed {len(chunks)} chunks from {filename}"
201
  except Exception as e:
 
206
  """Retrieve relevant context from vector store"""
207
  if self.vectorstore is None:
208
  return ""
 
209
  try:
210
  docs = self.vectorstore.similarity_search(query, k=k)
211
+ return "\n".join([doc.page_content for doc in docs])
 
212
  except Exception as e:
213
  logging.error(f"Error retrieving context: {e}", exc_info=True)
214
  return ""
215
 
216
  def summarize_text(self, text, max_length=500):
217
+ """Summarize long text using Juno AI prompts"""
218
  def _summarize():
 
219
  model = genai.GenerativeModel(GENERATIVE_MODEL)
 
 
220
  prompt = self.prompts.get_document_summarization_prompt(text, max_length)
221
+ return model.generate_content(prompt).text
 
 
 
222
  try:
223
  return self._retry_with_backoff(_summarize)
224
  except (ResourceExhausted, GoogleAPIError):
 
229
  return f"Error summarizing text: {str(e)}"
230
 
231
  def generate_response(self, user_message, context=""):
232
+ """Generate response using Juno AI prompts"""
233
  def _generate():
 
234
  model = genai.GenerativeModel(GENERATIVE_MODEL)
 
 
235
  conversation_history = []
236
  if self.chat_history:
237
+ for exchange in self.chat_history[-3:]:
238
+ if not exchange.get('fallback', False):
239
+ conversation_history.append({'user': exchange['user'], 'bot': exchange['bot'], 'timestamp': exchange.get('timestamp', '')})
240
+ prompt = self.prompts.get_conversation_prompt(user_message=user_message, context=context, conversation_history=conversation_history, memory_context=self.memory)
241
+ return model.generate_content(prompt).text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  try:
243
  bot_response = self._retry_with_backoff(_generate)
244
+ self.chat_history.append({"user": user_message, "bot": bot_response, "timestamp": datetime.now().isoformat()})
 
 
 
 
 
 
 
 
245
  self.update_memory(user_message, bot_response)
 
246
  return bot_response
 
247
  except (ResourceExhausted, GoogleAPIError):
248
  return self._fallback_response(user_message)
249
  except Exception as e:
 
252
 
253
  def update_memory(self, user_message, bot_response):
254
  """Update session memory with important information"""
255
+ if "memory" not in self.memory: self.memory["memory"] = []
256
+ self.memory["memory"].append({"user": user_message, "bot": bot_response, "timestamp": datetime.now().isoformat()})
257
+ if len(self.memory["memory"]) > 10: self.memory["memory"] = self.memory["memory"][-10:]
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  def scrape_web_content(self, url):
260
  """Scrape content from a web URL"""
261
  try:
262
  logging.info(f"Scraping web content from: {url}")
263
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
 
 
 
264
  response = requests.get(url, headers=headers, timeout=10)
265
  response.raise_for_status()
 
266
  soup = BeautifulSoup(response.content, 'html.parser')
267
+ for script in soup(["script", "style"]): script.decompose()
 
 
 
 
 
268
  text = soup.get_text()
 
 
269
  lines = (line.strip() for line in text.splitlines())
270
  chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
271
+ return ' '.join(chunk for chunk in chunks if chunk)[:10000]
 
 
272
  except Exception as e:
273
  logging.error(f"Error scraping URL '{url}': {e}", exc_info=True)
274
  return f"Error scraping URL: {str(e)}"
 
276
  def analyze_web_content(self, url, content):
277
  """Analyze scraped web content using Juno AI prompts"""
278
  def _analyze():
 
279
  model = genai.GenerativeModel(GENERATIVE_MODEL)
 
 
280
  prompt = self.prompts.get_web_content_analysis_prompt(url, content)
281
+ return model.generate_content(prompt).text
 
 
 
282
  try:
283
  return self._retry_with_backoff(_analyze)
284
  except (ResourceExhausted, GoogleAPIError):
 
291
  def generate_rag_response(self, user_query, context, sources=None):
292
  """Generate RAG response using Juno AI prompts"""
293
  def _generate_rag():
 
294
  model = genai.GenerativeModel(GENERATIVE_MODEL)
 
 
295
  context_chunks = [context[i:i+2000] for i in range(0, len(context), 2000)]
296
+ prompt = self.prompts.get_rag_response_prompt(user_query=user_query, retrieved_chunks=context_chunks[:3], source_info=sources)
297
+ return model.generate_content(prompt).text
 
 
 
 
 
 
 
 
 
298
  try:
299
  return self._retry_with_backoff(_generate_rag)
300
  except (ResourceExhausted, GoogleAPIError):
 
305
 
306
  def save_conversation(self, conversation_id, title=""):
307
  """Save current conversation to memory"""
308
+ if not title: title = f"Chat {datetime.now().strftime('%Y-%m-%d %H:%M')}"
309
+ conversation_data = {"id": conversation_id, "title": title, "messages": self.chat_history, "created_at": datetime.now().isoformat(), "last_updated": datetime.now().isoformat()}
310
+ if "conversations" not in self.memory: self.memory["conversations"] = {}
 
 
 
 
 
 
 
 
 
 
 
311
  self.memory["conversations"][conversation_id] = conversation_data
312
  logging.info(f"Conversation '{conversation_id}' saved with title '{title}'.")
313
  return conversation_data
 
342
  return False
343
 
344
  def generate_streaming_response(self, user_message, context=""):
345
+ """Generate streaming response using Juno AI prompts"""
346
  def _generate_stream():
 
347
  model = genai.GenerativeModel(GENERATIVE_MODEL)
 
 
348
  prompt = self.prompts.get_streaming_response_prompt(user_message, context)
349
+ return model.generate_content(prompt, stream=True)
 
 
 
350
  try:
351
  return self._retry_with_backoff(_generate_stream, max_retries=3, base_delay=1)
352
  except (ResourceExhausted, GoogleAPIError):
 
405
  return jsonify({'error': 'No file selected'}), 400
406
 
407
  if file and file.filename.lower().endswith('.pdf'):
408
+ text_content = chatbot.extract_text_from_pdf(file.read())
 
409
 
410
  if text_content.startswith("Error"):
411
  return jsonify({'error': text_content}), 400
 
516
  'streaming': False
517
  })
518
 
519
+ full_response, response_chunks = "", []
 
 
520
  try:
521
  for chunk in streaming_response:
522
  if chunk.text:
 
534
  'streaming': False
535
  })
536
 
537
+ chatbot.chat_history.append({"user": user_message, "bot": full_response, "timestamp": datetime.now().isoformat()})
 
 
 
 
538
  chatbot.update_memory(user_message, full_response)
 
539
  return jsonify({
540
  'response': full_response,
541
  'chunks': response_chunks,
 
553
  conversations = []
554
  if "conversations" in chatbot.memory:
555
  for conv_id, conv_data in chatbot.memory["conversations"].items():
556
+ conversations.append({'id': conv_id, 'title': conv_data['title'], 'created_at': conv_data['created_at'], 'last_updated': conv_data['last_updated'], 'message_count': len(conv_data['messages'])})
 
 
 
 
 
 
557
  conversations.sort(key=lambda x: x['last_updated'], reverse=True)
558
  return jsonify({'conversations': conversations})
559
  except Exception as e:
 
601
  try:
602
  data = request.json
603
  new_title = data.get('title', '')
 
604
  if not new_title:
605
  return jsonify({'error': 'No title provided'}), 400
 
606
  success = chatbot.rename_conversation(conversation_id, new_title)
607
  if success:
608
  return jsonify({'message': 'Conversation renamed successfully'})
 
617
  try:
618
  data = request.json
619
  new_message = data.get('message', '')
 
620
  if not new_message:
621
  return jsonify({'error': 'No message provided'}), 400
622
+ if 0 <= message_index < len( chatbot.chat_history):
 
623
  chatbot.chat_history[message_index]['user'] = new_message
624
  chatbot.chat_history[message_index]['edited'] = True
625
  chatbot.chat_history[message_index]['edited_at'] = datetime.now().isoformat()
626
  chatbot.chat_history = chatbot.chat_history[:message_index + 1]
 
627
  return jsonify({'message': 'Message edited successfully', 'updated_history': chatbot.chat_history})
628
  else:
629
  return jsonify({'error': 'Invalid message index'}), 400
 
632
  return jsonify({'error': 'An internal server error occurred.'}), 500
633
 
634
  if __name__ == '__main__':
635
+ logging.info("🚀 Starting Juno AI Server...")
636
+ logging.info("🤖 Advanced AI Assistant with Document Processing, Web Scraping, and Memory")
637
+ logging.info("🌟 Powered by Juno AI Prompts System")
638
+ port = int(os.environ.get("PORT", 7860))
639
+ app.run(debug=False, host='0.0.0.0', port=port)