Omartificial-Intelligence-Space commited on
Commit
0d2793f
·
verified ·
1 Parent(s): a76c6b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +516 -231
app.py CHANGED
@@ -10,18 +10,35 @@ from datetime import datetime, timezone, timedelta
10
  from dotenv import load_dotenv
11
  import json
12
 
13
- # Load environment variables
 
 
14
  load_dotenv()
15
 
16
  app = Flask(__name__)
17
  CORS(app)
18
 
19
  # Initialize Gemini client
20
- client = genai.Client(api_key=os.getenv('GOOGLE_API_KEY'))
 
 
 
 
 
 
 
 
21
 
22
- # In-memory storage for demo (in production, use a database)
 
 
 
 
 
 
 
23
  document_caches = {}
24
- user_sessions = {}
25
 
26
  # HTML template for the web interface
27
  HTML_TEMPLATE = """
@@ -31,62 +48,58 @@ HTML_TEMPLATE = """
31
  <meta charset="UTF-8">
32
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
33
  <title>Smart Document Analysis Platform</title>
 
34
  <style>
35
  * {
36
  margin: 0;
37
  padding: 0;
38
  box-sizing: border-box;
39
  }
40
-
41
  body {
42
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
43
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
44
  min-height: 100vh;
45
  color: #333;
 
46
  }
47
-
48
  .container {
49
  max-width: 1400px;
50
  margin: 0 auto;
51
  padding: 20px;
52
  min-height: 100vh;
 
 
53
  }
54
-
55
  .header {
56
  text-align: center;
57
  margin-bottom: 30px;
58
  color: white;
59
  }
60
-
61
  .header h1 {
62
  font-size: 2.8em;
63
  font-weight: 700;
64
  margin-bottom: 10px;
65
  text-shadow: 0 2px 4px rgba(0,0,0,0.3);
66
  }
67
-
68
  .header p {
69
  font-size: 1.2em;
70
  opacity: 0.9;
71
  font-weight: 300;
72
  }
73
-
74
  .main-content {
75
  display: grid;
76
  grid-template-columns: 1fr 1fr;
77
  gap: 30px;
78
- height: calc(100vh - 200px);
79
  }
80
-
81
- .left-panel {
82
- background: white;
83
- border-radius: 20px;
84
- padding: 30px;
85
- box-shadow: 0 20px 40px rgba(0,0,0,0.1);
86
- overflow-y: auto;
87
- }
88
-
89
- .right-panel {
90
  background: white;
91
  border-radius: 20px;
92
  padding: 30px;
@@ -94,7 +107,11 @@ HTML_TEMPLATE = """
94
  display: flex;
95
  flex-direction: column;
96
  }
97
-
 
 
 
 
98
  .panel-title {
99
  font-size: 1.5em;
100
  font-weight: 600;
@@ -104,11 +121,11 @@ HTML_TEMPLATE = """
104
  align-items: center;
105
  gap: 10px;
106
  }
107
-
108
  .upload-section {
109
  margin-bottom: 30px;
110
  }
111
-
112
  .upload-area {
113
  border: 2px dashed #667eea;
114
  border-radius: 15px;
@@ -117,30 +134,31 @@ HTML_TEMPLATE = """
117
  background: #f8fafc;
118
  transition: all 0.3s ease;
119
  margin-bottom: 20px;
 
120
  }
121
-
122
  .upload-area:hover {
123
  border-color: #764ba2;
124
  background: #f0f2ff;
125
  transform: translateY(-2px);
126
  }
127
-
128
  .upload-area.dragover {
129
  border-color: #764ba2;
130
  background: #e8f0ff;
131
  transform: scale(1.02);
132
  }
133
-
134
  .upload-icon {
135
  font-size: 3em;
136
  color: #667eea;
137
  margin-bottom: 15px;
138
  }
139
-
140
  .file-input {
141
  display: none;
142
  }
143
-
144
  .upload-btn {
145
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
146
  color: white;
@@ -153,12 +171,12 @@ HTML_TEMPLATE = """
153
  transition: all 0.3s ease;
154
  margin: 10px;
155
  }
156
-
157
  .upload-btn:hover {
158
  transform: translateY(-2px);
159
  box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
160
  }
161
-
162
  .url-input {
163
  width: 100%;
164
  padding: 15px;
@@ -168,12 +186,12 @@ HTML_TEMPLATE = """
168
  margin-bottom: 15px;
169
  transition: border-color 0.3s ease;
170
  }
171
-
172
  .url-input:focus {
173
  outline: none;
174
  border-color: #667eea;
175
  }
176
-
177
  .btn {
178
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
179
  color: white;
@@ -185,18 +203,18 @@ HTML_TEMPLATE = """
185
  font-weight: 500;
186
  transition: all 0.3s ease;
187
  }
188
-
189
  .btn:hover {
190
  transform: translateY(-1px);
191
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
192
  }
193
-
194
  .btn:disabled {
195
  opacity: 0.6;
196
  cursor: not-allowed;
197
  transform: none;
198
  }
199
-
200
  .chat-container {
201
  flex: 1;
202
  border: 1px solid #e2e8f0;
@@ -205,40 +223,44 @@ HTML_TEMPLATE = """
205
  padding: 20px;
206
  background: #f8fafc;
207
  margin-bottom: 20px;
 
 
208
  }
209
-
210
  .message {
211
  margin-bottom: 15px;
212
  padding: 15px;
213
  border-radius: 12px;
214
  max-width: 85%;
215
  animation: fadeIn 0.3s ease;
 
216
  }
217
-
218
  @keyframes fadeIn {
219
  from { opacity: 0; transform: translateY(10px); }
220
  to { opacity: 1; transform: translateY(0); }
221
  }
222
-
223
  .user-message {
224
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
225
  color: white;
226
  margin-left: auto;
227
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
228
  }
229
-
230
  .ai-message {
231
  background: white;
232
  color: #333;
233
  border: 1px solid #e2e8f0;
234
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 
235
  }
236
-
237
  .input-group {
238
  display: flex;
239
  gap: 10px;
240
  }
241
-
242
  .question-input {
243
  flex: 1;
244
  padding: 15px;
@@ -247,12 +269,12 @@ HTML_TEMPLATE = """
247
  font-size: 1em;
248
  transition: border-color 0.3s ease;
249
  }
250
-
251
  .question-input:focus {
252
  outline: none;
253
  border-color: #667eea;
254
  }
255
-
256
  .cache-info {
257
  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
258
  border-radius: 12px;
@@ -261,18 +283,28 @@ HTML_TEMPLATE = """
261
  color: white;
262
  box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
263
  }
264
-
265
  .cache-info h3 {
266
  margin-bottom: 10px;
267
  font-weight: 600;
268
  }
269
-
 
 
 
 
 
 
 
 
 
 
270
  .loading {
271
  text-align: center;
272
  padding: 40px;
273
  color: #666;
274
  }
275
-
276
  .loading-spinner {
277
  border: 3px solid #f3f3f3;
278
  border-top: 3px solid #667eea;
@@ -282,12 +314,12 @@ HTML_TEMPLATE = """
282
  animation: spin 1s linear infinite;
283
  margin: 0 auto 20px;
284
  }
285
-
286
  @keyframes spin {
287
  0% { transform: rotate(0deg); }
288
  100% { transform: rotate(360deg); }
289
  }
290
-
291
  .error {
292
  background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
293
  border-radius: 12px;
@@ -296,7 +328,7 @@ HTML_TEMPLATE = """
296
  margin-bottom: 20px;
297
  box-shadow: 0 4px 12px rgba(245, 101, 101, 0.3);
298
  }
299
-
300
  .success {
301
  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
302
  border-radius: 12px;
@@ -305,13 +337,13 @@ HTML_TEMPLATE = """
305
  margin-bottom: 20px;
306
  box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
307
  }
