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