proposal-writer / app.py
bluenevus's picture
Update app.py via AI Editor
034b23e
raw
history blame
18.5 kB
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
@app.callback(
Output("uploaded-doc-list", "children"),
Output("preview-content", "children"),
Output("upload-feedback", "children"),
Output("loading-output", "children"),
Output("chat-history", "children"),
[
Input("upload-document", "contents"),
Input("action-shred", "n_clicks"),
Input("action-generate", "n_clicks"),
Input("action-compliance", "n_clicks"),
Input("action-recover", "n_clicks"),
Input("action-virtual-board", "n_clicks"),
Input("action-loe", "n_clicks"),
Input("btn-send-chat", "n_clicks"),
Input({"type": "delete-doc-btn", "index": dash.ALL}, "n_clicks"),
],
[
State("upload-document", "filename"),
State("chat-input", "value"),
State("chat-history", "children"),
State("preview-content", "children"),
State("uploaded-doc-list", "children"),
],
prevent_initial_call=True
)
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.")