bluenevus commited on
Commit
5f3bd8e
·
1 Parent(s): 22a01d6

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +186 -201
app.py CHANGED
@@ -30,10 +30,9 @@ MAX_OUTPUT_TOKENS = 65536
30
 
31
  uploaded_documents = {}
32
  uploaded_documents_fileid = {}
33
- uploaded_proposals = {}
34
- uploaded_proposals_fileid = {}
35
  shredded_documents = {}
36
- shredded_document = None
37
  generated_response = None
38
 
39
  gemini_lock = Lock()
@@ -143,14 +142,14 @@ def save_compliance_as_docx(compliance_text, rfp_filename):
143
  memf.seek(0)
144
  return memf.read()
145
 
146
- def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_generated_filename=None):
147
- global shredded_document, generated_response
148
 
149
  logging.info(f"Process document called with action: {action}")
150
 
151
  doc_content = None
152
  doc_fileid = None
153
- if action in ["shred", "proposal", "compliance"]:
154
  if selected_filename and selected_filename in uploaded_documents:
155
  doc_content = uploaded_documents[selected_filename]
156
  doc_fileid = uploaded_documents_fileid.get(selected_filename)
@@ -165,7 +164,7 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
165
  if action == 'shred':
166
  if not doc_content:
167
  logging.warning("No uploaded document found for shredding.")
