Spaces:
Paused
Paused
Update app.py via AI Editor
Browse files
app.py
CHANGED
|
@@ -146,7 +146,7 @@ def save_proposal_as_docx(proposal_text, base_filename):
|
|
| 146 |
memf.seek(0)
|
| 147 |
return memf.read()
|
| 148 |
|
| 149 |
-
def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, cancel_event=None, stream=True):
|
| 150 |
global shredded_document, generated_response, stream_buffer
|
| 151 |
|
| 152 |
logging.info(f"Process document called with action: {action}")
|
|
@@ -277,7 +277,65 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
|
|
| 277 |
return result_holder["text"], None, result_holder["docx_name"], result_holder["text"]
|
| 278 |
|
| 279 |
elif action == 'compliance':
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
elif action == 'recover':
|
| 282 |
return "Recovery not implemented yet.", None, None, None
|
| 283 |
elif action == 'board':
|
|
@@ -549,6 +607,7 @@ app.layout = dbc.Container([
|
|
| 549 |
[
|
| 550 |
Input('shred-action-btn', 'n_clicks'),
|
| 551 |
Input('proposal-action-btn', 'n_clicks'),
|
|
|
|
| 552 |
Input('upload-document', 'contents'),
|
| 553 |
State('upload-document', 'filename'),
|
| 554 |
Input({'type': 'delete-doc-btn', 'index': ALL, 'group': 'rfp'}, 'n_clicks'),
|
|
@@ -573,7 +632,7 @@ app.layout = dbc.Container([
|
|
| 573 |
prevent_initial_call=True
|
| 574 |
)
|
| 575 |
def master_callback(
|
| 576 |
-
shred_clicks, proposal_clicks,
|
| 577 |
rfp_content, rfp_filename, rfp_delete_clicks, selected_doc,
|
| 578 |
proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
|
| 579 |
generated_delete_clicks, selected_generated,
|
|
@@ -598,10 +657,10 @@ def master_callback(
|
|
| 598 |
proposal_store = {'text': None, 'docx_name': None}
|
| 599 |
streaming = False
|
| 600 |
|
| 601 |
-
rfp_delete_clicks = safe_get_n_clicks(ctx,
|
| 602 |
-
proposal_delete_clicks = safe_get_n_clicks(ctx,
|
| 603 |
-
generated_delete_clicks = safe_get_n_clicks(ctx,
|
| 604 |
-
shredded_delete_clicks = safe_get_n_clicks(ctx,
|
| 605 |
|
| 606 |
uploaded_rfp_decoded_bytes = None
|
| 607 |
|
|
@@ -692,7 +751,7 @@ def master_callback(
|
|
| 692 |
if triggered_id and isinstance(rfp_delete_clicks, list):
|
| 693 |
for i, n_click in enumerate(rfp_delete_clicks):
|
| 694 |
if n_click:
|
| 695 |
-
btn_id = ctx.inputs_list[
|
| 696 |
del_filename = btn_id['index']
|
| 697 |
if del_filename in uploaded_documents:
|
| 698 |
del uploaded_documents[del_filename]
|
|
@@ -711,7 +770,7 @@ def master_callback(
|
|
| 711 |
if triggered_id and isinstance(proposal_delete_clicks, list):
|
| 712 |
for i, n_click in enumerate(proposal_delete_clicks):
|
| 713 |
if n_click:
|
| 714 |
-
btn_id = ctx.inputs_list[
|
| 715 |
del_filename = btn_id['index']
|
| 716 |
if del_filename in uploaded_proposals:
|
| 717 |
del uploaded_proposals[del_filename]
|
|
@@ -730,7 +789,7 @@ def master_callback(
|
|
| 730 |
if triggered_id and isinstance(generated_delete_clicks, list):
|
| 731 |
for i, n_click in enumerate(generated_delete_clicks):
|
| 732 |
if n_click:
|
| 733 |
-
btn_id = ctx.inputs_list[
|
| 734 |
del_filename = btn_id['index']
|
| 735 |
if del_filename in generated_documents:
|
| 736 |
del generated_documents[del_filename]
|
|
@@ -743,7 +802,7 @@ def master_callback(
|
|
| 743 |
if triggered_id and isinstance(shredded_delete_clicks, list):
|
| 744 |
for i, n_click in enumerate(shredded_delete_clicks):
|
| 745 |
if n_click:
|
| 746 |
-
btn_id = ctx.inputs_list[
|
| 747 |
del_filename = btn_id['index']
|
| 748 |
if del_filename in shredded_documents:
|
| 749 |
del shredded_documents[del_filename]
|
|
@@ -762,7 +821,7 @@ def master_callback(
|
|
| 762 |
|
| 763 |
output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
|
| 764 |
|
| 765 |
-
if triggered_id in ['shred-action-btn', 'proposal-action-btn']:
|
| 766 |
got_lock = gemini_lock.acquire(blocking=False)
|
| 767 |
if not got_lock:
|
| 768 |
output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
|
|
@@ -778,12 +837,13 @@ def master_callback(
|
|
| 778 |
stream_event.clear()
|
| 779 |
stream_buffer["preview"] = ""
|
| 780 |
streaming = True
|
| 781 |
-
def stream_gemini_thread(action, doc_value, chat_input, rfp_decoded_bytes):
|
| 782 |
try:
|
| 783 |
-
process_document(action, doc_value, chat_input, rfp_decoded_bytes, cancel_event=stream_event, stream=True)
|
| 784 |
finally:
|
| 785 |
gemini_lock.release()
|
| 786 |
-
|
|
|
|
| 787 |
t.daemon = True
|
| 788 |
t.start()
|
| 789 |
output_data_upload = dcc.Markdown("Starting Gemini operation...", style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
|
|
|
|
| 146 |
memf.seek(0)
|
| 147 |
return memf.read()
|
| 148 |
|
| 149 |
+
def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, cancel_event=None, stream=True, selected_generated_filename=None):
|
| 150 |
global shredded_document, generated_response, stream_buffer
|
| 151 |
|
| 152 |
logging.info(f"Process document called with action: {action}")
|
|
|
|
| 277 |
return result_holder["text"], None, result_holder["docx_name"], result_holder["text"]
|
| 278 |
|
| 279 |
elif action == 'compliance':
|
| 280 |
+
# --- COMPLIANCE FUNCTIONALITY ADDED HERE ---
|
| 281 |
+
# Get selected generated doc (proposal) and selected RFP doc
|
| 282 |
+
if not selected_generated_filename or selected_generated_filename not in generated_documents:
|
| 283 |
+
return "No generated document selected for compliance.", None, None, None
|
| 284 |
+
if not selected_filename or selected_filename not in uploaded_documents:
|
| 285 |
+
return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None
|
| 286 |
+
|
| 287 |
+
proposal_docx_bytes = generated_documents[selected_generated_filename]
|
| 288 |
+
rfp_text = uploaded_documents[selected_filename]
|
| 289 |
+
logging.info(f"Compliance check: comparing proposal [{selected_generated_filename}] to RFP [{selected_filename}]")
|
| 290 |
+
|
| 291 |
+
# Decode the proposal docx to text (best-effort, using python-docx)
|
| 292 |
+
proposal_text = ""
|
| 293 |
+
try:
|
| 294 |
+
file_stream = io.BytesIO(proposal_docx_bytes)
|
| 295 |
+
doc = Document(file_stream)
|
| 296 |
+
proposal_text = "\n".join([para.text for para in doc.paragraphs])
|
| 297 |
+
if not proposal_text.strip():
|
| 298 |
+
proposal_text = "[Unable to extract text from proposal docx]"
|
| 299 |
+
except Exception as e:
|
| 300 |
+
proposal_text = "[Error decoding proposal docx: {}]".format(str(e))
|
| 301 |
+
logging.error("Error extracting text from proposal docx: %s", e)
|
| 302 |
+
|
| 303 |
+
prompt = (
|
| 304 |
+
"You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
|
| 305 |
+
"Compare the proposal to the RFP requirements and win themes. "
|
| 306 |
+
"For each PWS section/requirement, determine if the proposal fully, partially, or does not address the requirement and win theme. "
|
| 307 |
+
"For each, give a finding and a recommendation to recover for compliance or to strengthen the response. "
|
| 308 |
+
"Return ONLY a markdown table (NO comments, NO intro, NO outro, NO summary) with the following columns: "
|
| 309 |
+
"| PWS Section Number | Section Name | Requirement Description | Finding | Recommendation to Recover for Compliance |. "
|
| 310 |
+
"Be concise and focus on actionable compliance gaps. Only the table, nothing else.\n\n"
|
| 311 |
+
f"---\nRFP/SOW/PWS/RFI ({selected_filename}):\n{rfp_text}\n"
|
| 312 |
+
"---\nGenerated Proposal Document:\n"
|
| 313 |
+
f"{proposal_text}\n"
|
| 314 |
+
)
|
| 315 |
+
result_holder = {"text": None, "docx_name": None}
|
| 316 |
+
|
| 317 |
+
def thread_compliance():
|
| 318 |
+
global stream_buffer
|
| 319 |
+
stream_buffer["preview"] = ""
|
| 320 |
+
try:
|
| 321 |
+
logging.info("Starting Gemini generate_content for compliance check.")
|
| 322 |
+
for partial in gemini_generate_content_stream(prompt, file_id=None, chat_input=None, cancel_event=cancel_event):
|
| 323 |
+
stream_buffer["preview"] = partial
|
| 324 |
+
if cancel_event is not None and cancel_event.is_set():
|
| 325 |
+
break
|
| 326 |
+
result = stream_buffer["preview"]
|
| 327 |
+
result_holder["text"] = result
|
| 328 |
+
logging.info("Compliance check completed.")
|
| 329 |
+
except Exception as e:
|
| 330 |
+
logging.error("Error during compliance check: %s", e)
|
| 331 |
+
result_holder["text"] = f"Error during compliance check: {e}"
|
| 332 |
+
|
| 333 |
+
stream_buffer["preview"] = ""
|
| 334 |
+
t = Thread(target=thread_compliance)
|
| 335 |
+
t.start()
|
| 336 |
+
t.join()
|
| 337 |
+
return result_holder["text"], None, None, result_holder["text"]
|
| 338 |
+
|
| 339 |
elif action == 'recover':
|
| 340 |
return "Recovery not implemented yet.", None, None, None
|
| 341 |
elif action == 'board':
|
|
|
|
| 607 |
[
|
| 608 |
Input('shred-action-btn', 'n_clicks'),
|
| 609 |
Input('proposal-action-btn', 'n_clicks'),
|
| 610 |
+
Input('compliance-action-btn', 'n_clicks'),
|
| 611 |
Input('upload-document', 'contents'),
|
| 612 |
State('upload-document', 'filename'),
|
| 613 |
Input({'type': 'delete-doc-btn', 'index': ALL, 'group': 'rfp'}, 'n_clicks'),
|
|
|
|
| 632 |
prevent_initial_call=True
|
| 633 |
)
|
| 634 |
def master_callback(
|
| 635 |
+
shred_clicks, proposal_clicks, compliance_clicks,
|
| 636 |
rfp_content, rfp_filename, rfp_delete_clicks, selected_doc,
|
| 637 |
proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
|
| 638 |
generated_delete_clicks, selected_generated,
|
|
|
|
| 657 |
proposal_store = {'text': None, 'docx_name': None}
|
| 658 |
streaming = False
|
| 659 |
|
| 660 |
+
rfp_delete_clicks = safe_get_n_clicks(ctx, 5)
|
| 661 |
+
proposal_delete_clicks = safe_get_n_clicks(ctx, 9)
|
| 662 |
+
generated_delete_clicks = safe_get_n_clicks(ctx, 11)
|
| 663 |
+
shredded_delete_clicks = safe_get_n_clicks(ctx, 13)
|
| 664 |
|
| 665 |
uploaded_rfp_decoded_bytes = None
|
| 666 |
|
|
|
|
| 751 |
if triggered_id and isinstance(rfp_delete_clicks, list):
|
| 752 |
for i, n_click in enumerate(rfp_delete_clicks):
|
| 753 |
if n_click:
|
| 754 |
+
btn_id = ctx.inputs_list[5][i]['id']
|
| 755 |
del_filename = btn_id['index']
|
| 756 |
if del_filename in uploaded_documents:
|
| 757 |
del uploaded_documents[del_filename]
|
|
|
|
| 770 |
if triggered_id and isinstance(proposal_delete_clicks, list):
|
| 771 |
for i, n_click in enumerate(proposal_delete_clicks):
|
| 772 |
if n_click:
|
| 773 |
+
btn_id = ctx.inputs_list[9][i]['id']
|
| 774 |
del_filename = btn_id['index']
|
| 775 |
if del_filename in uploaded_proposals:
|
| 776 |
del uploaded_proposals[del_filename]
|
|
|
|
| 789 |
if triggered_id and isinstance(generated_delete_clicks, list):
|
| 790 |
for i, n_click in enumerate(generated_delete_clicks):
|
| 791 |
if n_click:
|
| 792 |
+
btn_id = ctx.inputs_list[11][i]['id']
|
| 793 |
del_filename = btn_id['index']
|
| 794 |
if del_filename in generated_documents:
|
| 795 |
del generated_documents[del_filename]
|
|
|
|
| 802 |
if triggered_id and isinstance(shredded_delete_clicks, list):
|
| 803 |
for i, n_click in enumerate(shredded_delete_clicks):
|
| 804 |
if n_click:
|
| 805 |
+
btn_id = ctx.inputs_list[13][i]['id']
|
| 806 |
del_filename = btn_id['index']
|
| 807 |
if del_filename in shredded_documents:
|
| 808 |
del shredded_documents[del_filename]
|
|
|
|
| 821 |
|
| 822 |
output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
|
| 823 |
|
| 824 |
+
if triggered_id in ['shred-action-btn', 'proposal-action-btn', 'compliance-action-btn']:
|
| 825 |
got_lock = gemini_lock.acquire(blocking=False)
|
| 826 |
if not got_lock:
|
| 827 |
output_data_upload = html.Div("Another Gemini operation is in progress. Please wait or cancel.", style={"wordWrap": "break-word"})
|
|
|
|
| 837 |
stream_event.clear()
|
| 838 |
stream_buffer["preview"] = ""
|
| 839 |
streaming = True
|
| 840 |
+
def stream_gemini_thread(action, doc_value, chat_input, rfp_decoded_bytes, selected_generated_filename):
|
| 841 |
try:
|
| 842 |
+
process_document(action, doc_value, chat_input, rfp_decoded_bytes, cancel_event=stream_event, stream=True, selected_generated_filename=selected_generated_filename)
|
| 843 |
finally:
|
| 844 |
gemini_lock.release()
|
| 845 |
+
action_name = "shred" if triggered_id=="shred-action-btn" else ("proposal" if triggered_id=="proposal-action-btn" else "compliance")
|
| 846 |
+
t = Thread(target=stream_gemini_thread, args=(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, generated_doc_value))
|
| 847 |
t.daemon = True
|
| 848 |
t.start()
|
| 849 |
output_data_upload = dcc.Markdown("Starting Gemini operation...", style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
|