bluenevus commited on
Commit
df93eb7
·
1 Parent(s): f004df0

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +151 -172
app.py CHANGED
@@ -10,10 +10,6 @@ from docx import Document
10
  import mimetypes
11
  from threading import Lock
12
  import time
13
- import tempfile
14
- import shutil
15
- import uuid
16
- from flask import Flask, session, request
17
 
18
  import google.generativeai as genai
19
 
@@ -24,7 +20,6 @@ logging.basicConfig(
24
 
25
  app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
26
  server = app.server
27
- app.server.secret_key = os.environ.get('SECRET_KEY', str(uuid.uuid4()))
28
 
29
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
30
  genai.configure(api_key=GOOGLE_API_KEY)
@@ -33,42 +28,15 @@ GEMINI_MODEL = "models/gemini-2.5-pro-preview-03-25"
33
  MAX_INPUT_TOKENS = 1048576
34
  MAX_OUTPUT_TOKENS = 65536
35
 
36
- SESSION_DATA = {}
37
- SESSION_LOCKS = {}
38
-
39
- SESSION_FILE_ROOT = tempfile.gettempdir()
40
-
41
- def get_session_id():
42
- if 'session_id' not in session:
43
- session['session_id'] = str(uuid.uuid4())
44
- return session['session_id']
45
-
46
- def get_session_data():
47
- sid = get_session_id()
48
- if sid not in SESSION_DATA:
49
- SESSION_DATA[sid] = {
50
- 'uploaded_documents': {},
51
- 'uploaded_documents_fileid': {},
52
- 'uploaded_documents_bytes': {},
53
- 'proposals': {},
54
- 'proposals_fileid': {},
55
- 'shredded_documents': {},
56
- 'generated_response': None,
57
- 'lock': Lock(),
58
- 'tmpdir': create_session_tmpdir(sid)
59
- }
60
- SESSION_LOCKS[sid] = SESSION_DATA[sid]['lock']
61
- return SESSION_DATA[sid]
62
-
63
- def create_session_tmpdir(sid):
64
- tmpdir = os.path.join(SESSION_FILE_ROOT, f"dashrfp_{sid}")
65
- os.makedirs(tmpdir, exist_ok=True)
66
- return tmpdir
67
 
68
- def cleanup_session_tmpdir(sid):
69
- tmpdir = os.path.join(SESSION_FILE_ROOT, f"dashrfp_{sid}")
70
- if os.path.exists(tmpdir):
71
- shutil.rmtree(tmpdir, ignore_errors=True)
72
 
73
  def decode_document(decoded_bytes):
74
  try:
@@ -202,17 +170,21 @@ def save_loe_as_docx(loe_text, proposal_filename):
202
  memf.seek(0)
203
  return memf.read()
204
 
205
- def process_document(session_dict, action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_proposal_filename=None):
 
 
 
 
206
  doc_content = None
207
  doc_fileid = None
208
  if action in ["shred", "compliance", "virtual_board", "proposal", "recover"]:
209
- if selected_filename and selected_filename in session_dict['uploaded_documents']:
210
- doc_content = session_dict['uploaded_documents'][selected_filename]
211
- doc_fileid = session_dict['uploaded_documents_fileid'].get(selected_filename)
212
- elif session_dict['uploaded_documents']:
213
- doc_content = next(iter(session_dict['uploaded_documents'].values()))
214
- selected_filename = next(iter(session_dict['uploaded_documents'].keys()))
215
- doc_fileid = session_dict['uploaded_documents_fileid'].get(selected_filename)
216
  else:
217
  doc_content = None
218
  doc_fileid = None
@@ -234,20 +206,20 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
234
  if result and not result.startswith("Error"):
235
  docx_bytes = save_shredded_as_docx(result, selected_filename)
236
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
237
- session_dict['uploaded_documents'][generated_docx_name] = result
238
- session_dict['shredded_documents'][generated_docx_name] = docx_bytes
239
  return result, generated_docx_name, docx_bytes, None, None
240
  else:
241
  return result, None, None, None, None
242
 
243
  elif action == 'compliance':
244
- if not selected_proposal_filename or selected_proposal_filename not in session_dict['proposals']:
245
  return "No proposal document selected for compliance.", None, None, None, None
246
- if not selected_filename or selected_filename not in session_dict['uploaded_documents']:
247
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None, None
248
 
249
- rfp_text = session_dict['uploaded_documents'][selected_filename]
250
- proposal_text = session_dict['proposals'][selected_proposal_filename]
251
  logging.info(f"Compliance check: comparing proposal [{selected_proposal_filename}] to RFP [{selected_filename}]")
252
  prompt = (
253
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
@@ -265,20 +237,20 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
265
  if result and not result.startswith("Error"):
266
  docx_bytes = save_compliance_as_docx(result, selected_filename)
267
  compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
268
- session_dict['uploaded_documents'][compliance_docx_name] = result
269
- session_dict['shredded_documents'][compliance_docx_name] = docx_bytes
270
  return result, compliance_docx_name, docx_bytes, None, None
271
  else:
272
  return result, None, None, None, None
273
 
274
  elif action == 'virtual_board':
275
- if not selected_proposal_filename or selected_proposal_filename not in session_dict['proposals']:
276
  return "No proposal document selected for evaluation board.", None, None, None, None
277
- if not selected_filename or selected_filename not in session_dict['uploaded_documents']:
278
  return "No RFP/SOW/PWS/RFI document selected for evaluation board.", None, None, None, None
279
 
280
- rfp_text = session_dict['uploaded_documents'][selected_filename]
281
- proposal_text = session_dict['proposals'][selected_proposal_filename]
282
  logging.info(f"Evaluation Board: extracting criteria from RFP [{selected_filename}], evaluating proposal [{selected_proposal_filename}]")
283
  prompt = (
284
  "You are a federal acquisition evaluation board. Your job is to extract Section L and Section M (evaluation criteria) from the following RFP/SOW/PWS/RFI if they exist. "
@@ -295,8 +267,8 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
295
  if result and not result.startswith("Error"):
296
  docx_bytes = save_virtual_board_as_docx(result, selected_filename)
297
  board_docx_name = f"{os.path.splitext(selected_filename)[0]}_evaluation_board.docx"
298
- session_dict['uploaded_documents'][board_docx_name] = result
299
- session_dict['shredded_documents'][board_docx_name] = docx_bytes
300
  return result, board_docx_name, docx_bytes, None, None
301
  else:
302
  return result, None, None, None, None
@@ -306,12 +278,12 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
306
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
307
  return "No RFP/SOW/PWS/RFI document selected.", None, None, None, None
308
  rfp_filename = selected_filename
309
- rfp_fileid = session_dict['uploaded_documents_fileid'].get(selected_filename)
310
- if not rfp_fileid and rfp_filename in session_dict['uploaded_documents_bytes']:
311
  try:
312
- fileid = upload_to_gemini_file(session_dict['uploaded_documents_bytes'][rfp_filename], rfp_filename)
313
  if fileid:
314
- session_dict['uploaded_documents_fileid'][rfp_filename] = fileid
315
  rfp_fileid = fileid
316
  logging.info(f"RFP file {rfp_filename} uploaded to Gemini for proposal.")
317
  except Exception as e:
@@ -326,26 +298,26 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
326
  prompt += f"User additional instructions: {chat_input}\n"
327
  prompt += f"\n---\nRFP/SOW/PWS/RFI ({rfp_filename}):\n{doc_content}\n"
328
  result = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
329
- session_dict['generated_response'] = result
330
  if result and not result.startswith("Error"):
331
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
332
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
333
- session_dict['proposals'][generated_docx_name] = result
334
- session_dict['proposals_fileid'][generated_docx_name] = None
335
  return result, None, None, generated_docx_name, docx_bytes
336
  else:
337
  return result, None, None, None, None
338
 
339
  elif action == 'recover':
340
- if not selected_proposal_filename or selected_proposal_filename not in session_dict['proposals']:
341
  logging.error("No proposal document selected for recovery.")
342
  return "No proposal document selected for recovery.", None, None, None, None
343
- if not selected_filename or selected_filename not in session_dict['uploaded_documents']:
344
  logging.error("No compliance check or shredded requirements document selected for recovery.")
345
  return "No compliance check or shredded requirements document selected for recovery.", None, None, None, None
346
 
347
- findings_content = session_dict['uploaded_documents'][selected_filename]
348
- proposal_text = session_dict['proposals'][selected_proposal_filename]
349
  prompt = (
350
  "You are a proposal compliance recovery expert. Use the findings and recommendations table and the original proposal response provided below. "
351
  "Address ONLY those sections of the proposal that have a finding and a recommendation for improvement. "
@@ -365,8 +337,8 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
365
  base_name = os.path.splitext(selected_proposal_filename)[0]
366
  recovered_docx_name = f"{base_name}_recovered.docx"
367
  docx_bytes = save_proposal_as_docx(result, base_name)
368
- session_dict['proposals'][recovered_docx_name] = result
369
- session_dict['proposals_fileid'][recovered_docx_name] = None
370
  logging.info(f"Recovered proposal generated and saved as {recovered_docx_name}.")
371
  return result, None, None, recovered_docx_name, docx_bytes
372
  else:
@@ -374,10 +346,11 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
374
  return result, None, None, None, None
375
 
376
  elif action == 'loe':
377
- if not selected_proposal_filename or selected_proposal_filename not in session_dict['proposals']:
 
378
  logging.warning("No proposal document selected for LOE estimation.")
379
  return "No proposal document selected for LOE estimation.", None, None, None, None
380
- proposal_text = session_dict['proposals'][selected_proposal_filename]
381
  proposal_base_name = os.path.splitext(selected_proposal_filename)[0]
382
  prompt = (
383
  "You are a federal proposal cost and level of effort estimator. "
@@ -398,8 +371,8 @@ def process_document(session_dict, action, selected_filename=None, chat_input=No
398
  result = gemini_generate_content(prompt, file_id=None, chat_input=chat_input)
399
  if result and not result.startswith("Error"):
400
  loe_docx_name = f"{proposal_base_name}_loe.docx"
401
- session_dict['proposals'][loe_docx_name] = result
402
- session_dict['proposals_fileid'][loe_docx_name] = None
403
  docx_bytes = save_loe_as_docx(result, proposal_base_name)
404
  logging.info(f"LOE generated and saved as {loe_docx_name}")
405
  return result, None, None, loe_docx_name, docx_bytes
@@ -473,8 +446,7 @@ def get_proposals_list(proposaldict):
473
  return dbc.ListGroup(doc_list, flush=True)
474
 
475
  app.layout = dbc.Container([
476
- dcc.Store(id='preview-window-state', data='expanded', storage_type='session'),
477
- dcc.Store(id='session-cleanup', data=False, storage_type='session', clear_data=True),
478
  dbc.Row([
479
  dbc.Col([
480
  dbc.Card([
@@ -498,10 +470,12 @@ app.layout = dbc.Container([
498
  },
499
  multiple=False
500
  ),
501
- html.Div(id='documents-list'),
502
  dcc.Dropdown(
503
  id='select-document-dropdown',
 
504
  placeholder="Select a document to work with",
 
505
  style={"marginBottom": "10px"}
506
  ),
507
  ])
@@ -527,10 +501,12 @@ app.layout = dbc.Container([
527
  },
528
  multiple=False
529
  ),
530
- html.Div(id='proposals-list'),
531
  dcc.Dropdown(
532
  id='select-proposal-dropdown',
 
533
  placeholder="Select a proposal document",
 
534
  style={"marginBottom": "10px"}
535
  ),
536
  ])
@@ -616,8 +592,7 @@ def update_preview_window_style(state):
616
  State('select-proposal-dropdown', 'value'),
617
  State('chat-input', 'value'),
618
  Input('cancel-action-btn', 'n_clicks'),
619
- State('preview-window-state', 'data'),
620
- Input('session-cleanup', 'clear_data')
621
  ],
622
  prevent_initial_call=True
623
  )
@@ -625,12 +600,10 @@ def master_callback(
625
  shred_clicks, proposal_clicks, compliance_clicks, recover_clicks, board_clicks, loe_clicks,
626
  rfp_content, rfp_filename, doc_delete_clicks, selected_doc,
627
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
628
- chat_input, cancel_clicks, preview_window_state, session_cleanup_clear
629
  ):
630
  ctx = callback_context
631
  triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
632
- session_dict = get_session_data()
633
- lock = session_dict['lock']
634
 
635
  def safe_get_n_clicks(ctx, idx):
636
  try:
@@ -642,35 +615,29 @@ def master_callback(
642
  proposal_delete_clicks = safe_get_n_clicks(ctx, 12)
643
  uploaded_rfp_decoded_bytes = None
644
 
645
- # Session cleanup: only clear all data and temp dir if session-cleanup's clear_data is triggered (i.e., browser close)
646
- if triggered_id == 'session-cleanup' and session_cleanup_clear:
647
- sid = get_session_id()
648
- cleanup_session_tmpdir(sid)
649
- if sid in SESSION_DATA:
650
- del SESSION_DATA[sid]
651
- if sid in SESSION_LOCKS:
652
- del SESSION_LOCKS[sid]
653
- return html.Div("Session ended. All data cleared.", style={"wordWrap": "break-word"}), None, [], None, None, [], None, "expanded"
654
-
655
- # Cancel should just clear the preview and expand window, not clear session/docs
656
  if triggered_id == 'cancel-action-btn':
657
- doc_options = [{'label': fn, 'value': fn} for fn in session_dict['uploaded_documents'].keys()]
658
- doc_value = selected_doc if selected_doc in session_dict['uploaded_documents'] else (next(iter(session_dict['uploaded_documents']), None) if session_dict['uploaded_documents'] else None)
659
- proposals_list = get_proposals_list(session_dict['proposals'])
660
- proposal_options = [{'label': fn, 'value': fn} for fn in session_dict['proposals'].keys()]
661
- proposal_value = selected_proposal if selected_proposal in session_dict['proposals'] else (next(iter(session_dict['proposals']), None) if session_dict['proposals'] else None)
662
- documents_list = get_documents_list(session_dict['uploaded_documents'], session_dict['shredded_documents'])
 
663
  return (
664
- html.Div("", style={"wordWrap": "break-word"}),
665
  documents_list, doc_options, doc_value,
666
  proposals_list, proposal_options, proposal_value,
667
  "expanded"
668
  )
669
 
670
- # File uploads
671
- doc_value = selected_doc
672
- proposal_value = selected_proposal
673
-
674
  if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
675
  content_type, content_string = rfp_content.split(',')
676
  decoded = base64.b64decode(content_string)
@@ -680,12 +647,11 @@ def master_callback(
680
  if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
681
  fileid = upload_to_gemini_file(decoded, rfp_filename)
682
  if text is not None:
683
- session_dict['uploaded_documents'][rfp_filename] = text
684
- session_dict['uploaded_documents_bytes'][rfp_filename] = decoded
685
  if fileid:
686
- session_dict['uploaded_documents_fileid'][rfp_filename] = fileid
687
  logging.info(f"Document uploaded: {rfp_filename}")
688
- doc_value = rfp_filename # Set selection to the newly uploaded doc
689
  else:
690
  logging.error(f"Failed to decode uploaded document: {rfp_filename}")
691
 
@@ -697,76 +663,65 @@ def master_callback(
697
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
698
  fileid = upload_to_gemini_file(decoded, proposal_filename)
699
  if text is not None:
700
- session_dict['proposals'][proposal_filename] = text
701
  if fileid:
702
- session_dict['proposals_fileid'][proposal_filename] = fileid
703
  logging.info(f"Proposal uploaded: {proposal_filename}")
704
- proposal_value = proposal_filename # Set selection to the newly uploaded proposal
705
  else:
706
  logging.error(f"Failed to decode uploaded proposal: {proposal_filename}")
707
 
708
- # Deletion handling
709
- deleted_doc = False
710
  if triggered_id and isinstance(doc_delete_clicks, list):
711
  for i, n_click in enumerate(doc_delete_clicks):
712
  if n_click:
713
  btn_id = ctx.inputs_list[8][i]['id']
714
  del_filename = btn_id['index']
715
- if del_filename in session_dict['uploaded_documents']:
716
- del session_dict['uploaded_documents'][del_filename]
717
- if del_filename in session_dict['uploaded_documents_fileid']:
718
  try:
719
- genai.delete_file(session_dict['uploaded_documents_fileid'][del_filename])
720
  except Exception as e:
721
  logging.warning(f"Failed to delete Gemini file {del_filename}: {e}")
722
- del session_dict['uploaded_documents_fileid'][del_filename]
723
- if del_filename in session_dict['uploaded_documents_bytes']:
724
- del session_dict['uploaded_documents_bytes'][del_filename]
725
  logging.info(f"Document deleted: {del_filename}")
726
- if del_filename in session_dict['shredded_documents']:
727
- del session_dict['shredded_documents'][del_filename]
728
  logging.info(f"Shredded doc deleted: {del_filename}")
729
- if doc_value == del_filename:
730
- doc_value = None
731
- deleted_doc = True
732
  break
733
 
734
- deleted_proposal = False
735
  if triggered_id and isinstance(proposal_delete_clicks, list):
736
  for i, n_click in enumerate(proposal_delete_clicks):
737
  if n_click:
738
  btn_id = ctx.inputs_list[12][i]['id']
739
  del_filename = btn_id['index']
740
- if del_filename in session_dict['proposals']:
741
- del session_dict['proposals'][del_filename]
742
- if del_filename in session_dict['proposals_fileid']:
743
  try:
744
- genai.delete_file(session_dict['proposals_fileid'][del_filename])
745
  except Exception as e:
746
  logging.warning(f"Failed to delete Gemini proposal file {del_filename}: {e}")
747
- del session_dict['proposals_fileid'][del_filename]
748
  logging.info(f"Proposal deleted: {del_filename}")
749
- if proposal_value == del_filename:
750
- proposal_value = None
751
- deleted_proposal = True
752
  break
753
 
754
- doc_options = [{'label': fn, 'value': fn} for fn in session_dict['uploaded_documents'].keys()]
755
- # Always keep the selected doc if it's still present, otherwise select the first available or None
756
- if not doc_value or doc_value not in session_dict['uploaded_documents']:
757
- doc_value = next(iter(session_dict['uploaded_documents']), None)
758
- proposal_options = [{'label': fn, 'value': fn} for fn in session_dict['proposals'].keys()]
759
- if not proposal_value or proposal_value not in session_dict['proposals']:
760
- proposal_value = next(iter(session_dict['proposals']), None)
761
- documents_list = get_documents_list(session_dict['uploaded_documents'], session_dict['shredded_documents'])
762
- proposals_list = get_proposals_list(session_dict['proposals'])
763
 
764
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
765
 
766
- # Action buttons: always process immediately
767
- if triggered_id in ['shred-action-btn', 'compliance-action-btn', 'board-action-btn',
768
- 'proposal-action-btn', 'recover-action-btn', 'loe-action-btn']:
769
- got_lock = lock.acquire(blocking=False)
770
  if not got_lock:
771
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
772
  return (
@@ -778,50 +733,73 @@ def master_callback(
778
  try:
779
  if triggered_id == "shred-action-btn":
780
  action_name = "shred"
781
- result, generated_filename, generated_docx_bytes, _, _ = process_document(session_dict, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
782
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
783
  elif triggered_id == "compliance-action-btn":
784
  action_name = "compliance"
785
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
786
- session_dict, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
787
  )
788
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
789
  elif triggered_id == "board-action-btn":
790
  action_name = "virtual_board"
791
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
792
- session_dict, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
793
  )
794
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
795
- elif triggered_id == "proposal-action-btn":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  action_name = "proposal"
797
- selected_bytes = session_dict['uploaded_documents_bytes'].get(doc_value, None)
798
  result, _, _, generated_filename, generated_docx_bytes = process_document(
799
- session_dict, action_name, doc_value, chat_input, selected_bytes, None
800
  )
801
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
802
  elif triggered_id == "recover-action-btn":
803
  action_name = "recover"
804
  result, _, _, generated_filename, generated_docx_bytes = process_document(
805
- session_dict, action_name, doc_value, chat_input, None, proposal_value
806
  )
807
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
808
  elif triggered_id == "loe-action-btn":
809
  action_name = "loe"
810
  result, _, _, generated_filename, generated_docx_bytes = process_document(
811
- session_dict, action_name, None, chat_input, None, proposal_value
812
  )
813
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
814
  finally:
815
- lock.release()
816
- # After processing, update lists/options/values, but do NOT clear any document/proposal unless deleted
817
- doc_options = [{'label': fn, 'value': fn} for fn in session_dict['uploaded_documents'].keys()]
818
- if not doc_value or doc_value not in session_dict['uploaded_documents']:
819
- doc_value = next(iter(session_dict['uploaded_documents']), None)
820
- proposal_options = [{'label': fn, 'value': fn} for fn in session_dict['proposals'].keys()]
821
- if not proposal_value or proposal_value not in session_dict['proposals']:
822
- proposal_value = next(iter(session_dict['proposals']), None)
823
- documents_list = get_documents_list(session_dict['uploaded_documents'], session_dict['shredded_documents'])
824
- proposals_list = get_proposals_list(session_dict['proposals'])
825
  return (
826
  output_data_upload,
827
  documents_list, doc_options, doc_value,
@@ -829,7 +807,8 @@ def master_callback(
829
  "expanded"
830
  )
831
 
832
- # Always keep selections sticky, unless user has deleted the item
 
833
  return (
834
  output_data_upload,
835
  documents_list, doc_options, doc_value,
 
10
  import mimetypes
11
  from threading import Lock
12
  import time
 
 
 
 
13
 
14
  import google.generativeai as genai
15
 
 
20
 
21
  app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
22
  server = app.server
 
23
 
24
  GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "")
25
  genai.configure(api_key=GOOGLE_API_KEY)
 
28
  MAX_INPUT_TOKENS = 1048576
29
  MAX_OUTPUT_TOKENS = 65536
30
 
31
+ uploaded_documents = {}
32
+ uploaded_documents_fileid = {}
33
+ uploaded_documents_bytes = {}
34
+ proposals = {}
35
+ proposals_fileid = {}
36
+ shredded_documents = {}
37
+ generated_response = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ gemini_lock = Lock()
 
 
 
40
 
41
  def decode_document(decoded_bytes):
42
  try:
 
170
  memf.seek(0)
171
  return memf.read()
172
 
173
+ def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_proposal_filename=None):
174
+ global generated_response
175
+
176
+ logging.info(f"Process document called with action: {action}")
177
+
178
  doc_content = None
179
  doc_fileid = None
180
  if action in ["shred", "compliance", "virtual_board", "proposal", "recover"]:
181
+ if selected_filename and selected_filename in uploaded_documents:
182
+ doc_content = uploaded_documents[selected_filename]
183
+ doc_fileid = uploaded_documents_fileid.get(selected_filename)
184
+ elif uploaded_documents:
185
+ doc_content = next(iter(uploaded_documents.values()))
186
+ selected_filename = next(iter(uploaded_documents.keys()))
187
+ doc_fileid = uploaded_documents_fileid.get(selected_filename)
188
  else:
189
  doc_content = None
190
  doc_fileid = None
 
206
  if result and not result.startswith("Error"):
207
  docx_bytes = save_shredded_as_docx(result, selected_filename)
208
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
209
+ uploaded_documents[generated_docx_name] = result
210
+ shredded_documents[generated_docx_name] = docx_bytes
211
  return result, generated_docx_name, docx_bytes, None, None
212
  else:
213
  return result, None, None, None, None
214
 
215
  elif action == 'compliance':
216
+ if not selected_proposal_filename or selected_proposal_filename not in proposals:
217
  return "No proposal document selected for compliance.", None, None, None, None
218
+ if not selected_filename or selected_filename not in uploaded_documents:
219
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None, None
220
 
221
+ rfp_text = uploaded_documents[selected_filename]
222
+ proposal_text = proposals[selected_proposal_filename]
223
  logging.info(f"Compliance check: comparing proposal [{selected_proposal_filename}] to RFP [{selected_filename}]")
224
  prompt = (
225
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
 
237
  if result and not result.startswith("Error"):
238
  docx_bytes = save_compliance_as_docx(result, selected_filename)
239
  compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
240
+ uploaded_documents[compliance_docx_name] = result
241
+ shredded_documents[compliance_docx_name] = docx_bytes
242
  return result, compliance_docx_name, docx_bytes, None, None
243
  else:
244
  return result, None, None, None, None
245
 
246
  elif action == 'virtual_board':
247
+ if not selected_proposal_filename or selected_proposal_filename not in proposals:
248
  return "No proposal document selected for evaluation board.", None, None, None, None
249
+ if not selected_filename or selected_filename not in uploaded_documents:
250
  return "No RFP/SOW/PWS/RFI document selected for evaluation board.", None, None, None, None
251
 
252
+ rfp_text = uploaded_documents[selected_filename]
253
+ proposal_text = proposals[selected_proposal_filename]
254
  logging.info(f"Evaluation Board: extracting criteria from RFP [{selected_filename}], evaluating proposal [{selected_proposal_filename}]")
255
  prompt = (
256
  "You are a federal acquisition evaluation board. Your job is to extract Section L and Section M (evaluation criteria) from the following RFP/SOW/PWS/RFI if they exist. "
 
267
  if result and not result.startswith("Error"):
268
  docx_bytes = save_virtual_board_as_docx(result, selected_filename)
269
  board_docx_name = f"{os.path.splitext(selected_filename)[0]}_evaluation_board.docx"
270
+ uploaded_documents[board_docx_name] = result
271
+ shredded_documents[board_docx_name] = docx_bytes
272
  return result, board_docx_name, docx_bytes, None, None
273
  else:
274
  return result, None, None, None, None
 
278
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
279
  return "No RFP/SOW/PWS/RFI document selected.", None, None, None, None
280
  rfp_filename = selected_filename
281
+ rfp_fileid = uploaded_documents_fileid.get(selected_filename)
282
+ if not rfp_fileid and rfp_filename in uploaded_documents_bytes:
283
  try:
284
+ fileid = upload_to_gemini_file(uploaded_documents_bytes[rfp_filename], rfp_filename)
285
  if fileid:
286
+ uploaded_documents_fileid[rfp_filename] = fileid
287
  rfp_fileid = fileid
288
  logging.info(f"RFP file {rfp_filename} uploaded to Gemini for proposal.")
289
  except Exception as e:
 
298
  prompt += f"User additional instructions: {chat_input}\n"
299
  prompt += f"\n---\nRFP/SOW/PWS/RFI ({rfp_filename}):\n{doc_content}\n"
300
  result = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
301
+ generated_response = result
302
  if result and not result.startswith("Error"):
303
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
304
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
305
+ proposals[generated_docx_name] = result
306
+ proposals_fileid[generated_docx_name] = None
307
  return result, None, None, generated_docx_name, docx_bytes
308
  else:
309
  return result, None, None, None, None
310
 
311
  elif action == 'recover':
312
+ if not selected_proposal_filename or selected_proposal_filename not in proposals:
313
  logging.error("No proposal document selected for recovery.")
314
  return "No proposal document selected for recovery.", None, None, None, None
315
+ if not selected_filename or selected_filename not in uploaded_documents:
316
  logging.error("No compliance check or shredded requirements document selected for recovery.")
317
  return "No compliance check or shredded requirements document selected for recovery.", None, None, None, None
318
 
319
+ findings_content = uploaded_documents[selected_filename]
320
+ proposal_text = proposals[selected_proposal_filename]
321
  prompt = (
322
  "You are a proposal compliance recovery expert. Use the findings and recommendations table and the original proposal response provided below. "
323
  "Address ONLY those sections of the proposal that have a finding and a recommendation for improvement. "
 
337
  base_name = os.path.splitext(selected_proposal_filename)[0]
338
  recovered_docx_name = f"{base_name}_recovered.docx"
339
  docx_bytes = save_proposal_as_docx(result, base_name)
340
+ proposals[recovered_docx_name] = result
341
+ proposals_fileid[recovered_docx_name] = None
342
  logging.info(f"Recovered proposal generated and saved as {recovered_docx_name}.")
343
  return result, None, None, recovered_docx_name, docx_bytes
344
  else:
 
346
  return result, None, None, None, None
347
 
348
  elif action == 'loe':
349
+ # LOE estimation implementation
350
+ if not selected_proposal_filename or selected_proposal_filename not in proposals:
351
  logging.warning("No proposal document selected for LOE estimation.")
352
  return "No proposal document selected for LOE estimation.", None, None, None, None
353
+ proposal_text = proposals[selected_proposal_filename]
354
  proposal_base_name = os.path.splitext(selected_proposal_filename)[0]
355
  prompt = (
356
  "You are a federal proposal cost and level of effort estimator. "
 
371
  result = gemini_generate_content(prompt, file_id=None, chat_input=chat_input)
372
  if result and not result.startswith("Error"):
373
  loe_docx_name = f"{proposal_base_name}_loe.docx"
374
+ proposals[loe_docx_name] = result
375
+ proposals_fileid[loe_docx_name] = None
376
  docx_bytes = save_loe_as_docx(result, proposal_base_name)
377
  logging.info(f"LOE generated and saved as {loe_docx_name}")
378
  return result, None, None, loe_docx_name, docx_bytes
 
446
  return dbc.ListGroup(doc_list, flush=True)
447
 
448
  app.layout = dbc.Container([
449
+ dcc.Store(id='preview-window-state', data='expanded'),
 
450
  dbc.Row([
451
  dbc.Col([
452
  dbc.Card([
 
470
  },
471
  multiple=False
472
  ),
473
+ html.Div(id='documents-list', children=get_documents_list(uploaded_documents, shredded_documents)),
474
  dcc.Dropdown(
475
  id='select-document-dropdown',
476
+ options=[{'label': fn, 'value': fn} for fn in uploaded_documents.keys()],
477
  placeholder="Select a document to work with",
478
+ value=next(iter(uploaded_documents), None),
479
  style={"marginBottom": "10px"}
480
  ),
481
  ])
 
501
  },
502
  multiple=False
503
  ),
504
+ html.Div(id='proposals-list', children=get_proposals_list(proposals)),
505
  dcc.Dropdown(
506
  id='select-proposal-dropdown',
507
+ options=[{'label': fn, 'value': fn} for fn in proposals.keys()],
508
  placeholder="Select a proposal document",
509
+ value=next(iter(proposals), None),
510
  style={"marginBottom": "10px"}
511
  ),
512
  ])
 
592
  State('select-proposal-dropdown', 'value'),
593
  State('chat-input', 'value'),
594
  Input('cancel-action-btn', 'n_clicks'),
595
+ State('preview-window-state', 'data')
 
596
  ],
597
  prevent_initial_call=True
598
  )
 
600
  shred_clicks, proposal_clicks, compliance_clicks, recover_clicks, board_clicks, loe_clicks,
601
  rfp_content, rfp_filename, doc_delete_clicks, selected_doc,
602
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
603
+ chat_input, cancel_clicks, preview_window_state
604
  ):
605
  ctx = callback_context
606
  triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
 
 
607
 
608
  def safe_get_n_clicks(ctx, idx):
609
  try:
 
615
  proposal_delete_clicks = safe_get_n_clicks(ctx, 12)
616
  uploaded_rfp_decoded_bytes = None
617
 
618
+ global gemini_lock, uploaded_documents_bytes
619
+
620
+ if triggered_id in [
621
+ 'shred-action-btn', 'proposal-action-btn', 'compliance-action-btn',
622
+ 'recover-action-btn', 'board-action-btn', 'loe-action-btn'
623
+ ] and preview_window_state != "shrunk":
624
+ return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, "shrunk"
625
+
 
 
 
626
  if triggered_id == 'cancel-action-btn':
627
+ output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
628
+ doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
629
+ doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
630
+ proposals_list = get_proposals_list(proposals)
631
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
632
+ proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
633
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
634
  return (
635
+ output_data_upload,
636
  documents_list, doc_options, doc_value,
637
  proposals_list, proposal_options, proposal_value,
638
  "expanded"
639
  )
640
 
 
 
 
 
641
  if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
642
  content_type, content_string = rfp_content.split(',')
643
  decoded = base64.b64decode(content_string)
 
647
  if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
648
  fileid = upload_to_gemini_file(decoded, rfp_filename)
649
  if text is not None:
650
+ uploaded_documents[rfp_filename] = text
651
+ uploaded_documents_bytes[rfp_filename] = decoded
652
  if fileid:
653
+ uploaded_documents_fileid[rfp_filename] = fileid
654
  logging.info(f"Document uploaded: {rfp_filename}")
 
655
  else:
656
  logging.error(f"Failed to decode uploaded document: {rfp_filename}")
657
 
 
663
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
664
  fileid = upload_to_gemini_file(decoded, proposal_filename)
665
  if text is not None:
666
+ proposals[proposal_filename] = text
667
  if fileid:
668
+ proposals_fileid[proposal_filename] = fileid
669
  logging.info(f"Proposal uploaded: {proposal_filename}")
 
670
  else:
671
  logging.error(f"Failed to decode uploaded proposal: {proposal_filename}")
672
 
 
 
673
  if triggered_id and isinstance(doc_delete_clicks, list):
674
  for i, n_click in enumerate(doc_delete_clicks):
675
  if n_click:
676
  btn_id = ctx.inputs_list[8][i]['id']
677
  del_filename = btn_id['index']
678
+ if del_filename in uploaded_documents:
679
+ del uploaded_documents[del_filename]
680
+ if del_filename in uploaded_documents_fileid:
681
  try:
682
+ genai.delete_file(uploaded_documents_fileid[del_filename])
683
  except Exception as e:
684
  logging.warning(f"Failed to delete Gemini file {del_filename}: {e}")
685
+ del uploaded_documents_fileid[del_filename]
686
+ if del_filename in uploaded_documents_bytes:
687
+ del uploaded_documents_bytes[del_filename]
688
  logging.info(f"Document deleted: {del_filename}")
689
+ if del_filename in shredded_documents:
690
+ del shredded_documents[del_filename]
691
  logging.info(f"Shredded doc deleted: {del_filename}")
692
+ if selected_doc == del_filename:
693
+ selected_doc = None
 
694
  break
695
 
 
696
  if triggered_id and isinstance(proposal_delete_clicks, list):
697
  for i, n_click in enumerate(proposal_delete_clicks):
698
  if n_click:
699
  btn_id = ctx.inputs_list[12][i]['id']
700
  del_filename = btn_id['index']
701
+ if del_filename in proposals:
702
+ del proposals[del_filename]
703
+ if del_filename in proposals_fileid:
704
  try:
705
+ genai.delete_file(proposals_fileid[del_filename])
706
  except Exception as e:
707
  logging.warning(f"Failed to delete Gemini proposal file {del_filename}: {e}")
708
+ del proposals_fileid[del_filename]
709
  logging.info(f"Proposal deleted: {del_filename}")
710
+ if selected_proposal == del_filename:
711
+ selected_proposal = None
 
712
  break
713
 
714
+ doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
715
+ doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
716
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
717
+ proposals_list = get_proposals_list(proposals)
718
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
719
+ proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
 
 
 
720
 
721
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
722
 
723
+ if triggered_id in ['shred-action-btn', 'compliance-action-btn', 'board-action-btn']:
724
+ got_lock = gemini_lock.acquire(blocking=False)
 
 
725
  if not got_lock:
726
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
727
  return (
 
733
  try:
734
  if triggered_id == "shred-action-btn":
735
  action_name = "shred"
736
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
737
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
738
  elif triggered_id == "compliance-action-btn":
739
  action_name = "compliance"
740
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
741
+ action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
742
  )
743
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
744
  elif triggered_id == "board-action-btn":
745
  action_name = "virtual_board"
746
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
747
+ action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
748
  )
749
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
750
+ finally:
751
+ gemini_lock.release()
752
+ doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
753
+ doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
754
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
755
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
756
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
757
+ proposals_list = get_proposals_list(proposals)
758
+ return (
759
+ output_data_upload,
760
+ documents_list, doc_options, doc_value,
761
+ proposals_list, proposal_options, proposal_value,
762
+ "expanded"
763
+ )
764
+
765
+ if triggered_id in ['proposal-action-btn', 'recover-action-btn', 'loe-action-btn']:
766
+ got_lock = gemini_lock.acquire(blocking=False)
767
+ if not got_lock:
768
+ output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
769
+ return (
770
+ output_data_upload,
771
+ documents_list, doc_options, doc_value,
772
+ proposals_list, proposal_options, proposal_value,
773
+ "expanded"
774
+ )
775
+ try:
776
+ if triggered_id == "proposal-action-btn":
777
  action_name = "proposal"
778
+ selected_bytes = uploaded_documents_bytes.get(doc_value, None)
779
  result, _, _, generated_filename, generated_docx_bytes = process_document(
780
+ action_name, doc_value, chat_input, selected_bytes, None
781
  )
782
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
783
  elif triggered_id == "recover-action-btn":
784
  action_name = "recover"
785
  result, _, _, generated_filename, generated_docx_bytes = process_document(
786
+ action_name, doc_value, chat_input, None, proposal_value
787
  )
788
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
789
  elif triggered_id == "loe-action-btn":
790
  action_name = "loe"
791
  result, _, _, generated_filename, generated_docx_bytes = process_document(
792
+ action_name, None, chat_input, None, proposal_value
793
  )
794
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
795
  finally:
796
+ gemini_lock.release()
797
+ doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
798
+ doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
799
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
800
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
801
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
802
+ proposals_list = get_proposals_list(proposals)
 
 
 
803
  return (
804
  output_data_upload,
805
  documents_list, doc_options, doc_value,
 
807
  "expanded"
808
  )
809
 
810
+ doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
811
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
812
  return (
813
  output_data_upload,
814
  documents_list, doc_options, doc_value,