Spaces:
Paused
Paused
import dash | |
from dash import dcc, html, Input, Output, State, callback_context, no_update | |
import dash_bootstrap_components as dbc | |
import logging | |
import threading | |
import os | |
import base64 | |
import io | |
import uuid | |
import time | |
from flask import Flask | |
import requests | |
ALLOWED_EXTENSIONS = ('pdf', 'doc', 'docx', 'txt') | |
logging.basicConfig( | |
format="%(asctime)s %(levelname)s:%(message)s", | |
level=logging.INFO | |
) | |
logger = logging.getLogger(__name__) | |
uploaded_documents = {} | |
generated_content = {} | |
ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages" | |
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "YOUR_ANTHROPIC_API_KEY") | |
server = Flask(__name__) | |
os.environ["CUDA_VISIBLE_DEVICES"] = "0" | |
external_stylesheets = [dbc.themes.BOOTSTRAP] | |
app = dash.Dash( | |
__name__, | |
server=server, | |
external_stylesheets=external_stylesheets, | |
suppress_callback_exceptions=True, | |
title="Proposal Writing Assistant" | |
) | |
def allowed_file(filename): | |
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | |
def save_uploaded_file(file_content, filename): | |
doc_id = str(uuid.uuid4()) | |
uploaded_documents[doc_id] = { | |
"filename": filename, | |
"content": file_content | |
} | |
logger.info(f"Uploaded document saved: {filename} with id {doc_id}") | |
return doc_id | |
def anthropic_api_call(prompt, files=None, task_type=None, extra_instructions=""): | |
logger.info(f"Calling Anthropic API for task: {task_type}") | |
headers = { | |
"x-api-key": ANTHROPIC_API_KEY, | |
"content-type": "application/json" | |
} | |
data = { | |
"model": "claude-3-opus-20240229", | |
"messages": [ | |
{"role": "user", "content": prompt + "\n" + extra_instructions} | |
], | |
"max_tokens": 4096, | |
"temperature": 0.2 | |
} | |
try: | |
# response = requests.post(ANTHROPIC_API_URL, headers=headers, json=data, timeout=120) | |
# result = response.json().get('content', ['[Anthropic response placeholder]'])[0] | |
time.sleep(2) | |
result = f"[Simulated response for {task_type}]" | |
logger.info(f"Anthropic API success for task: {task_type}") | |
return result | |
except Exception as e: | |
logger.error(f"Anthropic API error: {str(e)}") | |
return f"Error: {str(e)}" | |
def parse_contents(contents, filename): | |
content_type, content_string = contents.split(',') | |
decoded = base64.b64decode(content_string) | |
try: | |
if filename.lower().endswith('.txt'): | |
preview = decoded.decode('utf-8')[:2048] | |
else: | |
preview = f"Preview not available for {filename}" | |
return preview | |
except Exception as e: | |
logger.error(f"Could not decode file {filename}: {e}") | |
return f"Error decoding {filename}" | |
def navbar(): | |
return dbc.Card( | |
[ | |
dbc.Nav( | |
[ | |
dbc.Button("Shred RFP/PWS/SOW/RFI", id="btn-shred", className="mb-2 btn-primary", style={"width": "100%"}), | |
dbc.Button("Generate Proposal Response", id="btn-generate", className="mb-2 btn-secondary", style={"width": "100%"}), | |
dbc.Button("Check Compliance", id="btn-compliance", className="mb-2 btn-tertiary", style={"width": "100%"}), | |
dbc.Button("Recover Document", id="btn-recover", className="mb-2 btn-primary", style={"width": "100%"}), | |
dbc.Button("Virtual Board", id="btn-virtual-board", className="mb-2 btn-secondary", style={"width": "100%"}), | |
dbc.Button("Estimate LOE", id="btn-loe", className="mb-2 btn-tertiary", style={"width": "100%"}), | |
], | |
vertical=True, | |
pills=False | |
), | |
html.Hr(), | |
html.Div( | |
[ | |
html.H6("Uploaded Documents"), | |
html.Ul( | |
id="uploaded-doc-list", | |
style={"listStyleType": "none", "paddingLeft": "0"} | |
), | |
] | |
), | |
], | |
body=True | |
) | |
def chat_window(): | |
return dbc.Card( | |
[ | |
html.Div( | |
[ | |
html.Div(id="chat-history", style={"height": "160px", "overflowY": "auto", "padding": "0.5rem"}), | |
dbc.InputGroup( | |
[ | |
dbc.Textarea(id="chat-input", placeholder="Send additional instructions...", style={"resize":"vertical", "wordWrap":"break-word", "width": "100%", "height": "60px"}), | |
dbc.Button("Send", id="btn-send-chat", className="btn-secondary", n_clicks=0), | |
], | |
className="mt-2" | |
), | |
] | |
), | |
], | |
body=True, | |
style={"marginBottom": "10px"} | |
) | |
def top_action_buttons(): | |
return html.Div( | |
[ | |
dbc.Button("Shred", id="action-shred", className="me-2 btn-primary", n_clicks=0, style={"minWidth": "120px"}), | |
dbc.Button("Generate Response", id="action-generate", className="me-2 btn-secondary", n_clicks=0, style={"minWidth": "180px"}), | |
dbc.Button("Check Compliance", id="action-compliance", className="me-2 btn-tertiary", n_clicks=0, style={"minWidth": "160px"}), | |
dbc.Button("Recover", id="action-recover", className="me-2 btn-primary", n_clicks=0, style={"minWidth": "120px"}), | |
dbc.Button("Virtual Board", id="action-virtual-board", className="me-2 btn-secondary", n_clicks=0, style={"minWidth": "160px"}), | |
dbc.Button("Estimate LOE", id="action-loe", className="btn-tertiary", n_clicks=0, style={"minWidth": "140px"}), | |
], | |
className="mb-3", | |
style={"display": "flex", "flexWrap": "wrap"} | |
) | |
def upload_area(): | |
return html.Div( | |
[ | |
dcc.Upload( | |
id="upload-document", | |
children=html.Div(["Drag & drop or click to select a file."]), | |
multiple=False, | |
style={ | |
"width": "100%", | |
"height": "70px", | |
"lineHeight": "70px", | |
"borderWidth": "1px", | |
"borderStyle": "dashed", | |
"borderRadius": "4px", | |
"textAlign": "center", | |
"marginBottom": "8px" | |
} | |
), | |
html.Div(id="upload-feedback") | |
] | |
) | |
def preview_area(): | |
return dbc.Card( | |
[ | |
html.H6("Document Preview / Output"), | |
html.Pre(id="preview-content", style={"whiteSpace": "pre-wrap", "wordWrap": "break-word", "maxHeight": "340px", "overflowY": "auto"}) | |
], | |
body=True | |
) | |
def main_layout(): | |
return dbc.Container( | |
[ | |
dbc.Row( | |
[ | |
dbc.Col( | |
html.H2("Proposal Writing Assistant", style={"margin": "12px 0"}), | |
width=12 | |
), | |
], | |
align="center", | |
style={"marginBottom": "8px"} | |
), | |
dbc.Row( | |
[ | |
dbc.Col( | |
navbar(), | |
width=3, | |
style={"minWidth": "220px", "maxWidth": "400px"} | |
), | |
dbc.Col( | |
dbc.Card( | |
[ | |
chat_window(), | |
top_action_buttons(), | |
upload_area(), | |
preview_area(), | |
dcc.Loading( | |
id="loading", | |
type="default", | |
children=html.Div(id="loading-output"), | |
style={"position": "absolute", "top": "6px", "left": "50%"} | |
), | |
], | |
body=True | |
), | |
width=9 | |
), | |
], | |
style={"minHeight": "90vh"} | |
), | |
], | |
fluid=True | |
) | |
app.layout = main_layout | |
def main_callback( | |
upload_contents, shred, generate, compliance, recover, virtual_board, loe, send_chat, delete_doc_clicks, | |
upload_filename, chat_input, chat_history, preview_content, uploaded_doc_list | |
): | |
triggered_id = callback_context.triggered[0]["prop_id"].split(".")[0] if callback_context.triggered else None | |
logger.info(f"Triggered callback: {triggered_id}") | |
feedback = no_update | |
loading_message = "" | |
new_preview_content = no_update | |
new_chat_history = chat_history if chat_history else [] | |
doc_list_items = [] | |
if triggered_id == "upload-document" and upload_contents and upload_filename: | |
if not allowed_file(upload_filename): | |
feedback = dbc.Alert("Unsupported file type. Please upload PDF, Word, or TXT.", color="danger", dismissable=True) | |
else: | |
doc_id = save_uploaded_file(upload_contents, upload_filename) | |
preview = parse_contents(upload_contents, upload_filename) | |
new_preview_content = f"{upload_filename}:\n\n{preview}" | |
feedback = dbc.Alert(f"Uploaded {upload_filename}", color="success", dismissable=True) | |
logger.info(f"File uploaded: {upload_filename}") | |
for doc_id, doc in uploaded_documents.items(): | |
doc_list_items.append( | |
html.Li( | |
[ | |
html.Span(doc['filename'], style={"marginRight": "8px"}), | |
dbc.Button("Delete", id={"type": "delete-doc-btn", "index": doc_id}, color="danger", size="sm", n_clicks=0) | |
], | |
style={"display": "flex", "justifyContent": "space-between", "alignItems": "center", "marginBottom": "5px"} | |
) | |
) | |
if isinstance(delete_doc_clicks, list) and any(delete_doc_clicks): | |
idx = delete_doc_clicks.index(max(delete_doc_clicks)) | |
doc_ids = list(uploaded_documents.keys()) | |
if idx < len(doc_ids): | |
deleted_doc = uploaded_documents.pop(doc_ids[idx]) | |
feedback = dbc.Alert(f"Deleted {deleted_doc['filename']}", color="info", dismissable=True) | |
logger.info(f"Document deleted: {deleted_doc['filename']}") | |
doc_list_items = [ | |
html.Li( | |
[ | |
html.Span(doc['filename'], style={"marginRight": "8px"}), | |
dbc.Button("Delete", id={"type": "delete-doc-btn", "index": doc_id}, color="danger", size="sm", n_clicks=0) | |
], | |
style={"display": "flex", "justifyContent": "space-between", "alignItems": "center", "marginBottom": "5px"} | |
) | |
for doc_id, doc in uploaded_documents.items() | |
] | |
new_preview_content = "" if not uploaded_documents else no_update | |
if len(uploaded_documents) == 0 and triggered_id not in ["upload-document", "btn-send-chat"]: | |
feedback = dbc.Alert("Please upload a document before performing actions.", color="warning", dismissable=True) | |
logger.warning("Attempted action without documents.") | |
return doc_list_items, new_preview_content, feedback, loading_message, new_chat_history | |
if triggered_id == "btn-send-chat" and chat_input and chat_input.strip(): | |
if not isinstance(new_chat_history, list): | |
new_chat_history = [] | |
new_chat_history.append(html.Div([ | |
html.Strong("You: "), html.Span(chat_input) | |
], style={"marginBottom": "0.25rem"})) | |
feedback = dbc.Alert("Chat message sent. Instructions will be used in next action.", color="info", dismissable=True) | |
logger.info(f"Chat message sent: {chat_input}") | |
last_chat = "" | |
if isinstance(new_chat_history, list) and new_chat_history: | |
for item in reversed(new_chat_history): | |
if isinstance(item, html.Div): | |
children = item.children | |
if len(children) > 1 and isinstance(children[1], html.Span): | |
last_chat = children[1].children | |
break | |
elif isinstance(chat_input, str): | |
last_chat = chat_input | |
if triggered_id in ["action-shred", "action-generate", "action-compliance", "action-recover", "action-virtual-board", "action-loe"]: | |
loading_message = dbc.Alert("Processing request, please wait...", color="primary", dismissable=False, style={"textAlign": "center"}) | |
doc_id, doc = next(iter(uploaded_documents.items())) | |
file_name = doc['filename'] | |
file_content = doc['content'] | |
action_type = triggered_id.replace("action-", "").replace("-", " ").title() | |
# DECODE the document content for use in the prompt | |
try: | |
content_type, content_string = file_content.split(',') | |
decoded = base64.b64decode(content_string) | |
if file_name.lower().endswith('.txt'): | |
document_text = decoded.decode('utf-8', errors='replace') | |
else: | |
document_text = f"[Start of document {file_name} as base64]\n{decoded[:350].hex()}...[truncated]\n[End of document]" | |
except Exception as e: | |
logger.error(f"Could not decode document {file_name} for Anthropic: {e}") | |
document_text = f"[Could not decode {file_name}]" | |
logger.info(f"Sending document content of length {len(document_text)} to Anthropic for {action_type}") | |
result_holder = {} | |
def threaded_api_call(): | |
if triggered_id == "action-shred": | |
prompt = ( | |
"Shred this document into requirements, organized by section. " | |
"Identify requirements by action words (shall, will, perform, etc). " | |
"Output as spreadsheet: PWS Section, Requirement.\n\n" | |
f"Document Content:\n{document_text}\n" | |
) | |
task_type = "Shred" | |
elif triggered_id == "action-generate": | |
prompt = ( | |
"Generate a detailed proposal response, organized by section/subsection. " | |
"Focus on approach, steps, workflow, people, processes, technology. " | |
"Include research validation and citations. Address Red Review findings.\n\n" | |
f"Document Content:\n{document_text}\n" | |
) | |
task_type = "Generate Proposal Response" | |
elif triggered_id == "action-compliance": | |
prompt = ( | |
"Check compliance of the proposal response against the shredded requirements. " | |
"Produce a spreadsheet: PWS number, requirement, finding, recommendation.\n\n" | |
f"Proposal Response Document Content:\n{document_text}\n" | |
) | |
task_type = "Check Compliance" | |
elif triggered_id == "action-recover": | |
prompt = ( | |
"Using the compliance spreadsheet, improve the document sections. " | |
"Address recommendations without materially changing content. " | |
"Organize improvements by PWS section headers/subheaders.\n\n" | |
f"Document Content:\n{document_text}\n" | |
) | |
task_type = "Recover Document" | |
elif triggered_id == "action-virtual-board": | |
prompt = ( | |
"Evaluate the proposal based on requirements and evaluation criteria. " | |
"Generate a section-by-section evaluation spreadsheet using ratings: " | |
"unsatisfactory, satisfactory, good, very good, excellent. Include explanations. " | |
"Base evaluation on sections L and M.\n\n" | |
f"Document Content:\n{document_text}\n" | |
) | |
task_type = "Virtual Board" | |
elif triggered_id == "action-loe": | |
prompt = ( | |
"Estimate Level of Effort for the proposal. Output spreadsheet: " | |
"PWS task area, brief description, labor categories, estimated hours per category.\n\n" | |
f"Document Content:\n{document_text}\n" | |
) | |
task_type = "Estimate LOE" | |
else: | |
prompt = "" | |
task_type = "Unknown" | |
logger.info(f"Prompt to Anthropic for {action_type}: {prompt[:400]}...[truncated]") | |
result_holder["result"] = anthropic_api_call(prompt, files=[file_content], task_type=task_type, extra_instructions=last_chat or "") | |
thread = threading.Thread(target=threaded_api_call) | |
thread.start() | |
thread.join() | |
result = result_holder.get("result", "[No result]") | |
generated_content[triggered_id] = result | |
new_preview_content = f"{action_type} Output for {file_name}:\n\n{result}" | |
feedback = dbc.Alert(f"{action_type} completed.", color="success", dismissable=True) | |
logger.info(f"{action_type} completed for {file_name}") | |
return doc_list_items, new_preview_content, feedback, loading_message, new_chat_history | |
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.") |