bluenevus commited on
Commit
12a0ee8
·
1 Parent(s): 914ff41

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +163 -121
app.py CHANGED
@@ -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,37 @@ 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 +194,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 +226,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 +257,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 +287,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 +298,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 +318,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 +357,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 +366,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 +390,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 +411,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 +420,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 +439,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 +452,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 +491,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 +522,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 +623,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 +638,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 +664,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 +680,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 +693,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 +716,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 +744,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 +756,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 +804,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 +813,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)
 
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
+ if hasattr(ctx, "request") and hasattr(ctx.request, "cookies") and "dash_session" in ctx.request.cookies:
38
+ return ctx.request.cookies["dash_session"]
39
+ # fallback - generate a new session id (this may only happen for local calls/tests)
40
+ return str(uuid.uuid4())
41
+
42
+ def get_session_data(session_id):
43
+ if session_id not in SESSION_STORE:
44
+ tempdir = tempfile.mkdtemp(prefix="rfp_session_")
45
+ SESSION_STORE[session_id] = {
46
+ "uploaded_documents": {},
47
+ "uploaded_documents_fileid": {},
48
+ "uploaded_documents_bytes": {},
49
+ "proposals": {},
50
+ "proposals_fileid": {},
51
+ "shredded_documents": {},
52
+ "generated_response": None,
53
+ "gemini_lock": Lock(),
54
+ "session_tempdir": tempdir
55
+ }
56
+ return SESSION_STORE[session_id]
57
 
58
+ def truncate_filename(filename, maxlen=30):
59
+ if len(filename) <= maxlen:
60
+ return filename
61
+ else:
62
+ partlen = (maxlen - 3) // 2
63
+ return filename[:partlen] + "..." + filename[-partlen:]
64
 
65
  def decode_document(decoded_bytes):
66
  try:
 
194
  memf.seek(0)
195
  return memf.read()
196
 
197
+ def process_document(sess_data, action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_proposal_filename=None):
 
 
 
 
198
  doc_content = None
199
  doc_fileid = None
200
  if action in ["shred", "compliance", "virtual_board", "proposal", "recover"]:
201
+ if selected_filename and selected_filename in sess_data["uploaded_documents"]:
202
+ doc_content = sess_data["uploaded_documents"][selected_filename]
203
+ doc_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
204
+ elif sess_data["uploaded_documents"]:
205
+ doc_content = next(iter(sess_data["uploaded_documents"].values()))
206
+ selected_filename = next(iter(sess_data["uploaded_documents"].keys()))
207
+ doc_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
208
  else:
209
  doc_content = None
210
  doc_fileid = None
 
226
  if result and not result.startswith("Error"):
227
  docx_bytes = save_shredded_as_docx(result, selected_filename)
228
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
229
+ sess_data["uploaded_documents"][generated_docx_name] = result
230
+ sess_data["shredded_documents"][generated_docx_name] = docx_bytes
231
  return result, generated_docx_name, docx_bytes, None, None
232
  else:
233
  return result, None, None, None, None
234
 
235
  elif action == 'compliance':
236
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
237
  return "No proposal document selected for compliance.", None, None, None, None
238
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
239
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None, None
240
 
241
+ rfp_text = sess_data["uploaded_documents"][selected_filename]
242
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
243
  logging.info(f"Compliance check: comparing proposal [{selected_proposal_filename}] to RFP [{selected_filename}]")
