bluenevus commited on
Commit
e70fe8e
·
1 Parent(s): 17ec0f3

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +169 -122
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os
2
  import base64
3
  import io
4
  import dash
@@ -9,7 +9,9 @@ import logging
9
  from docx import Document
10
  import mimetypes
11
  from threading import Lock
12
- import time
 
 
13
 
14
  import google.generativeai as genai
15
 
@@ -28,15 +30,42 @@ GEMINI_MODEL = "models/gemini-2.5-pro-preview-03-25"
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,21 +199,17 @@ def save_loe_as_docx(loe_text, proposal_filename):
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,20 +231,20 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,20 +262,20 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,8 +292,8 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,12 +303,12 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,26 +323,26 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,8 +362,8 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
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,10 +371,10 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
346
  return result, None, None, None, None
347
 
348
  elif action == 'loe':
349
- if not selected_proposal_filename or selected_proposal_filename not in proposals:
350
  logging.warning("No proposal document selected for LOE estimation.")
351
  return "No proposal document selected for LOE estimation.", None, None, None, None
352
- proposal_text = proposals[selected_proposal_filename]
353
  proposal_base_name = os.path.splitext(selected_proposal_filename)[0]
354
  prompt = (
355
  "You are a federal proposal cost and level of effort estimator. "
@@ -370,8 +395,8 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
370
  result = gemini_generate_content(prompt, file_id=None, chat_input=chat_input)
371
  if result and not result.startswith("Error"):
372
  loe_docx_name = f"{proposal_base_name}_loe.docx"
373
- proposals[loe_docx_name] = result
374
- proposals_fileid[loe_docx_name] = None
375
  docx_bytes = save_loe_as_docx(result, proposal_base_name)
376
  logging.info(f"LOE generated and saved as {loe_docx_name}")
377
  return result, None, None, loe_docx_name, docx_bytes
@@ -391,6 +416,7 @@ def get_documents_list(docdict, shreddedict):
391
  return html.Div("No documents uploaded or generated.", style={"wordWrap": "break-word"})
392
  doc_list = []
393
  for filename in all_docs:
 
394
  if filename.lower().endswith('.docx') and filename in shreddedict:
395
  b64 = base64.b64encode(shreddedict[filename]).decode('utf-8')
396
  mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
@@ -399,11 +425,11 @@ def get_documents_list(docdict, shreddedict):
399
  b64 = base64.b64encode(content.encode('utf-8')).decode('utf-8')
400
  mime = "text/plain"
401
  download_link = html.A(
402
- filename,
403
  href=f"data:{mime};base64,{b64}",
404
  download=filename,
405
  target="_blank",
406
- style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
407
  )
408
  doc_list.append(
409
  dbc.ListGroupItem([
@@ -418,6 +444,7 @@ def get_proposals_list(proposaldict):
418
  return html.Div("No proposals uploaded or generated.", style={"wordWrap": "break-word"})
419
  doc_list = []
420
  for filename in proposaldict:
 
421
  file_content = proposaldict[filename]
422
  try:
423
  if filename.lower().endswith('_loe.docx'):
@@ -430,11 +457,11 @@ def get_proposals_list(proposaldict):
430
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
431
  mime = "text/plain"
432
  download_link = html.A(
433
- filename,
434
  href=f"data:{mime};base64,{b64}",
435
  download=filename,
436
  target="_blank",
437
- style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
438
  )
439
  doc_list.append(
440
  dbc.ListGroupItem([
@@ -469,12 +496,12 @@ app.layout = dbc.Container([
469
  },
470
  multiple=False
471
  ),
472
- html.Div(id='documents-list', children=get_documents_list(uploaded_documents, shredded_documents)),
473
  dcc.Dropdown(
474
  id='select-document-dropdown',
475
- options=[{'label': fn, 'value': fn} for fn in uploaded_documents.keys()],
476
  placeholder="Select a document to work with",
477
- value=next(iter(uploaded_documents), None),
478
  style={"marginBottom": "10px"}
479
  ),
480
  ])
@@ -500,12 +527,12 @@ app.layout = dbc.Container([
500
  },
501
  multiple=False
502
  ),
503
- html.Div(id='proposals-list', children=get_proposals_list(proposals)),
504
  dcc.Dropdown(
505
  id='select-proposal-dropdown',
506
- options=[{'label': fn, 'value': fn} for fn in proposals.keys()],
507
  placeholder="Select a proposal document",
508
- value=next(iter(proposals), None),
509
  style={"marginBottom": "10px"}
510
  ),
511
  ])
@@ -601,6 +628,8 @@ def master_callback(
601
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
602
  chat_input, cancel_clicks, preview_window_state
603
  ):
 
 
604
  ctx = callback_context
605
  triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
606
 
@@ -614,17 +643,15 @@ def master_callback(
614
  proposal_delete_clicks = safe_get_n_clicks(ctx, 12)
615
  uploaded_rfp_decoded_bytes = None
616
 
617
- global gemini_lock, uploaded_documents_bytes
618
-
619
  # Cancel action
620
  if triggered_id == 'cancel-action-btn':
621
  output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
622
- doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
623
- doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
624
- proposals_list = get_proposals_list(proposals)
625
- proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
626
- proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
627
- documents_list = get_documents_list(uploaded_documents, shredded_documents)
628
  return (
629
  output_data_upload,
630
  documents_list, doc_options, doc_value,
@@ -642,13 +669,13 @@ def master_callback(
642
  if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
643
  fileid = upload_to_gemini_file(decoded, rfp_filename)
644
  if text is not None:
645
- uploaded_documents[rfp_filename] = text
646
- uploaded_documents_bytes[rfp_filename] = decoded
647
  if fileid:
648
- uploaded_documents_fileid[rfp_filename] = fileid
649
- logging.info(f"Document uploaded: {rfp_filename}")
650
  else:
651
- logging.error(f"Failed to decode uploaded document: {rfp_filename}")
652
 
653
  if triggered_id == 'upload-proposal' and proposal_content is not None and proposal_filename:
654
  content_type, content_string = proposal_content.split(',')
@@ -658,12 +685,12 @@ def master_callback(
658
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
659
  fileid = upload_to_gemini_file(decoded, proposal_filename)
660
  if text is not None:
661
- proposals[proposal_filename] = text
662
  if fileid:
663
- proposals_fileid[proposal_filename] = fileid
664
- logging.info(f"Proposal uploaded: {proposal_filename}")
665
  else:
666
- logging.error(f"Failed to decode uploaded proposal: {proposal_filename}")
667
 
668
  # Handle deletes
669
  if triggered_id and isinstance(doc_delete_clicks, list):
@@ -671,20 +698,20 @@ def master_callback(
671
  if n_click:
672
  btn_id = ctx.inputs_list[8][i]['id']
673
  del_filename = btn_id['index']
674
- if del_filename in uploaded_documents:
675
- del uploaded_documents[del_filename]
676
- if del_filename in uploaded_documents_fileid:
677
  try:
678
- genai.delete_file(uploaded_documents_fileid[del_filename])
679
  except Exception as e:
680
- logging.warning(f"Failed to delete Gemini file {del_filename}: {e}")
681
- del uploaded_documents_fileid[del_filename]
682
- if del_filename in uploaded_documents_bytes:
683
- del uploaded_documents_bytes[del_filename]
684
- logging.info(f"Document deleted: {del_filename}")
685
- if del_filename in shredded_documents:
686
- del shredded_documents[del_filename]
687
- logging.info(f"Shredded doc deleted: {del_filename}")
688
  if selected_doc == del_filename:
689
  selected_doc = None
690
  break
@@ -694,25 +721,25 @@ def master_callback(
694
  if n_click:
695
  btn_id = ctx.inputs_list[12][i]['id']
696
  del_filename = btn_id['index']
697
- if del_filename in proposals:
698
- del proposals[del_filename]
699
- if del_filename in proposals_fileid:
700
  try:
701
- genai.delete_file(proposals_fileid[del_filename])
702
  except Exception as e:
703
- logging.warning(f"Failed to delete Gemini proposal file {del_filename}: {e}")
704
- del proposals_fileid[del_filename]
705
- logging.info(f"Proposal deleted: {del_filename}")
706
  if selected_proposal == del_filename:
707
  selected_proposal = None
708
  break
709
 
710
- doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
711
- doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
712
- documents_list = get_documents_list(uploaded_documents, shredded_documents)
713
- proposals_list = get_proposals_list(proposals)
714
- proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
715
- proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
716
 
717
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
718
 
@@ -722,7 +749,7 @@ def master_callback(
722
  ]
723
 
724
  if triggered_id in action_btns:
725
- got_lock = gemini_lock.acquire(blocking=False)
726
  if not got_lock:
727
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
728
  return (
@@ -734,47 +761,47 @@ def master_callback(
734
  try:
735
  if triggered_id == "shred-action-btn":
736
  action_name = "shred"
737
- result, generated_filename, generated_docx_bytes, _, _ = process_document(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
738
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
739
  elif triggered_id == "compliance-action-btn":
740
  action_name = "compliance"
741
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
742
- action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
743
  )
744
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
745
  elif triggered_id == "board-action-btn":
746
  action_name = "virtual_board"
747
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
748
- action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
749
  )
750
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
751
  elif triggered_id == "proposal-action-btn":
752
  action_name = "proposal"
753
- selected_bytes = uploaded_documents_bytes.get(doc_value, None)
754
  result, _, _, generated_filename, generated_docx_bytes = process_document(
755
- action_name, doc_value, chat_input, selected_bytes, None
756
  )
757
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
758
  elif triggered_id == "recover-action-btn":
759
  action_name = "recover"
760
  result, _, _, generated_filename, generated_docx_bytes = process_document(
761
- action_name, doc_value, chat_input, None, proposal_value
762
  )
763
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
764
  elif triggered_id == "loe-action-btn":
765
  action_name = "loe"
766
  result, _, _, generated_filename, generated_docx_bytes = process_document(
767
- action_name, None, chat_input, None, proposal_value
768
  )
769
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
770
  finally:
771
- gemini_lock.release()
772
- doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
773
- doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
774
- proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
775
- proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
776
- documents_list = get_documents_list(uploaded_documents, shredded_documents)
777
- proposals_list = get_proposals_list(proposals)
778
  return (
779
  output_data_upload,
780
  documents_list, doc_options, doc_value,
@@ -782,8 +809,8 @@ def master_callback(
782
  "shrunk"
783
  )
784
 
785
- doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
786
- proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
787
  return (
788
  output_data_upload,
789
  documents_list, doc_options, doc_value,
@@ -791,6 +818,26 @@ def master_callback(
791
  "expanded"
792
  )
793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  if __name__ == '__main__':
795
  print("Starting the Dash application...")
796
  app.run(debug=True, host='0.0.0.0', port=7860, threaded=True)
 
1
+ # Generated code appears here# Generated code appears hereimport os
2
  import base64
3
  import io
4
  import dash
 
9
  from docx import Document
10
  import mimetypes
11
  from threading import Lock
12
+ import tempfile
13
+ import shutil
14
+ import uuid
15
 
16
  import google.generativeai as genai
17
 
 
30
  MAX_INPUT_TOKENS = 1048576
31
  MAX_OUTPUT_TOKENS = 65536
32
 
33
+ SESSION_STORE = {}
34
+
35
+ def get_session_id():
36
+ ctx = dash.callback_context
37
+ sid = None
38
+ # Dash >=2.9.0 supports flask request in ctx.request
39
+ if hasattr(ctx, "request") and hasattr(ctx.request, "cookies"):
40
+ cookies = ctx.request.cookies
41
+ sid = cookies.get('dash_session', None)
42
+ if not sid:
43
+ # fallback
44
+ sid = str(uuid.uuid4())
45
+ return sid
46
+
47
+ def get_session_data(session_id):
48
+ if session_id not in SESSION_STORE:
49
+ tempdir = tempfile.mkdtemp(prefix="rfp_session_")
50
+ SESSION_STORE[session_id] = {
51
+ "uploaded_documents": {},
52
+ "uploaded_documents_fileid": {},
53
+ "uploaded_documents_bytes": {},
54
+ "proposals": {},
55
+ "proposals_fileid": {},
56
+ "shredded_documents": {},
57
+ "generated_response": None,
58
+ "gemini_lock": Lock(),
59
+ "session_tempdir": tempdir
60
+ }
61
+ return SESSION_STORE[session_id]
62
 
63
+ def truncate_filename(filename, maxlen=30):
64
+ if len(filename) <= maxlen:
65
+ return filename
66
+ else:
67
+ partlen = (maxlen - 3) // 2
68
+ return filename[:partlen] + "..." + filename[-partlen:]
69
 
70
  def decode_document(decoded_bytes):
71
  try:
 
199
  memf.seek(0)
200
  return memf.read()
201
 
202
+ def process_document(sess_data, action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_proposal_filename=None):
 
 
 
 
203
  doc_content = None
204
  doc_fileid = None
205
  if action in ["shred", "compliance", "virtual_board", "proposal", "recover"]:
206
+ if selected_filename and selected_filename in sess_data["uploaded_documents"]:
207
+ doc_content = sess_data["uploaded_documents"][selected_filename]
208
+ doc_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
209
+ elif sess_data["uploaded_documents"]:
210
+ doc_content = next(iter(sess_data["uploaded_documents"].values()))
211
+ selected_filename = next(iter(sess_data["uploaded_documents"].keys()))
212
+ doc_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
213
  else:
214
  doc_content = None
215
  doc_fileid = None
 
231
  if result and not result.startswith("Error"):
232
  docx_bytes = save_shredded_as_docx(result, selected_filename)
233
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
234
+ sess_data["uploaded_documents"][generated_docx_name] = result
235
+ sess_data["shredded_documents"][generated_docx_name] = docx_bytes
236
  return result, generated_docx_name, docx_bytes, None, None
237
  else:
238
  return result, None, None, None, None
239
 
240
  elif action == 'compliance':
241
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
242
  return "No proposal document selected for compliance.", None, None, None, None
243
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
244
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None, None
245
 
246
+ rfp_text = sess_data["uploaded_documents"][selected_filename]
247
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
248
  logging.info(f"Compliance check: comparing proposal [{selected_proposal_filename}] to RFP [{selected_filename}]")
249
  prompt = (
250
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
 
262
  if result and not result.startswith("Error"):
263
  docx_bytes = save_compliance_as_docx(result, selected_filename)
264
  compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
265
+ sess_data["uploaded_documents"][compliance_docx_name] = result
266
+ sess_data["shredded_documents"][compliance_docx_name] = docx_bytes
267
  return result, compliance_docx_name, docx_bytes, None, None
268
  else:
269
  return result, None, None, None, None
270
 
271
  elif action == 'virtual_board':
272
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
273
  return "No proposal document selected for evaluation board.", None, None, None, None
274
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
275
  return "No RFP/SOW/PWS/RFI document selected for evaluation board.", None, None, None, None
276
 
277
+ rfp_text = sess_data["uploaded_documents"][selected_filename]
278
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
279
  logging.info(f"Evaluation Board: extracting criteria from RFP [{selected_filename}], evaluating proposal [{selected_proposal_filename}]")
280
  prompt = (
281
  "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. "
 
292
  if result and not result.startswith("Error"):
293
  docx_bytes = save_virtual_board_as_docx(result, selected_filename)
294
  board_docx_name = f"{os.path.splitext(selected_filename)[0]}_evaluation_board.docx"
295
+ sess_data["uploaded_documents"][board_docx_name] = result
296
+ sess_data["shredded_documents"][board_docx_name] = docx_bytes
297
  return result, board_docx_name, docx_bytes, None, None
298
  else:
299
  return result, None, None, None, None
 
303
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
304
  return "No RFP/SOW/PWS/RFI document selected.", None, None, None, None
305
  rfp_filename = selected_filename
306
+ rfp_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
307
+ if not rfp_fileid and rfp_filename in sess_data["uploaded_documents_bytes"]:
308
  try:
309
+ fileid = upload_to_gemini_file(sess_data["uploaded_documents_bytes"][rfp_filename], rfp_filename)
310
  if fileid:
311
+ sess_data["uploaded_documents_fileid"][rfp_filename] = fileid
312
  rfp_fileid = fileid
313
  logging.info(f"RFP file {rfp_filename} uploaded to Gemini for proposal.")
314
  except Exception as e:
 
323
  prompt += f"User additional instructions: {chat_input}\n"
324
  prompt += f"\n---\nRFP/SOW/PWS/RFI ({rfp_filename}):\n{doc_content}\n"
325
  result = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
326
+ sess_data["generated_response"] = result
327
  if result and not result.startswith("Error"):
328
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
329
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
330
+ sess_data["proposals"][generated_docx_name] = result
331
+ sess_data["proposals_fileid"][generated_docx_name] = None
332
  return result, None, None, generated_docx_name, docx_bytes
333
  else:
334
  return result, None, None, None, None
335
 
336
  elif action == 'recover':
337
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
338
  logging.error("No proposal document selected for recovery.")
339
  return "No proposal document selected for recovery.", None, None, None, None
340
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
341
  logging.error("No compliance check or shredded requirements document selected for recovery.")
342
  return "No compliance check or shredded requirements document selected for recovery.", None, None, None, None
343
 
344
+ findings_content = sess_data["uploaded_documents"][selected_filename]
345
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
346
  prompt = (
347
  "You are a proposal compliance recovery expert. Use the findings and recommendations table and the original proposal response provided below. "
348
  "Address ONLY those sections of the proposal that have a finding and a recommendation for improvement. "
 
362
  base_name = os.path.splitext(selected_proposal_filename)[0]
363
  recovered_docx_name = f"{base_name}_recovered.docx"
364
  docx_bytes = save_proposal_as_docx(result, base_name)
365
+ sess_data["proposals"][recovered_docx_name] = result
366
+ sess_data["proposals_fileid"][recovered_docx_name] = None
367
  logging.info(f"Recovered proposal generated and saved as {recovered_docx_name}.")
368
  return result, None, None, recovered_docx_name, docx_bytes
369
  else:
 
371
  return result, None, None, None, None
372
 
373
  elif action == 'loe':
374
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
375
  logging.warning("No proposal document selected for LOE estimation.")
376
  return "No proposal document selected for LOE estimation.", None, None, None, None
377
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
378
  proposal_base_name = os.path.splitext(selected_proposal_filename)[0]
379
  prompt = (
380
  "You are a federal proposal cost and level of effort estimator. "
 
395
  result = gemini_generate_content(prompt, file_id=None, chat_input=chat_input)
396
  if result and not result.startswith("Error"):
397
  loe_docx_name = f"{proposal_base_name}_loe.docx"
398
+ sess_data["proposals"][loe_docx_name] = result
399
+ sess_data["proposals_fileid"][loe_docx_name] = None
400
  docx_bytes = save_loe_as_docx(result, proposal_base_name)
401
  logging.info(f"LOE generated and saved as {loe_docx_name}")
402
  return result, None, None, loe_docx_name, docx_bytes
 
416
  return html.Div("No documents uploaded or generated.", style={"wordWrap": "break-word"})
417
  doc_list = []
418
  for filename in all_docs:
419
+ truncated = truncate_filename(filename)
420
  if filename.lower().endswith('.docx') and filename in shreddedict:
421
  b64 = base64.b64encode(shreddedict[filename]).decode('utf-8')
422
  mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
 
425
  b64 = base64.b64encode(content.encode('utf-8')).decode('utf-8')
426
  mime = "text/plain"
427
  download_link = html.A(
428
+ truncated,
429
  href=f"data:{mime};base64,{b64}",
430
  download=filename,
431
  target="_blank",
432
+ style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline", "maxWidth": "calc(100% - 70px)", "display": "inline-block", "verticalAlign": "middle"}
433
  )
434
  doc_list.append(
435
  dbc.ListGroupItem([
 
444
  return html.Div("No proposals uploaded or generated.", style={"wordWrap": "break-word"})
445
  doc_list = []
446
  for filename in proposaldict:
447
+ truncated = truncate_filename(filename)
448
  file_content = proposaldict[filename]
449
  try:
450
  if filename.lower().endswith('_loe.docx'):
 
457
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
458
  mime = "text/plain"
459
  download_link = html.A(
460
+ truncated,
461
  href=f"data:{mime};base64,{b64}",
462
  download=filename,
463
  target="_blank",
464
+ style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline", "maxWidth": "calc(100% - 70px)", "display": "inline-block", "verticalAlign": "middle"}
465
  )
466
  doc_list.append(
467
  dbc.ListGroupItem([
 
496
  },
497
  multiple=False
498
  ),
499
+ html.Div(id='documents-list'),
500
  dcc.Dropdown(
501
  id='select-document-dropdown',
502
+ options=[],
503
  placeholder="Select a document to work with",
504
+ value=None,
505
  style={"marginBottom": "10px"}
506
  ),
507
  ])
 
527
  },
528
  multiple=False
529
  ),
530
+ html.Div(id='proposals-list'),
531
  dcc.Dropdown(
532
  id='select-proposal-dropdown',
533
+ options=[],
534
  placeholder="Select a proposal document",
535
+ value=None,
536
  style={"marginBottom": "10px"}
537
  ),
538
  ])
 
628
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
629
  chat_input, cancel_clicks, preview_window_state
630
  ):
631
+ session_id = get_session_id()
632
+ sess_data = get_session_data(session_id)
633
  ctx = callback_context
634
  triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
635
 
 
643
  proposal_delete_clicks = safe_get_n_clicks(ctx, 12)
644
  uploaded_rfp_decoded_bytes = None
645
 
 
 
646
  # Cancel action
647
  if triggered_id == 'cancel-action-btn':
648
  output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
649
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
650
+ doc_value = selected_doc if selected_doc in sess_data["uploaded_documents"] else (next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None)
651
+ proposals_list = get_proposals_list(sess_data["proposals"])
652
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
653
+ proposal_value = selected_proposal if selected_proposal in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
654
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
655
  return (
656
  output_data_upload,
657
  documents_list, doc_options, doc_value,
 
669
  if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
670
  fileid = upload_to_gemini_file(decoded, rfp_filename)
671
  if text is not None:
672
+ sess_data["uploaded_documents"][rfp_filename] = text
673
+ sess_data["uploaded_documents_bytes"][rfp_filename] = decoded
674
  if fileid:
675
+ sess_data["uploaded_documents_fileid"][rfp_filename] = fileid
676
+ logging.info(f"[{session_id}] Document uploaded: {rfp_filename}")
677
  else:
678
+ logging.error(f"[{session_id}] Failed to decode uploaded document: {rfp_filename}")
679
 
680
  if triggered_id == 'upload-proposal' and proposal_content is not None and proposal_filename:
681
  content_type, content_string = proposal_content.split(',')
 
685
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
686
  fileid = upload_to_gemini_file(decoded, proposal_filename)
687
  if text is not None:
688
+ sess_data["proposals"][proposal_filename] = text
689
  if fileid:
690
+ sess_data["proposals_fileid"][proposal_filename] = fileid
691
+ logging.info(f"[{session_id}] Proposal uploaded: {proposal_filename}")
692
  else:
693
+ logging.error(f"[{session_id}] Failed to decode uploaded proposal: {proposal_filename}")
694
 
695
  # Handle deletes
696
  if triggered_id and isinstance(doc_delete_clicks, list):
 
698
  if n_click:
699
  btn_id = ctx.inputs_list[8][i]['id']
700
  del_filename = btn_id['index']
701
+ if del_filename in sess_data["uploaded_documents"]:
702
+ del sess_data["uploaded_documents"][del_filename]
703
+ if del_filename in sess_data["uploaded_documents_fileid"]:
704
  try:
705
+ genai.delete_file(sess_data["uploaded_documents_fileid"][del_filename])
706
  except Exception as e:
707
+ logging.warning(f"[{session_id}] Failed to delete Gemini file {del_filename}: {e}")
708
+ del sess_data["uploaded_documents_fileid"][del_filename]
709
+ if del_filename in sess_data["uploaded_documents_bytes"]:
710
+ del sess_data["uploaded_documents_bytes"][del_filename]
711
+ logging.info(f"[{session_id}] Document deleted: {del_filename}")
712
+ if del_filename in sess_data["shredded_documents"]:
713
+ del sess_data["shredded_documents"][del_filename]
714
+ logging.info(f"[{session_id}] Shredded doc deleted: {del_filename}")
715
  if selected_doc == del_filename:
716
  selected_doc = None
717
  break
 
721
  if n_click:
722
  btn_id = ctx.inputs_list[12][i]['id']
723
  del_filename = btn_id['index']
724
+ if del_filename in sess_data["proposals"]:
725
+ del sess_data["proposals"][del_filename]
726
+ if del_filename in sess_data["proposals_fileid"]:
727
  try:
728
+ genai.delete_file(sess_data["proposals_fileid"][del_filename])
729
  except Exception as e:
730
+ logging.warning(f"[{session_id}] Failed to delete Gemini proposal file {del_filename}: {e}")
731
+ del sess_data["proposals_fileid"][del_filename]
732
+ logging.info(f"[{session_id}] Proposal deleted: {del_filename}")
733
  if selected_proposal == del_filename:
734
  selected_proposal = None
735
  break
736
 
737
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
738
+ doc_value = selected_doc if selected_doc in sess_data["uploaded_documents"] else (next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None)
739
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
740
+ proposals_list = get_proposals_list(sess_data["proposals"])
741
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
742
+ proposal_value = selected_proposal if selected_proposal in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
743
 
744
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
745
 
 
749
  ]
750
 
751
  if triggered_id in action_btns:
752
+ got_lock = sess_data["gemini_lock"].acquire(blocking=False)
753
  if not got_lock:
754
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
755
  return (
 
761
  try:
762
  if triggered_id == "shred-action-btn":
763
  action_name = "shred"
764
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
765
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
766
  elif triggered_id == "compliance-action-btn":
767
  action_name = "compliance"
768
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
769
+ sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
770
  )
771
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
772
  elif triggered_id == "board-action-btn":
773
  action_name = "virtual_board"
774
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
775
+ sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
776
  )
777
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
778
  elif triggered_id == "proposal-action-btn":
779
  action_name = "proposal"
780
+ selected_bytes = sess_data["uploaded_documents_bytes"].get(doc_value, None)
781
  result, _, _, generated_filename, generated_docx_bytes = process_document(
782
+ sess_data, action_name, doc_value, chat_input, selected_bytes, None
783
  )
784
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
785
  elif triggered_id == "recover-action-btn":
786
  action_name = "recover"
787
  result, _, _, generated_filename, generated_docx_bytes = process_document(
788
+ sess_data, action_name, doc_value, chat_input, None, proposal_value
789
  )
790
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
791
  elif triggered_id == "loe-action-btn":
792
  action_name = "loe"
793
  result, _, _, generated_filename, generated_docx_bytes = process_document(
794
+ sess_data, action_name, None, chat_input, None, proposal_value
795
  )
796
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
797
  finally:
798
+ sess_data["gemini_lock"].release()
799
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
800
+ doc_value = doc_value if doc_value in sess_data["uploaded_documents"] else (next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None)
801
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
802
+ proposal_value = proposal_value if proposal_value in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
803
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
804
+ proposals_list = get_proposals_list(sess_data["proposals"])
805
  return (
806
  output_data_upload,
807
  documents_list, doc_options, doc_value,
 
809
  "shrunk"
810
  )
811
 
812
+ doc_value = doc_value if doc_value in sess_data["uploaded_documents"] else (next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None)
813
+ proposal_value = proposal_value if proposal_value in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
814
  return (
815
  output_data_upload,
816
  documents_list, doc_options, doc_value,
 
818
  "expanded"
819
  )
820
 
821
+ @app.callback(
822
+ Output('documents-list', 'children'),
823
+ Output('select-document-dropdown', 'options'),
824
+ Output('select-document-dropdown', 'value'),
825
+ Output('proposals-list', 'children'),
826
+ Output('select-proposal-dropdown', 'options'),
827
+ Output('select-proposal-dropdown', 'value'),
828
+ Input('output-data-upload', 'children')
829
+ )
830
+ def update_lists_on_output(children):
831
+ session_id = get_session_id()
832
+ sess_data = get_session_data(session_id)
833
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
834
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
835
+ doc_value = next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None
836
+ proposals_list = get_proposals_list(sess_data["proposals"])
837
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
838
+ proposal_value = next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None
839
+ return documents_list, doc_options, doc_value, proposals_list, proposal_options, proposal_value
840
+
841
  if __name__ == '__main__':
842
  print("Starting the Dash application...")
843
  app.run(debug=True, host='0.0.0.0', port=7860, threaded=True)