Spaces:
Runtime error
Runtime error
You can now upload PDFs, CSVs, or images, and the agent will process them accordingly. (#2)
Browse files- app.py to support PDF, CSV, and DOC files alongside images, as requested. (3edcefed5e91960b696cd0ab36f9388741e60424)
app.py
CHANGED
|
@@ -32,8 +32,9 @@ import json
|
|
| 32 |
import warnings
|
| 33 |
import logging
|
| 34 |
import time
|
|
|
|
| 35 |
from functools import lru_cache
|
| 36 |
-
from typing import Optional
|
| 37 |
from datetime import datetime
|
| 38 |
|
| 39 |
# Suppress ResourceWarning for cleaner output
|
|
@@ -322,6 +323,20 @@ so be accurate and thorough.
|
|
| 322 |
# AGENT CREATION
|
| 323 |
# =============================================================================
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
_cached_agent = None
|
| 326 |
|
| 327 |
|
|
@@ -382,12 +397,95 @@ def create_enhanced_agent():
|
|
| 382 |
return _cached_agent
|
| 383 |
|
| 384 |
|
| 385 |
-
def query_agent(question: str) -> str:
|
| 386 |
-
"""Process question with the enhanced agent."""
|
| 387 |
try:
|
| 388 |
logger.info(f"Processing query: {question}")
|
|
|
|
|
|
|
|
|
|
| 389 |
agent = create_enhanced_agent()
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
logger.info("Query completed successfully")
|
| 392 |
return str(result)
|
| 393 |
except Exception as e:
|
|
@@ -411,8 +509,8 @@ app.add_middleware(
|
|
| 411 |
)
|
| 412 |
|
| 413 |
|
| 414 |
-
|
| 415 |
-
|
| 416 |
|
| 417 |
|
| 418 |
class AnswerResponse(BaseModel):
|
|
@@ -430,7 +528,7 @@ async def index():
|
|
| 430 |
async def ask_question(request: QuestionRequest):
|
| 431 |
"""Process a question and return the answer."""
|
| 432 |
try:
|
| 433 |
-
answer = query_agent(request.question)
|
| 434 |
return AnswerResponse(
|
| 435 |
answer=answer,
|
| 436 |
metrics=_metrics.get_stats()
|
|
@@ -731,12 +829,23 @@ def get_ui_html() -> str:
|
|
| 731 |
|
| 732 |
<div class="main-content">
|
| 733 |
<div class="chat-section">
|
|
|
|
| 734 |
<div class="input-area">
|
| 735 |
<textarea
|
| 736 |
id="questionInput"
|
| 737 |
placeholder="Ask a question about fraud models, applications, or policies..."
|
| 738 |
onkeydown="if(event.key === 'Enter' && !event.shiftKey) {{ event.preventDefault(); askQuestion(); }}"
|
| 739 |
></textarea>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
<button id="askBtn" onclick="askQuestion()">๐ Analyze</button>
|
| 741 |
</div>
|
| 742 |
|
|
@@ -816,53 +925,110 @@ Enter your question above and click "Analyze" to get started.</pre>
|
|
| 816 |
examplesContainer.appendChild(btn);
|
| 817 |
}});
|
| 818 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
async function askQuestion() {{
|
| 820 |
const input = document.getElementById('questionInput');
|
|
|
|
| 821 |
const btn = document.getElementById('askBtn');
|
| 822 |
const responseArea = document.getElementById('responseArea');
|
| 823 |
-
const responseText = document.getElementById('responseText');
|
| 824 |
|
| 825 |
const question = input.value.trim();
|
| 826 |
-
if (!question) return;
|
| 827 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
btn.disabled = true;
|
| 829 |
btn.textContent = 'โณ Processing...';
|
| 830 |
-
responseArea.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing
|
| 831 |
|
| 832 |
try {{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
const response = await fetch('/api/ask', {{
|
| 834 |
method: 'POST',
|
| 835 |
headers: {{
|
| 836 |
-
'Content-Type': 'application/json'
|
| 837 |
}},
|
| 838 |
-
body: JSON.stringify(
|
| 839 |
}});
|
| 840 |
|
| 841 |
-
if (!response.ok) {{
|
| 842 |
-
throw new Error(`HTTP error! status: ${{response.status}}`);
|
| 843 |
-
}}
|
| 844 |
-
|
| 845 |
const data = await response.json();
|
| 846 |
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
}}
|
| 859 |
|
| 860 |
}} catch (error) {{
|
| 861 |
-
responseArea.innerHTML =
|
| 862 |
-
document.getElementById('responseText').textContent = 'Error: ' + error.message;
|
| 863 |
}} finally {{
|
|
|
|
| 864 |
btn.disabled = false;
|
| 865 |
btn.textContent = '๐ Analyze';
|
|
|
|
|
|
|
|
|
|
| 866 |
}}
|
| 867 |
}}
|
| 868 |
|
|
|
|
| 32 |
import warnings
|
| 33 |
import logging
|
| 34 |
import time
|
| 35 |
+
import base64
|
| 36 |
from functools import lru_cache
|
| 37 |
+
from typing import Optional, List
|
| 38 |
from datetime import datetime
|
| 39 |
|
| 40 |
# Suppress ResourceWarning for cleaner output
|
|
|
|
| 323 |
# AGENT CREATION
|
| 324 |
# =============================================================================
|
| 325 |
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
class FilePayload(BaseModel):
|
| 329 |
+
data: str # Base64 encoded data
|
| 330 |
+
format: str
|
| 331 |
+
name: str = "file"
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
class QuestionRequest(BaseModel):
|
| 335 |
+
question: str
|
| 336 |
+
files: Optional[List[FilePayload]] = None
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
|
| 340 |
_cached_agent = None
|
| 341 |
|
| 342 |
|
|
|
|
| 397 |
return _cached_agent
|
| 398 |
|
| 399 |
|
| 400 |
+
def query_agent(question: str, files: Optional[List[FilePayload]] = None) -> str:
|
| 401 |
+
"""Process question with the enhanced agent, optionally including files."""
|
| 402 |
try:
|
| 403 |
logger.info(f"Processing query: {question}")
|
| 404 |
+
if files:
|
| 405 |
+
logger.info(f"Query includes {len(files)} files")
|
| 406 |
+
|
| 407 |
agent = create_enhanced_agent()
|
| 408 |
+
|
| 409 |
+
if files:
|
| 410 |
+
# Try to use official types if available
|
| 411 |
+
try:
|
| 412 |
+
from strands.types.content import ImageContent, DocumentContent
|
| 413 |
+
from strands.types.media import ImageSource, DocumentSource
|
| 414 |
+
|
| 415 |
+
message_content = [{"text": question}]
|
| 416 |
+
|
| 417 |
+
image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'}
|
| 418 |
+
|
| 419 |
+
for file_obj in files:
|
| 420 |
+
try:
|
| 421 |
+
# Remove header if present
|
| 422 |
+
base64_data = file_obj.data
|
| 423 |
+
if "," in base64_data:
|
| 424 |
+
base64_data = base64_data.split(",")[1]
|
| 425 |
+
|
| 426 |
+
file_bytes = base64.b64decode(base64_data)
|
| 427 |
+
fmt = file_obj.format.lower()
|
| 428 |
+
|
| 429 |
+
if fmt in image_formats:
|
| 430 |
+
# Handle Image
|
| 431 |
+
image_block = ImageContent(
|
| 432 |
+
format=fmt if fmt != 'jpg' else 'jpeg', # Normalize jpg
|
| 433 |
+
source=ImageSource(bytes=file_bytes)
|
| 434 |
+
)
|
| 435 |
+
message_content.append({"image": image_block})
|
| 436 |
+
else:
|
| 437 |
+
# Handle Document
|
| 438 |
+
doc_block = DocumentContent(
|
| 439 |
+
format=fmt,
|
| 440 |
+
name=file_obj.name,
|
| 441 |
+
source=DocumentSource(bytes=file_bytes)
|
| 442 |
+
)
|
| 443 |
+
message_content.append({"document": doc_block})
|
| 444 |
+
|
| 445 |
+
except Exception as err:
|
| 446 |
+
logger.error(f"Failed to process file {file_obj.name}: {err}")
|
| 447 |
+
|
| 448 |
+
except ImportError:
|
| 449 |
+
# Fallback for older versions or missing imports
|
| 450 |
+
logger.info("Using legacy/dict construction (strands.types not found)")
|
| 451 |
+
message_content = [{"text": question}]
|
| 452 |
+
|
| 453 |
+
image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'}
|
| 454 |
+
|
| 455 |
+
for file_obj in files:
|
| 456 |
+
try:
|
| 457 |
+
base64_data = file_obj.data
|
| 458 |
+
if "," in base64_data:
|
| 459 |
+
base64_data = base64_data.split(",")[1]
|
| 460 |
+
|
| 461 |
+
file_bytes = base64.b64decode(base64_data)
|
| 462 |
+
fmt = file_obj.format.lower()
|
| 463 |
+
|
| 464 |
+
if fmt in image_formats:
|
| 465 |
+
message_content.append({
|
| 466 |
+
"image": {
|
| 467 |
+
"format": fmt if fmt != 'jpg' else 'jpeg',
|
| 468 |
+
"source": {"bytes": file_bytes},
|
| 469 |
+
},
|
| 470 |
+
})
|
| 471 |
+
else:
|
| 472 |
+
message_content.append({
|
| 473 |
+
"document": {
|
| 474 |
+
"format": fmt,
|
| 475 |
+
"name": file_obj.name,
|
| 476 |
+
"source": {"bytes": file_bytes},
|
| 477 |
+
},
|
| 478 |
+
})
|
| 479 |
+
|
| 480 |
+
except Exception as err:
|
| 481 |
+
logger.error(f"Failed to process file: {err}")
|
| 482 |
+
|
| 483 |
+
# Call agent with list payload
|
| 484 |
+
result = agent(message_content)
|
| 485 |
+
else:
|
| 486 |
+
# Standard text-only call
|
| 487 |
+
result = agent(question)
|
| 488 |
+
|
| 489 |
logger.info("Query completed successfully")
|
| 490 |
return str(result)
|
| 491 |
except Exception as e:
|
|
|
|
| 509 |
)
|
| 510 |
|
| 511 |
|
| 512 |
+
|
| 513 |
+
|
| 514 |
|
| 515 |
|
| 516 |
class AnswerResponse(BaseModel):
|
|
|
|
| 528 |
async def ask_question(request: QuestionRequest):
|
| 529 |
"""Process a question and return the answer."""
|
| 530 |
try:
|
| 531 |
+
answer = query_agent(request.question, request.files)
|
| 532 |
return AnswerResponse(
|
| 533 |
answer=answer,
|
| 534 |
metrics=_metrics.get_stats()
|
|
|
|
| 829 |
|
| 830 |
<div class="main-content">
|
| 831 |
<div class="chat-section">
|
| 832 |
+
|
| 833 |
<div class="input-area">
|
| 834 |
<textarea
|
| 835 |
id="questionInput"
|
| 836 |
placeholder="Ask a question about fraud models, applications, or policies..."
|
| 837 |
onkeydown="if(event.key === 'Enter' && !event.shiftKey) {{ event.preventDefault(); askQuestion(); }}"
|
| 838 |
></textarea>
|
| 839 |
+
</div>
|
| 840 |
+
|
| 841 |
+
<div class="controls-area" style="display: flex; gap: 10px; margin-bottom: 20px; align-items: center;">
|
| 842 |
+
<label for="fileInput" style="cursor: pointer; display: flex; align-items: center; gap: 5px; color: #00d4ff; font-size: 0.9rem;">
|
| 843 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
|
| 844 |
+
Add File
|
| 845 |
+
</label>
|
| 846 |
+
<input type="file" id="fileInput" accept="image/*, .pdf, .csv, .doc, .docx, .xls, .xlsx, .txt, .md, .html" style="display: none;" onchange="updateFileLabel()">
|
| 847 |
+
<span id="fileName" style="color: #888; font-size: 0.8rem; margin-right: auto;"></span>
|
| 848 |
+
|
| 849 |
<button id="askBtn" onclick="askQuestion()">๐ Analyze</button>
|
| 850 |
</div>
|
| 851 |
|
|
|
|
| 925 |
examplesContainer.appendChild(btn);
|
| 926 |
}});
|
| 927 |
|
| 928 |
+
function updateFileLabel() {{
|
| 929 |
+
const input = document.getElementById('fileInput');
|
| 930 |
+
const fileName = document.getElementById('fileName');
|
| 931 |
+
if (input.files && input.files.length > 0) {{
|
| 932 |
+
fileName.textContent = input.files[0].name;
|
| 933 |
+
}} else {{
|
| 934 |
+
fileName.textContent = "";
|
| 935 |
+
}}
|
| 936 |
+
}}
|
| 937 |
+
|
| 938 |
+
async function convertFileToBase64(file) {{
|
| 939 |
+
return new Promise((resolve, reject) => {{
|
| 940 |
+
const reader = new FileReader();
|
| 941 |
+
reader.onload = () => resolve(reader.result);
|
| 942 |
+
reader.onerror = error => reject(error);
|
| 943 |
+
reader.readAsDataURL(file);
|
| 944 |
+
}});
|
| 945 |
+
}}
|
| 946 |
+
|
| 947 |
async function askQuestion() {{
|
| 948 |
const input = document.getElementById('questionInput');
|
| 949 |
+
const fileInput = document.getElementById('fileInput');
|
| 950 |
const btn = document.getElementById('askBtn');
|
| 951 |
const responseArea = document.getElementById('responseArea');
|
|
|
|
| 952 |
|
| 953 |
const question = input.value.trim();
|
|
|
|
| 954 |
|
| 955 |
+
if (!question && (!fileInput.files || fileInput.files.length === 0)) return;
|
| 956 |
+
|
| 957 |
+
// UI updates
|
| 958 |
+
input.disabled = true;
|
| 959 |
btn.disabled = true;
|
| 960 |
btn.textContent = 'โณ Processing...';
|
| 961 |
+
responseArea.innerHTML = '<div class="loading"><div class="spinner"></div>Analyzing request...</div>';
|
| 962 |
|
| 963 |
try {{
|
| 964 |
+
const payload = {{
|
| 965 |
+
question: question,
|
| 966 |
+
files: []
|
| 967 |
+
}};
|
| 968 |
+
|
| 969 |
+
// Handle file upload
|
| 970 |
+
if (fileInput.files && fileInput.files.length > 0) {{
|
| 971 |
+
const file = fileInput.files[0];
|
| 972 |
+
const base64Data = await convertFileToBase64(file);
|
| 973 |
+
|
| 974 |
+
// Determine format
|
| 975 |
+
let format = "txt"; // default
|
| 976 |
+
|
| 977 |
+
// Try from type
|
| 978 |
+
if (file.type) {{
|
| 979 |
+
const subtype = file.type.split('/')[1];
|
| 980 |
+
if (subtype) format = subtype;
|
| 981 |
+
}}
|
| 982 |
+
|
| 983 |
+
// Try from extension (more reliable for docs)
|
| 984 |
+
const nameParts = file.name.split('.');
|
| 985 |
+
if (nameParts.length > 1) {{
|
| 986 |
+
format = nameParts[nameParts.length - 1].toLowerCase();
|
| 987 |
+
}}
|
| 988 |
+
|
| 989 |
+
payload.files.push({{
|
| 990 |
+
data: base64Data,
|
| 991 |
+
format: format,
|
| 992 |
+
name: file.name
|
| 993 |
+
}});
|
| 994 |
+
}}
|
| 995 |
+
|
| 996 |
const response = await fetch('/api/ask', {{
|
| 997 |
method: 'POST',
|
| 998 |
headers: {{
|
| 999 |
+
'Content-Type': 'application/json'
|
| 1000 |
}},
|
| 1001 |
+
body: JSON.stringify(payload)
|
| 1002 |
}});
|
| 1003 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
const data = await response.json();
|
| 1005 |
|
| 1006 |
+
if (response.ok) {{
|
| 1007 |
+
responseArea.innerHTML = '<pre id="responseText"></pre>';
|
| 1008 |
+
document.getElementById('responseText').textContent = data.answer;
|
| 1009 |
+
|
| 1010 |
+
// Update metrics
|
| 1011 |
+
if (data.metrics) {{
|
| 1012 |
+
document.getElementById('totalSearches').textContent = data.metrics.total_searches || 0;
|
| 1013 |
+
document.getElementById('cacheRate').textContent =
|
| 1014 |
+
((data.metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
|
| 1015 |
+
document.getElementById('avgTime').textContent =
|
| 1016 |
+
(data.metrics.avg_query_time || 0).toFixed(2) + 's';
|
| 1017 |
+
document.getElementById('errors').textContent = data.metrics.errors || 0;
|
| 1018 |
+
}}
|
| 1019 |
+
}} else {{
|
| 1020 |
+
responseArea.innerHTML = `<pre style="color: #ff6b6b">Error: ${{data.detail || 'Unknown error occurred'}}</pre>`;
|
| 1021 |
}}
|
| 1022 |
|
| 1023 |
}} catch (error) {{
|
| 1024 |
+
responseArea.innerHTML = `<pre style="color: #ff6b6b">Network Error: ${{error.message}}</pre>`;
|
|
|
|
| 1025 |
}} finally {{
|
| 1026 |
+
input.disabled = false;
|
| 1027 |
btn.disabled = false;
|
| 1028 |
btn.textContent = '๐ Analyze';
|
| 1029 |
+
input.focus();
|
| 1030 |
+
fileInput.value = "";
|
| 1031 |
+
updateFileLabel();
|
| 1032 |
}}
|
| 1033 |
}}
|
| 1034 |
|