244
  prompt = (
245
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
 
257
  if result and not result.startswith("Error"):
258
  docx_bytes = save_compliance_as_docx(result, selected_filename)
259
  compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
260
+ sess_data["uploaded_documents"][compliance_docx_name] = result
261
+ sess_data["shredded_documents"][compliance_docx_name] = docx_bytes
262
  return result, compliance_docx_name, docx_bytes, None, None
263
  else:
264
  return result, None, None, None, None
265
 
266
  elif action == 'virtual_board':
267
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
268
  return "No proposal document selected for evaluation board.", None, None, None, None
269
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
270
  return "No RFP/SOW/PWS/RFI document selected for evaluation board.", None, None, None, None
271
 
272
+ rfp_text = sess_data["uploaded_documents"][selected_filename]
273
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
274
  logging.info(f"Evaluation Board: extracting criteria from RFP [{selected_filename}], evaluating proposal [{selected_proposal_filename}]")
275
  prompt = (
276
  "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. "
 
287
  if result and not result.startswith("Error"):
288
  docx_bytes = save_virtual_board_as_docx(result, selected_filename)
289
  board_docx_name = f"{os.path.splitext(selected_filename)[0]}_evaluation_board.docx"
290
+ sess_data["uploaded_documents"][board_docx_name] = result
291
+ sess_data["shredded_documents"][board_docx_name] = docx_bytes
292
  return result, board_docx_name, docx_bytes, None, None
293
  else:
294
  return result, None, None, None, None
 
298
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
299
  return "No RFP/SOW/PWS/RFI document selected.", None, None, None, None
300
  rfp_filename = selected_filename
301
+ rfp_fileid = sess_data["uploaded_documents_fileid"].get(selected_filename)
302
+ if not rfp_fileid and rfp_filename in sess_data["uploaded_documents_bytes"]:
303
  try:
304
+ fileid = upload_to_gemini_file(sess_data["uploaded_documents_bytes"][rfp_filename], rfp_filename)
305
  if fileid:
306
+ sess_data["uploaded_documents_fileid"][rfp_filename] = fileid
307
  rfp_fileid = fileid
308
  logging.info(f"RFP file {rfp_filename} uploaded to Gemini for proposal.")
309
  except Exception as e:
 
318
  prompt += f"User additional instructions: {chat_input}\n"
319
  prompt += f"\n---\nRFP/SOW/PWS/RFI ({rfp_filename}):\n{doc_content}\n"
320
  result = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
321
+ sess_data["generated_response"] = result
322
  if result and not result.startswith("Error"):
323
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
324
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
325
+ sess_data["proposals"][generated_docx_name] = result
326
+ sess_data["proposals_fileid"][generated_docx_name] = None
327
  return result, None, None, generated_docx_name, docx_bytes
328
  else:
329
  return result, None, None, None, None
330
 
331
  elif action == 'recover':
332
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
333
  logging.error("No proposal document selected for recovery.")
334
  return "No proposal document selected for recovery.", None, None, None, None
335
+ if not selected_filename or selected_filename not in sess_data["uploaded_documents"]:
336
  logging.error("No compliance check or shredded requirements document selected for recovery.")
337
  return "No compliance check or shredded requirements document selected for recovery.", None, None, None, None
338
 
339
+ findings_content = sess_data["uploaded_documents"][selected_filename]
340
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
341
  prompt = (
342
  "You are a proposal compliance recovery expert. Use the findings and recommendations table and the original proposal response provided below. "
343
  "Address ONLY those sections of the proposal that have a finding and a recommendation for improvement. "
 
357
  base_name = os.path.splitext(selected_proposal_filename)[0]
358
  recovered_docx_name = f"{base_name}_recovered.docx"
359
  docx_bytes = save_proposal_as_docx(result, base_name)
360
+ sess_data["proposals"][recovered_docx_name] = result
361
+ sess_data["proposals_fileid"][recovered_docx_name] = None
362
  logging.info(f"Recovered proposal generated and saved as {recovered_docx_name}.")
363
  return result, None, None, recovered_docx_name, docx_bytes
364
  else:
 
366
  return result, None, None, None, None
367
 
368
  elif action == 'loe':
369
+ if not selected_proposal_filename or selected_proposal_filename not in sess_data["proposals"]:
370
  logging.warning("No proposal document selected for LOE estimation.")
371
  return "No proposal document selected for LOE estimation.", None, None, None, None
372
+ proposal_text = sess_data["proposals"][selected_proposal_filename]
373
  proposal_base_name = os.path.splitext(selected_proposal_filename)[0]
374
  prompt = (
375
  "You are a federal proposal cost and level of effort estimator. "
 
390
  result = gemini_generate_content(prompt, file_id=None, chat_input=chat_input)
391
  if result and not result.startswith("Error"):
392
  loe_docx_name = f"{proposal_base_name}_loe.docx"
393
+ sess_data["proposals"][loe_docx_name] = result
394
+ sess_data["proposals_fileid"][loe_docx_name] = None
395
  docx_bytes = save_loe_as_docx(result, proposal_base_name)
396
  logging.info(f"LOE generated and saved as {loe_docx_name}")
397
  return result, None, None, loe_docx_name, docx_bytes
 
411
  return html.Div("No documents uploaded or generated.", style={"wordWrap": "break-word"})
412
  doc_list = []
413
  for filename in all_docs:
414
+ truncated = truncate_filename(filename)
415
  if filename.lower().endswith('.docx') and filename in shreddedict:
416
  b64 = base64.b64encode(shreddedict[filename]).decode('utf-8')
417
  mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
 
420
  b64 = base64.b64encode(content.encode('utf-8')).decode('utf-8')
421
  mime = "text/plain"
422
  download_link = html.A(
423
+ truncated,
424
  href=f"data:{mime};base64,{b64}",
425
  download=filename,
426
  target="_blank",
427
+ style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline", "maxWidth": "calc(100% - 70px)", "display": "inline-block", "verticalAlign": "middle"}
428
  )
429
  doc_list.append(
430
  dbc.ListGroupItem([
 
439
  return html.Div("No proposals uploaded or generated.", style={"wordWrap": "break-word"})
440
  doc_list = []
441
  for filename in proposaldict:
442
+ truncated = truncate_filename(filename)
443
  file_content = proposaldict[filename]
444
  try:
445
  if filename.lower().endswith('_loe.docx'):
 
452
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
453
  mime = "text/plain"
454
  download_link = html.A(
455
+ truncated,
456
  href=f"data:{mime};base64,{b64}",
457
  download=filename,
458
  target="_blank",
459
+ style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline", "maxWidth": "calc(100% - 70px)", "display": "inline-block", "verticalAlign": "middle"}
460
  )
461
  doc_list.append(
462
  dbc.ListGroupItem([
 
491
  },
492
  multiple=False
493
  ),
494
+ html.Div(id='documents-list'),
495
  dcc.Dropdown(
496
  id='select-document-dropdown',
497
+ options=[],
498
  placeholder="Select a document to work with",
499
+ value=None,
500
  style={"marginBottom": "10px"}
501
  ),
502
  ])
 
522
  },
523
  multiple=False
524
  ),
525
+ html.Div(id='proposals-list'),
526
  dcc.Dropdown(
527
  id='select-proposal-dropdown',
528
+ options=[],
529
  placeholder="Select a proposal document",
530
+ value=None,
531
  style={"marginBottom": "10px"}
532
  ),
533
  ])
 
623
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
624
  chat_input, cancel_clicks, preview_window_state
625
  ):
626
+ session_id = get_session_id()
627
+ sess_data = get_session_data(session_id)
628
  ctx = callback_context
629
  triggered_id = ctx.triggered[0]['prop_id'].split('.')[0] if ctx.triggered else None
630
 
 
638
  proposal_delete_clicks = safe_get_n_clicks(ctx, 12)
639
  uploaded_rfp_decoded_bytes = None
640
 
 
 
641
  # Cancel action
642
  if triggered_id == 'cancel-action-btn':
643
  output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
644
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
645
+ 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)
646
+ proposals_list = get_proposals_list(sess_data["proposals"])
647
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
648
+ proposal_value = selected_proposal if selected_proposal in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
649
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
650
  return (
651
  output_data_upload,
652
  documents_list, doc_options, doc_value,
 
664
  if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
665
  fileid = upload_to_gemini_file(decoded, rfp_filename)
666
  if text is not None:
667
+ sess_data["uploaded_documents"][rfp_filename] = text
668
+ sess_data["uploaded_documents_bytes"][rfp_filename] = decoded
669
  if fileid:
670
+ sess_data["uploaded_documents_fileid"][rfp_filename] = fileid
671
+ logging.info(f"[{session_id}] Document uploaded: {rfp_filename}")
672
  else:
673
+ logging.error(f"[{session_id}] Failed to decode uploaded document: {rfp_filename}")
674
 
675
  if triggered_id == 'upload-proposal' and proposal_content is not None and proposal_filename:
676
  content_type, content_string = proposal_content.split(',')
 
680
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
681
  fileid = upload_to_gemini_file(decoded, proposal_filename)
682
  if text is not None:
683
+ sess_data["proposals"][proposal_filename] = text
684
  if fileid:
685
+ sess_data["proposals_fileid"][proposal_filename] = fileid
686
+ logging.info(f"[{session_id}] Proposal uploaded: {proposal_filename}")
687
  else:
688
+ logging.error(f"[{session_id}] Failed to decode uploaded proposal: {proposal_filename}")
689
 
690
  # Handle deletes
691
  if triggered_id and isinstance(doc_delete_clicks, list):
 
693
  if n_click:
694
  btn_id = ctx.inputs_list[8][i]['id']
695
  del_filename = btn_id['index']
696
+ if del_filename in sess_data["uploaded_documents"]:
697
+ del sess_data["uploaded_documents"][del_filename]
698
+ if del_filename in sess_data["uploaded_documents_fileid"]:
699
  try:
700
+ genai.delete_file(sess_data["uploaded_documents_fileid"][del_filename])
701
  except Exception as e:
702
+ logging.warning(f"[{session_id}] Failed to delete Gemini file {del_filename}: {e}")
703
+ del sess_data["uploaded_documents_fileid"][del_filename]
704
+ if del_filename in sess_data["uploaded_documents_bytes"]:
705
+ del sess_data["uploaded_documents_bytes"][del_filename]
706
+ logging.info(f"[{session_id}] Document deleted: {del_filename}")
707
+ if del_filename in sess_data["shredded_documents"]:
708
+ del sess_data["shredded_documents"][del_filename]
709
+ logging.info(f"[{session_id}] Shredded doc deleted: {del_filename}")
710
  if selected_doc == del_filename:
711
  selected_doc = None
712
  break
 
716
  if n_click:
717
  btn_id = ctx.inputs_list[12][i]['id']
718
  del_filename = btn_id['index']
719
+ if del_filename in sess_data["proposals"]:
720
+ del sess_data["proposals"][del_filename]
721
+ if del_filename in sess_data["proposals_fileid"]:
722
  try:
723
+ genai.delete_file(sess_data["proposals_fileid"][del_filename])
724
  except Exception as e:
725
+ logging.warning(f"[{session_id}] Failed to delete Gemini proposal file {del_filename}: {e}")
726
+ del sess_data["proposals_fileid"][del_filename]
727
+ logging.info(f"[{session_id}] Proposal deleted: {del_filename}")
728
  if selected_proposal == del_filename:
729
  selected_proposal = None
730
  break
731
 
732
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
733
+ 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)
734
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
735
+ proposals_list = get_proposals_list(sess_data["proposals"])
736
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
737
+ proposal_value = selected_proposal if selected_proposal in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
738
 
