Spaces:
Paused
Paused
Update app.py via AI Editor
Browse files
app.py
CHANGED
@@ -11,6 +11,8 @@ import dash_bootstrap_components as dbc
|
|
11 |
from dash import html, dcc, Input, Output, State, dash_table, callback_context
|
12 |
|
13 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
14 |
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
15 |
import anthropic
|
16 |
anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
|
@@ -31,36 +33,52 @@ document_types = {
|
|
31 |
}
|
32 |
|
33 |
def process_document(contents, filename):
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
def call_claude(prompt, max_tokens=2048):
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
def spreadsheet_to_df(text):
|
56 |
lines = [l.strip() for l in text.splitlines() if '|' in l]
|
57 |
-
if not lines:
|
|
|
58 |
header = lines[0].strip('|').split('|')
|
59 |
data = [l.strip('|').split('|') for l in lines[1:]]
|
60 |
return pd.DataFrame(data, columns=[h.strip() for h in header])
|
61 |
|
62 |
-
def generate_content(document, doc_type):
|
63 |
-
prompt = f"{document_types[doc_type]}\n\
|
|
|
|
|
|
|
|
|
64 |
response = call_claude(prompt, max_tokens=4096)
|
65 |
df = spreadsheet_to_df(response)
|
66 |
return response, df
|
@@ -104,25 +122,47 @@ def make_textarea(btn_id, placeholder):
|
|
104 |
style={'height': '80px', 'marginBottom': '10px', 'width': '100%', 'whiteSpace': 'pre-wrap', 'overflowWrap': 'break-word'}
|
105 |
)
|
106 |
|
107 |
-
def
|
108 |
return dbc.Card(
|
109 |
dbc.CardBody([
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
dcc.Loading(html.Div(id=f'{tab_id}-output'), type="default", parent_style={'justifyContent': 'center'}),
|
114 |
-
dbc.Button(f"Download {label} Report", id=f"{tab_id}-download-btn", className="mt-2 btn-secondary", n_clicks=0),
|
115 |
-
dcc.Download(id=f"{tab_id}-download")
|
116 |
-
]), className="mb-4"
|
117 |
)
|
118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
tab_cards = {tab["id"]: make_tab(tab["id"], tab["label"]) for tab in main_tabs}
|
120 |
|
121 |
nav_items = [
|
122 |
dbc.NavLink(tab["label"], href="#", id=f"nav-{tab['id']}", active=(tab["id"] == "shred")) for tab in main_tabs
|
123 |
]
|
124 |
|
125 |
-
# Render all tab cards, only one visible at a time
|
126 |
def all_tabs_div():
|
127 |
return html.Div(
|
128 |
[
|
@@ -173,6 +213,45 @@ def display_tab(*nav_clicks):
|
|
173 |
styles.append({"display": "none"})
|
174 |
return styles
|
175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
@app.callback(
|
177 |
[Output(f'{tab_id}-output', 'children') for tab_id in tab_cards] +
|
178 |
[Output(f"{tab_id}-download", "data") for tab_id in tab_cards],
|
@@ -181,14 +260,17 @@ def display_tab(*nav_clicks):
|
|
181 |
[State(f'{tab_id}-upload', 'contents') for tab_id in tab_cards] +
|
182 |
[State(f'{tab_id}-upload', 'filename') for tab_id in tab_cards] +
|
183 |
[State(f'{tab_id}-instructions', 'value') for tab_id in tab_cards] +
|
184 |
-
[State(f'{tab_id}-output', 'children') for tab_id in tab_cards]
|
|
|
185 |
)
|
186 |
def handle_all_tabs(*args):
|
187 |
n = len(tab_cards)
|
188 |
outputs = [None] * (n * 2)
|
189 |
ctx = callback_context
|
190 |
-
if not ctx.triggered:
|
|
|
191 |
trig = ctx.triggered[0]['prop_id']
|
|
|
192 |
for idx, tab_id in enumerate(tab_cards):
|
193 |
gen_btn = f"{tab_id}-btn.n_clicks"
|
194 |
dl_btn = f"{tab_id}-download-btn.n_clicks"
|
@@ -198,31 +280,57 @@ def handle_all_tabs(*args):
|
|
198 |
filename_idx = idx + n
|
199 |
instr_idx = idx + 2 * n
|
200 |
prev_output_idx = idx + 3 * n
|
|
|
201 |
|
202 |
if trig == gen_btn:
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
|
|
|
|
|
|
222 |
else:
|
223 |
-
outputs[out_idx] =
|
224 |
else:
|
225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
226 |
elif trig == dl_btn:
|
227 |
prev_output = args[prev_output_idx]
|
228 |
if prev_output and hasattr(prev_output, 'props') and 'data' in prev_output.props:
|
|
|
11 |
from dash import html, dcc, Input, Output, State, dash_table, callback_context
|
12 |
|
13 |
logging.basicConfig(level=logging.INFO)
|
14 |
+
logger = logging.getLogger("microhealth-pws")
|
15 |
+
|
16 |
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
|
17 |
import anthropic
|
18 |
anthropic_client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
|
|
|
33 |
}
|
34 |
|
35 |
def process_document(contents, filename):
|
36 |
+
try:
|
37 |
+
content_type, content_string = contents.split(',')
|
38 |
+
decoded = base64.b64decode(content_string)
|
39 |
+
if filename.lower().endswith('.docx'):
|
40 |
+
doc = Document(BytesIO(decoded))
|
41 |
+
text = "\n".join([p.text for p in doc.paragraphs])
|
42 |
+
return text
|
43 |
+
elif filename.lower().endswith('.pdf'):
|
44 |
+
pdf = PdfReader(BytesIO(decoded))
|
45 |
+
text = "".join(page.extract_text() or "" for page in pdf.pages)
|
46 |
+
return text
|
47 |
+
else:
|
48 |
+
return f"Unsupported file format: {filename}"
|
49 |
+
except Exception as e:
|
50 |
+
logger.error(f"Error processing document {filename}: {e}")
|
51 |
+
return f"Failed to process document: {e}"
|
52 |
|
53 |
def call_claude(prompt, max_tokens=2048):
|
54 |
+
try:
|
55 |
+
res = anthropic_client.messages.create(
|
56 |
+
model=CLAUDE3_SONNET_MODEL,
|
57 |
+
max_tokens=max_tokens,
|
58 |
+
temperature=0.1,
|
59 |
+
system="You are a world class proposal consultant and proposal manager.",
|
60 |
+
messages=[{"role": "user", "content": prompt}]
|
61 |
+
)
|
62 |
+
logger.info("Anthropic API call successful.")
|
63 |
+
return res.content[0].text if hasattr(res, "content") else str(res)
|
64 |
+
except Exception as e:
|
65 |
+
logger.error(f"Anthropic API error: {e}")
|
66 |
+
return f"Anthropic API error: {e}"
|
67 |
|
68 |
def spreadsheet_to_df(text):
|
69 |
lines = [l.strip() for l in text.splitlines() if '|' in l]
|
70 |
+
if not lines:
|
71 |
+
return pd.DataFrame()
|
72 |
header = lines[0].strip('|').split('|')
|
73 |
data = [l.strip('|').split('|') for l in lines[1:]]
|
74 |
return pd.DataFrame(data, columns=[h.strip() for h in header])
|
75 |
|
76 |
+
def generate_content(document, doc_type, instructions=""):
|
77 |
+
prompt = f"{document_types[doc_type]}\n\n"
|
78 |
+
if instructions:
|
79 |
+
prompt += f"Additional Instructions:\n{instructions}\n\n"
|
80 |
+
prompt += f"Document:\n{document}\n\nOutput only one spreadsheet table, use | as column separator."
|
81 |
+
logger.info(f"Generating content for {doc_type} with prompt length {len(prompt)}")
|
82 |
response = call_claude(prompt, max_tokens=4096)
|
83 |
df = spreadsheet_to_df(response)
|
84 |
return response, df
|
|
|
122 |
style={'height': '80px', 'marginBottom': '10px', 'width': '100%', 'whiteSpace': 'pre-wrap', 'overflowWrap': 'break-word'}
|
123 |
)
|
124 |
|
125 |
+
def make_shred_doc_preview():
|
126 |
return dbc.Card(
|
127 |
dbc.CardBody([
|
128 |
+
html.Div(id="shred-upload-preview", style={"whiteSpace": "pre-wrap", "overflowWrap": "break-word"}),
|
129 |
+
dbc.Button("Delete Document", id="shred-delete-btn", className="mt-2 btn-tertiary", n_clicks=0)
|
130 |
+
]), className="mb-2", id="shred-doc-preview-card", style={"display": "none"}
|
|
|
|
|
|
|
|
|
131 |
)
|
132 |
|
133 |
+
def make_tab(tab_id, label):
|
134 |
+
if tab_id == "shred":
|
135 |
+
# Insert doc preview card for shred
|
136 |
+
return dbc.Card(
|
137 |
+
dbc.CardBody([
|
138 |
+
make_textarea(tab_id, f"Instructions for {label} (optional)"),
|
139 |
+
make_upload(tab_id),
|
140 |
+
make_shred_doc_preview(),
|
141 |
+
dbc.Button(f"Generate {label}", id=f'{tab_id}-btn', className="mt-2 btn-primary", n_clicks=0),
|
142 |
+
dcc.Loading(html.Div(id=f'{tab_id}-output'), id="loading", type="default", parent_style={'justifyContent': 'center'}),
|
143 |
+
dbc.Button(f"Download {label} Report", id=f"{tab_id}-download-btn", className="mt-2 btn-secondary", n_clicks=0),
|
144 |
+
dcc.Download(id=f"{tab_id}-download"),
|
145 |
+
dcc.Store(id="shred-upload-store")
|
146 |
+
]), className="mb-4"
|
147 |
+
)
|
148 |
+
else:
|
149 |
+
return dbc.Card(
|
150 |
+
dbc.CardBody([
|
151 |
+
make_textarea(tab_id, f"Instructions for {label} (optional)"),
|
152 |
+
make_upload(tab_id),
|
153 |
+
dbc.Button(f"Generate {label}", id=f'{tab_id}-btn', className="mt-2 btn-primary", n_clicks=0),
|
154 |
+
dcc.Loading(html.Div(id=f'{tab_id}-output'), type="default", parent_style={'justifyContent': 'center'}),
|
155 |
+
dbc.Button(f"Download {label} Report", id=f"{tab_id}-download-btn", className="mt-2 btn-secondary", n_clicks=0),
|
156 |
+
dcc.Download(id=f"{tab_id}-download")
|
157 |
+
]), className="mb-4"
|
158 |
+
)
|
159 |
+
|
160 |
tab_cards = {tab["id"]: make_tab(tab["id"], tab["label"]) for tab in main_tabs}
|
161 |
|
162 |
nav_items = [
|
163 |
dbc.NavLink(tab["label"], href="#", id=f"nav-{tab['id']}", active=(tab["id"] == "shred")) for tab in main_tabs
|
164 |
]
|
165 |
|
|
|
166 |
def all_tabs_div():
|
167 |
return html.Div(
|
168 |
[
|
|
|
213 |
styles.append({"display": "none"})
|
214 |
return styles
|
215 |
|
216 |
+
@app.callback(
|
217 |
+
[
|
218 |
+
Output("shred-upload-store", "data"),
|
219 |
+
Output("shred-upload-preview", "children"),
|
220 |
+
Output("shred-doc-preview-card", "style"),
|
221 |
+
],
|
222 |
+
[
|
223 |
+
Input("shred-upload", "contents"),
|
224 |
+
Input("shred-delete-btn", "n_clicks")
|
225 |
+
],
|
226 |
+
[
|
227 |
+
State("shred-upload", "filename"),
|
228 |
+
State("shred-upload-store", "data"),
|
229 |
+
],
|
230 |
+
prevent_initial_call=True
|
231 |
+
)
|
232 |
+
def update_shred_upload(contents, delete_clicks, filename, stored_data):
|
233 |
+
triggered = callback_context.triggered
|
234 |
+
logger.info("Shred upload callback triggered.")
|
235 |
+
if not triggered:
|
236 |
+
return dash.no_update, dash.no_update, dash.no_update
|
237 |
+
trig_id = triggered[0]["prop_id"].split(".")[0]
|
238 |
+
if trig_id == "shred-upload":
|
239 |
+
if contents and filename:
|
240 |
+
logger.info(f"Document uploaded in Shred: {filename}")
|
241 |
+
text = process_document(contents, filename)
|
242 |
+
preview = html.Div([
|
243 |
+
html.B(f"Uploaded: {filename}"),
|
244 |
+
html.Br(),
|
245 |
+
html.Div(text[:2000] + ("..." if len(text) > 2000 else ""), style={"whiteSpace": "pre-wrap", "overflowWrap": "break-word", "fontSize": "small"})
|
246 |
+
])
|
247 |
+
return {"contents": contents, "filename": filename, "preview": text[:2000]}, preview, {"display": "block"}
|
248 |
+
else:
|
249 |
+
return None, "", {"display": "none"}
|
250 |
+
elif trig_id == "shred-delete-btn":
|
251 |
+
logger.info("Shred document deleted by user.")
|
252 |
+
return None, "", {"display": "none"}
|
253 |
+
return dash.no_update, dash.no_update, dash.no_update
|
254 |
+
|
255 |
@app.callback(
|
256 |
[Output(f'{tab_id}-output', 'children') for tab_id in tab_cards] +
|
257 |
[Output(f"{tab_id}-download", "data") for tab_id in tab_cards],
|
|
|
260 |
[State(f'{tab_id}-upload', 'contents') for tab_id in tab_cards] +
|
261 |
[State(f'{tab_id}-upload', 'filename') for tab_id in tab_cards] +
|
262 |
[State(f'{tab_id}-instructions', 'value') for tab_id in tab_cards] +
|
263 |
+
[State(f'{tab_id}-output', 'children') for tab_id in tab_cards] +
|
264 |
+
[State("shred-upload-store", "data")]
|
265 |
)
|
266 |
def handle_all_tabs(*args):
|
267 |
n = len(tab_cards)
|
268 |
outputs = [None] * (n * 2)
|
269 |
ctx = callback_context
|
270 |
+
if not ctx.triggered:
|
271 |
+
return outputs
|
272 |
trig = ctx.triggered[0]['prop_id']
|
273 |
+
logger.info(f"Main callback triggered by {trig}")
|
274 |
for idx, tab_id in enumerate(tab_cards):
|
275 |
gen_btn = f"{tab_id}-btn.n_clicks"
|
276 |
dl_btn = f"{tab_id}-download-btn.n_clicks"
|
|
|
280 |
filename_idx = idx + n
|
281 |
instr_idx = idx + 2 * n
|
282 |
prev_output_idx = idx + 3 * n
|
283 |
+
shred_upload_store_idx = 4 * n
|
284 |
|
285 |
if trig == gen_btn:
|
286 |
+
logger.info(f"Generate button pressed for {tab_id}")
|
287 |
+
if tab_id == "shred":
|
288 |
+
# Use stored doc for Shred
|
289 |
+
shred_data = args[shred_upload_store_idx]
|
290 |
+
instr = args[instr_idx] or ""
|
291 |
+
if shred_data and "contents" in shred_data and "filename" in shred_data:
|
292 |
+
doc = process_document(shred_data["contents"], shred_data["filename"])
|
293 |
+
logger.info(f"Shred document will be sent to Anthropic with instructions: {instr}")
|
294 |
+
content, df = generate_content(doc, "Shred", instr)
|
295 |
+
if not df.empty:
|
296 |
+
outputs[out_idx] = dash_table.DataTable(
|
297 |
+
data=df.to_dict('records'),
|
298 |
+
columns=[{'name': i, 'id': i} for i in df.columns],
|
299 |
+
style_table={'overflowX': 'auto'},
|
300 |
+
style_cell={'textAlign': 'left', 'padding': '5px'},
|
301 |
+
style_header={'fontWeight': 'bold'}
|
302 |
+
)
|
303 |
+
else:
|
304 |
+
outputs[out_idx] = html.Div([
|
305 |
+
html.B("Anthropic Response Preview:"),
|
306 |
+
dcc.Markdown(content)
|
307 |
+
])
|
308 |
else:
|
309 |
+
outputs[out_idx] = "Please upload a document to begin."
|
310 |
else:
|
311 |
+
upload = args[upload_idx]
|
312 |
+
filename = args[filename_idx]
|
313 |
+
instr = args[instr_idx] or ""
|
314 |
+
doc_type = tab_id.replace('-', ' ').title().replace(' ', '')
|
315 |
+
doc_type = next((k for k in document_types if k.lower().replace(' ', '') == tab_id.replace('-', '')), tab_id.title())
|
316 |
+
if upload and filename:
|
317 |
+
doc = process_document(upload, filename)
|
318 |
+
else:
|
319 |
+
doc = ""
|
320 |
+
if doc or tab_id == "virtual-board":
|
321 |
+
content, df = generate_content(doc, doc_type, instr)
|
322 |
+
if not df.empty:
|
323 |
+
outputs[out_idx] = dash_table.DataTable(
|
324 |
+
data=df.to_dict('records'),
|
325 |
+
columns=[{'name': i, 'id': i} for i in df.columns],
|
326 |
+
style_table={'overflowX': 'auto'},
|
327 |
+
style_cell={'textAlign': 'left', 'padding': '5px'},
|
328 |
+
style_header={'fontWeight': 'bold'}
|
329 |
+
)
|
330 |
+
else:
|
331 |
+
outputs[out_idx] = dcc.Markdown(content)
|
332 |
+
else:
|
333 |
+
outputs[out_idx] = "Please upload a document to begin."
|
334 |
elif trig == dl_btn:
|
335 |
prev_output = args[prev_output_idx]
|
336 |
if prev_output and hasattr(prev_output, 'props') and 'data' in prev_output.props:
|