|
from flask import Flask, request, jsonify, render_template_string |
|
from flask_cors import CORS |
|
from google import genai |
|
from google.genai import types |
|
import os |
|
import io |
|
import httpx |
|
import uuid |
|
from datetime import datetime, timezone, timedelta |
|
from dotenv import load_dotenv |
|
import json |
|
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
app = Flask(__name__) |
|
CORS(app) |
|
|
|
|
|
|
|
api_key = os.getenv('GOOGLE_API_KEY') |
|
if not api_key: |
|
print("Error: GOOGLE_API_KEY environment variable not set.") |
|
|
|
|
|
|
|
|
|
pass |
|
|
|
try: |
|
client = genai.Client(api_key=api_key) |
|
except Exception as e: |
|
print(f"Failed to initialize Gemini client: {e}") |
|
client = None |
|
|
|
|
|
|
|
document_caches = {} |
|
user_sessions = {} |
|
|
|
|
|
HTML_TEMPLATE = """ |
|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Smart Document Analysis Platform</title> |
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
min-height: 100vh; |
|
color: #333; |
|
line-height: 1.6; |
|
} |
|
|
|
.container { |
|
max-width: 1400px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
min-height: 100vh; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.header { |
|
text-align: center; |
|
margin-bottom: 30px; |
|
color: white; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2.8em; |
|
font-weight: 700; |
|
margin-bottom: 10px; |
|
text-shadow: 0 2px 4px rgba(0,0,0,0.3); |
|
} |
|
|
|
.header p { |
|
font-size: 1.2em; |
|
opacity: 0.9; |
|
font-weight: 300; |
|
} |
|
|
|
.main-content { |
|
display: grid; |
|
grid-template-columns: 1fr 1fr; |
|
gap: 30px; |
|
flex-grow: 1; |
|
} |
|
|
|
.left-panel, .right-panel { |
|
background: white; |
|
border-radius: 20px; |
|
padding: 30px; |
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.left-panel { |
|
overflow-y: auto; /* Allow scrolling if content is tall */ |
|
} |
|
|
|
.panel-title { |
|
font-size: 1.5em; |
|
font-weight: 600; |
|
margin-bottom: 20px; |
|
color: #2d3748; |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
} |
|
|
|
.upload-section { |
|
margin-bottom: 30px; |
|
} |
|
|
|
.upload-area { |
|
border: 2px dashed #667eea; |
|
border-radius: 15px; |
|
padding: 40px; |
|
text-align: center; |
|
background: #f8fafc; |
|
transition: all 0.3s ease; |
|
margin-bottom: 20px; |
|
cursor: pointer; /* Indicate clickable area */ |
|
} |
|
|
|
.upload-area:hover { |
|
border-color: #764ba2; |
|
background: #f0f2ff; |
|
transform: translateY(-2px); |
|
} |
|
|
|
.upload-area.dragover { |
|
border-color: #764ba2; |
|
background: #e8f0ff; |
|
transform: scale(1.02); |
|
} |
|
|
|
.upload-icon { |
|
font-size: 3em; |
|
color: #667eea; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.file-input { |
|
display: none; |
|
} |
|
|
|
.upload-btn { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
border: none; |
|
padding: 15px 30px; |
|
border-radius: 25px; |
|
cursor: pointer; |
|
font-size: 1.1em; |
|
font-weight: 500; |
|
transition: all 0.3s ease; |
|
margin: 10px; |
|
} |
|
|
|
.upload-btn:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); |
|
} |
|
|
|
.url-input { |
|
width: 100%; |
|
padding: 15px; |
|
border: 2px solid #e2e8f0; |
|
border-radius: 10px; |
|
font-size: 1em; |
|
margin-bottom: 15px; |
|
transition: border-color 0.3s ease; |
|
} |
|
|
|
.url-input:focus { |
|
outline: none; |
|
border-color: #667eea; |
|
} |
|
|
|
.btn { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
border: none; |
|
padding: 12px 25px; |
|
border-radius: 20px; |
|
cursor: pointer; |
|
font-size: 1em; |
|
font-weight: 500; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.btn:hover { |
|
transform: translateY(-1px); |
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); |
|
} |
|
|
|
.btn:disabled { |
|
opacity: 0.6; |
|
cursor: not-allowed; |
|
transform: none; |
|
} |
|
|
|
.chat-container { |
|
flex: 1; |
|
border: 1px solid #e2e8f0; |
|
border-radius: 15px; |
|
overflow-y: auto; |
|
padding: 20px; |
|
background: #f8fafc; |
|
margin-bottom: 20px; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.message { |
|
margin-bottom: 15px; |
|
padding: 15px; |
|
border-radius: 12px; |
|
max-width: 85%; |
|
animation: fadeIn 0.3s ease; |
|
word-wrap: break-word; /* Ensure long words wrap */ |
|
} |
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(10px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
|
|
.user-message { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
margin-left: auto; |
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); |
|
} |
|
|
|
.ai-message { |
|
background: white; |
|
color: #333; |
|
border: 1px solid #e2e8f0; |
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
margin-right: auto; /* Align AI messages to the left */ |
|
} |
|
|
|
.input-group { |
|
display: flex; |
|
gap: 10px; |
|
} |
|
|
|
.question-input { |
|
flex: 1; |
|
padding: 15px; |
|
border: 2px solid #e2e8f0; |
|
border-radius: 12px; |
|
font-size: 1em; |
|
transition: border-color 0.3s ease; |
|
} |
|
|
|
.question-input:focus { |
|
outline: none; |
|
border-color: #667eea; |
|
} |
|
|
|
.cache-info { |
|
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); |
|
border-radius: 12px; |
|
padding: 20px; |
|
margin-bottom: 20px; |
|
color: white; |
|
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3); |
|
} |
|
|
|
.cache-info h3 { |
|
margin-bottom: 10px; |
|
font-weight: 600; |
|
} |
|
|
|
.cache-info p { |
|
font-size: 0.9em; |
|
margin-bottom: 5px; |
|
} |
|
|
|
.cache-info p:last-child { |
|
margin-bottom: 0; |
|
} |
|
|
|
|
|
.loading { |
|
text-align: center; |
|
padding: 40px; |
|
color: #666; |
|
} |
|
|
|
.loading-spinner { |
|
border: 3px solid #f3f3f3; |
|
border-top: 3px solid #667eea; |
|
border-radius: 50%; |
|
width: 40px; |
|
height: 40px; |
|
animation: spin 1s linear infinite; |
|
margin: 0 auto 20px; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
.error { |
|
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); |
|
border-radius: 12px; |
|
padding: 15px; |
|
color: white; |
|
margin-bottom: 20px; |
|
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.3); |
|
} |
|
|
|
.success { |
|
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); |
|
border-radius: 12px; |
|
padding: 15px; |
|
color: white; |
|
margin-bottom: 20px; |
|
box-shadow: 0 4px 12px rgba(72, 187, 120, 0.3); |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.main-content { |
|
grid-template-columns: 1fr; |
|
gap: 20px; |
|
} |
|
|
|
.header h1 { |
|
font-size: 2em; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="header"> |
|
<h1>📚 Smart Document Analysis Platform</h1> |
|
<p>Upload PDF documents once, ask questions forever with Gemini API caching</p> |
|
<p style="font-size:0.9em; margin-top: 5px; opacity: 0.8;">Powered by Google Gemini API - Explicit Caching</p> |
|
</div> |
|
|
|
<div class="main-content"> |
|
<!-- Left Panel - Upload Section --> |
|
<div class="left-panel"> |
|
<div class="panel-title"> |
|
📤 Upload PDF Document |
|
</div> |
|
|
|
<div class="upload-section"> |
|
<div class="upload-area" id="uploadArea"> |
|
<div class="upload-icon">📄</div> |
|
<p>Drag and drop your PDF file here, or click to select</p> |
|
<input type="file" id="fileInput" class="file-input" accept=".pdf"> |
|
<!-- The button triggers the hidden file input --> |
|
<button type="button" class="upload-btn" onclick="document.getElementById('fileInput').click()"> |
|
Choose PDF File |
|
</button> |
|
</div> |
|
|
|
<div style="margin-top: 20px;"> |
|
<h3>Or provide a URL:</h3> |
|
<input type="url" id="urlInput" class="url-input" placeholder="https://example.com/document.pdf"> |
|
<button type="button" class="btn" onclick="uploadFromUrl()">Upload from URL</button> |
|
</div> |
|
</div> |
|
|
|
<div id="loading" class="loading" style="display: none;"> |
|
<div class="loading-spinner"></div> |
|
<p id="loadingText">Processing your PDF... This may take a moment.</p> |
|
</div> |
|
|
|
<div id="error" class="error" style="display: none;"></div> |
|
<div id="success" class="success" style="display: none;"></div> |
|
</div> |
|
|
|
<!-- Right Panel - Chat Section --> |
|
<div class="right-panel"> |
|
<div class="panel-title"> |
|
💬 Ask Questions |
|
</div> |
|
|
|
<div id="cacheInfo" class="cache-info" style="display: none;"> |
|
<h3>✅ Document Cached Successfully!</h3> |
|
<p>Your PDF has been cached using Gemini API. You can now ask multiple questions without re-uploading.</p> |
|
<p><strong>Cache ID:</strong> <span id="cacheId"></span></p> |
|
<p><strong>Tokens Cached:</strong> <span id="tokenCount"></span></p> |
|
<p>Note: Caching is ideal for larger documents (typically 1024+ tokens required).</p> |
|
</div> |
|
|
|
<div class="chat-container" id="chatContainer"> |
|
<div class="message ai-message"> |
|
👋 Hello! Upload a PDF document using the panel on the left, and I'll help you analyze it using Gemini API caching! |
|
</div> |
|
</div> |
|
|
|
<div class="input-group"> |
|
<input type="text" id="questionInput" class="question-input" placeholder="Ask a question about your document..." disabled> |
|
<button type="button" class="btn" onclick="askQuestion()" id="askBtn" disabled>Ask</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<script> |
|
let currentCacheId = null; |
|
|
|
// Disable input/button initially |
|
document.getElementById('questionInput').disabled = true; |
|
document.getElementById('askBtn').disabled = true; |
|
|
|
// File upload handling |
|
const uploadArea = document.getElementById('uploadArea'); |
|
const fileInput = document.getElementById('fileInput'); |
|
|
|
// Prevent default drag behaviors |
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
|
uploadArea.addEventListener(eventName, preventDefaults, false); |
|
}); |
|
|
|
function preventDefaults (e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
} |
|
|
|
// Highlight drop area when item is dragged over |
|
['dragenter', 'dragover'].forEach(eventName => { |
|
uploadArea.addEventListener(eventName, () => uploadArea.classList.add('dragover'), false); |
|
}); |
|
|
|
['dragleave', 'drop'].forEach(eventName => { |
|
uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('dragover'), false); |
|
}); |
|
|
|
// Handle dropped files |
|
uploadArea.addEventListener('drop', handleDrop, false); |
|
|
|
function handleDrop(e) { |
|
const dt = e.dataTransfer; |
|
const files = dt.files; |
|
if (files.length > 0) { |
|
uploadFile(files[0]); |
|
} |
|
} |
|
|
|
fileInput.addEventListener('change', (e) => { |
|
if (e.target.files.length > 0) { |
|
uploadFile(e.target.files[0]); |
|
// Clear the input so the same file can be selected again if needed |
|
e.target.value = ''; |
|
} |
|
}); |
|
|
|
async function uploadFile(file) { |
|
if (!file.type.includes('pdf')) { |
|
showError('Please select a PDF file.'); |
|
return; |
|
} |
|
|
|
// Clear previous status messages |
|
hideError(); |
|
hideSuccess(); |
|
document.getElementById('cacheInfo').style.display = 'none'; // Hide old cache info |
|
currentCacheId = null; // Clear old cache ID |
|
|
|
showLoading('Uploading PDF...'); |
|
|
|
const formData = new FormData(); |
|
formData.append('file', file); |
|
|
|
try { |
|
const response = await fetch('/upload', { |
|
method: 'POST', |
|
body: formData |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
currentCacheId = result.cache_id; |
|
document.getElementById('cacheId').textContent = result.cache_id; |
|
document.getElementById('tokenCount').textContent = result.token_count; |
|
document.getElementById('cacheInfo').style.display = 'block'; |
|
showSuccess('PDF uploaded and cached successfully! You can now ask questions.'); |
|
|
|
// Enable chat input and button |
|
document.getElementById('questionInput').disabled = false; |
|
document.getElementById('askBtn').disabled = false; |
|
document.getElementById('questionInput').focus(); // Focus input |
|
|
|
// Add initial message |
|
addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai'); |
|
|
|
} else { |
|
showError(result.error); |
|
// Disable chat input/button if upload/cache failed |
|
document.getElementById('questionInput').disabled = true; |
|
document.getElementById('askBtn').disabled = true; |
|
} |
|
} catch (error) { |
|
showError('Error uploading file: ' + error.message); |
|
// Disable chat input/button on network/server error |
|
document.getElementById('questionInput').disabled = true; |
|
document.getElementById('askBtn').disabled = true; |
|
} finally { |
|
hideLoading(); |
|
} |
|
} |
|
|
|
async function uploadFromUrl() { |
|
const url = document.getElementById('urlInput').value; |
|
if (!url.trim()) { |
|
showError('Please enter a valid URL.'); |
|
return; |
|
} |
|
|
|
// Clear previous status messages |
|
hideError(); |
|
hideSuccess(); |
|
document.getElementById('cacheInfo').style.display = 'none'; // Hide old cache info |
|
currentCacheId = null; // Clear old cache ID |
|
|
|
showLoading('Uploading PDF from URL...'); |
|
|
|
try { |
|
const response = await fetch('/upload-url', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ url: url }) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
currentCacheId = result.cache_id; |
|
document.getElementById('cacheId').textContent = result.cache_id; |
|
document.getElementById('tokenCount').textContent = result.token_count; |
|
document.getElementById('cacheInfo').style.display = 'block'; |
|
showSuccess('PDF uploaded and cached successfully! You can now ask questions.'); |
|
|
|
// Enable chat input and button |
|
document.getElementById('questionInput').disabled = false; |
|
document.getElementById('askBtn').disabled = false; |
|
document.getElementById('questionInput').focus(); // Focus input |
|
|
|
// Add initial message |
|
addMessage("I've analyzed your PDF document. What would you like to know about it?", 'ai'); |
|
|
|
} else { |
|
showError(result.error); |
|
// Disable chat input/button if upload/cache failed |
|
document.getElementById('questionInput').disabled = true; |
|
document.getElementById('askBtn').disabled = true; |
|
} |
|
} catch (error) { |
|
showError('Error uploading from URL: ' + error.message); |
|
// Disable chat input/button on network/server error |
|
document.getElementById('questionInput').disabled = true; |
|
document.getElementById('askBtn').disabled = false; // Should be false? Fix: should be true |
|
} finally { |
|
hideLoading(); |
|
} |
|
} |
|
|
|
async function askQuestion() { |
|
const questionInput = document.getElementById('questionInput'); |
|
const question = questionInput.value.trim(); |
|
if (!question) return; // Don't send empty questions |
|
|
|
if (!currentCacheId) { |
|
showError('Please upload a PDF document first.'); |
|
return; |
|
} |
|
|
|
// Add user message to chat |
|
addMessage(question, 'user'); |
|
questionInput.value = ''; // Clear input immediately |
|
|
|
// Show loading state |
|
const askBtn = document.getElementById('askBtn'); |
|
const originalText = askBtn.textContent; |
|
askBtn.textContent = 'Generating...'; |
|
askBtn.disabled = true; |
|
questionInput.disabled = true; // Disable input while generating |
|
|
|
try { |
|
const response = await fetch('/ask', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
}, |
|
body: JSON.stringify({ |
|
question: question, |
|
cache_id: currentCacheId // Use our internal cache_id |
|
}) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
addMessage(result.answer, 'ai'); |
|
} else { |
|
addMessage('Error: ' + result.error, 'ai'); |
|
} |
|
} catch (error) { |
|
addMessage('Error: ' + error.message, 'ai'); |
|
} finally { |
|
askBtn.textContent = originalText; |
|
askBtn.disabled = false; |
|
questionInput.disabled = false; // Re-enable input |
|
questionInput.focus(); // Put focus back on input |
|
// Ensure button is disabled only if no cache is active |
|
if (!currentCacheId) { |
|
askBtn.disabled = true; |
|
questionInput.disabled = true; |
|
} |
|
} |
|
} |
|
|
|
function addMessage(text, sender) { |
|
const chatContainer = document.getElementById('chatContainer'); |
|
const messageDiv = document.createElement('div'); |
|
messageDiv.className = `message ${sender}-message`; |
|
|
|
// Use innerHTML to handle potential formatting like newlines or markdown |
|
// (Basic textContent might be sufficient depending on expected AI output) |
|
// For simplicity here, sticking to textContent as AI might output plain text |
|
messageDiv.textContent = text; |
|
|
|
// Basic handling for newlines |
|
messageDiv.style.whiteSpace = 'pre-wrap'; |
|
|
|
chatContainer.appendChild(messageDiv); |
|
chatContainer.scrollTop = chatContainer.scrollHeight; // Auto-scroll to latest message |
|
} |
|
|
|
function showLoading(text = 'Processing...') { |
|
document.getElementById('loadingText').textContent = text; |
|
document.getElementById('loading').style.display = 'block'; |
|
} |
|
|
|
function hideLoading() { |
|
document.getElementById('loading').style.display = 'none'; |
|
} |
|
|
|
function showError(message) { |
|
const errorDiv = document.getElementById('error'); |
|
errorDiv.textContent = message; |
|
errorDiv.style.display = 'block'; |
|
// Auto-hide after 5 seconds |
|
setTimeout(() => { |
|
errorDiv.style.display = 'none'; |
|
}, 5000); |
|
} |
|
|
|
function showSuccess(message) { |
|
const successDiv = document.getElementById('success'); |
|
successDiv.textContent = message; |
|
successDiv.style.display = 'block'; |
|
// Auto-hide after 5 seconds |
|
setTimeout(() => { |
|
successDiv.style.display = 'none'; |
|
}, 5000); |
|
} |
|
|
|
function hideError() { |
|
document.getElementById('error').style.display = 'none'; |
|
} |
|
|
|
function hideSuccess() { |
|
document.getElementById('success').style.display = 'none'; |
|
} |
|
|
|
// Enter key to ask question |
|
document.getElementById('questionInput').addEventListener('keypress', (e) => { |
|
// Check if the input is not disabled and the key is Enter |
|
if (!document.getElementById('questionInput').disabled && e.key === 'Enter') { |
|
e.preventDefault(); // Prevent default form submission if input is part of a form |
|
askQuestion(); |
|
} |
|
}); |
|
|
|
// Initial message visibility |
|
// 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 |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
|
|
@app.route('/') |
|
def index(): |
|
|
|
if not api_key: |
|
|
|
print("Warning: API key not set. API calls will fail.") |
|
return render_template_string(HTML_TEMPLATE) |
|
|
|
@app.route('/health', methods=['GET']) |
|
def health_check(): |
|
|
|
|
|
if client is None and api_key is not None: |
|
return jsonify({"status": "unhealthy", "reason": "Gemini client failed to initialize"}), 500 |
|
|
|
|
|
return jsonify({"status": "healthy"}), 200 |
|
|
|
|
|
@app.route('/upload', methods=['POST']) |
|
def upload_file(): |
|
if client is None or api_key is None: |
|
return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500 |
|
|
|
try: |
|
if 'file' not in request.files: |
|
return jsonify({'success': False, 'error': 'No file provided'}) |
|
|
|
file = request.files['file'] |
|
|
|
if file.filename == '': |
|
return jsonify({'success': False, 'error': 'No file selected'}) |
|
|
|
|
|
file_content = file.read() |
|
file_io = io.BytesIO(file_content) |
|
|
|
|
|
|
|
|
|
|
|
document = None |
|
try: |
|
|
|
|
|
document = client.upload_file( |
|
file=(file.filename, file_io, 'application/pdf'), |
|
|
|
) |
|
print(f"File uploaded successfully to Gemini File API: {document.name}") |
|
|
|
|
|
except Exception as upload_error: |
|
|
|
error_msg = str(upload_error) |
|
print(f"Error uploading file to Gemini API: {error_msg}") |
|
|
|
if "file content size exceeds limit" in error_msg.lower(): |
|
return jsonify({'success': False, 'error': f'Error uploading file: File size exceeds API limit. {error_msg}'}), 413 |
|
return jsonify({'success': False, 'error': f'Error uploading file to Gemini API: {error_msg}'}), 500 |
|
|
|
|
|
|
|
cache = None |
|
try: |
|
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." |
|
|
|
|
|
|
|
model = 'models/gemini-2.0-flash-001' |
|
|
|
print(f"Attempting to create cache for file: {document.name}") |
|
cache = client.caches.create( |
|
model=model, |
|
config=types.CreateCachedContentConfig( |
|
display_name=f'pdf document cache: {file.filename}', |
|
system_instruction=system_instruction, |
|
contents=[document], |
|
ttl="3600s", |
|
) |
|
) |
|
print(f"Cache created successfully: {cache.name}") |
|
|
|
|
|
|
|
cache_id = str(uuid.uuid4()) |
|
document_caches[cache_id] = { |
|
'gemini_cache_name': cache.name, |
|
'document_name': file.filename, |
|
'gemini_file_name': document.name, |
|
'created_at': datetime.now().isoformat(), |
|
'expires_at': (datetime.now(timezone.utc) + timedelta(seconds=3600)).isoformat(), |
|
} |
|
|
|
|
|
|
|
token_count = 'Unknown' |
|
if hasattr(cache, 'usage_metadata') and cache.usage_metadata: |
|
token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown') |
|
print(f"Cached token count: {token_count}") |
|
|
|
|
|
return jsonify({ |
|
'success': True, |
|
'cache_id': cache_id, |
|
'token_count': token_count |
|
}) |
|
|
|
except Exception as cache_error: |
|
error_msg = str(cache_error) |
|
print(f"Cache creation failed: {error_msg}") |
|
|
|
if document and hasattr(document, 'name'): |
|
try: |
|
client.files.delete(document.name) |
|
print(f"Cleaned up uploaded file {document.name} after caching failure.") |
|
except Exception as cleanup_error: |
|
print(f"Failed to clean up file {document.name}: {cleanup_error}") |
|
|
|
|
|
|
|
|
|
if "Cached content is too small" in error_msg or "minimum size" in error_msg.lower() or "tokens required" in error_msg.lower(): |
|
return jsonify({ |
|
'success': False, |
|
'error': f'PDF content is too small for caching. Minimum token count varies by model, but is typically 1024+ for Flash. {error_msg}', |
|
'suggestion': 'Try uploading a longer document or combine multiple documents.' |
|
}), 400 |
|
else: |
|
|
|
return jsonify({'success': False, 'error': f'Error creating cache with Gemini API: {error_msg}'}), 500 |
|
|
|
|
|
except Exception as e: |
|
print(f"An unexpected error occurred during upload process: {str(e)}") |
|
return jsonify({'success': False, 'error': str(e)}), 500 |
|
|
|
@app.route('/upload-url', methods=['POST']) |
|
def upload_from_url(): |
|
if client is None or api_key is None: |
|
return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500 |
|
|
|
try: |
|
data = request.get_json() |
|
url = data.get('url') |
|
|
|
if not url: |
|
return jsonify({'success': False, 'error': 'No URL provided'}), 400 |
|
|
|
|
|
response = None |
|
try: |
|
|
|
|
|
response = httpx.get(url, follow_redirects=True, timeout=30.0) |
|
response.raise_for_status() |
|
|
|
|
|
content_type = response.headers.get('Content-Type', '').lower() |
|
if 'application/pdf' not in content_type: |
|
print(f"Warning: URL content type is not application/pdf: {content_type}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
except httpx.HTTPStatusError as e: |
|
print(f"HTTP error downloading file from URL {url}: {e.response.status_code} - {e.response.text}") |
|
return jsonify({'success': False, 'error': f'HTTP error downloading file from URL: {e.response.status_code} - {e.response.text}'}), e.response.status_code |
|
except httpx.RequestError as e: |
|
print(f"Error downloading file from URL {url}: {e}") |
|
return jsonify({'success': False, 'error': f'Error downloading file from URL: {e}'}), 500 |
|
|
|
|
|
file_io = io.BytesIO(response.content) |
|
|
|
|
|
|
|
|
|
|
|
document = None |
|
try: |
|
|
|
filename = os.path.basename(url) |
|
if not filename or '.' not in filename: |
|
filename = 'downloaded_document.pdf' |
|
|
|
|
|
mime_type = content_type if 'application/pdf' in content_type else 'application/pdf' |
|
|
|
|
|
document = client.upload_file( |
|
file=(filename, file_io, mime_type), |
|
display_name=url |
|
) |
|
print(f"File from URL uploaded successfully to Gemini File API: {document.name}") |
|
|
|
|
|
|
|
except Exception as upload_error: |
|
|
|
error_msg = str(upload_error) |
|
print(f"Error uploading file from URL to Gemini API: {error_msg}") |
|
|
|
if "file content size exceeds limit" in error_msg.lower(): |
|
return jsonify({'success': False, 'error': f'Error uploading file: File size exceeds API limit. {error_msg}'}), 413 |
|
return jsonify({'success': False, 'error': f'Error uploading file from URL to Gemini API: {error_msg}'}), 500 |
|
|
|
|
|
|
|
|
|
cache = None |
|
try: |
|
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." |
|
|
|
|
|
model = 'models/gemini-2.0-flash-001' |
|
|
|
print(f"Attempting to create cache for file: {document.name}") |
|
cache = client.caches.create( |
|
model=model, |
|
config=types.CreateCachedContentConfig( |
|
display_name=f'pdf document cache: {url}', |
|
system_instruction=system_instruction, |
|
contents=[document], |
|
ttl="3600s", |
|
) |
|
) |
|
print(f"Cache created successfully: {cache.name}") |
|
|
|
|
|
|
|
|
|
cache_id = str(uuid.uuid4()) |
|
document_caches[cache_id] = { |
|
'gemini_cache_name': cache.name, |
|
'document_name': url, |
|
'gemini_file_name': document.name, |
|
'created_at': datetime.now().isoformat(), |
|
'expires_at': (datetime.now(timezone.utc) + timedelta(seconds=3600)).isoformat(), |
|
} |
|
|
|
|
|
token_count = 'Unknown' |
|
if hasattr(cache, 'usage_metadata') and cache.usage_metadata: |
|
token_count = getattr(cache.usage_metadata, 'cached_token_count', 'Unknown') |
|
print(f"Cached token count: {token_count}") |
|
|
|
|
|
return jsonify({ |
|
'success': True, |
|
'cache_id': cache_id, |
|
'token_count': token_count |
|
}) |
|
|
|
except Exception as cache_error: |
|
error_msg = str(cache_error) |
|
print(f"Cache creation failed: {error_msg}") |
|
|
|
if document and hasattr(document, 'name'): |
|
try: |
|
client.files.delete(document.name) |
|
print(f"Cleaned up uploaded file {document.name} after caching failure.") |
|
except Exception as cleanup_error: |
|
print(f"Failed to clean up file {document.name}: {cleanup_error}") |
|
|
|
|
|
if "Cached content is too small" in error_msg or "minimum size" in error_msg.lower() or "tokens required" in error_msg.lower(): |
|
return jsonify({ |
|
'success': False, |
|
'error': f'PDF content is too small for caching. Minimum token count varies by model, but is typically 1024+ for Flash. {error_msg}', |
|
'suggestion': 'Try uploading a longer document or combine multiple documents.' |
|
}), 400 |
|
else: |
|
|
|
return jsonify({'success': False, 'error': f'Error creating cache with Gemini API: {error_msg}'}), 500 |
|
|
|
|
|
except Exception as e: |
|
print(f"An unexpected error occurred during URL upload process: {str(e)}") |
|
return jsonify({'success': False, 'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/ask', methods=['POST']) |
|
def ask_question(): |
|
if client is None or api_key is None: |
|
return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500 |
|
|
|
try: |
|
data = request.get_json() |
|
question = data.get('question') |
|
cache_id = data.get('cache_id') |
|
|
|
if not question or not cache_id: |
|
return jsonify({'success': False, 'error': 'Missing question or cache_id'}), 400 |
|
|
|
|
|
|
|
if cache_id not in document_caches: |
|
|
|
|
|
print(f"Cache ID {cache_id} not found in local storage.") |
|
return jsonify({'success': False, 'error': 'Cache not found or expired. Please upload the document again.'}), 404 |
|
|
|
|
|
cache_info = document_caches[cache_id] |
|
gemini_cache_name = cache_info['gemini_cache_name'] |
|
print(f"Using Gemini cache name: {gemini_cache_name} for question.") |
|
|
|
|
|
|
|
response = client.models.generate_content( |
|
model='models/gemini-2.0-flash-001', |
|
contents=[{'text': question}], |
|
generation_config=types.GenerateContentConfig( |
|
cached_content=gemini_cache_name |
|
) |
|
) |
|
|
|
|
|
answer = "Could not generate response from the model." |
|
if response and response.candidates: |
|
|
|
answer_parts = [] |
|
for candidate in response.candidates: |
|
if candidate.content and candidate.content.parts: |
|
for part in candidate.content.parts: |
|
if hasattr(part, 'text'): |
|
answer_parts.append(part.text) |
|
|
|
|
|
|
|
|
|
|
|
if answer_parts: |
|
answer = "".join(answer_parts) |
|
else: |
|
|
|
answer = "Model returned content without text parts (e.g., tool calls)." |
|
print(f"Model returned non-text parts: {response.candidates}") |
|
|
|
elif response and response.prompt_feedback and response.prompt_feedback.block_reason: |
|
|
|
block_reason = response.prompt_feedback.block_reason.name |
|
block_message = getattr(response.prompt_feedback, 'block_reason_message', 'No message provided') |
|
answer = f"Request blocked by safety filters. Reason: {block_reason}. Message: {block_message}" |
|
print(f"Request blocked: {block_reason} - {block_message}") |
|
|
|
else: |
|
|
|
print(f"Unexpected response structure from API: {response}") |
|
|
|
|
|
return jsonify({ |
|
'success': True, |
|
'answer': answer |
|
}) |
|
|
|
except Exception as e: |
|
print(f"An error occurred during question asking: {str(e)}") |
|
|
|
error_msg = str(e) |
|
if "Resource has been exhausted" in error_msg: |
|
error_msg = "API rate limit or quota exceeded. Please try again later." |
|
elif "cached_content refers to a resource that has been deleted" in error_msg: |
|
error_msg = "The cached document has expired or was deleted from Gemini API. Please upload the document again." |
|
|
|
if cache_id in document_caches: |
|
print(f"Removing local entry for cache_id {cache_id} as API confirmed deletion.") |
|
del document_caches[cache_id] |
|
elif "invalid cached_content value" in error_msg: |
|
error_msg = "Invalid cache reference. The cached document might have expired or been deleted. Please upload the document again." |
|
|
|
if cache_id in document_caches: |
|
print(f"Removing local entry for cache_id {cache_id} as API confirmed deletion (invalid reference).") |
|
del document_caches[cache_id] |
|
elif "model does not exist" in error_msg: |
|
error_msg = "The specified model is not available." |
|
|
|
|
|
return jsonify({'success': False, 'error': f'Error from Gemini API: {error_msg}'}), 500 |
|
|
|
|
|
@app.route('/caches', methods=['GET']) |
|
def list_caches(): |
|
|
|
|
|
try: |
|
caches = [] |
|
for cache_id, cache_info in list(document_caches.items()): |
|
|
|
|
|
try: |
|
|
|
api_cache_info = client.caches.get(name=cache_info['gemini_cache_name']) |
|
|
|
caches.append({ |
|
'cache_id': cache_id, |
|
'document_name': cache_info['document_name'], |
|
'gemini_cache_name': cache_info['gemini_cache_name'], |
|
'created_at': cache_info['created_at'], |
|
'expires_at': getattr(api_cache_info, 'expire_time', 'Unknown'), |
|
'cached_token_count': getattr(api_cache_info.usage_metadata, 'cached_token_count', 'Unknown') if hasattr(api_cache_info, 'usage_metadata') else 'Unknown' |
|
}) |
|
except Exception as e: |
|
|
|
print(f"Gemini cache {cache_info['gemini_cache_name']} for local ID {cache_id} not found via API. Removing from local storage. Error: {e}") |
|
del document_caches[cache_id] |
|
|
|
|
|
return jsonify({'success': True, 'caches': caches}) |
|
|
|
except Exception as e: |
|
print(f"An error occurred listing caches: {str(e)}") |
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/cache/<cache_id>', methods=['DELETE']) |
|
def delete_cache(cache_id): |
|
if client is None or api_key is None: |
|
return jsonify({'success': False, 'error': 'API key not configured or Gemini client failed to initialize.'}), 500 |
|
|
|
try: |
|
if cache_id not in document_caches: |
|
return jsonify({'success': False, 'error': 'Cache not found'}), 404 |
|
|
|
cache_info = document_caches[cache_id] |
|
gemini_cache_name_to_delete = cache_info['gemini_cache_name'] |
|
gemini_file_name_to_delete = cache_info['gemini_file_name'] |
|
|
|
|
|
|
|
try: |
|
client.caches.delete(gemini_cache_name_to_delete) |
|
print(f"Gemini cache deleted: {gemini_cache_name_to_delete}") |
|
except Exception as delete_error: |
|
error_msg = str(delete_error) |
|
print(f"Error deleting Gemini cache {gemini_cache_name_to_delete}: {error_msg}") |
|
|
|
if "Resource not found" in error_msg: |
|
print(f"Gemini cache {gemini_cache_name_to_delete} already gone from API.") |
|
else: |
|
|
|
return jsonify({'success': False, 'error': f'Failed to delete cache from API: {error_msg}'}), 500 |
|
|
|
|
|
|
|
if gemini_file_name_to_delete: |
|
try: |
|
client.files.delete(gemini_file_name_to_delete) |
|
print(f"Associated Gemini file deleted: {gemini_file_name_to_delete}") |
|
except Exception as file_delete_error: |
|
error_msg = str(file_delete_error) |
|
print(f"Error deleting Gemini file {gemini_file_name_to_delete}: {error_msg}") |
|
if "Resource not found" in error_msg: |
|
print(f"Gemini file {gemini_file_name_to_delete} already gone from API.") |
|
else: |
|
|
|
pass |
|
|
|
|
|
|
|
del document_caches[cache_id] |
|
print(f"Local cache entry deleted for ID: {cache_id}") |
|
|
|
return jsonify({'success': True, 'message': 'Cache and associated file deleted successfully'}) |
|
|
|
except Exception as e: |
|
print(f"An unexpected error occurred during cache deletion process: {str(e)}") |
|
return jsonify({'success': False, 'error': str(e)}), 500 |
|
|
|
|
|
if __name__ == '__main__': |
|
import os |
|
port = int(os.environ.get("PORT", 7860)) |
|
print(f"Starting Flask app on port {port}") |
|
|
|
|
|
app.run(debug=True, host='0.0.0.0', port=port, threaded=True) |