308
-
309
  @media (max-width: 768px) {
310
  .main-content {
311
  grid-template-columns: 1fr;
312
  gap: 20px;
313
  }
314
-
315
  .header h1 {
316
  font-size: 2em;
317
  }
@@ -323,146 +355,190 @@ HTML_TEMPLATE = """
323
  <div class="header">
324
  <h1>📚 Smart Document Analysis Platform</h1>
325
  <p>Upload PDF documents once, ask questions forever with Gemini API caching</p>
 
326
  </div>
327
-
328
  <div class="main-content">
329
  <!-- Left Panel - Upload Section -->
330
  <div class="left-panel">
331
  <div class="panel-title">
332
  📤 Upload PDF Document
333
  </div>
334
-
335
  <div class="upload-section">
336
  <div class="upload-area" id="uploadArea">
337
  <div class="upload-icon">📄</div>
338
  <p>Drag and drop your PDF file here, or click to select</p>
339
  <input type="file" id="fileInput" class="file-input" accept=".pdf">
340
- <button class="upload-btn" onclick="document.getElementById('fileInput').click()">
 
341
  Choose PDF File
342
  </button>
343
  </div>
344
-
345
  <div style="margin-top: 20px;">
346
  <h3>Or provide a URL:</h3>
347
  <input type="url" id="urlInput" class="url-input" placeholder="https://example.com/document.pdf">
348
- <button class="btn" onclick="uploadFromUrl()">Upload from URL</button>
349
  </div>
350
  </div>
351
-
352
  <div id="loading" class="loading" style="display: none;">
353
  <div class="loading-spinner"></div>
354
  <p id="loadingText">Processing your PDF... This may take a moment.</p>
355
  </div>
356
-
357
  <div id="error" class="error" style="display: none;"></div>
358
  <div id="success" class="success" style="display: none;"></div>
359
  </div>
360
-
361
  <!-- Right Panel - Chat Section -->
362
  <div class="right-panel">
363
  <div class="panel-title">
364
  💬 Ask Questions
365
  </div>
366
-
367
  <div id="cacheInfo" class="cache-info" style="display: none;">
368
  <h3>✅ Document Cached Successfully!</h3>
369
  <p>Your PDF has been cached using Gemini API. You can now ask multiple questions without re-uploading.</p>
370
  <p><strong>Cache ID:</strong> <span id="cacheId"></span></p>
371
  <p><strong>Tokens Cached:</strong> <span id="tokenCount"></span></p>
 
372
  </div>
373
-
374
  <div class="chat-container" id="chatContainer">
375
  <div class="message ai-message">
376
- 👋 Hello! I'm ready to analyze your PDF documents. Upload a document to get started!
377
  </div>
378
  </div>
379
-
380
  <div class="input-group">
381
- <input type="text" id="questionInput" class="question-input" placeholder="Ask a question about your document...">
382
- <button class="btn" onclick="askQuestion()" id="askBtn">Ask</button>
383
  </div>
384
  </div>
385
  </div>
386
  </div>
387
-
388
  <script>
389
  let currentCacheId = null;
390
-
 
 
 
 
391
  // File upload handling
392
  const uploadArea = document.getElementById('uploadArea');
393
  const fileInput = document.getElementById('fileInput');
394
-
395
- uploadArea.addEventListener('dragover', (e) => {
 
 
 
 
 
396
  e.preventDefault();
397
- uploadArea.classList.add('dragover');
 
 
 
 
 
398
  });
399
-
400
- uploadArea.addEventListener('dragleave', () => {
401
- uploadArea.classList.remove('dragover');
402
  });
403
-
404
- uploadArea.addEventListener('drop', (e) => {
405
- e.preventDefault();
406
- uploadArea.classList.remove('dragover');
407
- const files = e.dataTransfer.files;
 
 
408
  if (files.length > 0) {
409
  uploadFile(files[0]);
410
  }
411
- });
412
-
413
  fileInput.addEventListener('change', (e) => {
414
  if (e.target.files.length > 0) {
415
  uploadFile(e.target.files[0]);
 
 
416
  }
417
  });
418
-
419
  async function uploadFile(file) {
420
  if (!file.type.includes('pdf')) {
421
  showError('Please select a PDF file.');
422
  return;
423
  }
424
-
 
 
 
 
 
 
425
  showLoading('Uploading PDF...');
426
-
427
  const formData = new FormData();
428
  formData.append('file', file);
429
-
430
  try {
431
  const response = await fetch('/upload', {
432
  method: 'POST',
433
  body: formData
434
  });
435
-
436
  const result = await response.json();
437
-
438
  if (result.success) {
439
  currentCacheId = result.cache_id;
440
  document.getElementById('cacheId').textContent = result.cache_id;
441
  document.getElementById('tokenCount').textContent = result.token_count;
442
  document.getElementById('cacheInfo').style.display = 'block';
443
- showSuccess('PDF uploaded and cached successfully!');
444
-
 
 
 
 
 
445
  // Add initial message
446
  addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai');
 
447
  } else {
448
  showError(result.error);
 
 
 
449
  }
450
  } catch (error) {
451
  showError('Error uploading file: ' + error.message);
 
 
 
452
  } finally {
453
  hideLoading();
454
  }
455
  }
456
-
457
  async function uploadFromUrl() {
458
  const url = document.getElementById('urlInput').value;
459
- if (!url) {
460
  showError('Please enter a valid URL.');
461
  return;
462
  }
463
-
 
 
 
 
 
 
464
  showLoading('Uploading PDF from URL...');
465
-
466
  try {
467
  const response = await fetch('/upload-url', {
468
  method: 'POST',
@@ -471,47 +547,61 @@ HTML_TEMPLATE = """
471
  },
472
  body: JSON.stringify({ url: url })
473
  });
474
-
475
  const result = await response.json();
476
-
477
  if (result.success) {
478
  currentCacheId = result.cache_id;
479
  document.getElementById('cacheId').textContent = result.cache_id;
480
  document.getElementById('tokenCount').textContent = result.token_count;
481
  document.getElementById('cacheInfo').style.display = 'block';
482
- showSuccess('PDF uploaded and cached successfully!');
483
-
 
 
 
 
 
484
  // Add initial message
485
  addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai');
 
486
  } else {
487
  showError(result.error);
 
 
 
488
  }
489
  } catch (error) {
490
  showError('Error uploading from URL: ' + error.message);
 
 
 
491
  } finally {
492
  hideLoading();
493
  }
494
  }
