Spaces:
Paused
Paused
Update app.py via AI Editor
Browse files
app.py
CHANGED
|
@@ -132,7 +132,7 @@ def save_proposal_as_docx(proposal_text, base_filename):
|
|
| 132 |
memf.seek(0)
|
| 133 |
return memf.read()
|
| 134 |
|
| 135 |
-
def process_document(action, selected_filename=None, chat_input=None):
|
| 136 |
global shredded_document, generated_response
|
| 137 |
logging.info(f"Process document called with action: {action}")
|
| 138 |
|
|
@@ -195,6 +195,16 @@ def process_document(action, selected_filename=None, chat_input=None):
|
|
| 195 |
return "No RFP/SOW/PWS/RFI document selected.", None, None, None
|
| 196 |
rfp_filename = selected_filename
|
| 197 |
rfp_fileid = uploaded_documents_fileid.get(selected_filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
prompt = (
|
| 199 |
"Respond to the following RFP/SOW/PWS/RFI by creating a highly detailed proposal response that follows each section and subsection header. "
|
| 200 |
"The response to each section and subsection will be compliant and compelling, focusing on describing the approach, and how the labor category uses a specific industry standard process in a workflow described in steps, and how technology is used. "
|
|
@@ -207,9 +217,12 @@ def process_document(action, selected_filename=None, chat_input=None):
|
|
| 207 |
logging.info(f"Sending proposal prompt to Gemini. RFP: {rfp_filename}")
|
| 208 |
result_holder = {"text": None, "docx_name": None}
|
| 209 |
def thread_proposal():
|
|
|
|
|
|
|
| 210 |
try:
|
| 211 |
logging.info("Connecting to Gemini for proposal.")
|
| 212 |
response = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
|
|
|
|
| 213 |
logging.info("Received proposal results from Gemini.")
|
| 214 |
docx_bytes = save_proposal_as_docx(response, rfp_filename)
|
| 215 |
generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
|
|
@@ -217,8 +230,9 @@ def process_document(action, selected_filename=None, chat_input=None):
|
|
| 217 |
result_holder["text"] = response
|
| 218 |
result_holder["docx_name"] = generated_docx_name
|
| 219 |
except Exception as e:
|
|
|
|
| 220 |
logging.error("Error during Gemini proposal request: %s", e)
|
| 221 |
-
result_holder["text"] =
|
| 222 |
t = Thread(target=thread_proposal)
|
| 223 |
t.start()
|
| 224 |
t.join()
|
|
@@ -239,9 +253,32 @@ def get_uploaded_doc_list(docdict):
|
|
| 239 |
return html.Div("No documents uploaded.", style={"wordWrap": "break-word"})
|
| 240 |
doc_list = []
|
| 241 |
for filename in docdict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
doc_list.append(
|
| 243 |
dbc.ListGroupItem([
|
| 244 |
-
|
| 245 |
dbc.Button("Delete", id={'type': 'delete-doc-btn', 'index': filename, 'group': 'rfp'}, size="sm", color="danger", className="float-end ms-2")
|
| 246 |
], className="d-flex justify-content-between align-items-center")
|
| 247 |
)
|
|
@@ -254,15 +291,14 @@ def get_shredded_doc_list(shreddict):
|
|
| 254 |
for filename in shreddict:
|
| 255 |
b64 = base64.b64encode(shreddict[filename]).decode('utf-8')
|
| 256 |
download_link = html.A(
|
| 257 |
-
|
| 258 |
href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
|
| 259 |
download=filename,
|
| 260 |
target="_blank",
|
| 261 |
-
style={"wordWrap": "break-word", "marginRight": "10px"}
|
| 262 |
)
|
| 263 |
doc_list.append(
|
| 264 |
dbc.ListGroupItem([
|
| 265 |
-
html.Span(filename, style={"wordWrap": "break-word", "marginRight": "10px"}),
|
| 266 |
download_link,
|
| 267 |
dbc.Button("Delete", id={'type': 'delete-shredded-btn', 'index': filename, 'group': 'shredded'}, size="sm", color="danger", className="float-end ms-2")
|
| 268 |
], className="d-flex justify-content-between align-items-center")
|
|
@@ -274,9 +310,28 @@ def get_uploaded_proposal_list(docdict):
|
|
| 274 |
return html.Div("No proposal documents uploaded.", style={"wordWrap": "break-word"})
|
| 275 |
doc_list = []
|
| 276 |
for filename in docdict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
doc_list.append(
|
| 278 |
dbc.ListGroupItem([
|
| 279 |
-
|
| 280 |
dbc.Button("Delete", id={'type': 'delete-proposal-btn', 'index': filename, 'group': 'proposal'}, size="sm", color="danger", className="float-end ms-2")
|
| 281 |
], className="d-flex justify-content-between align-items-center")
|
| 282 |
)
|
|
@@ -287,9 +342,18 @@ def get_generated_doc_list(docdict):
|
|
| 287 |
return html.Div("No generated documents yet.", style={"wordWrap": "break-word"})
|
| 288 |
doc_list = []
|
| 289 |
for filename in docdict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
doc_list.append(
|
| 291 |
dbc.ListGroupItem([
|
| 292 |
-
|
| 293 |
dbc.Button("Delete", id={'type': 'delete-generated-btn', 'index': filename, 'group': 'generated'}, size="sm", color="danger", className="float-end ms-2")
|
| 294 |
], className="d-flex justify-content-between align-items-center")
|
| 295 |
)
|
|
@@ -494,9 +558,12 @@ def master_callback(
|
|
| 494 |
generated_delete_clicks = safe_get_n_clicks(ctx, 10)
|
| 495 |
shredded_delete_clicks = safe_get_n_clicks(ctx, 12)
|
| 496 |
|
|
|
|
|
|
|
| 497 |
if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
|
| 498 |
content_type, content_string = rfp_content.split(',')
|
| 499 |
decoded = base64.b64decode(content_string)
|
|
|
|
| 500 |
text = decode_document(decoded)
|
| 501 |
fileid = None
|
| 502 |
if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
|
|
@@ -600,7 +667,7 @@ def master_callback(
|
|
| 600 |
output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
|
| 601 |
|
| 602 |
if triggered_id == 'shred-action-btn':
|
| 603 |
-
output_data_upload = dcc.Loading(type="default", children=html.Div("Shredding document...", style={"wordWrap": "break-word"}))
|
| 604 |
shred_text, _, shredded_docx_name, shredded_text = process_document('shred', doc_value, chat_input)
|
| 605 |
shred_store = {'text': shred_text, 'docx_name': shredded_docx_name}
|
| 606 |
|
|
@@ -620,8 +687,16 @@ def master_callback(
|
|
| 620 |
)
|
| 621 |
|
| 622 |
if triggered_id == 'proposal-action-btn':
|
| 623 |
-
output_data_upload = dcc.Loading(type="default", children=html.Div("Generating proposal...", style={"wordWrap": "break-word"}))
|
| 624 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
proposal_store = {'text': proposal_text, 'docx_name': proposal_docx_name}
|
| 626 |
new_generated_doc_value = generated_doc_value
|
| 627 |
if proposal_docx_name:
|
|
|
|
| 132 |
memf.seek(0)
|
| 133 |
return memf.read()
|
| 134 |
|
| 135 |
+
def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None):
|
| 136 |
global shredded_document, generated_response
|
| 137 |
logging.info(f"Process document called with action: {action}")
|
| 138 |
|
|
|
|
| 195 |
return "No RFP/SOW/PWS/RFI document selected.", None, None, None
|
| 196 |
rfp_filename = selected_filename
|
| 197 |
rfp_fileid = uploaded_documents_fileid.get(selected_filename)
|
| 198 |
+
# Upload file to Gemini if not already uploaded (if rfp_decoded_bytes provided and no fileid)
|
| 199 |
+
if not rfp_fileid and rfp_decoded_bytes is not None:
|
| 200 |
+
try:
|
| 201 |
+
fileid = upload_to_gemini_file(rfp_decoded_bytes, rfp_filename)
|
| 202 |
+
if fileid:
|
| 203 |
+
uploaded_documents_fileid[rfp_filename] = fileid
|
| 204 |
+
rfp_fileid = fileid
|
| 205 |
+
logging.info(f"RFP file {rfp_filename} uploaded to Gemini for proposal.")
|
| 206 |
+
except Exception as e:
|
| 207 |
+
logging.error(f"Failed to upload RFP file {rfp_filename} for proposal: {e}")
|
| 208 |
prompt = (
|
| 209 |
"Respond to the following RFP/SOW/PWS/RFI by creating a highly detailed proposal response that follows each section and subsection header. "
|
| 210 |
"The response to each section and subsection will be compliant and compelling, focusing on describing the approach, and how the labor category uses a specific industry standard process in a workflow described in steps, and how technology is used. "
|
|
|
|
| 217 |
logging.info(f"Sending proposal prompt to Gemini. RFP: {rfp_filename}")
|
| 218 |
result_holder = {"text": None, "docx_name": None}
|
| 219 |
def thread_proposal():
|
| 220 |
+
global generated_response
|
| 221 |
+
generated_response = ""
|
| 222 |
try:
|
| 223 |
logging.info("Connecting to Gemini for proposal.")
|
| 224 |
response = gemini_generate_content(prompt, file_id=rfp_fileid, chat_input=chat_input)
|
| 225 |
+
generated_response = response
|
| 226 |
logging.info("Received proposal results from Gemini.")
|
| 227 |
docx_bytes = save_proposal_as_docx(response, rfp_filename)
|
| 228 |
generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
|
|
|
|
| 230 |
result_holder["text"] = response
|
| 231 |
result_holder["docx_name"] = generated_docx_name
|
| 232 |
except Exception as e:
|
| 233 |
+
generated_response = f"Error during Gemini completion: {e}"
|
| 234 |
logging.error("Error during Gemini proposal request: %s", e)
|
| 235 |
+
result_holder["text"] = generated_response
|
| 236 |
t = Thread(target=thread_proposal)
|
| 237 |
t.start()
|
| 238 |
t.join()
|
|
|
|
| 253 |
return html.Div("No documents uploaded.", style={"wordWrap": "break-word"})
|
| 254 |
doc_list = []
|
| 255 |
for filename in docdict:
|
| 256 |
+
file_content = docdict[filename]
|
| 257 |
+
# Always encode as docx for download, but if not docx, encode as txt
|
| 258 |
+
if filename.lower().endswith('.docx'):
|
| 259 |
+
mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 260 |
+
b64 = None
|
| 261 |
+
# docx as text, not as file: re-encode to docx for download
|
| 262 |
+
try:
|
| 263 |
+
# If it's a docx, but only text available, save as docx
|
| 264 |
+
docx_bytes = save_shredded_as_docx(file_content, filename)
|
| 265 |
+
b64 = base64.b64encode(docx_bytes).decode('utf-8')
|
| 266 |
+
except Exception:
|
| 267 |
+
b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
|
| 268 |
+
else:
|
| 269 |
+
mime = "text/plain"
|
| 270 |
+
# Always encode as utf-8 text for download
|
| 271 |
+
b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
|
| 272 |
+
download_link = html.A(
|
| 273 |
+
filename,
|
| 274 |
+
href=f"data:{mime};base64,{b64}",
|
| 275 |
+
download=filename,
|
| 276 |
+
target="_blank",
|
| 277 |
+
style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
|
| 278 |
+
)
|
| 279 |
doc_list.append(
|
| 280 |
dbc.ListGroupItem([
|
| 281 |
+
download_link,
|
| 282 |
dbc.Button("Delete", id={'type': 'delete-doc-btn', 'index': filename, 'group': 'rfp'}, size="sm", color="danger", className="float-end ms-2")
|
| 283 |
], className="d-flex justify-content-between align-items-center")
|
| 284 |
)
|
|
|
|
| 291 |
for filename in shreddict:
|
| 292 |
b64 = base64.b64encode(shreddict[filename]).decode('utf-8')
|
| 293 |
download_link = html.A(
|
| 294 |
+
filename,
|
| 295 |
href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
|
| 296 |
download=filename,
|
| 297 |
target="_blank",
|
| 298 |
+
style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
|
| 299 |
)
|
| 300 |
doc_list.append(
|
| 301 |
dbc.ListGroupItem([
|
|
|
|
| 302 |
download_link,
|
| 303 |
dbc.Button("Delete", id={'type': 'delete-shredded-btn', 'index': filename, 'group': 'shredded'}, size="sm", color="danger", className="float-end ms-2")
|
| 304 |
], className="d-flex justify-content-between align-items-center")
|
|
|
|
| 310 |
return html.Div("No proposal documents uploaded.", style={"wordWrap": "break-word"})
|
| 311 |
doc_list = []
|
| 312 |
for filename in docdict:
|
| 313 |
+
file_content = docdict[filename]
|
| 314 |
+
if filename.lower().endswith('.docx'):
|
| 315 |
+
mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 316 |
+
b64 = None
|
| 317 |
+
try:
|
| 318 |
+
docx_bytes = save_proposal_as_docx(file_content, filename)
|
| 319 |
+
b64 = base64.b64encode(docx_bytes).decode('utf-8')
|
| 320 |
+
except Exception:
|
| 321 |
+
b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
|
| 322 |
+
else:
|
| 323 |
+
mime = "text/plain"
|
| 324 |
+
b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
|
| 325 |
+
download_link = html.A(
|
| 326 |
+
filename,
|
| 327 |
+
href=f"data:{mime};base64,{b64}",
|
| 328 |
+
download=filename,
|
| 329 |
+
target="_blank",
|
| 330 |
+
style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
|
| 331 |
+
)
|
| 332 |
doc_list.append(
|
| 333 |
dbc.ListGroupItem([
|
| 334 |
+
download_link,
|
| 335 |
dbc.Button("Delete", id={'type': 'delete-proposal-btn', 'index': filename, 'group': 'proposal'}, size="sm", color="danger", className="float-end ms-2")
|
| 336 |
], className="d-flex justify-content-between align-items-center")
|
| 337 |
)
|
|
|
|
| 342 |
return html.Div("No generated documents yet.", style={"wordWrap": "break-word"})
|
| 343 |
doc_list = []
|
| 344 |
for filename in docdict:
|
| 345 |
+
docx_bytes = docdict[filename]
|
| 346 |
+
b64 = base64.b64encode(docx_bytes).decode('utf-8')
|
| 347 |
+
download_link = html.A(
|
| 348 |
+
filename,
|
| 349 |
+
href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
|
| 350 |
+
download=filename,
|
| 351 |
+
target="_blank",
|
| 352 |
+
style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
|
| 353 |
+
)
|
| 354 |
doc_list.append(
|
| 355 |
dbc.ListGroupItem([
|
| 356 |
+
download_link,
|
| 357 |
dbc.Button("Delete", id={'type': 'delete-generated-btn', 'index': filename, 'group': 'generated'}, size="sm", color="danger", className="float-end ms-2")
|
| 358 |
], className="d-flex justify-content-between align-items-center")
|
| 359 |
)
|
|
|
|
| 558 |
generated_delete_clicks = safe_get_n_clicks(ctx, 10)
|
| 559 |
shredded_delete_clicks = safe_get_n_clicks(ctx, 12)
|
| 560 |
|
| 561 |
+
uploaded_rfp_decoded_bytes = None
|
| 562 |
+
|
| 563 |
if triggered_id == 'upload-document' and rfp_content is not None and rfp_filename:
|
| 564 |
content_type, content_string = rfp_content.split(',')
|
| 565 |
decoded = base64.b64decode(content_string)
|
| 566 |
+
uploaded_rfp_decoded_bytes = decoded
|
| 567 |
text = decode_document(decoded)
|
| 568 |
fileid = None
|
| 569 |
if rfp_filename.lower().endswith(('.pdf', '.docx', '.xlsx', '.xls')):
|
|
|
|
| 667 |
output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
|
| 668 |
|
| 669 |
if triggered_id == 'shred-action-btn':
|
| 670 |
+
output_data_upload = dcc.Loading(type="default", children=html.Div("Shredding document...", style={"wordWrap": "break-word"}), style={"textAlign": "center"})
|
| 671 |
shred_text, _, shredded_docx_name, shredded_text = process_document('shred', doc_value, chat_input)
|
| 672 |
shred_store = {'text': shred_text, 'docx_name': shredded_docx_name}
|
| 673 |
|
|
|
|
| 687 |
)
|
| 688 |
|
| 689 |
if triggered_id == 'proposal-action-btn':
|
| 690 |
+
output_data_upload = dcc.Loading(type="default", children=html.Div("Generating proposal...", style={"wordWrap": "break-word"}), style={"textAlign": "center"})
|
| 691 |
+
# get decoded bytes for the selected RFP doc for upload if needed
|
| 692 |
+
rfp_decoded_bytes = None
|
| 693 |
+
if doc_value and doc_value in uploaded_documents:
|
| 694 |
+
# Try to get from upload-document if that was just uploaded
|
| 695 |
+
if rfp_content is not None and rfp_filename == doc_value:
|
| 696 |
+
content_type, content_string = rfp_content.split(',')
|
| 697 |
+
rfp_decoded_bytes = base64.b64decode(content_string)
|
| 698 |
+
# otherwise, not available, unless user re-uploads
|
| 699 |
+
proposal_text, _, proposal_docx_name, proposal_text_preview = process_document('proposal', doc_value, chat_input, rfp_decoded_bytes)
|
| 700 |
proposal_store = {'text': proposal_text, 'docx_name': proposal_docx_name}
|
| 701 |
new_generated_doc_value = generated_doc_value
|
| 702 |
if proposal_docx_name:
|