Spaces:
Paused
Paused
Update app.py via AI Editor
Browse files
app.py
CHANGED
@@ -16,6 +16,7 @@ import logging
|
|
16 |
import uuid
|
17 |
import xlsxwriter # Needed for Excel export engine
|
18 |
import threading # For multi-threading
|
|
|
19 |
|
20 |
# --- Logging Configuration ---
|
21 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -75,7 +76,7 @@ uploaded_pink_content = {}
|
|
75 |
uploaded_red_content = {}
|
76 |
uploaded_gold_content = {}
|
77 |
|
78 |
-
# {session_id: {'doc': content, 'type': doc_type}} - Store the currently displayed document
|
79 |
current_display_document = {}
|
80 |
|
81 |
# --- Document Types ---
|
@@ -96,7 +97,7 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
|
|
96 |
dcc.Store(id='session-id', storage_type='session'), # Store for unique session ID
|
97 |
# Title Row
|
98 |
dbc.Row(
|
99 |
-
dbc.Col(html.H1("Proposal AI Assistant", className="text-center my-4"), width=12)
|
100 |
),
|
101 |
|
102 |
# Progress Indicator Row
|
@@ -118,7 +119,7 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
|
|
118 |
|
119 |
# Main Content Row
|
120 |
dbc.Row([
|
121 |
-
# Left Column (Nav/Upload) -
|
122 |
dbc.Col(
|
123 |
dbc.Card(
|
124 |
dbc.CardBody([
|
@@ -151,14 +152,16 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
|
|
151 |
) for doc_type in document_types.keys()]
|
152 |
])
|
153 |
)
|
154 |
-
])
|
155 |
-
|
156 |
-
|
|
|
|
|
157 |
className="mb-3 mb-lg-0",
|
158 |
style={'paddingRight': '15px'} # Add padding between columns
|
159 |
),
|
160 |
|
161 |
-
# Right Column (Status/Preview/Controls/Chat) -
|
162 |
dbc.Col(
|
163 |
dbc.Card(
|
164 |
dbc.CardBody([
|
@@ -194,9 +197,11 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
|
|
194 |
)
|
195 |
]), className="mb-3"
|
196 |
)
|
197 |
-
])
|
198 |
-
|
199 |
-
|
|
|
|
|
200 |
style={'paddingLeft': '15px'} # Add padding between columns
|
201 |
)
|
202 |
])
|
@@ -214,6 +219,36 @@ def get_session_id(session_id_value=None):
|
|
214 |
logging.info(f"Generated new session ID: {new_id}")
|
215 |
return new_id
|
216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
217 |
def process_document(contents, filename):
|
218 |
"""Processes uploaded file content (PDF or DOCX) and returns text, or None and error message."""
|
219 |
if contents is None:
|
@@ -256,44 +291,56 @@ def process_document(contents, filename):
|
|
256 |
logging.error(f"Error processing document {filename}: {e}", exc_info=True)
|
257 |
return None, f"Error processing file {filename}: {str(e)}"
|
258 |
|
259 |
-
def get_combined_uploaded_text(session_id):
|
260 |
-
"""Combines text content of
|
261 |
with data_lock:
|
262 |
-
session_files =
|
263 |
if not session_files:
|
264 |
return ""
|
265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
266 |
|
267 |
-
def generate_ai_document(session_id, doc_type, input_docs, context_docs=None):
|
268 |
-
"""Generates document using Gemini AI. Updates current_display for the session."""
|
269 |
-
global current_display_document # Modifying global state
|
270 |
|
|
|
|
|
271 |
if not model:
|
272 |
logging.error(f"[{session_id}] Gemini AI model not initialized.")
|
273 |
-
return "Error: AI Model not configured. Please check API Key."
|
274 |
if not input_docs or not any(doc.strip() for doc in input_docs if doc):
|
275 |
logging.warning(f"[{session_id}] generate_ai_document called for {doc_type} with no valid input documents.")
|
276 |
-
return f"Error: Missing required input document(s) for {doc_type} generation."
|
277 |
|
278 |
combined_input = "\n\n---\n\n".join(filter(None, input_docs))
|
279 |
combined_context = "\n\n---\n\n".join(filter(None, context_docs)) if context_docs else ""
|
280 |
|
281 |
-
#
|
|
|
|
|
|
|
282 |
prompt = f"""**Objective:** Generate the '{doc_type}' document.
|
283 |
-
**Your Role:** Act as an expert proposal writer/analyst.
|
284 |
**Core Instructions:**
|
285 |
-
1. **Adhere Strictly to the Task:** Generate *only* the content for the '{doc_type}'. Do not add introductions, summaries, or conversational filler unless it's part of the requested document format itself.
|
286 |
-
2. **Follow Format Guidelines:**
|
287 |
-
|
288 |
-
* **
|
289 |
-
|
290 |
-
* **
|
291 |
-
* **
|
|
|
|
|
|
|
|
|
292 |
**Provided Documents:**
|
293 |
-
**Context Document(s)
|
294 |
```text
|
295 |
{combined_context if combined_context else "N/A"}
|
296 |
```
|
297 |
-
**Primary Input Document(s)
|
298 |
```text
|
299 |
{combined_input}
|
|
|
16 |
import uuid
|
17 |
import xlsxwriter # Needed for Excel export engine
|
18 |
import threading # For multi-threading
|
19 |
+
import time # For progress indicator
|
20 |
|
21 |
# --- Logging Configuration ---
|
22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
76 |
uploaded_red_content = {}
|
77 |
uploaded_gold_content = {}
|
78 |
|
79 |
+
# {session_id: {'doc': content, 'type': doc_type, 'format': format}} - Store the currently displayed document, its type, and format for download/chat per session
|
80 |
current_display_document = {}
|
81 |
|
82 |
# --- Document Types ---
|
|
|
97 |
dcc.Store(id='session-id', storage_type='session'), # Store for unique session ID
|
98 |
# Title Row
|
99 |
dbc.Row(
|
100 |
+
dbc.Col(html.H1("Proposal AI Assistant", className="text-center my-4"), width=12)
|
101 |
),
|
102 |
|
103 |
# Progress Indicator Row
|
|
|
119 |
|
120 |
# Main Content Row
|
121 |
dbc.Row([
|
122 |
+
# Left Column (Nav/Upload) - lg=4 (approx 33%)
|
123 |
dbc.Col(
|
124 |
dbc.Card(
|
125 |
dbc.CardBody([
|
|
|
152 |
) for doc_type in document_types.keys()]
|
153 |
])
|
154 |
)
|
155 |
+
]),
|
156 |
+
# color="light", # Let CSS handle background
|
157 |
+
className="h-100 left-nav-card", # Add custom class for CSS targeting
|
158 |
+
),
|
159 |
+
width=12, lg=4, # Full width on small, 4/12 on large
|
160 |
className="mb-3 mb-lg-0",
|
161 |
style={'paddingRight': '15px'} # Add padding between columns
|
162 |
),
|
163 |
|
164 |
+
# Right Column (Status/Preview/Controls/Chat) - lg=8 (approx 67%)
|
165 |
dbc.Col(
|
166 |
dbc.Card(
|
167 |
dbc.CardBody([
|
|
|
197 |
)
|
198 |
]), className="mb-3"
|
199 |
)
|
200 |
+
]),
|
201 |
+
# color="white", # Let CSS handle background
|
202 |
+
className="h-100 right-nav-card", # Add custom class for CSS targeting
|
203 |
+
),
|
204 |
+
width=12, lg=8, # Full width on small, 8/12 on large
|
205 |
style={'paddingLeft': '15px'} # Add padding between columns
|
206 |
)
|
207 |
])
|
|
|
219 |
logging.info(f"Generated new session ID: {new_id}")
|
220 |
return new_id
|
221 |
|
222 |
+
def parse_generated_content(content_text):
|
223 |
+
"""Attempts to parse AI-generated content into a DataFrame if it looks like a table."""
|
224 |
+
try:
|
225 |
+
# Simple check: does it contain multiple lines and pipe characters?
|
226 |
+
if content_text and '\n' in content_text and '|' in content_text:
|
227 |
+
# Try parsing as Markdown-like table (skip lines that don't fit)
|
228 |
+
lines = [line.strip() for line in content_text.strip().split('\n')]
|
229 |
+
# Remove separator lines like |---|---|
|
230 |
+
lines = [line for line in lines if not all(c in '-| ' for c in line)]
|
231 |
+
if len(lines) > 1:
|
232 |
+
# Use the first line as header, split by '|'
|
233 |
+
header = [h.strip() for h in lines[0].strip('|').split('|')]
|
234 |
+
data_rows = []
|
235 |
+
for line in lines[1:]:
|
236 |
+
values = [v.strip() for v in line.strip('|').split('|')]
|
237 |
+
if len(values) == len(header): # Ensure matching column count
|
238 |
+
data_rows.append(values)
|
239 |
+
else:
|
240 |
+
logging.warning(f"Skipping row due to mismatched columns: {line}")
|
241 |
+
|
242 |
+
if data_rows:
|
243 |
+
df = pd.DataFrame(data_rows, columns=header)
|
244 |
+
logging.info("Successfully parsed generated content as DataFrame.")
|
245 |
+
return df
|
246 |
+
except Exception as e:
|
247 |
+
logging.warning(f"Could not parse content into DataFrame: {e}. Treating as plain text.")
|
248 |
+
# If parsing fails or it doesn't look like a table, return None
|
249 |
+
logging.info("Content does not appear to be a table or parsing failed. Treating as plain text.")
|
250 |
+
return None
|
251 |
+
|
252 |
def process_document(contents, filename):
|
253 |
"""Processes uploaded file content (PDF or DOCX) and returns text, or None and error message."""
|
254 |
if contents is None:
|
|
|
291 |
logging.error(f"Error processing document {filename}: {e}", exc_info=True)
|
292 |
return None, f"Error processing file {filename}: {str(e)}"
|
293 |
|
294 |
+
def get_combined_uploaded_text(session_id, file_dict):
|
295 |
+
"""Combines text content of files in the provided dictionary for a session."""
|
296 |
with data_lock:
|
297 |
+
session_files = file_dict.get(session_id, {})
|
298 |
if not session_files:
|
299 |
return ""
|
300 |
+
# Combine content, adding filenames for context if multiple files
|
301 |
+
if len(session_files) > 1:
|
302 |
+
return "\n\n--- FILE BREAK ---\n\n".join(
|
303 |
+
f"**File: {fname}**\n\n{content}" for fname, content in session_files.items()
|
304 |
+
)
|
305 |
+
else:
|
306 |
+
return next(iter(session_files.values()), "")
|
307 |
|
|
|
|
|
|
|
308 |
|
309 |
+
def generate_ai_document(session_id, doc_type, input_docs, context_docs=None):
|
310 |
+
"""Generates document using Gemini AI. Returns generated content and format ('text' or 'dataframe')."""
|
311 |
if not model:
|
312 |
logging.error(f"[{session_id}] Gemini AI model not initialized.")
|
313 |
+
return "Error: AI Model not configured. Please check API Key.", 'text'
|
314 |
if not input_docs or not any(doc.strip() for doc in input_docs if doc):
|
315 |
logging.warning(f"[{session_id}] generate_ai_document called for {doc_type} with no valid input documents.")
|
316 |
+
return f"Error: Missing required input document(s) for {doc_type} generation.", 'text'
|
317 |
|
318 |
combined_input = "\n\n---\n\n".join(filter(None, input_docs))
|
319 |
combined_context = "\n\n---\n\n".join(filter(None, context_docs)) if context_docs else ""
|
320 |
|
321 |
+
# Define expected output format based on doc_type
|
322 |
+
is_spreadsheet_type = doc_type in ["Shred", "Pink Review", "Red Review", "Gold Review", "LOE", "Virtual Board"]
|
323 |
+
output_format_instruction = """**Output Format:** Structure the output as a clear, parseable Markdown table. Use '|' as the column delimiter. Define meaningful column headers relevant to the task (e.g., PWS_Section, Requirement, Action_Verb for Shred; Section, Requirement, Compliance_Status, Finding, Recommendation for Reviews; Section, Task, Estimated_Hours, Resource_Type for LOE). Ensure each row corresponds to a distinct item (e.g., requirement, finding, task).""" if is_spreadsheet_type else """**Output Format:** Write professional, compelling proposal prose. Use clear paragraphs and standard formatting. Address all requirements logically. Avoid tables unless explicitly part of the proposal structure."""
|
324 |
+
|
325 |
prompt = f"""**Objective:** Generate the '{doc_type}' document.
|
326 |
+
**Your Role:** Act as an expert proposal writer/analyst specialized in government contracting.
|
327 |
**Core Instructions:**
|
328 |
+
1. **Adhere Strictly to the Task:** Generate *only* the content for the '{doc_type}'. Do not add introductions, summaries, explanations, or conversational filler unless it's part of the requested document format itself (e.g., an executive summary within a proposal draft).
|
329 |
+
2. **Follow Format Guidelines:** {output_format_instruction}
|
330 |
+
3. **Content Requirements:**
|
331 |
+
* **Shred:** Identify requirements (explicit and implied), action verbs (shall, will, must, provide, perform, etc.), and PWS section references.
|
332 |
+
* **Proposal Sections (Pink, Red, Gold):** Write compliant and compelling content. Directly address requirements from the Context Document(s). Detail the 'how' (approach, methodology, tools). Incorporate win themes, strengths, and discriminators. Substantiate claims. Use active voice ("Our team will..."). Ensure compliance with evaluation criteria (e.g., Section L/M). Clearly map responses back to PWS requirements.
|
333 |
+
* **Reviews (Pink, Red, Gold):** Evaluate the submitted draft against the requirements (Shred/PWS) and previous review findings (if applicable). Identify compliance issues, gaps, weaknesses, and areas for improvement. Provide actionable recommendations. Be specific and reference relevant sections.
|
334 |
+
* **LOE:** Estimate the Level of Effort (hours, resource types) required to fulfill each major task or requirement identified in the Shred/PWS. Justify estimates briefly if necessary.
|
335 |
+
* **Virtual Board:** Simulate a source selection evaluation. Assess the final proposal against the PWS/Shred and evaluation criteria (Sec L/M). Assign strengths, weaknesses, deficiencies, risks. Provide a summary evaluation.
|
336 |
+
4. **Utilize Provided Documents:**
|
337 |
+
* **Context Document(s):** These provide the baseline or reference material (e.g., Shredded Requirements, PWS Section L/M, Previous Review Findings). Refer to them diligently.
|
338 |
+
* **Primary Input Document(s):** This is the main subject of the task (e.g., the PWS text to be Shredded, the Pink draft to be Reviewed, the Red Review findings to incorporate into the Gold draft). Analyze and process this document according to the task.
|
339 |
**Provided Documents:**
|
340 |
+
**Context Document(s):**
|
341 |
```text
|
342 |
{combined_context if combined_context else "N/A"}
|
343 |
```
|
344 |
+
**Primary Input Document(s):**
|
345 |
```text
|
346 |
{combined_input}
|