495
-
496
  async function askQuestion() {
497
- const question = document.getElementById('questionInput').value;
498
- if (!question.trim()) return;
499
-
 
500
  if (!currentCacheId) {
501
  showError('Please upload a PDF document first.');
502
  return;
503
  }
504
-
505
  // Add user message to chat
506
  addMessage(question, 'user');
507
- document.getElementById('questionInput').value = '';
508
-
509
  // Show loading state
510
  const askBtn = document.getElementById('askBtn');
511
  const originalText = askBtn.textContent;
512
  askBtn.textContent = 'Generating...';
513
  askBtn.disabled = true;
514
-
 
515
  try {
516
  const response = await fetch('/ask', {
517
  method: 'POST',
@@ -520,12 +610,12 @@ HTML_TEMPLATE = """
520
  },
521
  body: JSON.stringify({
522
  question: question,
523
- cache_id: currentCacheId
524
  })
525
  });
526
-
527
  const result = await response.json();
528
-
529
  if (result.success) {
530
  addMessage(result.answer, 'ai');
531
  } else {
@@ -536,70 +626,112 @@ HTML_TEMPLATE = """
536
  } finally {
537
  askBtn.textContent = originalText;
538
  askBtn.disabled = false;
 
 
 
 
 
 
 
539
  }
540
  }
541
-
542
  function addMessage(text, sender) {
543
  const chatContainer = document.getElementById('chatContainer');
544
  const messageDiv = document.createElement('div');
545
  messageDiv.className = `message ${sender}-message`;
 
 
 
 
546
  messageDiv.textContent = text;
 
 
 
 
547
  chatContainer.appendChild(messageDiv);
548
- chatContainer.scrollTop = chatContainer.scrollHeight;
549
  }
550
-
551
  function showLoading(text = 'Processing...') {
552
  document.getElementById('loadingText').textContent = text;
553
  document.getElementById('loading').style.display = 'block';
554
  }
555
-
556
  function hideLoading() {
557
  document.getElementById('loading').style.display = 'none';
558
  }
559
-
560
  function showError(message) {
561
  const errorDiv = document.getElementById('error');
562
  errorDiv.textContent = message;
563
  errorDiv.style.display = 'block';
 
564
  setTimeout(() => {
565
  errorDiv.style.display = 'none';
566
  }, 5000);
567
  }
568
-
569
  function showSuccess(message) {
570
  const successDiv = document.getElementById('success');
571
  successDiv.textContent = message;
572
  successDiv.style.display = 'block';
 
573
  setTimeout(() => {
574
  successDiv.style.display = 'none';
575
  }, 5000);
576
  }
577
-
 
 
 
 
 
 
 
 
578
  // Enter key to ask question
579
  document.getElementById('questionInput').addEventListener('keypress', (e) => {
580
- if (e.key === 'Enter') {
 
 
581
  askQuestion();
582
  }
583
  });
 
 
 
584
  </script>
585
  </body>
586
  </html>
587
  """
588
 
589
- # ... (imports and initial setup) ...
590
 
591
  @app.route('/')
592
  def index():
 
 
 
 
593
  return render_template_string(HTML_TEMPLATE)
594
 
595
- # Add health check endpoint
596
  @app.route('/health', methods=['GET'])
597
  def health_check():
598
  # A simple endpoint to check if the application is running
 
 
 
 
 
599
  return jsonify({"status": "healthy"}), 200
600
 
 
601
  @app.route('/upload', methods=['POST'])
602
  def upload_file():
 
 
 
603
  try:
604
  if 'file' not in request.files:
605
  return jsonify({'success': False, 'error': 'No file provided'})
@@ -617,21 +749,34 @@ def upload_file():
617
  # Upload to Gemini File API using the correct method client.upload_file
618
  # Pass the file content as a tuple (filename, file-like object, mime_type)
619
  # This replaces the incorrect client.files.upload call
 
620
  try:
 
 
621
  document = client.upload_file(
622
- file=(file.filename, file_io, 'application/pdf'),
623
- display_name=file.filename # Optional: provide a display name
624
  )
625
- print(f"File uploaded successfully: {document.name}") # Log for debugging
 
 
626
  except Exception as upload_error:
627
- return jsonify({'success': False, 'error': f'Error uploading file to Gemini API: {str(upload_error)}'})
 
 
 
 
 
 
628
  # --- END CORRECTED FILE UPLOAD CALL ---
629
 
630
  # Create cache with system instruction
 
631
  try:
632
  system_instruction = "You are an expert document analyzer. Provide detailed, accurate answers based on the uploaded document content. Always be helpful and thorough in your responses."
633
 
634
  # Use the correct model format as per documentation
 
635
  model = 'models/gemini-2.0-flash-001'
636
 
637
  print(f"Attempting to create cache for file: {document.name}") # Log
@@ -640,79 +785,102 @@ def upload_file():
640
  config=types.CreateCachedContentConfig(
641
  display_name=f'pdf document cache: {file.filename}', # Use filename in display_name
642
  system_instruction=system_instruction,
643
- contents=[document], # document is the File object returned by upload_file
644
- ttl="3600s", # 1 hour TTL
645
  )
646
  )
647
  print(f"Cache created successfully: {cache.name}") # Log
648
 
649
- # Store cache info
 
650
  cache_id = str(uuid.uuid4())
651
  document_caches[cache_id] = {
652
- 'cache_name': cache.name,
653
  'document_name': file.filename,
654
- 'created_at': datetime.now().isoformat()
 
 
655
  }
656
 
657
  # Get token count from cache metadata if available
 
658
  token_count = 'Unknown'
659
  if hasattr(cache, 'usage_metadata') and cache.usage_metadata:
660
  token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
 
 
661
 
662
  return jsonify({
663
  'success': True,
664
- 'cache_id': cache_id,
665
  'token_count': token_count
666
  })
667
 
668
  except Exception as cache_error:
669
- print(f"Cache creation failed: {str(cache_error)}") # Log the cache error
670
- # If caching fails due to small content, provide alternative approach
671
- # Note: The exact error message might vary, checking substring is a bit fragile
672
- # A better way might be to count tokens first, but requires API call
673
- if "Cached content is too small" in str(cache_error) or "minimum" in str(cache_error).lower():
674
- # Attempt to delete the uploaded file if caching failed (optional but good cleanup)
675
  try:
676
  client.files.delete(document.name)
677
  print(f"Cleaned up uploaded file {document.name} after caching failure.")
678
  except Exception as cleanup_error:
679
  print(f"Failed to clean up file {document.name}: {cleanup_error}")
680
 
 
 
 
 
681
  return jsonify({
682
  'success': False,
683
- 'error': 'PDF content is too small for caching. Please upload a larger document. Minimum token count varies by model, but is typically 1024+.',
684
  'suggestion': 'Try uploading a longer document or combine multiple documents.'
685
- })
686
  else:
687
- # Attempt to delete the uploaded file if caching failed
688
- try:
689
- client.files.delete(document.name)
690
- print(f"Cleaned up uploaded file {document.name} after caching failure.")
691
- except Exception as cleanup_error:
692
- print(f"Failed to clean up file {document.name}: {cleanup_error}")
693
- raise cache_error # Re-raise other errors
694
 
695
  except Exception as e:
696
- print(f"An unexpected error occurred during upload: {str(e)}") # Log general errors
697
- return jsonify({'success': False, 'error': str(e)})
698
 
699
  @app.route('/upload-url', methods=['POST'])
700
  def upload_from_url():
 
 
 
701
  try:
702
  data = request.get_json()
703
  url = data.get('url')
704
 
705
  if not url:
706
- return jsonify({'success': False, 'error': 'No URL provided'})
707
 
708
  # Download file from URL
 
709
  try:
710
- response = httpx.get(url)
 
 
711
  response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
 
 
 
 
 
 
 
 
 
 
 
712
  except httpx.HTTPStatusError as e:
713
- return jsonify({'success': False, 'error': f'HTTP error downloading file from URL: {e.response.status_code} - {e.response.text}'})
 
714
  except httpx.RequestError as e:
715
- return jsonify({'success': False, 'error': f'Error downloading file from URL: {e}'})
 
716
 
717
 
718
  file_io = io.BytesIO(response.content)
@@ -720,19 +888,39 @@ def upload_from_url():
720
  # --- CORRECTED FILE UPLOAD CALL ---
721
  # Upload to Gemini File API using the correct method client.upload_file
722
  # Pass the file content as a tuple (filename, file-like object, mime_type)
723
- # Use a generic filename for the file-like object
 
724
  try:
 
 
 
 
 
 
 
 
 
725
  document = client.upload_file(
726
- file=('downloaded_document.pdf', file_io, 'application/pdf'), # Use a placeholder filename
727
- display_name=url # Use the URL as display name
728
  )
729
- print(f"File uploaded successfully: {document.name}") # Log
 
 
 
730
  except Exception as upload_error:
731
- return jsonify({'success': False, 'error': f'Error uploading file to Gemini API: {str(upload_error)}'})
 
 
 
 
 
 
732
  # --- END CORRECTED FILE UPLOAD CALL ---
733
 
734
 
735
  # Create cache with system instruction
 
736
  try:
737
  system_instruction = "You are an expert document analyzer. Provide detailed, accurate answers based on the uploaded document content. Always be helpful and thorough in your responses."
738
 
@@ -743,111 +931,136 @@ def upload_from_url():
743
  cache = client.caches.create(
744
  model=model,
745
  config=types.CreateCachedContentConfig(
746
- display_name=f'pdf document cache: {url}', # Use URL in display_name
747
  system_instruction=system_instruction,
748
- contents=[document], # document is the File object returned by upload_file
749
- ttl="3600s", # 1 hour TTL
750
  )
751
  )
752
  print(f"Cache created successfully: {cache.name}") # Log
753
 
754
- # Store cache info
 
 
755
  cache_id = str(uuid.uuid4())
756
  document_caches[cache_id] = {
757
- 'cache_name': cache.name,
758
- 'document_name': url,
759
- 'created_at': datetime.now().isoformat()
 
 
760
  }
761
 
762
  # Get token count from cache metadata if available
763
  token_count = 'Unknown'
764
  if hasattr(cache, 'usage_metadata') and cache.usage_metadata:
765
  token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
 
766
 
767
 
768
  return jsonify({
769
  'success': True,
770
- 'cache_id': cache_id,
771
  'token_count': token_count
772
  })
773
 
774
  except Exception as cache_error:
775
- print(f"Cache creation failed: {str(cache_error)}") # Log the cache error
776
- # If caching fails due to small content, provide alternative approach
777
- if "Cached content is too small" in str(cache_error) or "minimum" in str(cache_error).lower():
778
- # Attempt to delete the uploaded file if caching failed (optional but good cleanup)
779
- try:
780
  client.files.delete(document.name)
781
  print(f"Cleaned up uploaded file {document.name} after caching failure.")
782
- except Exception as cleanup_error:
783
- print(f"Failed to clean up file {document.name}: {cleanup_error}")
784
-
785
- return jsonify({
786
- 'success': False,
787
- 'error': 'PDF content is too small for caching. Please upload a larger document. Minimum token count varies by model, but is typically 1024+.',
788
- 'suggestion': 'Try uploading a longer document or combine multiple documents.'
789
- })
 
 
790
  else:
791
- # Attempt to delete the uploaded file if caching failed
792
- try:
793
- client.files.delete(document.name)
794
- print(f"Cleaned up uploaded file {document.name} after caching failure.")
795
- except Exception as cleanup_error:
796
- print(f"Failed to clean up file {document.name}: {cleanup_error}")
797
- raise cache_error # Re-raise other errors
798
 
799
 
800
  except Exception as e:
801
- print(f"An unexpected error occurred during URL upload: {str(e)}") # Log general errors
802
- return jsonify({'success': False, 'error': str(e)})
803
 
804
- # ... (ask_question, list_caches, delete_cache routes remain largely the same) ...
805
 
806
  @app.route('/ask', methods=['POST'])
807
  def ask_question():
 
 
 
808
  try:
809
  data = request.get_json()
810
  question = data.get('question')
811
  cache_id = data.get('cache_id')
812
 
813
  if not question or not cache_id:
814
- return jsonify({'success': False, 'error': 'Missing question or cache_id'})
815
 
 
 
816
  if cache_id not in document_caches:
817
- # Check if the cache still exists in Gemini API if it's not in our local map
818
- # This adds robustness if the server restarts or cache expires
819
- try:
820
- cache_info_api = client.caches.get(name=document_caches[cache_id]['cache_name']) # Need cache_name from stored info
821
- # If get succeeds, update local cache (or handle this differently)
822
- # For simplicity here, let's just fail if not in local map as it's in-memory
823
- return jsonify({'success': False, 'error': 'Cache not found or expired. Please upload the document again.'})
824
- except Exception as get_error:
825
- # If get fails, it's definitely gone
826
- if cache_id in document_caches: # Clean up local entry if API confirms deletion/expiry
827
- del document_caches[cache_id]
828
- return jsonify({'success': False, 'error': 'Cache not found or expired. Please upload the document again.'})
829
-
830
 
 
831
  cache_info = document_caches[cache_id]
 
 
 
832
 
833
  # Generate response using cached content with correct model format
834
  response = client.models.generate_content(
835
- model='models/gemini-2.0-flash-001',
836
- contents=question, # User's question
837
- generation_config=types.GenerateContentConfig( # generation_config takes GenerateContentConfig
838
- cached_content=cache_info['cache_name']
839
  )
840
  )
841
 
842
  # Check if response has parts before accessing .text
843
- answer = "Could not generate response."
844
- if response and response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
845
- answer = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  elif response and response.prompt_feedback and response.prompt_feedback.block_reason:
847
- answer = f"Request blocked: {response.prompt_feedback.block_reason.name}"
848
- else:
849
- print(f"Unexpected response structure: {response}") # Log unexpected structure
 
 
850
 
 
 
 
 
851
 
852
  return jsonify({
853
  'success': True,
@@ -856,50 +1069,122 @@ def ask_question():
856
 
857
  except Exception as e:
858
  print(f"An error occurred during question asking: {str(e)}") # Log errors
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  return jsonify({'success': False, 'error': str(e)})
860
 
861
- # ... (list_caches, delete_cache remain largely the same) ...
862
 
863
  @app.route('/cache/<cache_id>', methods=['DELETE'])
864
  def delete_cache(cache_id):
 
 
 
865
  try:
866
  if cache_id not in document_caches:
867
- return jsonify({'success': False, 'error': 'Cache not found'})
868
 
869
  cache_info = document_caches[cache_id]
 
 
 
870
 
871
- # Delete from Gemini API
872
  try:
873
- client.caches.delete(cache_info['cache_name'])
874
- print(f"Gemini cache deleted: {cache_info['cache_name']}") # Log
875
  except Exception as delete_error:
876
- print(f"Error deleting Gemini cache {cache_info['cache_name']}: {delete_error}") # Log
877
- # Decide if you want to fail if API deletion fails or just remove local entry
878
- # For robustness, maybe log and still remove local entry? Or return error?
879
- # Let's return an error for now.
880
- return jsonify({'success': False, 'error': f'Failed to delete cache from API: {str(delete_error)}'})
 
 
 
881
 
882
- # Remove from local storage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  del document_caches[cache_id]
884
  print(f"Local cache entry deleted for ID: {cache_id}") # Log
885
 
886
- return jsonify({'success': True, 'message': 'Cache deleted successfully'})
887
 
888
  except Exception as e:
889
- print(f"An unexpected error occurred during cache deletion: {str(e)}") # Log
890
- return jsonify({'success': False, 'error': str(e)})
891
 
892
 
893
  if __name__ == '__main__':
894
  import os
895
- # Ensure GOOGLE_API_KEY is set
896
- if not os.getenv('GOOGLE_API_KEY'):
897
- print("Error: GOOGLE_API_KEY environment variable not set.")
898
- # exit(1) # Or handle appropriately
899
- # For local testing with debug=True, you might pass it directly or ensure your .env is loaded
900
- pass # Allow running without key for now if needed, but API calls will fail
901
-
902
  port = int(os.environ.get("PORT", 7860))
903
  print(f"Starting Flask app on port {port}") # Log start
904
  # In production, set debug=False
905
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
10
  from dotenv import load_dotenv
11
  import json
12
 
13
+ # Load environment variables from a .env file
14
+ # This is useful for local development. In production on platforms like Hugging Face,
15
+ # you'll set these as environment variables directly in the settings.
16
  load_dotenv()
17
 
18
  app = Flask(__name__)
19
  CORS(app)
20
 
21
  # Initialize Gemini client
22
+ # The API key should be loaded from environment variables
23
+ api_key = os.getenv('GOOGLE_API_KEY')
24
+ if not api_key:
25
+ print("Error: GOOGLE_API_KEY environment variable not set.")
26
+ # In a real app, you might exit or raise an exception here.
27
+ # For this example, we'll print an error but allow the app to start;
28
+ # API calls will fail if the key is missing.
29
+ # If running locally, make sure you have a .env file with GOOGLE_API_KEY=YOUR_API_KEY
30
+ pass # Allows the app to run without a key for debugging non-API parts
31
 
32
+ try:
33
+ client = genai.Client(api_key=api_key)
34
+ except Exception as e:
35
+ print(f"Failed to initialize Gemini client: {e}")
36
+ client = None # Set client to None if initialization fails
37
+
38
+ # In-memory storage for demo (in production, use a database like Redis or PostgreSQL)
39
+ # Maps our internal cache_id (UUID) to Gemini's cache_name and other info
40
  document_caches = {}
41
+ user_sessions = {} # Not used in this version, but kept from template
42
 
43
  # HTML template for the web interface
44
  HTML_TEMPLATE = """
 
48
  <meta charset="UTF-8">
49
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
50
  <title>Smart Document Analysis Platform</title>
51
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
52
  <style>
53
  * {
54
  margin: 0;
55
  padding: 0;
56
  box-sizing: border-box;
57
  }
58
+
59
  body {
60
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
61
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
62
  min-height: 100vh;
63
  color: #333;
64
+ line-height: 1.6;
65
  }
66
+
67
  .container {
68
  max-width: 1400px;
69
  margin: 0 auto;
70
  padding: 20px;
71
  min-height: 100vh;
72
+ display: flex;
73
+ flex-direction: column;
74
  }
75
+
76
  .header {
77
  text-align: center;
78
  margin-bottom: 30px;
79
  color: white;
80
  }
81
+
82
  .header h1 {
83
  font-size: 2.8em;
84
  font-weight: 700;
85
  margin-bottom: 10px;
86
  text-shadow: 0 2px 4px rgba(0,0,0,0.3);
87
  }
88
+
89
  .header p {
90
  font-size: 1.2em;
91
  opacity: 0.9;
92
  font-weight: 300;
93
  }
94
+
95
  .main-content {
96
  display: grid;
97
  grid-template-columns: 1fr 1fr;
98
  gap: 30px;
99
+ flex-grow: 1;
100
  }
101
+
102
+ .left-panel, .right-panel {
 
 
 
 
 
 
 
 
103
  background: white;
104
  border-radius: 20px;
105
  padding: 30px;
 
107
  display: flex;
108
  flex-direction: column;
109
  }
110
+
111
+ .left-panel {
112
+ overflow-y: auto; /* Allow scrolling if content is tall */
113
+ }
114
+
115
  .panel-title {
116
  font-size: 1.5em;
117
  font-weight: 600;
 
121
  align-items: center;
122
  gap: 10px;
123
  }
124
+
125
  .upload-section {
126
  margin-bottom: 30px;
127
  }
128
+
129
  .upload-area {
130
  border: 2px dashed #667eea;
131
  border-radius: 15px;
 
134
  background: #f8fafc;
135
  transition: all 0.3s ease;
136
  margin-bottom: 20px;
137
+ cursor: pointer; /* Indicate clickable area */
138
  }
139
+
140
  .upload-area:hover {
141
  border-color: #764ba2;
142
  background: #f0f2ff;
143
  transform: translateY(-2px);
144
  }
145
+
146
  .upload-area.dragover {
147
  border-color: #764ba2;
148
  background: #e8f0ff;
149
  transform: scale(1.02);
150
  }
151
+
152
  .upload-icon {
153
  font-size: 3em;
154
  color: #667eea;
155
  margin-bottom: 15px;
156
  }
157
+
158
  .file-input {
159
  display: none;
160
  }
161
+
162
  .upload-btn {
163
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
164
  color: white;
 
171
  transition: all 0.3s ease;
172
  margin: 10px;
173
  }
174
+
175
  .upload-btn:hover {
176
  transform: translateY(-2px);
177
  box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
178
  }
179
+
180
  .url-input {
181
  width: 100%;
182
  padding: 15px;
 
186
  margin-bottom: 15px;
187
  transition: border-color 0.3s ease;
188
  }
189
+
190
  .url-input:focus {
191
  outline: none;
192
  border-color: #667eea;
193
  }
194
+
195
  .btn {
196
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
197
  color: white;
 
203
  font-weight: 500;
204
  transition: all 0.3s ease;
205
  }
206
+
207
  .btn:hover {
208
  transform: translateY(-1px);
209
  box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
210
  }
211
+
212
  .btn:disabled {
213
  opacity: 0.6;
214
  cursor: not-allowed;
215
  transform: none;
216
  }
217
+
218
  .chat-container {
219
  flex: 1;
220
  border: 1px solid #e2e8f0;
 
223
  padding: 20px;
224
  background: #f8fafc;
225
  margin-bottom: 20px;
226
+ display: flex;
227
+ flex-direction: column;
228
  }
229
+
230
  .message {
231
  margin-bottom: 15px;
232
  padding: 15px;
233
  border-radius: 12px;
234
  max-width: 85%;
235
  animation: fadeIn 0.3s ease;
236
+ word-wrap: break-word; /* Ensure long words wrap */
237
  }
238
+
239
  @keyframes fadeIn {
240
  from { opacity: 0; transform: translateY(10px); }
241
  to { opacity: 1; transform: translateY(0); }
242
  }
243
+
244
  .user-message {
245
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
246
  color: white;
247
  margin-left: auto;
248
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
249
  }
250
+
251
  .ai-message {
252
  background: white;
253
  color: #333;
254
  border: 1px solid #e2e8f0;
255
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
256
+ margin-right: auto; /* Align AI messages to the left */
257
  }
258
+
259
  .input-group {
260
  display: flex;
261
  gap: 10px;
262
  }
263
+
264
  .question-input {
265
  flex: 1;
266
  padding: 15px;
 
269
  font-size: 1em;
270
  transition: border-color 0.3s ease;
271
  }
272
+
273
  .question-input:focus {
274
  outline: none;
275
  border-color: #667eea;
276
  }
277
+
278
  .cache-info {
279
  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
280
  border-radius: 12px;
 
283
  color: white;
284
  box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
285
  }
286
+
287
  .cache-info h3 {
288
  margin-bottom: 10px;
289
  font-weight: 600;
290
  }
291
+
292
+ .cache-info p {
293
+ font-size: 0.9em;
294
+ margin-bottom: 5px;
295
+ }
296
+
297
+ .cache-info p:last-child {
298
+ margin-bottom: 0;
299
+ }
300
+
301
+
302
  .loading {
303
  text-align: center;
304
  padding: 40px;
305
  color: #666;
306
  }
307
+
308
  .loading-spinner {
309
  border: 3px solid #f3f3f3;
310
  border-top: 3px solid #667eea;
 
314
  animation: spin 1s linear infinite;
315
  margin: 0 auto 20px;
316
  }
317
+
318
  @keyframes spin {
319
  0% { transform: rotate(0deg); }
320
  100% { transform: rotate(360deg); }
321
  }
322
+
323
  .error {
324
  background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
325
  border-radius: 12px;
 
328
  margin-bottom: 20px;
329
  box-shadow: 0 4px 12px rgba(245, 101, 101, 0.3);
330
  }
331
+
332
  .success {
333
  background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
334
  border-radius: 12px;
 
337
  margin-bottom: 20px;
338
  box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3);
339
  }
340
+
341
  @media (max-width: 768px) {
342
  .main-content {
343
  grid-template-columns: 1fr;
344
  gap: 20px;
345
  }
346
+
347
  .header h1 {
348
  font-size: 2em;
349
  }
 
355
  <div class="header">
356
  <h1>📚 Smart Document Analysis Platform</h1>
357
  <p>Upload PDF documents once, ask questions forever with Gemini API caching</p>
358
+ <p style="font-size:0.9em; margin-top: 5px; opacity: 0.8;">Powered by Google Gemini API - Explicit Caching</p>
359
  </div>
360
+
361
  <div class="main-content">
362
  <!-- Left Panel - Upload Section -->
363
  <div class="left-panel">
364
  <div class="panel-title">
365
  📤 Upload PDF Document
366
  </div>
367
+
368
  <div class="upload-section">
369
  <div class="upload-area" id="uploadArea">
370
  <div class="upload-icon">📄</div>
371
  <p>Drag and drop your PDF file here, or click to select</p>
372
  <input type="file" id="fileInput" class="file-input" accept=".pdf">
373
+ <!-- The button triggers the hidden file input -->
374
+ <button type="button" class="upload-btn" onclick="document.getElementById('fileInput').click()">
375
  Choose PDF File
376
  </button>
377
  </div>
378
+
379
  <div style="margin-top: 20px;">
380
  <h3>Or provide a URL:</h3>
381
  <input type="url" id="urlInput" class="url-input" placeholder="https://example.com/document.pdf">
382
+ <button type="button" class="btn" onclick="uploadFromUrl()">Upload from URL</button>
383
  </div>
384
  </div>
385
+
386
  <div id="loading" class="loading" style="display: none;">
387
  <div class="loading-spinner"></div>
388
  <p id="loadingText">Processing your PDF... This may take a moment.</p>
389
  </div>
390
+
391
  <div id="error" class="error" style="display: none;"></div>
392
  <div id="success" class="success" style="display: none;"></div>
393
  </div>
394
+
395
  <!-- Right Panel - Chat Section -->
396
  <div class="right-panel">
397
  <div class="panel-title">
398
  💬 Ask Questions
399
  </div>
400
+
401
  <div id="cacheInfo" class="cache-info" style="display: none;">
402
  <h3>✅ Document Cached Successfully!</h3>
403
  <p>Your PDF has been cached using Gemini API. You can now ask multiple questions without re-uploading.</p>
404
  <p><strong>Cache ID:</strong> <span id="cacheId"></span></p>
405
  <p><strong>Tokens Cached:</strong> <span id="tokenCount"></span></p>
406
+ <p>Note: Caching is ideal for larger documents (typically 1024+ tokens required).</p>
407
  </div>
408
+
409
  <div class="chat-container" id="chatContainer">
410
  <div class="message ai-message">
411
+ 👋 Hello! Upload a PDF document using the panel on the left, and I'll help you analyze it using Gemini API caching!
412
  </div>
413
  </div>
414
+
415
  <div class="input-group">
416
+ <input type="text" id="questionInput" class="question-input" placeholder="Ask a question about your document..." disabled>
417
+ <button type="button" class="btn" onclick="askQuestion()" id="askBtn" disabled>Ask</button>
418
  </div>
419
  </div>
420
  </div>
421
  </div>
 
422
  <script>
423
  let currentCacheId = null;
424
+
425
+ // Disable input/button initially
426
+ document.getElementById('questionInput').disabled = true;
427
+ document.getElementById('askBtn').disabled = true;
428
+
429
  // File upload handling
430
  const uploadArea = document.getElementById('uploadArea');
431
  const fileInput = document.getElementById('fileInput');
432
+
433
+ // Prevent default drag behaviors
434
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
435
+ uploadArea.addEventListener(eventName, preventDefaults, false);
436
+ });
437
+
438
+ function preventDefaults (e) {
439
  e.preventDefault();
440
+ e.stopPropagation();
441
+ }
442
+
443
+ // Highlight drop area when item is dragged over
444
+ ['dragenter', 'dragover'].forEach(eventName => {
445
+ uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false);
446
  });
