Omartificial-Intelligence-Space commited on
Commit
acbb36d
·
verified ·
1 Parent(s): 4cc9c49

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +220 -615
app.py CHANGED
@@ -10,35 +10,18 @@ from datetime import datetime, timezone, timedelta
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,58 +31,62 @@ 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,11 +94,7 @@ HTML_TEMPLATE = """
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,11 +104,11 @@ HTML_TEMPLATE = """
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,31 +117,30 @@ HTML_TEMPLATE = """
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,12 +153,12 @@ HTML_TEMPLATE = """
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,12 +168,12 @@ HTML_TEMPLATE = """
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,18 +185,18 @@ HTML_TEMPLATE = """
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,44 +205,40 @@ HTML_TEMPLATE = """
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,12 +247,12 @@ HTML_TEMPLATE = """
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,28 +261,18 @@ HTML_TEMPLATE = """
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,12 +282,12 @@ HTML_TEMPLATE = """
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,7 +296,7 @@ HTML_TEMPLATE = """
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,13 +305,13 @@ HTML_TEMPLATE = """
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,190 +323,146 @@ HTML_TEMPLATE = """
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,61 +471,47 @@ HTML_TEMPLATE = """
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,12 +520,12 @@ HTML_TEMPLATE = """
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,565 +536,260 @@ HTML_TEMPLATE = """
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'})
738
-
739
  file = request.files['file']
740
-
741
  if file.filename == '':
742
  return jsonify({'success': False, 'error': 'No file selected'})
743
-
744
  # Read file content
745
  file_content = file.read()
746
  file_io = io.BytesIO(file_content)
747
-
748
- # --- CORRECTED FILE UPLOAD CALL ---
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
783
  cache = client.caches.create(
784
  model=model,
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)
887
-
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
-
927
  # Use the correct model format as per documentation
928
  model = 'models/gemini-2.0-flash-001'
929
-
930
- print(f"Attempting to create cache for file: {document.name}") # Log
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,
1067
- 'answer': answer
1068
  })
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)
 
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
  <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
  display: flex;
95
  flex-direction: column;
96
  }
97
+
 
 
 
 
98
  .panel-title {
99
  font-size: 1.5em;
100
  font-weight: 600;
 
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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  <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
  },
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
  },
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
  } 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
  @app.route('/')
590
  def index():
 
 
 
 
591
  return render_template_string(HTML_TEMPLATE)
592
 
 
 
 
 
 
 
 
 
 
 
 
593
  @app.route('/upload', methods=['POST'])
594
  def upload_file():
 
 
 
595
  try:
596
  if 'file' not in request.files:
597
  return jsonify({'success': False, 'error': 'No file provided'})
598
+
599
  file = request.files['file']
600
+
601
  if file.filename == '':
602
  return jsonify({'success': False, 'error': 'No file selected'})
603
+
604
  # Read file content
605
  file_content = file.read()
606
  file_io = io.BytesIO(file_content)
607
+
608
+ # Upload to Gemini File API
609
+ document = client.files.upload(
610
+ file=file_io,
611
+ config=dict(mime_type='application/pdf')
612
+ )
613
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  # Create cache with system instruction
 
615
  try:
616
  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."
617
+
618
  # Use the correct model format as per documentation
 
619
  model = 'models/gemini-2.0-flash-001'
620
+
 
621
  cache = client.caches.create(
622
  model=model,
623
  config=types.CreateCachedContentConfig(
624
+ display_name='pdf document cache',
625
  system_instruction=system_instruction,
626
+ contents=[document],
627
+ ttl="3600s", # 1 hour TTL
628
  )
629
  )
630
+
631
+ # Store cache info
 
 
632
  cache_id = str(uuid.uuid4())
633
  document_caches[cache_id] = {
634
+ 'cache_name': cache.name,
635
  'document_name': file.filename,
636
+ 'created_at': datetime.now().isoformat()
 
 
637
  }
638
+
 
 
 
 
 
 
 
 
639
  return jsonify({
640
  'success': True,
641
+ 'cache_id': cache_id,
642
+ 'token_count': getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
643
  })
644
+
645
  except Exception as cache_error:
646
+ # If caching fails due to small content, provide alternative approach
647
+ if "Cached content is too small" in str(cache_error):
648
+ return jsonify({
649
+ 'success': False,
650
+ 'error': 'PDF is too small for caching. Please upload a larger document (minimum 4,096 tokens required).',
651
+ 'suggestion': 'Try uploading a longer document or combine multiple documents.'
652
+ })
 
 
 
 
 
 
 
 
 
 
 
 
653
  else:
654
+ raise cache_error
655
+
 
 
656
  except Exception as e:
657
+ return jsonify({'success': False, 'error': str(e)})
 
658
 
659
  @app.route('/upload-url', methods=['POST'])