168
- return "No document uploaded.", None, None, None
169
  prompt = (
170
  "Analyze the following RFP/PWS/SOW/RFI and generate a requirements spreadsheet. Return ONLY the markdown spreadsheet and NOTHING else"
171
  "Identify requirements by action words like 'shall', 'will', 'perform', etc. Also analye the document and put a column for recommended win theme for each requirement identified. Organize by PWS section and requirement. "
@@ -175,21 +174,53 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
175
  prompt += f"User additional instructions: {chat_input}\n"
176
  prompt += f"\nFile Name: {selected_filename}\n\n"
177
  result = gemini_generate_content(prompt, file_id=doc_fileid, chat_input=chat_input)
178
- shredded_document = result
179
  if result and not result.startswith("Error"):
180
  docx_bytes = save_shredded_as_docx(result, selected_filename)
181
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
 
182
  shredded_documents[generated_docx_name] = docx_bytes
183
- if generated_docx_name not in uploaded_documents:
184
- uploaded_documents[generated_docx_name] = result
185
- return result, None, generated_docx_name, result
186
  else:
187
- return result, None, None, result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  elif action == 'proposal':
190
  if not doc_content:
191
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
192
- return "No RFP/SOW/PWS/RFI document selected.", None, None, None
193
  rfp_filename = selected_filename
194
  rfp_fileid = uploaded_documents_fileid.get(selected_filename)
195
  if not rfp_fileid and rfp_decoded_bytes is not None:
@@ -215,67 +246,36 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
215
  if result and not result.startswith("Error"):
216
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
217
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
218
- uploaded_proposals[generated_docx_name] = result
219
- uploaded_proposals_fileid[generated_docx_name] = None
220
- return result, None, generated_docx_name, result
221
- else:
222
- return result, None, None, result
223
-
224
- elif action == 'compliance':
225
- if not selected_generated_filename or selected_generated_filename not in uploaded_proposals:
226
- return "No generated document selected for compliance.", None, None, None
227
- if not selected_filename or selected_filename not in uploaded_documents:
228
- return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None
229
-
230
- rfp_text = uploaded_documents[selected_filename]
231
- proposal_text = uploaded_proposals[selected_generated_filename]
232
- logging.info(f"Compliance check: comparing proposal [{selected_generated_filename}] to RFP [{selected_filename}]")
233
-
234
- prompt = (
235
- "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
236
- "Compare the proposal to the RFP requirements and win themes. "
237
- "For each PWS section/requirement, determine if the proposal fully, partially, or does not address the requirement and win theme. "
238
- "For each, give a finding and a recommendation to recover for compliance or to strengthen the response. "
239
- "Return ONLY a markdown table (NO comments, NO intro, NO outro, NO summary) with the following columns: "
240
- "| PWS Section Number | Section Name | Requirement Description | Finding | Recommendation to Recover for Compliance |. "
241
- "Be concise and focus on actionable compliance gaps. Only the table, nothing else.\n\n"
242
- f"---\nRFP/SOW/PWS/RFI ({selected_filename}):\n{rfp_text}\n"
243
- "---\nGenerated Proposal Document:\n"
244
- f"{proposal_text}\n"
245
- )
246
- result = gemini_generate_content(prompt, file_id=None, chat_input=None)
247
- if result and not result.startswith("Error"):
248
- docx_bytes = save_compliance_as_docx(result, selected_filename)
249
- compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
250
- uploaded_documents[compliance_docx_name] = result
251
- shredded_documents[compliance_docx_name] = docx_bytes
252
- return result, None, compliance_docx_name, result
253
  else:
254
- return result, None, None, result
255
 
256
  elif action == 'recover':
257
- return "Recovery not implemented yet.", None, None, None
258
- elif action == 'board':
259
- return "Virtual board not implemented yet.", None, None, None
260
  elif action == 'loe':
261
- return "LOE estimation not implemented yet.", None, None, None
262
- return "Action not implemented yet.", None, None, None
263
-
264
- def get_uploaded_doc_list(docdict):
265
- if not docdict:
266
- return html.Div("No documents uploaded.", style={"wordWrap": "break-word"})
 
 
 
 
 
 
267
  doc_list = []
268
- for filename in docdict:
269
- file_content = docdict[filename]
270
- mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" if filename.lower().endswith('.docx') else "text/plain"
271
- try:
272
- if filename.lower().endswith('.docx') and filename in shredded_documents:
273
- docx_bytes = shredded_documents[filename]
274
- b64 = base64.b64encode(docx_bytes).decode('utf-8')
275
- else:
276
- b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
277
- except Exception:
278
- b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
279
  download_link = html.A(
280
  filename,
281
  href=f"data:{mime};base64,{b64}",
@@ -286,44 +286,24 @@ def get_uploaded_doc_list(docdict):
286
  doc_list.append(
287
  dbc.ListGroupItem([
288
  download_link,
289
- dbc.Button("Delete", id={'type': 'delete-doc-btn', 'index': filename, 'group': 'rfp'}, size="sm", color="danger", className="float-end ms-2")
290
  ], className="d-flex justify-content-between align-items-center")
291
  )
292
  return dbc.ListGroup(doc_list, flush=True)
293
 
294
- def get_shredded_doc_list(shreddict):
295
- if not shreddict:
296
- return html.Div("No shredded requirements yet.", style={"wordWrap": "break-word"})
297
  doc_list = []
298
- for filename in shreddict:
299
- b64 = base64.b64encode(shreddict[filename]).decode('utf-8')
300
- download_link = html.A(
301
- filename,
302
- href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
303
- download=filename,
304
- target="_blank",
305
- style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
306
- )
307
- doc_list.append(
308
- dbc.ListGroupItem([
309
- download_link,
310
- dbc.Button("Delete", id={'type': 'delete-shredded-btn', 'index': filename, 'group': 'shredded'}, size="sm", color="danger", className="float-end ms-2")
311
- ], className="d-flex justify-content-between align-items-center")
312
- )
313
- return dbc.ListGroup(doc_list, flush=True)
314
-
315
- def get_uploaded_proposal_list(docdict):
316
- if not docdict:
317
- return html.Div("No proposal documents uploaded.", style={"wordWrap": "break-word"})
318
- doc_list = []
319
- for filename in docdict:
320
- file_content = docdict[filename]
321
- mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
322
  try:
323
  docx_bytes = save_proposal_as_docx(file_content, filename)
324
  b64 = base64.b64encode(docx_bytes).decode('utf-8')
 
325
  except Exception:
326
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
 
327
  download_link = html.A(
328
  filename,
329
  href=f"data:{mime};base64,{b64}",
@@ -343,17 +323,8 @@ app.layout = dbc.Container([
343
  dbc.Row([
344
  dbc.Col([
345
  dbc.Card([
346
- dbc.CardHeader(html.H5("RFP/SOW/PWS/RFI")),
347
  dbc.CardBody([
348
- html.H6("Uploaded Documents"),
349
- html.Div(get_uploaded_doc_list(uploaded_documents), id='uploaded-doc-list'),
350
- dcc.Dropdown(
351
- id='select-document-dropdown',
352
- options=[{'label': fn, 'value': fn} for fn in uploaded_documents.keys()],
353
- placeholder="Select a document to work with",
354
- value=next(iter(uploaded_documents), None),
355
- style={"marginBottom": "10px"}
356
- ),
357
  dcc.Upload(
358
  id='upload-document',
359
  children=html.Div([
@@ -372,23 +343,19 @@ app.layout = dbc.Container([
372
  },
373
  multiple=False
374
  ),
375
- html.Hr(style={"marginTop": "20px", "marginBottom": "10px"}),
376
- html.H6("Shredded/Compliance Documents"),
377
- html.Div(get_shredded_doc_list(shredded_documents), id='shredded-doc-list')
 
 
 
 
 
378
  ])
379
  ], className="mb-3"),
380
  dbc.Card([
381
- dbc.CardHeader(html.H5("Proposal")),
382
  dbc.CardBody([
383
- html.H6("Generated Proposals"),
384
- html.Div(get_uploaded_proposal_list(uploaded_proposals), id='uploaded-proposal-list'),
385
- dcc.Dropdown(
386
- id='select-proposal-dropdown',
387
- options=[{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()],
388
- placeholder="Select a proposal document",
389
- value=next(iter(uploaded_proposals), None),
390
- style={"marginBottom": "10px"}
391
- ),
392
  dcc.Upload(
393
  id='upload-proposal',
394
  children=html.Div([
@@ -406,7 +373,15 @@ app.layout = dbc.Container([
406
  'margin': '10px'
407
  },
408
  multiple=False
409
- )
 
 
 
 
 
 
 
 
410
  ])
411
  ], className="mb-3"),
412
  ], style={'minWidth': '260px', 'width':'30vw','maxWidth':'30vw'}, width=3),
@@ -441,11 +416,10 @@ app.layout = dbc.Container([
441
 
442
  @app.callback(
443
  Output('output-data-upload', 'children'),
444
- Output('uploaded-doc-list', 'children'),
445
  Output('select-document-dropdown', 'options'),
446
  Output('select-document-dropdown', 'value'),
447
- Output('shredded-doc-list', 'children'),
448
- Output('uploaded-proposal-list', 'children'),
449
  Output('select-proposal-dropdown', 'options'),
450
  Output('select-proposal-dropdown', 'value'),
451
  [
@@ -454,14 +428,12 @@ app.layout = dbc.Container([
454
  Input('compliance-action-btn', 'n_clicks'),
455
  Input('upload-document', 'contents'),
456
  State('upload-document', 'filename'),
457
- Input({'type': 'delete-doc-btn', 'index': ALL, 'group': 'rfp'}, 'n_clicks'),
458
  State('select-document-dropdown', 'value'),
459
  Input('upload-proposal', 'contents'),
460
  State('upload-proposal', 'filename'),
461
  Input({'type': 'delete-proposal-btn', 'index': ALL, 'group': 'proposal'}, 'n_clicks'),
462
  State('select-proposal-dropdown', 'value'),
463
- Input({'type': 'delete-shredded-btn', 'index': ALL, 'group': 'shredded'}, 'n_clicks'),
464
- State('shredded-doc-list', 'children'),
465
  State('chat-input', 'value'),
466
  Input('cancel-action-btn', 'n_clicks')
467
  ],
@@ -469,9 +441,8 @@ app.layout = dbc.Container([
469
  )
470
  def master_callback(
471
  shred_clicks, proposal_clicks, compliance_clicks,
472
- rfp_content, rfp_filename, rfp_delete_clicks, selected_doc,
473
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
474
- shredded_delete_clicks, shredded_doc_children,
475
  chat_input, cancel_clicks
476
  ):
477
  ctx = callback_context
@@ -483,12 +454,8 @@ def master_callback(
483
  except Exception:
484
  return []
485
 
486
- upload_triggered = False
487
-
488
- rfp_delete_clicks = safe_get_n_clicks(ctx, 5)
489
  proposal_delete_clicks = safe_get_n_clicks(ctx, 9)
490
- shredded_delete_clicks = safe_get_n_clicks(ctx, 11)
491
-
492
  uploaded_rfp_decoded_bytes = None
493
 
494
  global gemini_lock
@@ -497,16 +464,14 @@ def master_callback(
497
  output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
498
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
499
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
500
- shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
501
- uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
502
- uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
503
- proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
504
- proposal_value = selected_proposal if selected_proposal in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
505
  return (
506
  output_data_upload,
507
- uploaded_doc_list, doc_options, doc_value,
508
- shredded_doc_list_items,
509
- uploaded_proposal_list, proposal_options, proposal_value
510
  )
511
 
512
  if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
@@ -524,7 +489,6 @@ def master_callback(
524
  logging.info(f"Document uploaded: {rfp_filename}")
525
  else:
526
  logging.error(f"Failed to decode uploaded document: {rfp_filename}")
527
- upload_triggered = True
528
 
529
  if triggered_id == 'upload-proposal' and proposal_content is not None and proposal_filename:
530
  content_type, content_string = proposal_content.split(',')
@@ -534,16 +498,15 @@ def master_callback(
534
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
535
  fileid = upload_to_gemini_file(decoded, proposal_filename)
536
  if text is not None:
537
- uploaded_proposals[proposal_filename] = text
538
  if fileid:
539
- uploaded_proposals_fileid[proposal_filename] = fileid
540
  logging.info(f"Proposal uploaded: {proposal_filename}")
541
  else:
542
  logging.error(f"Failed to decode uploaded proposal: {proposal_filename}")
543
- upload_triggered = True
544
 
545
- if triggered_id and isinstance(rfp_delete_clicks, list):
546
- for i, n_click in enumerate(rfp_delete_clicks):
547
  if n_click:
548
  btn_id = ctx.inputs_list[5][i]['id']
549
  del_filename = btn_id['index']
@@ -556,99 +519,121 @@ def master_callback(
556
  logging.warning(f"Failed to delete Gemini file {del_filename}: {e}")
557
  del uploaded_documents_fileid[del_filename]
558
  logging.info(f"Document deleted: {del_filename}")
559
- if selected_doc == del_filename:
560
- selected_doc = None
561
- upload_triggered = True
562
- break
 
 
563
 
564
  if triggered_id and isinstance(proposal_delete_clicks, list):
565
  for i, n_click in enumerate(proposal_delete_clicks):
566
  if n_click:
567
  btn_id = ctx.inputs_list[9][i]['id']
568
  del_filename = btn_id['index']
569
- if del_filename in uploaded_proposals:
570
- del uploaded_proposals[del_filename]
571
- if del_filename in uploaded_proposals_fileid:
572
  try:
573
- genai.delete_file(uploaded_proposals_fileid[del_filename])
574
  except Exception as e:
575
  logging.warning(f"Failed to delete Gemini proposal file {del_filename}: {e}")
576
- del uploaded_proposals_fileid[del_filename]
577
  logging.info(f"Proposal deleted: {del_filename}")
578
- if selected_proposal == del_filename:
579
- selected_proposal = None
580
- upload_triggered = True
581
- break
582
-
583
- if triggered_id and isinstance(shredded_delete_clicks, list):
584
- for i, n_click in enumerate(shredded_delete_clicks):
585
- if n_click:
586
- btn_id = ctx.inputs_list[11][i]['id']
587
- del_filename = btn_id['index']
588
- if del_filename in shredded_documents:
589
- del shredded_documents[del_filename]
590
- logging.info(f"Shredded doc deleted: {del_filename}")
591
- upload_triggered = True
592
  break
593
 
594
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
595
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
596
- shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
597
- uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
598
- uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
599
- proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
600
- proposal_value = selected_proposal if selected_proposal in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
601
 
602
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
603
 
604
- if triggered_id in ['shred-action-btn', 'proposal-action-btn', 'compliance-action-btn']:
605
  got_lock = gemini_lock.acquire(blocking=False)
606
  if not got_lock:
607
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
608
  return (
609
  output_data_upload,
610
- uploaded_doc_list, doc_options, doc_value,
611
- shredded_doc_list_items,
612
- uploaded_proposal_list, proposal_options, proposal_value
613
  )
614
  try:
615
- action_name = "shred" if triggered_id=="shred-action-btn" else ("proposal" if triggered_id=="proposal-action-btn" else "compliance")
616
- result, _, _, _ = process_document(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, selected_proposal)
617
- output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
 
 
 
 
 
 
 
 
 
 
 
618
  finally:
619
  gemini_lock.release()
620
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
621
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
622
- proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
623
- proposal_value = proposal_value if proposal_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
624
- shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
625
- uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
626
- uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
627
  return (
628
  output_data_upload,
629
- uploaded_doc_list, doc_options, doc_value,
630
- shredded_doc_list_items,
631
- uploaded_proposal_list, proposal_options, proposal_value
632
  )
633
 
634
- if upload_triggered:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
636
- proposal_value = proposal_value if proposal_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
637
- output_data_upload = html.Div("Upload/Delete completed.", style={"wordWrap": "break-word"})
 
 
638
  return (
639
  output_data_upload,
640
- uploaded_doc_list, doc_options, doc_value,
641
- shredded_doc_list_items,
642
- uploaded_proposal_list, proposal_options, proposal_value
643
  )
644
 
645
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
646
- proposal_value = proposal_value if proposal_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
647
  return (
648
  output_data_upload,
649
- uploaded_doc_list, doc_options, doc_value,
650
- shredded_doc_list_items,
651
- uploaded_proposal_list, proposal_options, proposal_value
652
  )
653
 
654
  if __name__ == '__main__':
 
30
 
31
  uploaded_documents = {}
32
  uploaded_documents_fileid = {}
33
+ proposals = {}
34
+ proposals_fileid = {}
35
  shredded_documents = {}
 
36
  generated_response = None
37
 
38
  gemini_lock = Lock()
 
142
  memf.seek(0)
143
  return memf.read()
144
 
145
+ def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, selected_proposal_filename=None):
146
+ global generated_response
147
 
148
  logging.info(f"Process document called with action: {action}")
149
 
150
  doc_content = None
151
  doc_fileid = None
152
+ if action in ["shred", "compliance", "virtual_board"]:
153
  if selected_filename and selected_filename in uploaded_documents:
154
  doc_content = uploaded_documents[selected_filename]
155
  doc_fileid = uploaded_documents_fileid.get(selected_filename)
 
164
  if action == 'shred':
165
  if not doc_content:
166
  logging.warning("No uploaded document found for shredding.")
167
+ return "No document uploaded.", None, None, None, None
168
  prompt = (
169
  "Analyze the following RFP/PWS/SOW/RFI and generate a requirements spreadsheet. Return ONLY the markdown spreadsheet and NOTHING else"
170
  "Identify requirements by action words like 'shall', 'will', 'perform', etc. Also analye the document and put a column for recommended win theme for each requirement identified. Organize by PWS section and requirement. "
 
174
  prompt += f"User additional instructions: {chat_input}\n"
175
  prompt += f"\nFile Name: {selected_filename}\n\n"
176
  result = gemini_generate_content(prompt, file_id=doc_fileid, chat_input=chat_input)
 
177
  if result and not result.startswith("Error"):
178
  docx_bytes = save_shredded_as_docx(result, selected_filename)
179
  generated_docx_name = f"{os.path.splitext(selected_filename)[0]}_shredded.docx"
180
+ uploaded_documents[generated_docx_name] = result
181
  shredded_documents[generated_docx_name] = docx_bytes
182
+ return result, generated_docx_name, docx_bytes, None, None
 
 
183
  else:
184
+ return result, None, None, None, None
185
+
186
+ elif action == 'compliance':
187
+ if not selected_proposal_filename or selected_proposal_filename not in proposals:
188
+ return "No proposal document selected for compliance.", None, None, None, None
189
+ if not selected_filename or selected_filename not in uploaded_documents:
190
+ return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None, None
191
+
192
+ rfp_text = uploaded_documents[selected_filename]
193
+ proposal_text = proposals[selected_proposal_filename]
194
+ logging.info(f"Compliance check: comparing proposal [{selected_proposal_filename}] to RFP [{selected_filename}]")
195
+ prompt = (
196
+ "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
197
+ "Compare the proposal to the RFP requirements and win themes. "
198
+ "For each PWS section/requirement, determine if the proposal fully, partially, or does not address the requirement and win theme. "
199
+ "For each, give a finding and a recommendation to recover for compliance or to strengthen the response. "
200
+ "Return ONLY a markdown table (NO comments, NO intro, NO outro, NO summary) with the following columns: "
201
+ "| PWS Section Number | Section Name | Requirement Description | Finding | Recommendation to Recover for Compliance |. "
202
+ "Be concise and focus on actionable compliance gaps. Only the table, nothing else.\n\n"
203
+ f"---\nRFP/SOW/PWS/RFI ({selected_filename}):\n{rfp_text}\n"
204
+ "---\nGenerated Proposal Document:\n"
205
+ f"{proposal_text}\n"
206
+ )
207
+ result = gemini_generate_content(prompt, file_id=None, chat_input=None)
208
+ if result and not result.startswith("Error"):
209
+ docx_bytes = save_compliance_as_docx(result, selected_filename)
210
+ compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
211
+ uploaded_documents[compliance_docx_name] = result
212
+ shredded_documents[compliance_docx_name] = docx_bytes
213
+ return result, compliance_docx_name, docx_bytes, None, None
214
+ else:
215
+ return result, None, None, None, None
216
+
217
+ elif action == 'virtual_board':
218
+ return "Virtual board not implemented yet.", None, None, None, None
219
 
220
  elif action == 'proposal':
221
  if not doc_content:
222
  logging.warning("No RFP/SOW/PWS/RFI document selected for proposal action.")
223
+ return "No RFP/SOW/PWS/RFI document selected.", None, None, None, None
224
  rfp_filename = selected_filename
225
  rfp_fileid = uploaded_documents_fileid.get(selected_filename)
226
  if not rfp_fileid and rfp_decoded_bytes is not None:
 
246
  if result and not result.startswith("Error"):
247
  docx_bytes = save_proposal_as_docx(result, rfp_filename)
248
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
249
+ proposals[generated_docx_name] = result
250
+ proposals_fileid[generated_docx_name] = None
251
+ return result, None, None, generated_docx_name, docx_bytes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  else:
253
+ return result, None, None, None, None
254
 
255
  elif action == 'recover':
256
+ return "Recovery not implemented yet.", None, None, None, None
 
 
257
  elif action == 'loe':
258
+ return "LOE estimation not implemented yet.", None, None, None, None
259
+ return "Action not implemented yet.", None, None, None, None
260
+
261
+ def get_documents_list(docdict, shreddedict):
262
+ all_docs = {}
263
+ for filename, text in docdict.items():
264
+ all_docs[filename] = text
265
+ for filename, docx_bytes in shreddedict.items():
266
+ if filename not in all_docs:
267
+ all_docs[filename] = None
268
+ if not all_docs:
269
+ return html.Div("No documents uploaded or generated.", style={"wordWrap": "break-word"})
270
  doc_list = []
271
+ for filename in all_docs:
272
+ if filename.lower().endswith('.docx') and filename in shreddedict:
273
+ b64 = base64.b64encode(shreddedict[filename]).decode('utf-8')
274
+ mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
275
+ else:
276
+ content = docdict.get(filename, "")
277
+ b64 = base64.b64encode(content.encode('utf-8')).decode('utf-8')
278
+ mime = "text/plain"
 
 
 
279
  download_link = html.A(
280
  filename,
281
  href=f"data:{mime};base64,{b64}",
 
286
  doc_list.append(
287
  dbc.ListGroupItem([
288
  download_link,
289
+ dbc.Button("Delete", id={'type': 'delete-doc-btn', 'index': filename, 'group': 'doc'}, size="sm", color="danger", className="float-end ms-2")
290
  ], className="d-flex justify-content-between align-items-center")
291
  )
292
  return dbc.ListGroup(doc_list, flush=True)
293
 
294
+ def get_proposals_list(proposaldict):
295
+ if not proposaldict:
296
+ return html.Div("No proposals uploaded or generated.", style={"wordWrap": "break-word"})
297
  doc_list = []
298
+ for filename in proposaldict:
299
+ file_content = proposaldict[filename]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  try:
301
  docx_bytes = save_proposal_as_docx(file_content, filename)
302
  b64 = base64.b64encode(docx_bytes).decode('utf-8')
303
+ mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
304
  except Exception:
305
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
306
+ mime = "text/plain"
307
  download_link = html.A(
308
  filename,
309
  href=f"data:{mime};base64,{b64}",
 
323
  dbc.Row([
324
  dbc.Col([
325
  dbc.Card([
326
+ dbc.CardHeader(html.H5("Documents")),
327
  dbc.CardBody([
 
 
 
 
 
 
 
 
 
328
  dcc.Upload(
329
  id='upload-document',
330
  children=html.Div([
 
343
  },
344
  multiple=False
345
  ),
346
+ html.Div(id='documents-list', children=get_documents_list(uploaded_documents, shredded_documents)),
347
+ dcc.Dropdown(
348
+ id='select-document-dropdown',
349
+ options=[{'label': fn, 'value': fn} for fn in uploaded_documents.keys()],
350
+ placeholder="Select a document to work with",
351
+ value=next(iter(uploaded_documents), None),
352
+ style={"marginBottom": "10px"}
353
+ ),
354
  ])
355
  ], className="mb-3"),
356
  dbc.Card([
357
+ dbc.CardHeader(html.H5("Proposals")),
358
  dbc.CardBody([
 
 
 
 
 
 
 
 
 
359
  dcc.Upload(
360
  id='upload-proposal',
361
  children=html.Div([
 
373
  'margin': '10px'
374
  },
375
  multiple=False
376
+ ),
377
+ html.Div(id='proposals-list', children=get_proposals_list(proposals)),
378
+ dcc.Dropdown(
379
+ id='select-proposal-dropdown',
380
+ options=[{'label': fn, 'value': fn} for fn in proposals.keys()],
381
+ placeholder="Select a proposal document",
382
+ value=next(iter(proposals), None),
383
+ style={"marginBottom": "10px"}
384
+ ),
385
  ])
386
  ], className="mb-3"),
387
  ], style={'minWidth': '260px', 'width':'30vw','maxWidth':'30vw'}, width=3),
 
416
 
417
  @app.callback(
418
  Output('output-data-upload', 'children'),
419
+ Output('documents-list', 'children'),
420
  Output('select-document-dropdown', 'options'),
421
  Output('select-document-dropdown', 'value'),
422
+ Output('proposals-list', 'children'),
 
423
  Output('select-proposal-dropdown', 'options'),
424
  Output('select-proposal-dropdown', 'value'),
425
  [
 
428
  Input('compliance-action-btn', 'n_clicks'),
429
  Input('upload-document', 'contents'),
430
  State('upload-document', 'filename'),
431
+ Input({'type': 'delete-doc-btn', 'index': ALL, 'group': 'doc'}, 'n_clicks'),
432
  State('select-document-dropdown', 'value'),
433
  Input('upload-proposal', 'contents'),
434
  State('upload-proposal', 'filename'),
435
  Input({'type': 'delete-proposal-btn', 'index': ALL, 'group': 'proposal'}, 'n_clicks'),
436
  State('select-proposal-dropdown', 'value'),
 
 
437
  State('chat-input', 'value'),
438
  Input('cancel-action-btn', 'n_clicks')
439
  ],
 
441
  )
442
  def master_callback(
443
  shred_clicks, proposal_clicks, compliance_clicks,
444
+ rfp_content, rfp_filename, doc_delete_clicks, selected_doc,
445
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
 
446
  chat_input, cancel_clicks
447
  ):
448
  ctx = callback_context
 
454
  except Exception:
455
  return []
456
 
457
+ doc_delete_clicks = safe_get_n_clicks(ctx, 5)
 
 
458
  proposal_delete_clicks = safe_get_n_clicks(ctx, 9)
 
 
459
  uploaded_rfp_decoded_bytes = None
460
 
461
  global gemini_lock
 
464
  output_data_upload = html.Div("[Cancelled by user]\n", style={"wordWrap": "break-word"})
465
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
466
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
467
+ proposals_list = get_proposals_list(proposals)
468
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
469
+ proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
470
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
 
471
  return (
472
  output_data_upload,
473
+ documents_list, doc_options, doc_value,
474
+ proposals_list, proposal_options, proposal_value
 
475
  )
476
 
477
  if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
 
489
  logging.info(f"Document uploaded: {rfp_filename}")
490
  else:
491
  logging.error(f"Failed to decode uploaded document: {rfp_filename}")
 
492
 
493
  if triggered_id == 'upload-proposal' and proposal_content is not None and proposal_filename:
494
  content_type, content_string = proposal_content.split(',')
 
498
  if proposal_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
499
  fileid = upload_to_gemini_file(decoded, proposal_filename)
500
  if text is not None:
501
+ proposals[proposal_filename] = text
502
  if fileid:
503
+ proposals_fileid[proposal_filename] = fileid
504
  logging.info(f"Proposal uploaded: {proposal_filename}")
505
  else:
506
  logging.error(f"Failed to decode uploaded proposal: {proposal_filename}")
 
507
 
508
+ if triggered_id and isinstance(doc_delete_clicks, list):
509
+ for i, n_click in enumerate(doc_delete_clicks):
510
  if n_click:
511
  btn_id = ctx.inputs_list[5][i]['id']
512
  del_filename = btn_id['index']
 
519
  logging.warning(f"Failed to delete Gemini file {del_filename}: {e}")
520
  del uploaded_documents_fileid[del_filename]
521
  logging.info(f"Document deleted: {del_filename}")
522
+ if del_filename in shredded_documents:
523
+ del shredded_documents[del_filename]
524
+ logging.info(f"Shredded doc deleted: {del_filename}")
525
+ if selected_doc == del_filename:
526
+ selected_doc = None
527
+ break
528
 
529
  if triggered_id and isinstance(proposal_delete_clicks, list):
530
  for i, n_click in enumerate(proposal_delete_clicks):
531
  if n_click:
532
  btn_id = ctx.inputs_list[9][i]['id']
533
  del_filename = btn_id['index']
534
+ if del_filename in proposals:
535
+ del proposals[del_filename]
536
+ if del_filename in proposals_fileid:
537
  try:
538
+ genai.delete_file(proposals_fileid[del_filename])
539
  except Exception as e:
540
  logging.warning(f"Failed to delete Gemini proposal file {del_filename}: {e}")
541
+ del proposals_fileid[del_filename]
542
  logging.info(f"Proposal deleted: {del_filename}")
543
+ if selected_proposal == del_filename:
544
+ selected_proposal = None
 
 
 
 
 
 
 
 
 
 
 
 
545
  break
546
 
547
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
548
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
549
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
550
+ proposals_list = get_proposals_list(proposals)
551
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
552
+ proposal_value = selected_proposal if selected_proposal in proposals else (next(iter(proposals), None) if proposals else None)
 
553
 
554
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
555
 
556
+ if triggered_id in ['shred-action-btn', 'compliance-action-btn', 'board-action-btn']:
557
  got_lock = gemini_lock.acquire(blocking=False)
558
  if not got_lock:
559
  output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
560
  return (
561
  output_data_upload,
562
+ documents_list, doc_options, doc_value,
563
+ proposals_list, proposal_options, proposal_value
 
564
  )
565
  try:
566
+ if triggered_id == "shred-action-btn":
567
+ action_name = "shred"
568
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
569
+ output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
570
+ elif triggered_id == "compliance-action-btn":
571
+ action_name = "compliance"
572
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(
573
+ action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, proposal_value
574
+ )
575
+ output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
576
+ elif triggered_id == "board-action-btn":
577
+ action_name = "virtual_board"
578
+ result, generated_filename, generated_docx_bytes, _, _ = process_document(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None)
579
+ output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
580
  finally:
581
  gemini_lock.release()
582
  doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
583
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
584
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
585
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
586
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
587
+ proposals_list = get_proposals_list(proposals)
 
588
  return (
589
  output_data_upload,
590
+ documents_list, doc_options, doc_value,
591
+ proposals_list, proposal_options, proposal_value
 
592
  )
593
 
594
+ if triggered_id in ['proposal-action-btn', 'recover-action-btn', 'loe-action-btn']:
595
+ got_lock = gemini_lock.acquire(blocking=False)
596
+ if not got_lock:
597
+ output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
598
+ return (
599
+ output_data_upload,
600
+ documents_list, doc_options, doc_value,
601
+ proposals_list, proposal_options, proposal_value
602
+ )
603
+ try:
604
+ if triggered_id == "proposal-action-btn":
605
+ action_name = "proposal"
606
+ elif triggered_id == "recover-action-btn":
607
+ action_name = "recover"
608
+ elif triggered_id == "loe-action-btn":
609
+ action_name = "loe"
610
+ else:
611
+ action_name = None
612
+ if action_name:
613
+ result, _, _, generated_filename, generated_docx_bytes = process_document(
614
+ action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, None
615
+ )
616
+ output_data_upload = dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
617
+ finally:
618
+ gemini_lock.release()
619
+ doc_options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()]
620
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
621
+ proposal_options = [{'label': fn, 'value': fn} for fn in proposals.keys()]
622
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
623
+ documents_list = get_documents_list(uploaded_documents, shredded_documents)
624
+ proposals_list = get_proposals_list(proposals)
625
  return (
626
  output_data_upload,
627
+ documents_list, doc_options, doc_value,
628
+ proposals_list, proposal_options, proposal_value
 
629
  )
630
 
631
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
632
+ proposal_value = proposal_value if proposal_value in proposals else (next(iter(proposals), None) if proposals else None)
633
  return (
634
  output_data_upload,
635
+ documents_list, doc_options, doc_value,
636
+ proposals_list, proposal_options, proposal_value
 
637
  )
638
 
639
  if __name__ == '__main__':