447
+
448
+ ['dragleave', 'drop'].forEach(eventName => {
449
+ uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false);
450
  });
451
+
452
+ // Handle dropped files
453
+ uploadArea.addEventListener('drop', handleDrop, false);
454
+
455
+ function handleDrop(e) {
456
+ const dt = e.dataTransfer;
457
+ const files = dt.files;
458
  if (files.length > 0) {
459
  uploadFile(files[0]);
460
  }
461
+ }
462
+
463
  fileInput.addEventListener('change', (e) => {
464
  if (e.target.files.length > 0) {
465
  uploadFile(e.target.files[0]);
466
+ // Clear the input so the same file can be selected again if needed
467
+ e.target.value = '';
468
  }
469
  });
470
+
471
  async function uploadFile(file) {
472
  if (!file.type.includes('pdf')) {
473
  showError('Please select a PDF file.');
474
  return;
475
  }
476
+
477
+ // Clear previous status messages
478
+ hideError();
479
+ hideSuccess();
480
+ document.getElementById('cacheInfo').style.display = 'none'; // Hide old cache info
481
+ currentCacheId = null; // Clear old cache ID
482
+
483
  showLoading('Uploading PDF...');
484
+
485
  const formData = new FormData();
486
  formData.append('file', file);
487
+
488
  try {
489
  const response = await fetch('/upload', {
490
  method: 'POST',
491
  body: formData
492
  });
493
+
494
  const result = await response.json();
495
+
496
  if (result.success) {
497
  currentCacheId = result.cache_id;
498
  document.getElementById('cacheId').textContent = result.cache_id;
499
  document.getElementById('tokenCount').textContent = result.token_count;
500
  document.getElementById('cacheInfo').style.display = 'block';
501
+ showSuccess('PDF uploaded and cached successfully! You can now ask questions.');
502
+
503
+ // Enable chat input and button
504
+ document.getElementById('questionInput').disabled = false;
505
+ document.getElementById('askBtn').disabled = false;
506
+ document.getElementById('questionInput').focus(); // Focus input
507
+
508
  // Add initial message
509
  addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai');
510
+
511
  } else {
512
  showError(result.error);
513
+ // Disable chat input/button if upload/cache failed
514
+ document.getElementById('questionInput').disabled = true;
515
+ document.getElementById('askBtn').disabled = true;
516
  }
517
  } catch (error) {
518
  showError('Error uploading file: ' + error.message);
519
+ // Disable chat input/button on network/server error
520
+ document.getElementById('questionInput').disabled = true;
521
+ document.getElementById('askBtn').disabled = true;
522
  } finally {
523
  hideLoading();
524
  }
525
  }
