Spaces:
Paused
Paused
import os | |
import base64 | |
import io | |
import dash | |
from dash import dcc, html, Input, Output, State, callback_context | |
import dash_bootstrap_components as dbc | |
import pandas as pd | |
import anthropic | |
from threading import Thread | |
import logging | |
logging.basicConfig( | |
level=logging.INFO, | |
format='[%(asctime)s] %(levelname)s - %(message)s' | |
) | |
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) | |
server = app.server | |
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY", "") | |
anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_KEY) | |
CLAUDE3_SONNET_MODEL = "claude-3-7-sonnet-20250219" | |
CLAUDE3_MAX_CONTEXT_TOKENS = 200_000 | |
CLAUDE3_MAX_OUTPUT_TOKENS = 64_000 | |
uploaded_documents = {} # filename: content | |
shredded_document = None | |
generated_response = None | |
def decode_document(decoded_bytes): | |
try: | |
content = decoded_bytes.decode('utf-8') | |
logging.info("Document decoded as UTF-8.") | |
return content | |
except UnicodeDecodeError as e_utf8: | |
try: | |
content = decoded_bytes.decode('latin-1') | |
logging.warning("Document decoded as Latin-1 due to utf-8 decode error: %s", e_utf8) | |
return content | |
except Exception as e: | |
logging.error("Document decode failed for both utf-8 and latin-1: %s", e) | |
return None | |
def anthropic_stream_generate(prompt): | |
stream_result = [] | |
try: | |
with anthropic_client.messages.create( | |
model=CLAUDE3_SONNET_MODEL, | |
max_tokens=CLAUDE3_MAX_OUTPUT_TOKENS, | |
messages=[{"role": "user", "content": prompt}], | |
stream=True | |
) as stream: | |
for event in stream: | |
if event.type == "content_block_delta": | |
piece = event.delta.text | |
stream_result.append(piece) | |
logging.debug(f"Streaming piece: {piece}") | |
return ''.join(stream_result) | |
except Exception as e: | |
logging.error("Error during anthropic streaming request: %s", e) | |
return f"Error during streaming: {e}" | |
def process_document(action, selected_filename=None, chat_input=None): | |
global shredded_document, generated_response | |
logging.info(f"Process document called with action: {action}") | |
doc_content = None | |
if selected_filename and selected_filename in uploaded_documents: | |
doc_content = uploaded_documents[selected_filename] | |
elif uploaded_documents: | |
doc_content = next(iter(uploaded_documents.values())) | |
selected_filename = next(iter(uploaded_documents.keys())) | |
else: | |
doc_content = None | |
if action == 'shred': | |
if not doc_content: | |
logging.warning("No uploaded document found for shredding.") | |
return "No document uploaded." | |
prompt = ( | |
"Analyze the following RFP/PWS/SOW/RFI and generate a requirements spreadsheet. " | |
"Identify requirements by action words like 'shall', 'will', 'perform', etc. Organize by PWS section and requirement. " | |
"Do not write as if responding to the proposal.\n" | |
) | |
if chat_input: | |
prompt += f"User additional instructions: {chat_input}\n" | |
prompt += f"\nFile Name: {selected_filename}\n\n{doc_content}" | |
def thread_shred(): | |
global shredded_document | |
shredded_document = "" | |
try: | |
logging.info("Starting streaming shredding operation with Anthropics.") | |
result = anthropic_stream_generate(prompt) | |
shredded_document = result | |
logging.info("Document shredded successfully.") | |
except Exception as e: | |
shredded_document = f"Error during shredding: {e}" | |
logging.error("Error in thread_shred: %s", e) | |
shredded_document = "Shredding in progress..." | |
t = Thread(target=thread_shred) | |
t.start() | |
t.join() | |
return shredded_document | |
elif action == 'generate': | |
if not shredded_document: | |
logging.warning("No shredded document found when generating response.") | |
return "Shredded document not available." | |
prompt = ( | |
"Create a highly detailed proposal response based on the following PWS requirements. " | |
"Be compliant and compelling. Focus on describing the approach, steps, workflow, people, processes, and technology. " | |
"Refer to research that validates the approach and cite sources with measurable outcomes.\n" | |
) | |
if chat_input: | |
prompt += f"User additional instructions: {chat_input}\n" | |
prompt += f"\nFile Name: {selected_filename}\n\n{shredded_document}" | |
def thread_generate(): | |
global generated_response | |
generated_response = "" | |
try: | |
logging.info("Starting streaming generation operation with Anthropics.") | |
result = anthropic_stream_generate(prompt) | |
generated_response = result | |
logging.info("Proposal response generated successfully.") | |
except Exception as e: | |
generated_response = f"Error during generation: {e}" | |
logging.error("Error in thread_generate: %s", e) | |
generated_response = "Generating response..." | |
t = Thread(target=thread_generate) | |
t.start() | |
t.join() | |
return generated_response | |
elif action == 'compliance': | |
return "Compliance checking not implemented yet." | |
elif action == 'recover': | |
return "Recovery not implemented yet." | |
elif action == 'board': | |
return "Virtual board not implemented yet." | |
elif action == 'loe': | |
return "LOE estimation not implemented yet." | |
return "Action not implemented yet." | |
def get_uploaded_doc_list(): | |
if not uploaded_documents: | |
return html.Div("No documents uploaded.", style={"wordWrap": "break-word"}) | |
doc_list = [] | |
for filename in uploaded_documents: | |
doc_list.append( | |
dbc.ListGroupItem([ | |
html.Span(filename, style={"wordWrap": "break-word"}), | |
dbc.Button("Delete", id={'type': 'delete-doc-btn', 'index': filename}, size="sm", color="danger", className="float-end ms-2") | |
], className="d-flex justify-content-between align-items-center") | |
) | |
return dbc.ListGroup(doc_list, flush=True) | |
app.layout = dbc.Container([ | |
dbc.Row([ | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardHeader(html.H3("Navigation")), | |
dbc.CardBody([ | |
dbc.Button("Shred RFP/PWS/SOW/RFI", id="shred-btn", className="mb-2 w-100 btn-primary"), | |
dbc.Button("Generate Proposal Response", id="generate-btn", className="mb-2 w-100 btn-secondary"), | |
dbc.Button("Check Compliance", id="compliance-btn", className="mb-2 w-100 btn-tertiary"), | |
dbc.Button("Recover Document", id="recover-btn", className="mb-2 w-100 btn-tertiary"), | |
dbc.Button("Virtual Board", id="board-btn", className="mb-2 w-100 btn-tertiary"), | |
dbc.Button("Estimate LOE", id="loe-btn", className="mb-2 w-100 btn-tertiary"), | |
]) | |
], className="mb-3"), | |
dbc.Card([ | |
dbc.CardHeader(html.H5("Uploaded Documents")), | |
dbc.CardBody([ | |
html.Div(id='uploaded-doc-list') | |
]) | |
]) | |
], width=3, style={'minWidth': '260px'}), | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardHeader(html.H2("RFP Proposal Assistant", style={'wordWrap': 'break-word'})), | |
dbc.CardBody([ | |
dbc.Form([ | |
dbc.Textarea(id="chat-input", placeholder="Enter additional instructions...", style={"width":"100%", "wordWrap": "break-word"}, className="mb-2"), | |
]), | |
html.Div([ | |
dbc.Button("Shred", id="shred-action-btn", className="mr-2 btn-primary"), | |
dbc.Button("Generate", id="generate-action-btn", className="mr-2 btn-secondary"), | |
dbc.Button("Check Compliance", id="compliance-action-btn", className="mr-2 btn-tertiary"), | |
dbc.Button("Recover", id="recover-action-btn", className="mr-2 btn-tertiary"), | |
dbc.Button("Virtual Board", id="board-action-btn", className="mr-2 btn-tertiary"), | |
dbc.Button("LOE", id="loe-action-btn", className="btn-tertiary"), | |
], className="mt-3 mb-3"), | |
dcc.Dropdown( | |
id='select-document-dropdown', | |
options=[{'label': fn, 'value': fn} for fn in uploaded_documents.keys()], | |
placeholder="Select a document to work with", | |
value=next(iter(uploaded_documents), None), | |
style={"marginBottom": "10px"} | |
), | |
dcc.Upload( | |
id='upload-document', | |
children=html.Div([ | |
'Drag and Drop or ', | |
html.A('Select Files') | |
]), | |
style={ | |
'width': '100%', | |
'height': '60px', | |
'lineHeight': '60px', | |
'borderWidth': '1px', | |
'borderStyle': 'dashed', | |
'borderRadius': '5px', | |
'textAlign': 'center', | |
'margin': '10px' | |
}, | |
multiple=False | |
), | |
html.Div(id='output-document-upload'), | |
dcc.Loading( | |
id="loading", | |
type="default", | |
children=html.Div(id="output-data-upload"), | |
style={"textAlign": "center"} | |
) | |
]) | |
], style={'backgroundColor': 'white'}) | |
], width=9) | |
], style={'marginTop':'20px'}) | |
], fluid=True) | |
def update_uploaded_docs(content, filename, delete_clicks, children, selected_doc): | |
ctx = callback_context | |
triggered = ctx.triggered | |
changed_id = "" | |
if triggered: | |
changed_id = triggered[0]['prop_id'].split('.')[0] | |
# Handle upload | |
if content is not None and filename: | |
content_type, content_string = content.split(',') | |
decoded = base64.b64decode(content_string) | |
text = decode_document(decoded) | |
if text is not None: | |
uploaded_documents[filename] = text | |
logging.info(f"Document uploaded: {filename}") | |
else: | |
logging.error(f"Failed to decode uploaded document: {filename}") | |
# Handle delete | |
if delete_clicks: | |
for i, n_click in enumerate(delete_clicks): | |
if n_click: | |
btn_id = ctx.inputs_list[2][i]['id'] | |
del_filename = btn_id['index'] | |
if del_filename in uploaded_documents: | |
del uploaded_documents[del_filename] | |
logging.info(f"Document deleted: {del_filename}") | |
if selected_doc == del_filename: | |
selected_doc = next(iter(uploaded_documents), None) | |
break | |
options = [{'label': fn, 'value': fn} for fn in uploaded_documents.keys()] | |
value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None) | |
return get_uploaded_doc_list(), options, value | |
def handle_upload(content, filename): | |
logging.info("Upload callback triggered (output-document-upload).") | |
if content is not None and filename: | |
content_type, content_string = content.split(',') | |
decoded = base64.b64decode(content_string) | |
text = decode_document(decoded) | |
if text is None: | |
return html.Div("Error: Could not decode document. Please upload a valid text file.", style={"wordWrap": "break-word"}) | |
else: | |
return html.Div(f"Document '{filename}' uploaded successfully.", style={"wordWrap": "break-word"}) | |
return "" | |
def handle_actions(shred_clicks, generate_clicks, compliance_clicks, recover_clicks, board_clicks, loe_clicks, chat_input, selected_filename): | |
ctx = callback_context | |
if not ctx.triggered: | |
logging.info("No action triggered yet.") | |
return html.Div("No action taken yet.", style={"wordWrap": "break-word"}) | |
button_id = ctx.triggered[0]['prop_id'].split('.')[0] | |
logging.info(f"Button pressed: {button_id}") | |
result = "" | |
if button_id == 'shred-action-btn': | |
result = process_document('shred', selected_filename, chat_input) | |
elif button_id == 'generate-action-btn': | |
result = process_document('generate', selected_filename, chat_input) | |
elif button_id == 'compliance-action-btn': | |
result = process_document('compliance', selected_filename, chat_input) | |
elif button_id == 'recover-action-btn': | |
result = process_document('recover', selected_filename, chat_input) | |
elif button_id == 'board-action-btn': | |
result = process_document('board', selected_filename, chat_input) | |
elif button_id == 'loe-action-btn': | |
result = process_document('loe', selected_filename, chat_input) | |
else: | |
result = "Action not implemented yet." | |
if isinstance(result, str) and result.strip().startswith("Error"): | |
return html.Div(result, style={"wordWrap": "break-word"}) | |
if isinstance(result, str) and ("not implemented" in result or "No document uploaded" in result or "Shredding in progress" in result or "Generating response" in result or "Shredded document not available" in result): | |
return html.Div(result, style={"wordWrap": "break-word"}) | |
return dcc.Markdown(result, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"}) | |
if __name__ == '__main__': | |
print("Starting the Dash application...") | |
app.run(debug=True, host='0.0.0.0', port=7860, threaded=True) | |
print("Dash application has finished running.") |