bluenevus commited on
Commit
17ec0f3
·
verified ·
1 Parent(s): 12a0ee8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -163
app.py CHANGED
@@ -9,9 +9,7 @@ import logging
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,37 +28,15 @@ GEMINI_MODEL = "models/gemini-2.5-pro-preview-03-25"
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,17 +170,21 @@ def save_loe_as_docx(loe_text, proposal_filename):
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,20 +206,20 @@ def process_document(sess_data, action, selected_filename=None, chat_input=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,20 +237,20 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,8 +267,8 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,12 +278,12 @@ def process_document(sess_data, action, selected_filename=None, chat_input=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,26 +298,26 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,8 +337,8 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,10 +346,10 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,8 +370,8 @@ def process_document(sess_data, action, selected_filename=None, chat_input=None,
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,7 +391,6 @@ def get_documents_list(docdict, shreddedict):
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,11 +399,11 @@ def get_documents_list(docdict, shreddedict):
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,7 +418,6 @@ def get_proposals_list(proposaldict):
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,11 +430,11 @@ def get_proposals_list(proposaldict):
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,12 +469,12 @@ app.layout = dbc.Container([
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,12 +500,12 @@ app.layout = dbc.Container([
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,8 +601,6 @@ def master_callback(
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,15 +614,17 @@ def master_callback(
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,13 +642,13 @@ def master_callback(
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,12 +658,12 @@ def master_callback(
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,20 +671,20 @@ def master_callback(
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,25 +694,25 @@ def master_callback(
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,7 +722,7 @@ def master_callback(
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,47 +734,47 @@ def master_callback(
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,8 +782,8 @@ def master_callback(
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,26 +791,6 @@ def master_callback(
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)
 
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
  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
+ 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
  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
  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
  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
  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
  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
  },
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
  },
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
  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
  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
  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
  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
  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
  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
  ]
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
  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
  "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
  "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)