526
+
527
  async function uploadFromUrl() {
528
  const url = document.getElementById('urlInput').value;
529
+ if (!url.trim()) {
530
  showError('Please enter a valid URL.');
531
  return;
532
  }
533
+
534
+ // Clear previous status messages
535
+ hideError();
536
+ hideSuccess();
537
+ document.getElementById('cacheInfo').style.display = 'none'; // Hide old cache info
538
+ currentCacheId = null; // Clear old cache ID
539
+
540
  showLoading('Uploading PDF from URL...');
541
+
542
  try {
543
  const response = await fetch('/upload-url', {
544
  method: 'POST',
 
547
  },
548
  body: JSON.stringify({ url: url })
549
  });
550
+
551
  const result = await response.json();
552
+
553
  if (result.success) {
554
  currentCacheId = result.cache_id;
555
  document.getElementById('cacheId').textContent = result.cache_id;
556
  document.getElementById('tokenCount').textContent = result.token_count;
557
  document.getElementById('cacheInfo').style.display = 'block';
558
+ showSuccess('PDF uploaded and cached successfully! You can now ask questions.');
559
+
560
+ // Enable chat input and button
561
+ document.getElementById('questionInput').disabled = false;
562
+ document.getElementById('askBtn').disabled = false;
563
+ document.getElementById('questionInput').focus(); // Focus input
564
+
565
  // Add initial message
566
  addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai');
567
+
568
  } else {
569
  showError(result.error);
570
+ // Disable chat input/button if upload/cache failed
571
+ document.getElementById('questionInput').disabled = true;
572
+ document.getElementById('askBtn').disabled = true;
573
  }
574
  } catch (error) {
575
  showError('Error uploading from URL: ' + error.message);
576
+ // Disable chat input/button on network/server error
577
+ document.getElementById('questionInput').disabled = true;
578
+ document.getElementById('askBtn').disabled = false; // Should be false? Fix: should be true
579
  } finally {
580
  hideLoading();
581
  }