739
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
740
 
 
744
  ]
745
 
746
  if triggered_id in action_btns:
747
+ got_lock = sess_data["gemini_lock"].acquire(blocking=False)
748
  if not got_lock:
749
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
750
  return (
 
756
  try:
757
  if triggered_id == "shred-action-btn":
758
  action_name = "shred"
759
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
760
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
761
  elif triggered_id == "compliance-action-btn":
762
  action_name = "compliance"
763
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
764
+ sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
765
  )
766
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
767
  elif triggered_id == "board-action-btn":
768
  action_name = "virtual_board"
769
  result, generated_filename, generated_docx_bytes, _, _ = process_document(
770
+ sess_data, action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
771
  )
772
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
773
  elif triggered_id == "proposal-action-btn":
774
  action_name = "proposal"
775
+ selected_bytes = sess_data["uploaded_documents_bytes"].get(doc_value, None)
776
  result, _, _, generated_filename, generated_docx_bytes = process_document(
777
+ sess_data, action_name, doc_value, chat_input, selected_bytes, None
778
  )
779
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
780
  elif triggered_id == "recover-action-btn":
781
  action_name = "recover"
782
  result, _, _, generated_filename, generated_docx_bytes = process_document(
783
+ sess_data, action_name, doc_value, chat_input, None, proposal_value
784
  )
