chrisjcc commited on
Commit
ad75af3
ยท
verified ยท
1 Parent(s): d92c3ac

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)

Files changed (1) hide show
  1. app.py +195 -29
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
- result = agent(question)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- class QuestionRequest(BaseModel):
415
- question: str
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 your question...</div>';
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({{ question }}),
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
- responseArea.innerHTML = '<pre id="responseText"></pre>';
848
- document.getElementById('responseText').textContent = data.answer;
849
-
850
- // Update metrics
851
- if (data.metrics) {{
852
- document.getElementById('totalSearches').textContent = data.metrics.total_searches || 0;
853
- document.getElementById('cacheRate').textContent =
854
- ((data.metrics.cache_hit_rate || 0) * 100).toFixed(0) + '%';
855
- document.getElementById('avgTime').textContent =
856
- (data.metrics.avg_query_time || 0).toFixed(2) + 's';
857
- document.getElementById('errors').textContent = data.metrics.errors || 0;
 
 
 
 
858
  }}
859
 
860
  }} catch (error) {{
861
- responseArea.innerHTML = '<pre id="responseText" style="color: #ff6b6b;"></pre>';
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