582
  }
583
+
584
  async function askQuestion() {
585
+ const questionInput = document.getElementById('questionInput');
586
+ const question = questionInput.value.trim();
587
+ if (!question) return; // Don't send empty questions
588
+
589
  if (!currentCacheId) {
590
  showError('Please upload a PDF document first.');
591
  return;
592
  }
593
+
594
  // Add user message to chat
595
  addMessage(question, 'user');
596
+ questionInput.value = ''; // Clear input immediately
597
+
598
  // Show loading state
599
  const askBtn = document.getElementById('askBtn');
600
  const originalText = askBtn.textContent;
601
  askBtn.textContent = 'Generating...';
602
  askBtn.disabled = true;
603
+ questionInput.disabled = true; // Disable input while generating
604
+
605
  try {
606
  const response = await fetch('/ask', {
607
  method: 'POST',
 
610
  },
611
  body: JSON.stringify({
612
  question: question,
613
+ cache_id: currentCacheId // Use our internal cache_id
614
  })
615
  });
616
+
617
  const result = await response.json();
618
+
619
  if (result.success) {
620
  addMessage(result.answer, 'ai');
621
  } else {
 
626
  } finally {
627
  askBtn.textContent = originalText;
628
  askBtn.disabled = false;
629
+ questionInput.disabled = false; // Re-enable input
630
+ questionInput.focus(); // Put focus back on input
631
+ // Ensure button is disabled only if no cache is active
632
+ if (!currentCacheId) {
633
+ askBtn.disabled = true;
634
+ questionInput.disabled = true;
635
+ }
636
  }
637
  }
638
+
639
  function addMessage(text, sender) {
640
  const chatContainer = document.getElementById('chatContainer');
641
  const messageDiv = document.createElement('div');
642
  messageDiv.className = `message ${sender}-message`;
643
+
644
+ // Use innerHTML to handle potential formatting like newlines or markdown
645
+ // (Basic textContent might be sufficient depending on expected AI output)
646
+ // For simplicity here, sticking to textContent as AI might output plain text
647
  messageDiv.textContent = text;
648
+
649
+ // Basic handling for newlines
650
+ messageDiv.style.whiteSpace = 'pre-wrap';
651
+
652
  chatContainer.appendChild(messageDiv);
653
+ chatContainer.scrollTop = chatContainer.scrollHeight; // Auto-scroll to latest message
654
  }
655
+
656
  function showLoading(text = 'Processing...') {
657
  document.getElementById('loadingText').textContent = text;
658
  document.getElementById('loading').style.display = 'block';
659
  }
660
+
661
  function hideLoading() {
662
  document.getElementById('loading').style.display = 'none';
663
  }
664
+
665
  function showError(message) {
666
  const errorDiv = document.getElementById('error');
667
  errorDiv.textContent = message;
668
  errorDiv.style.display = 'block';
669
+ // Auto-hide after 5 seconds
670
  setTimeout(() => {
671
  errorDiv.style.display = 'none';
672
  }, 5000);
673
  }
674
+
675
  function showSuccess(message) {
676
  const successDiv = document.getElementById('success');
677
  successDiv.textContent = message;
678
  successDiv.style.display = 'block';
679
+ // Auto-hide after 5 seconds
680
  setTimeout(() => {
681
  successDiv.style.display = 'none';
682
  }, 5000);
683
  }
684
+
685
+ function hideError() {
686
+ document.getElementById('error').style.display = 'none';
687
+ }
688
+
689
+ function hideSuccess() {
690
+ document.getElementById('success').style.display = 'none';
691
+ }
692
+
693
  // Enter key to ask question
694
  document.getElementById('questionInput').addEventListener('keypress', (e) => {
695
+ // Check if the input is not disabled and the key is Enter
696
+ if (!document.getElementById('questionInput').disabled && e.key === 'Enter') {
697
+ e.preventDefault(); // Prevent default form submission if input is part of a form
698
  askQuestion();
699
  }
700
  });
701
+
702
+ // Initial message visibility
703
+ // addMessage("👋 Hello! Upload a PDF document using the panel on the left, and I'll help you analyze it using Gemini API caching!", 'ai'); // Added this directly in HTML
704
  </script>
705
  </body>
706
  </html>
