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"})
|