660
  def upload_from_url():
 
 
 
661
  try:
662
  data = request.get_json()
663
  url = data.get('url')
664
+
665
  if not url:
666
+ return jsonify({'success': False, 'error': 'No URL provided'})
667
+
668
  # Download file from URL
669
+ response = httpx.get(url)
670
+ response.raise_for_status()
671
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  file_io = io.BytesIO(response.content)
673
+
674
+ # Upload to Gemini File API
675
+ document = client.files.upload(
676
+ file=file_io,
677
+ config=dict(mime_type='application/pdf')
678
+ )
679
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  # Create cache with system instruction
 
681
  try:
682
  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."
683
+
684
  # Use the correct model format as per documentation
685
  model = 'models/gemini-2.0-flash-001'
686
+
 
687
  cache = client.caches.create(
688
  model=model,
689
  config=types.CreateCachedContentConfig(
690
+ display_name='pdf document cache',
691
  system_instruction=system_instruction,
692
+ contents=[document],
693
+ ttl="3600s", # 1 hour TTL
694
  )
695
  )
696
+
697
+ # Store cache info
 
 
 
698
  cache_id = str(uuid.uuid4())
699
  document_caches[cache_id] = {
700
+ 'cache_name': cache.name,
701
+ 'document_name': url,
702
+ 'created_at': datetime.now().isoformat()
 
 
703
  }
704
+
 
 
 
 
 
 
 
705
  return jsonify({
706
  'success': True,
707
+ 'cache_id': cache_id,
708
+ 'token_count': getattr(cache.usage_metadata, 'cached_token_count', 'Unknown')
709
  })
710
+
711
  except Exception as cache_error:
712
+ # If caching fails due to small content, provide alternative approach
713
+ if "Cached content is too small" in str(cache_error):
714
+ return jsonify({
715
+ 'success': False,
716
+ 'error': 'PDF is too small for caching. Please upload a larger document (minimum 4,096 tokens required).',
717
+ 'suggestion': 'Try uploading a longer document or combine multiple documents.'
718
+ })
 
 
 
 
 
 
 
 
 
 
719
  else:
720
+ raise cache_error
721
+
 
 
722
  except Exception as e:
723
+ return jsonify({'success': False, 'error': str(e)})
 
 
724
 
725
  @app.route('/ask', methods=['POST'])
726
  def ask_question():
 
 
 
727
  try:
728
  data = request.get_json()
729
  question = data.get('question')
730
  cache_id = data.get('cache_id')
731
+
732
  if not question or not cache_id:
733
+ return jsonify({'success': False, 'error': 'Missing question or cache_id'})
734
+
 
 
735
  if cache_id not in document_caches:
736
+ return jsonify({'success': False, 'error': 'Cache not found'})
737
+
 
 
 
 
738
  cache_info = document_caches[cache_id]
739
+
 
 
 
740
  # Generate response using cached content with correct model format
741
  response = client.models.generate_content(
742
+ model='models/gemini-2.0-flash-001',
743
+ contents=question,
744
+ config=types.GenerateContentConfig(
745
+ cached_content=cache_info['cache_name']
746
  )
747
  )
748
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
  return jsonify({
750
  'success': True,
751
+ 'answer': response.text
752
  })
753
+
754
  except Exception as e:
755
+ return jsonify({'success': False, 'error': str(e)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
 
757
  @app.route('/caches', methods=['GET'])
758
  def list_caches():
 
 
759
  try:
760
  caches = []
761
+ for cache_id, cache_info in document_caches.items():
762
+ caches.append({
763
+ 'cache_id': cache_id,
764
+ 'document_name': cache_info['document_name'],
765
+ 'created_at': cache_info['created_at']
766
+ })
767
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  return jsonify({'success': True, 'caches': caches})
769
+
770
  except Exception as e:
 
771
  return jsonify({'success': False, 'error': str(e)})
772
 
 
773
  @app.route('/cache/<cache_id>', methods=['DELETE'])
774
  def delete_cache(cache_id):
 
 
 
775
  try:
776
  if cache_id not in document_caches:
777
+ return jsonify({'success': False, 'error': 'Cache not found'})
778
+
779
  cache_info = document_caches[cache_id]
780
+
781
+ # Delete from Gemini API
782
+ client.caches.delete(cache_info['cache_name'])
783
+
784
+ # Remove from local storage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
785
  del document_caches[cache_id]
786
+
787
+ return jsonify({'success': True, 'message': 'Cache deleted successfully'})
788
+
 
789
  except Exception as e:
790
+ return jsonify({'success': False, 'error': str(e)})
 
 
791
 
792
  if __name__ == '__main__':
793
  import os
794
  port = int(os.environ.get("PORT", 7860))
795
+ app.run(debug=True, host='0.0.0.0', port=port)