707
  """
708
 
709
+ # --- Flask Routes ---
710
 
711
  @app.route('/')
712
  def index():
713
+ # Ensure API key is set before rendering, or add a warning to the template
714
+ if not api_key:
715
+ # You could modify the template or pass a variable to indicate error state
716
+ print("Warning: API key not set. API calls will fail.")
717
  return render_template_string(HTML_TEMPLATE)
718
 
 
719
  @app.route('/health', methods=['GET'])
720
  def health_check():
721
  # A simple endpoint to check if the application is running
722
+ # Can optionally check API client status if needed, but basic 200 is common.
723
+ if client is None and api_key is not None: # Client failed to initialize despite key being present
724
+ return jsonify({"status": "unhealthy", "reason": "Gemini client failed to initialize"}), 500
725
+ # Note: This doesn't check if the API key is *valid* or if the API is reachable,
726
+ # just if the Flask app is running and the client object was created.
727
  return jsonify({"status": "healthy"}), 200
728
 
729
+
730
  @app.route('/upload', methods=['POST'])
731
  def upload_file():
732
+ if client is None or api_key is None:
733
+ return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500
734
+
735
  try:
736
  if 'file' not in request.files:
737
  return jsonify({'success': False, 'error': 'No file provided'})
 
749
  # Upload to Gemini File API using the correct method client.upload_file
750
  # Pass the file content as a tuple (filename, file-like object, mime_type)
751
  # This replaces the incorrect client.files.upload call
752
+ document = None # Initialize document variable
753
  try:
754
+ # The mime_type is crucial for the API to correctly process the file.
755
+ # The filename is used as the display_name by default if not provided.
756
  document = client.upload_file(
757
+ file=(file.filename, file_io, 'application/pdf'), # Use the 'file' argument with tuple format
758
+ # display_name=file.filename # Optional: explicitly provide a display name
759
  )
760
+ print(f"File uploaded successfully to Gemini File API: {document.name}") # Log for debugging
761
+ # Note: client.upload_file returns a google.generativeai.types.File object
762
+ # which contains the resource name (e.g., 'files/xyz123').
763
  except Exception as upload_error:
764
+ # Attempt to provide more specific feedback if possible
765
+ error_msg = str(upload_error)
766
+ print(f"Error uploading file to Gemini API: {error_msg}")
767
+ # Check for common upload errors like exceeding file size limits
768
+ if "file content size exceeds limit" in error_msg.lower():
769
+ return jsonify({'success': False, 'error': f'Error uploading file: File size exceeds API limit. {error_msg}'}), 413 # 413 Payload Too Large
770
+ return jsonify({'success': False, 'error': f'Error uploading file to Gemini API: {error_msg}'}), 500
771
  # --- END CORRECTED FILE UPLOAD CALL ---
772
 
773
  # Create cache with system instruction
774
+ cache = None # Initialize cache variable
775
  try:
776
  system_instruction = "You are an expert document analyzer. Provide detailed, accurate answers based on the uploaded document content. Always be helpful and thorough in your responses."
777
 
778
  # Use the correct model format as per documentation
779
+ # Using a specific stable version is recommended for production
780
  model = 'models/gemini-2.0-flash-001'
781
 
782
  print(f"Attempting to create cache for file: {document.name}") # Log
 
785
  config=types.CreateCachedContentConfig(
786
  display_name=f'pdf document cache: {file.filename}', # Use filename in display_name
787
  system_instruction=system_instruction,
788
+ contents=[document], # contents should be a list of content parts. document is already a File object, which is a valid content part type.
789
+ ttl="3600s", # 1 hour TTL. Use string format like "300s" or "1h".
790
  )
791
  )
792
  print(f"Cache created successfully: {cache.name}") # Log
793
 
794
+ # Store cache info in our in-memory dictionary
795
+ # We map our internal UUID cache_id to the Gemini API's cache.name (resource name)
796
  cache_id = str(uuid.uuid4())
797
  document_caches[cache_id] = {
798
+ 'gemini_cache_name': cache.name, # Store the Gemini API resource name
799
  'document_name': file.filename,
800
+ 'gemini_file_name': document.name, # Also store the Gemini File API resource name for cleanup
801
+ 'created_at': datetime.now().isoformat(),
802
+ 'expires_at': (datetime.now(timezone.utc) + timedelta(seconds=3600)).isoformat(), # Store expiry time for reference
803
  }
804
 
805
  # Get token count from cache metadata if available
806
+ # Note: cached_token_count might be available on the cache object after creation
807
  token_count = 'Unknown'
808
  if hasattr(cache, 'usage_metadata') and cache.usage_metadata:
809
  token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
810
+ print(f"Cached token count: {token_count}")
811
+
812
 
813
  return jsonify({
814
  'success': True,
815
+ 'cache_id': cache_id, # Return our internal ID
816
  'token_count': token_count
817
  })
818
 
819
  except Exception as cache_error:
820
+ error_msg = str(cache_error)
821
+ print(f"Cache creation failed: {error_msg}") # Log the cache error
822
+ # If caching fails, attempt to delete the uploaded file to clean up.
823
+ if document and hasattr(document, 'name'):
 
 
824
  try:
825
  client.files.delete(document.name)
826
  print(f"Cleaned up uploaded file {document.name} after caching failure.")
827
  except Exception as cleanup_error:
828
  print(f"Failed to clean up file {document.name}: {cleanup_error}")
829
 
830
+ # Handle specific cache creation errors
831
+ # Note: The exact error message for content size can vary or might not be specific
832
+ # The documentation mentions minimum tokens for caching.
833
+ if "Cached content is too small" in error_msg or "minimum size" in error_msg.lower() or "tokens required" in error_msg.lower():
834
  return jsonify({
835
  'success': False,
836
+ 'error': f'PDF content is too small for caching. Minimum token count varies by model, but is typically 1024+ for Flash. {error_msg}',
837
  'suggestion': 'Try uploading a longer document or combine multiple documents.'
838
+ }), 400 # 400 Bad Request - client error
839
  else:
840
+ # Re-raise other unexpected errors or return a generic error
841
+ return jsonify({'success': False, 'error': f'Error creating cache with Gemini API: {error_msg}'}), 500
842
+
 
 
 
 
843
 
844
  except Exception as e:
845
+ print(f"An unexpected error occurred during upload process: {str(e)}") # Log general errors
846
+ return jsonify({'success': False, 'error': str(e)}), 500
847
 
848
  @app.route('/upload-url', methods=['POST'])
849
  def upload_from_url():
850
+ if client is None or api_key is None:
851
+ return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500
852
+
853
  try:
854
  data = request.get_json()
855
  url = data.get('url')
856
 
857
  if not url:
858
+ return jsonify({'success': False, 'error': 'No URL provided'}), 400 # 400 Bad Request
859
 
860
  # Download file from URL
861
+ response = None
862
  try:
863
+ # Use stream=True for potentially large files, although httpx handles it well.
864
+ # Add a timeout to prevent hanging on unresponsive URLs.
865
+ response = httpx.get(url, follow_redirects=True, timeout=30.0)
866
  response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
867
+
868
+ # Basic check for PDF mime type (optional but good practice)
869
+ content_type = response.headers.get('Content-Type', '').lower()
870
+ if 'application/pdf' not in content_type:
871
+ print(f"Warning: URL content type is not application/pdf: {content_type}")
872
+ # Decide if you want to block non-PDFs or try to upload anyway
873
+ # For now, we'll proceed but log a warning. API might reject it.
874
+ # If strictly PDF required, return an error here:
875
+ # return jsonify({'success': False, 'error': f'URL does not point to a PDF document (Content-Type: {content_type})'}), 415 # 415 Unsupported Media Type
876
+
877
+
878
  except httpx.HTTPStatusError as e:
879
+ print(f"HTTP error downloading file from URL {url}: {e.response.status_code} - {e.response.text}")
880
+ return jsonify({'success': False, 'error': f'HTTP error downloading file from URL: {e.response.status_code} - {e.response.text}'}), e.response.status_code
881
  except httpx.RequestError as e:
882
+ print(f"Error downloading file from URL {url}: {e}")
883
+ return jsonify({'success': False, 'error': f'Error downloading file from URL: {e}'}), 500
884
 
885
 
886
  file_io = io.BytesIO(response.content)
 
888
  # --- CORRECTED FILE UPLOAD CALL ---
889
  # Upload to Gemini File API using the correct method client.upload_file
890
  # Pass the file content as a tuple (filename, file-like object, mime_type)
891
+ # Use a generic filename for the file-like object if none derived from URL
892
+ document = None # Initialize document variable
893
  try:
894
+ # Attempt to get filename from URL or headers, otherwise use generic
895
+ filename = os.path.basename(url)
896
+ if not filename or '.' not in filename:
897
+ filename = 'downloaded_document.pdf' # Default generic name
898
+
899
+ # Use the mime type from the response headers if available and looks right
900
+ mime_type = content_type if 'application/pdf' in content_type else 'application/pdf'
901
+
902
+
903
  document = client.upload_file(
904
+ file=(filename, file_io, mime_type), # Use parsed filename and mime_type
905
+ display_name=url # Use the URL as display name in Gemini API
906
  )
907
+ print(f"File from URL uploaded successfully to Gemini File API: {document.name}") # Log
908
+ # Note: client.upload_file returns a google.generativeai.types.File object
909
+ # which contains the resource name (e.g., 'files/xyz123').
910
+
911
  except Exception as upload_error:
912
+ # Attempt to provide more specific feedback if possible
913
+ error_msg = str(upload_error)
914
+ print(f"Error uploading file from URL to Gemini API: {error_msg}")
915
+ # Check for common upload errors like exceeding file size limits
916
+ if "file content size exceeds limit" in error_msg.lower():
917
+ return jsonify({'success': False, 'error': f'Error uploading file: File size exceeds API limit. {error_msg}'}), 413 # 413 Payload Too Large
918
+ return jsonify({'success': False, 'error': f'Error uploading file from URL to Gemini API: {error_msg}'}), 500
919
  # --- END CORRECTED FILE UPLOAD CALL ---
920
 
921
 
922
  # Create cache with system instruction
923
+ cache = None # Initialize cache variable
924
  try:
925
  system_instruction = "You are an expert document analyzer. Provide detailed, accurate answers based on the uploaded document content. Always be helpful and thorough in your responses."
926
 
 
931
  cache = client.caches.create(
932
  model=model,
933
  config=types.CreateCachedContentConfig(
934
+ display_name=f'pdf document cache: {url}', # Use URL in display_name for cache
935
  system_instruction=system_instruction,
936
+ contents=[document], # contents should be a list containing the File object
937
+ ttl="3600s", # 1 hour TTL. Use string format like "300s" or "1h".
938
  )
939
  )
940
  print(f"Cache created successfully: {cache.name}") # Log
941
 
942
+
943
+ # Store cache info in our in-memory dictionary
944
+ # We map our internal UUID cache_id to the Gemini API's cache.name (resource name)
945
  cache_id = str(uuid.uuid4())
946
  document_caches[cache_id] = {
947
+ 'gemini_cache_name': cache.name, # Store the Gemini API resource name
948
+ 'document_name': url, # Store the URL as the document name
949
+ 'gemini_file_name': document.name, # Also store the Gemini File API resource name for cleanup
950
+ 'created_at': datetime.now().isoformat(),
951
+ 'expires_at': (datetime.now(timezone.utc) + timedelta(seconds=3600)).isoformat(), # Store expiry time for reference
952
  }
953
 
954
  # Get token count from cache metadata if available
955
  token_count = 'Unknown'
956
  if hasattr(cache, 'usage_metadata') and cache.usage_metadata:
957
  token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
958
+ print(f"Cached token count: {token_count}")
959
 
960
 
961
  return jsonify({
962
  'success': True,
963
+ 'cache_id': cache_id, # Return our internal ID
964
  'token_count': token_count
965
  })
966
 
967
  except Exception as cache_error:
968
+ error_msg = str(cache_error)
969
+ print(f"Cache creation failed: {error_msg}") # Log the cache error
970
+ # If caching fails, attempt to delete the uploaded file to clean up.
971
+ if document and hasattr(document, 'name'):
972
+ try:
973
  client.files.delete(document.name)
974
  print(f"Cleaned up uploaded file {document.name} after caching failure.")
975
+ except Exception as cleanup_error:
976
+ print(f"Failed to clean up file {document.name}: {cleanup_error}")
977
+
978
+ # Handle specific cache creation errors
979
+ if "Cached content is too small" in error_msg or "minimum size" in error_msg.lower() or "tokens required" in error_msg.lower():
980
+ return jsonify({
981
+ 'success': False,
982
+ 'error': f'PDF content is too small for caching. Minimum token count varies by model, but is typically 1024+ for Flash. {error_msg}',
983
+ 'suggestion': 'Try uploading a longer document or combine multiple documents.'
984
+ }), 400 # 400 Bad Request - client error
985
  else:
986
+ # Re-raise other unexpected errors or return a generic error
987
+ return jsonify({'success': False, 'error': f'Error creating cache with Gemini API: {error_msg}'}), 500
 
 
 
 
 
988
 
989
 
990
  except Exception as e:
991
+ print(f"An unexpected error occurred during URL upload process: {str(e)}") # Log general errors
992
+ return jsonify({'success': False, 'error': str(e)}), 500
993
 
 
994
 
995
  @app.route('/ask', methods=['POST'])
996
  def ask_question():
997
+ if client is None or api_key is None:
998
+ return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500
999
+
1000
  try:
1001
  data = request.get_json()
1002
  question = data.get('question')
1003
  cache_id = data.get('cache_id')
1004
 
1005
  if not question or not cache_id:
1006
+ return jsonify({'success': False, 'error': 'Missing question or cache_id'}), 400 # 400 Bad Request
1007
 
1008
+ # --- CORRECTED CACHE LOOKUP ---
1009
+ # Check if our internal cache_id exists in the in-memory dictionary
1010
  if cache_id not in document_caches:
1011
+ # If not found, it's either an invalid ID, expired, or the server restarted.
1012
+ # For this simple demo, we treat it as unavailable.
1013
+ print(f"Cache ID {cache_id} not found in local storage.")
1014
+ return jsonify({'success': False, 'error': 'Cache not found or expired. Please upload the document again.'}), 404 # 404 Not Found
 
 
 
 
 
 
 
 
 
1015
 
1016
+ # If found, retrieve the Gemini API cache name
1017
  cache_info = document_caches[cache_id]
1018
+ gemini_cache_name = cache_info['gemini_cache_name']
1019
+ print(f"Using Gemini cache name: {gemini_cache_name} for question.")
1020
+ # --- END CORRECTED CACHE LOOKUP ---
1021
 
1022
  # Generate response using cached content with correct model format
1023
  response = client.models.generate_content(
1024
+ model='models/gemini-2.0-flash-001', # Ensure using the model the cache was created with
1025
+ contents=[{'text': question}], # User's question as text content part
1026
+ generation_config=types.GenerateContentConfig(
1027
+ cached_content=gemini_cache_name # Use the retrieved Gemini cache name
1028
  )
1029
  )
1030
 
1031
  # Check if response has parts before accessing .text
1032
+ answer = "Could not generate response from the model."
1033
+ if response and response.candidates:
1034
+ # Handle potential tool_code or other non-text parts if necessary
1035
+ answer_parts = []
1036
+ for candidate in response.candidates:
1037
+ if candidate.content and candidate.content.parts:
1038
+ for part in candidate.content.parts:
1039
+ if hasattr(part, 'text'):
1040
+ answer_parts.append(part.text)
1041
+ # Add handling for other part types if needed (e.g., tool_code, function_response)
1042
+ # elif hasattr(part, 'tool_code'):
1043
+ # answer_parts.append(f"\n```tool_code\n{part.tool_code.code}\n```\n")
1044
+ # elif hasattr(part, 'function_response'):
1045
+ # answer_parts.append(f"\n```function_response\n{json.dumps(part.function_response, indent=2)}\n```\n")
1046
+ if answer_parts:
1047
+ answer = "".join(answer_parts)
1048
+ else:
1049
+ # Handle cases where candidates exist but have no text parts (e.g., tool calls)
1050
+ answer = "Model returned content without text parts (e.g., tool calls)."
1051
+ print(f"Model returned non-text parts: {response.candidates}") # Log for debugging
1052
+
1053
  elif response and response.prompt_feedback and response.prompt_feedback.block_reason:
1054
+ # Handle cases where the prompt was blocked
1055
+ block_reason = response.prompt_feedback.block_reason.name
1056
+ block_message = getattr(response.prompt_feedback, 'block_reason_message', 'No message provided')
1057
+ answer = f"Request blocked by safety filters. Reason: {block_reason}. Message: {block_message}"
1058
+ print(f"Request blocked: {block_reason} - {block_message}")
1059
 
1060
+ else:
1061
+ # Handle other unexpected response structures
1062
+ print(f"Unexpected response structure from API: {response}")
1063
+ # answer stays as the initial "Could not generate response..." message
1064
 
1065
  return jsonify({
1066
  'success': True,
 
1069
 
1070
  except Exception as e:
1071
  print(f"An error occurred during question asking: {str(e)}") # Log errors
1072
+ # Attempt to provide more specific API error messages
1073
+ error_msg = str(e)
1074
+ if "Resource has been exhausted" in error_msg:
1075
+ error_msg = "API rate limit or quota exceeded. Please try again later."
1076
+ elif "cached_content refers to a resource that has been deleted" in error_msg:
1077
+ error_msg = "The cached document has expired or was deleted from Gemini API. Please upload the document again."
1078
+ # Clean up local entry if API confirms deletion/expiry
1079
+ if cache_id in document_caches:
1080
+ print(f"Removing local entry for cache_id {cache_id} as API confirmed deletion.")
1081
+ del document_caches[cache_id]
1082
+ elif "invalid cached_content value" in error_msg:
1083
+ error_msg = "Invalid cache reference. The cached document might have expired or been deleted. Please upload the document again."
1084
+ # Clean up local entry if API confirms deletion/expiry
1085
+ if cache_id in document_caches:
1086
+ print(f"Removing local entry for cache_id {cache_id} as API confirmed deletion (invalid reference).")
1087
+ del document_caches[cache_id]
1088
+ elif "model does not exist" in error_msg:
1089
+ error_msg = "The specified model is not available."
1090
+
1091
+
1092
+ return jsonify({'success': False, 'error': f'Error from Gemini API: {error_msg}'}), 500 # 500 Internal Server Error
1093
+
1094
+
1095
+ @app.route('/caches', methods=['GET'])
1096
+ def list_caches():
1097
+ # Lists caches stored *in this application's memory*.
1098
+ # It does NOT list caches directly from the Gemini API unless you add that logic.
1099
+ try:
1100
+ caches = []
1101
+ for cache_id, cache_info in list(document_caches.items()): # Use list() to iterate safely if modification occurs during iteration
1102
+ # Optional: Check if the cache still exists in Gemini API before listing
1103
+ # This adds complexity and potential API calls, so skipping for simple demo
1104
+ try:
1105
+ # Attempt to get cache metadata from API to confirm existence/details
1106
+ api_cache_info = client.caches.get(name=cache_info['gemini_cache_name'])
1107
+ # If successful, add to list
1108
+ caches.append({
1109
+ 'cache_id': cache_id, # Our internal ID
1110
+ 'document_name': cache_info['document_name'],
1111
+ 'gemini_cache_name': cache_info['gemini_cache_name'], # Include Gemini name
1112
+ 'created_at': cache_info['created_at'],
1113
+ 'expires_at': getattr(api_cache_info, 'expire_time', 'Unknown'), # Get actual expiry from API
1114
+ 'cached_token_count': getattr(api_cache_info.usage_metadata, 'cached_token_count', 'Unknown') if hasattr(api_cache_info, 'usage_metadata') else 'Unknown'
1115
+ })
1116
+ except Exception as e:
1117
+ # If API lookup fails (e.g., cache expired/deleted), remove from our local map
1118
+ print(f"Gemini cache {cache_info['gemini_cache_name']} for local ID {cache_id} not found via API. Removing from local storage. Error: {e}")
1119
+ del document_caches[cache_id]
1120
+ # Don't add it to the list of active caches
1121
+
1122
+ return jsonify({'success': True, 'caches': caches})
1123
+
1124
+ except Exception as e:
1125
+ print(f"An error occurred listing caches: {str(e)}")
1126
  return jsonify({'success': False, 'error': str(e)})
1127
 
 
1128
 
1129
  @app.route('/cache/<cache_id>', methods=['DELETE'])
1130
  def delete_cache(cache_id):
1131
+ if client is None or api_key is None:
1132
+ return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500
1133
+
1134
  try:
1135
  if cache_id not in document_caches:
1136
+ return jsonify({'success': False, 'error': 'Cache not found'}), 404 # 404 Not Found
1137
 
1138
  cache_info = document_caches[cache_id]
1139
+ gemini_cache_name_to_delete = cache_info['gemini_cache_name']
1140
+ gemini_file_name_to_delete = cache_info['gemini_file_name']
1141
+
1142
 
1143
+ # Delete from Gemini API Cache Service
1144
  try:
1145
+ client.caches.delete(gemini_cache_name_to_delete)
1146
+ print(f"Gemini cache deleted: {gemini_cache_name_to_delete}") # Log
1147
  except Exception as delete_error:
1148
+ error_msg = str(delete_error)
1149
+ print(f"Error deleting Gemini cache {gemini_cache_name_to_delete}: {error_msg}") # Log
1150
+ # Handle case where the cache was already gone (e.g. expired)
1151
+ if "Resource not found" in error_msg:
1152
+ print(f"Gemini cache {gemini_cache_name_to_delete} already gone from API.")
1153
+ else:
1154
+ # For other errors, you might want to stop and return the error
1155
+ return jsonify({'success': False, 'error': f'Failed to delete cache from API: {error_msg}'}), 500
1156
 
1157
+
1158
+ # Also delete the associated file from Gemini File API to free up storage
1159
+ if gemini_file_name_to_delete:
1160
+ try:
1161
+ client.files.delete(gemini_file_name_to_delete)
1162
+ print(f"Associated Gemini file deleted: {gemini_file_name_to_delete}") # Log
1163
+ except Exception as file_delete_error:
1164
+ error_msg = str(file_delete_error)
1165
+ print(f"Error deleting Gemini file {gemini_file_name_to_delete}: {error_msg}") # Log
1166
+ if "Resource not found" in error_msg:
1167
+ print(f"Gemini file {gemini_file_name_to_delete} already gone from API.")
1168
+ else:
1169
+ # Log but continue, deleting the cache is the primary goal
1170
+ pass
1171
+
1172
+
1173
+ # Remove from local storage *after* attempting API deletion
1174
  del document_caches[cache_id]
1175
  print(f"Local cache entry deleted for ID: {cache_id}") # Log
1176
 
1177
+ return jsonify({'success': True, 'message': 'Cache and associated file deleted successfully'})
1178
 
1179
  except Exception as e:
1180
+ print(f"An unexpected error occurred during cache deletion process: {str(e)}") # Log
1181
+ return jsonify({'success': False, 'error': str(e)}), 500
1182
 
1183
 
1184
  if __name__ == '__main__':
1185
  import os
 
 
 
 
 
 
 
1186
  port = int(os.environ.get("PORT", 7860))
1187
  print(f"Starting Flask app on port {port}") # Log start
1188
  # In production, set debug=False
1189
+ # Use threaded=True or a production WSGI server (like Gunicorn) for concurrent requests
1190
+ app.run(debug=True, host='0.0.0.0', port=port, threaded=True)