785
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
786
  elif triggered_id == "loe-action-btn":
787
  action_name = "loe"
788
  result, _, _, generated_filename, generated_docx_bytes = process_document(
789
+ sess_data, action_name, None, chat_input, None, proposal_value
790
  )
791
  output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
792
  finally:
793
+ sess_data["gemini_lock"].release()
794
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
795
+ 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)
796
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
797
+ proposal_value = proposal_value if proposal_value in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
798
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
799
+ proposals_list = get_proposals_list(sess_data["proposals"])
800
  return (
801
  output_data_upload,
802
  documents_list, doc_options, doc_value,
 
804
  "shrunk"
805
  )
806
 
807
+ 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)
808
+ proposal_value = proposal_value if proposal_value in sess_data["proposals"] else (next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None)
809
  return (
810
  output_data_upload,
811
  documents_list, doc_options, doc_value,
 
813
  "expanded"
814
  )
815
 
816
+ @app.callback(
817
+ Output('documents-list', 'children'),
818
+ Output('select-document-dropdown', 'options'),
819
+ Output('select-document-dropdown', 'value'),
820
+ Output('proposals-list', 'children'),
821
+ Output('select-proposal-dropdown', 'options'),
822
+ Output('select-proposal-dropdown', 'value'),
823
+ Input('output-data-upload', 'children')
824
+ )
825
+ def update_lists_on_output(children):
826
+ session_id = get_session_id()
827
+ sess_data = get_session_data(session_id)
828
+ documents_list = get_documents_list(sess_data["uploaded_documents"], sess_data["shredded_documents"])
829
+ doc_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["uploaded_documents"].keys()]
830
+ doc_value = next(iter(sess_data["uploaded_documents"]), None) if sess_data["uploaded_documents"] else None
831
+ proposals_list = get_proposals_list(sess_data["proposals"])
832
+ proposal_options = [{'label': truncate_filename(fn), 'value': fn} for fn in sess_data["proposals"].keys()]
833
+ proposal_value = next(iter(sess_data["proposals"]), None) if sess_data["proposals"] else None
834
+ return documents_list, doc_options, doc_value, proposals_list, proposal_options, proposal_value
835
+
836
  if __name__ == '__main__':
837
  print("Starting the Dash application...")
838
  app.run(debug=True, host='0.0.0.0', port=7860, threaded=True)