Update app.py
Browse files
app.py
CHANGED
|
@@ -10,7 +10,6 @@ from retry import retry
|
|
| 10 |
import time
|
| 11 |
from datetime import datetime
|
| 12 |
import base64
|
| 13 |
-
import uuid
|
| 14 |
|
| 15 |
# Print statement to confirm script initialization
|
| 16 |
print("Starting Project Closure Readiness Evaluator app...")
|
|
@@ -65,10 +64,12 @@ else:
|
|
| 65 |
def init_salesforce():
|
| 66 |
if not SALESFORCE_AVAILABLE:
|
| 67 |
logging.error("Salesforce library not available. Skipping connection.")
|
| 68 |
-
return None,
|
| 69 |
|
| 70 |
try:
|
| 71 |
-
logging.info("Attempting to connect to Salesforce
|
|
|
|
|
|
|
| 72 |
sf = Salesforce(
|
| 73 |
username=SF_USERNAME,
|
| 74 |
password=SF_PASSWORD,
|
|
@@ -76,55 +77,13 @@ def init_salesforce():
|
|
| 76 |
instance_url=SF_INSTANCE_URL
|
| 77 |
)
|
| 78 |
logging.info("Salesforce connected successfully")
|
| 79 |
-
|
| 80 |
# Test read access on Project_Closure_Handover__c
|
| 81 |
test_query = sf.query("SELECT Id FROM Project_Closure_Handover__c LIMIT 1")
|
| 82 |
-
logging.info(f"Test query result (read access
|
| 83 |
-
|
| 84 |
-
# Describe Project_Closure_Handover__c to find the related object for Project_ID__c
|
| 85 |
object_description = sf.Project_Closure_Handover__c.describe()
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
if field['name'] == 'Project_ID__c':
|
| 89 |
-
project_id_field = field
|
| 90 |
-
break
|
| 91 |
-
|
| 92 |
-
if not project_id_field:
|
| 93 |
-
logging.error("Project_ID__c field not found on Project_Closure_Handover__c")
|
| 94 |
-
return None, None, "Project_ID__c field not found on Project_Closure_Handover__c"
|
| 95 |
-
|
| 96 |
-
# Check if Project_ID__c is a reference (Lookup) field
|
| 97 |
-
if not project_id_field['referenceTo']:
|
| 98 |
-
logging.error("Project_ID__c is not a reference field. It must be a Lookup or Master-Detail field.")
|
| 99 |
-
return None, None, "Project_ID__c is not a reference field"
|
| 100 |
-
|
| 101 |
-
related_object = project_id_field['referenceTo'][0] # e.g., 'Projects__c'
|
| 102 |
-
logging.info(f"Project_ID__c references object: {related_object}")
|
| 103 |
-
|
| 104 |
-
# Test read access on the related object
|
| 105 |
-
related_query = sf.query(f"SELECT Id FROM {related_object} LIMIT 1")
|
| 106 |
-
logging.info(f"{related_object} test query result (read access): {related_query}")
|
| 107 |
-
|
| 108 |
-
# Describe the related object to log its fields (for debugging)
|
| 109 |
-
related_object_description = getattr(sf, related_object).describe()
|
| 110 |
-
related_fields = [field['name'] for field in related_object_description['fields']]
|
| 111 |
-
logging.info(f"Fields on {related_object}: {related_fields}")
|
| 112 |
-
|
| 113 |
-
# Log some sample data from the related object to confirm records exist
|
| 114 |
-
sample_data_query = sf.query(f"SELECT Id, Name FROM {related_object} LIMIT 5")
|
| 115 |
-
logging.info(f"Sample data from {related_object}: {sample_data_query['records']}")
|
| 116 |
-
|
| 117 |
-
# Test object permissions for Project_Closure_Handover__c
|
| 118 |
-
permissions = {
|
| 119 |
-
"createable": object_description.get("createable", False),
|
| 120 |
-
"updateable": object_description.get("updateable", False)
|
| 121 |
-
}
|
| 122 |
-
logging.info(f"Object permissions for Project_Closure_Handover__c: {permissions}")
|
| 123 |
-
if not permissions["createable"]:
|
| 124 |
-
logging.error("User lacks create permission on Project_Closure_Handover__c")
|
| 125 |
-
return None, None, "User lacks create permission on Project_Closure_Handover__c"
|
| 126 |
-
|
| 127 |
-
return sf, related_object, "Salesforce connected successfully"
|
| 128 |
except SalesforceError as e:
|
| 129 |
logging.error(f"Salesforce authentication failed: {str(e)}")
|
| 130 |
logging.error("Possible issues: Incorrect credentials, IP restrictions, or insufficient permissions.")
|
|
@@ -134,37 +93,6 @@ def init_salesforce():
|
|
| 134 |
logging.error("Check your Salesforce org settings, network restrictions, or API access.")
|
| 135 |
raise
|
| 136 |
|
| 137 |
-
# Function to get the Salesforce record ID for a project based on its custom identifier
|
| 138 |
-
def get_project_record_id(sf, related_object, project_code):
|
| 139 |
-
try:
|
| 140 |
-
# Sanitize the project_code to prevent SOQL injection
|
| 141 |
-
project_code = bleach.clean(project_code.strip())
|
| 142 |
-
|
| 143 |
-
# Try multiple fields to match the project_code
|
| 144 |
-
fields_to_try = ['Name', 'Project_Code__c', 'External_ID__c']
|
| 145 |
-
record_id = None
|
| 146 |
-
for field in fields_to_try:
|
| 147 |
-
query = f"SELECT Id FROM {related_object} WHERE {field} = '{project_code}' LIMIT 1"
|
| 148 |
-
logging.info(f"Trying query: {query}")
|
| 149 |
-
result = sf.query(query)
|
| 150 |
-
logging.debug(f"Query result for {field}: {result}")
|
| 151 |
-
if result['totalSize'] > 0:
|
| 152 |
-
record_id = result['records'][0]['Id']
|
| 153 |
-
logging.info(f"Found project record ID: {record_id} for {field}: {project_code} in {related_object}")
|
| 154 |
-
return record_id, "Success"
|
| 155 |
-
else:
|
| 156 |
-
logging.info(f"No project found with {field}: {project_code} in {related_object}")
|
| 157 |
-
|
| 158 |
-
# If no record is found after trying all fields
|
| 159 |
-
logging.error(f"No project found with {project_code} in {related_object} after trying fields: {fields_to_try}")
|
| 160 |
-
return None, f"No project found with {project_code} in {related_object} after trying fields: {fields_to_try}"
|
| 161 |
-
except SalesforceError as e:
|
| 162 |
-
logging.error(f"Failed to query project record: {str(e)}")
|
| 163 |
-
return None, f"Salesforce query error: {str(e)}"
|
| 164 |
-
except Exception as e:
|
| 165 |
-
logging.error(f"Error querying project record: {str(e)}")
|
| 166 |
-
return None, f"Error querying project: {str(e)}"
|
| 167 |
-
|
| 168 |
# Summarize text using Hugging Face Inference API
|
| 169 |
def summarize_text(text, max_length=100, min_length=30):
|
| 170 |
if not HF_AVAILABLE:
|
|
@@ -186,68 +114,48 @@ def summarize_text(text, max_length=100, min_length=30):
|
|
| 186 |
return text # Fallback to original text
|
| 187 |
|
| 188 |
# Create Salesforce record in custom object Project_Closure_Handover__c
|
| 189 |
-
def create_salesforce_record(
|
| 190 |
if not SALESFORCE_AVAILABLE:
|
| 191 |
logging.error("Salesforce library not available. Skipping record creation.")
|
| 192 |
return "Salesforce library not available"
|
| 193 |
|
| 194 |
try:
|
| 195 |
-
sf,
|
| 196 |
-
if not sf
|
| 197 |
logging.error(f"Skipping Salesforce record creation due to connection failure: {connection_message}")
|
| 198 |
return connection_message
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
project_record_id, project_query_message = get_project_record_id(sf, related_object, project_id)
|
| 202 |
-
if not project_record_id:
|
| 203 |
-
logging.error(f"Failed to get project record ID: {project_query_message}")
|
| 204 |
-
return f"Error: {project_query_message}"
|
| 205 |
-
|
| 206 |
-
# Summarize checklist_summary and missing_summary
|
| 207 |
summarized_checklist = summarize_text(checklist_summary)
|
| 208 |
summarized_missing = summarize_text(missing_summary)
|
| 209 |
|
| 210 |
# Ensure inputs are properly formatted
|
| 211 |
-
project_id = str(project_id)[:80] if project_id else "Unknown" # For display purposes
|
| 212 |
score = float(score) if score is not None else 0.0
|
| 213 |
-
checklist_summary = str(checklist_summary)
|
| 214 |
-
summarized_checklist = str(summarized_checklist)
|
| 215 |
-
missing_summary = str(missing_summary)
|
| 216 |
-
summarized_missing = str(summarized_missing)
|
| 217 |
-
status = str(status)
|
| 218 |
-
logs = str(logs)
|
| 219 |
-
qa_report = str(qa_report)
|
| 220 |
-
punch_list_text = str(punch_list_text)
|
| 221 |
missing_documents = len(missing_summary.split(", ")) if missing_summary and missing_summary != "None" else 0
|
| 222 |
open_punch_items = int(open_punch_items) if open_punch_items is not None else 0
|
| 223 |
-
evaluated_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 224 |
-
alert_sent =
|
| 225 |
-
|
|
|
|
| 226 |
|
| 227 |
-
#
|
| 228 |
-
required_fields = {
|
| 229 |
-
"Project_ID__c": project_record_id,
|
| 230 |
-
"Readiness_Score__c": score,
|
| 231 |
-
"Checklist_Summary__c": checklist_summary,
|
| 232 |
-
"Status__c": status,
|
| 233 |
-
"Evaluated_At__c": evaluated_at
|
| 234 |
-
}
|
| 235 |
-
for field, value in required_fields.items():
|
| 236 |
-
if not value:
|
| 237 |
-
logging.error(f"Required field {field} is missing or empty")
|
| 238 |
-
return f"Error: Required field {field} is missing or empty"
|
| 239 |
-
|
| 240 |
-
# Create the record
|
| 241 |
record = {
|
| 242 |
-
"Project_ID__c": project_record_id, # Use the Salesforce record ID
|
| 243 |
"Readiness_Score__c": score,
|
| 244 |
"Checklist_Summary__c": checklist_summary,
|
| 245 |
"Missing_Documents__c": missing_documents,
|
| 246 |
"Status__c": status,
|
| 247 |
"Summarized_Missing_Items__c": summarized_missing,
|
| 248 |
"Alert_Sent__c": alert_sent,
|
| 249 |
-
"Client_PDF_Pack_URL__c": "",
|
| 250 |
-
"Closure_Pack_URL__c": "",
|
| 251 |
"Escalation_Flag__c": escalation_flag,
|
| 252 |
"Evaluated_At__c": evaluated_at,
|
| 253 |
"Logs__c": logs,
|
|
@@ -256,59 +164,57 @@ def create_salesforce_record(project_id, score, checklist_summary, missing_summa
|
|
| 256 |
"QA_Report__c": qa_report
|
| 257 |
}
|
| 258 |
|
| 259 |
-
logging.debug(f"
|
| 260 |
result = sf.Project_Closure_Handover__c.create(record)
|
|
|
|
| 261 |
record_id = result.get('id')
|
| 262 |
-
|
| 263 |
-
logging.error("Failed to retrieve record ID from Salesforce response")
|
| 264 |
-
return "Error: Failed to create Salesforce record"
|
| 265 |
-
|
| 266 |
-
logging.info(f"Successfully created Salesforce record: {record_id}")
|
| 267 |
|
| 268 |
-
# Attach PDF if
|
| 269 |
pdf_download_url = ""
|
| 270 |
if pdf_path and os.path.exists(pdf_path):
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
logging.info(f"Updated record {record_id} with PDF URL: {pdf_download_url}")
|
| 299 |
-
except Exception as e:
|
| 300 |
-
logging.error(f"Failed to attach PDF: {str(e)}")
|
| 301 |
-
return f"Record created (ID: {record_id}), but failed to attach PDF: {str(e)}"
|
| 302 |
else:
|
| 303 |
-
logging.warning(f"No PDF file found at {pdf_path}. Skipping attachment.")
|
| 304 |
|
| 305 |
-
return f"Record created successfully. Record ID: {record_id}. PDF URL: {pdf_download_url}"
|
| 306 |
except SalesforceError as e:
|
| 307 |
-
logging.error(f"Salesforce error: {str(e)}")
|
| 308 |
logging.error("Possible issues: Object permissions, field-level security, validation rules, or required fields.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
return f"Salesforce error: {str(e)}"
|
| 310 |
except Exception as e:
|
| 311 |
-
logging.error(f"Failed to create Salesforce record: {str(e)}")
|
| 312 |
return f"Error creating record: {str(e)}"
|
| 313 |
|
| 314 |
# Clean input to prevent injection attacks
|
|
@@ -318,23 +224,27 @@ def sanitize_input(text):
|
|
| 318 |
return bleach.clean(text.strip())
|
| 319 |
|
| 320 |
# Rule-based completeness engine with weighted scoring
|
| 321 |
-
def evaluate_readiness(
|
| 322 |
try:
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
| 324 |
score = 0
|
| 325 |
missing_items = []
|
| 326 |
checklist_details = []
|
| 327 |
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
logs = sanitize_input(logs)
|
| 334 |
qa_report = sanitize_input(qa_report)
|
| 335 |
punch_list_text = sanitize_input(punch_list_text)
|
| 336 |
|
| 337 |
-
# Process Project Logs
|
| 338 |
log_keywords = r"complete|handover done|finished|closed|successful"
|
| 339 |
negative_keywords = r"issue|pending|incomplete|problem|delay"
|
| 340 |
logs_pass = logs and re.search(log_keywords, logs.lower())
|
|
@@ -343,15 +253,18 @@ def evaluate_readiness(project_id, logs, qa_report, punch_list_text):
|
|
| 343 |
if logs_pass and not logs_negative:
|
| 344 |
score += LOGS_WEIGHT
|
| 345 |
checklist_details.append("Logs: Completed")
|
|
|
|
| 346 |
elif logs_pass and logs_negative:
|
| 347 |
-
score += LOGS_WEIGHT // 2
|
| 348 |
missing_items.append("Issues detected in logs")
|
| 349 |
checklist_details.append("Logs: Partially Completed (issues detected)")
|
|
|
|
| 350 |
else:
|
| 351 |
missing_items.append("Project Logs Incomplete")
|
| 352 |
checklist_details.append("Logs: Pending")
|
|
|
|
| 353 |
|
| 354 |
-
# Process QA Report
|
| 355 |
qa_keywords = r"approved|passed|cleared"
|
| 356 |
qa_pass = qa_report and re.search(qa_keywords, qa_report.lower())
|
| 357 |
if qa_pass:
|
|
@@ -360,8 +273,9 @@ def evaluate_readiness(project_id, logs, qa_report, punch_list_text):
|
|
| 360 |
else:
|
| 361 |
missing_items.append("QA Approval Missing")
|
| 362 |
checklist_details.append("QA Report: Pending")
|
|
|
|
| 363 |
|
| 364 |
-
# Process Punch List
|
| 365 |
punch_keywords = r"none|resolved|closed|no issues"
|
| 366 |
punch_pass = punch_list_text and re.search(punch_keywords, punch_list_text.lower())
|
| 367 |
if punch_pass:
|
|
@@ -370,16 +284,26 @@ def evaluate_readiness(project_id, logs, qa_report, punch_list_text):
|
|
| 370 |
else:
|
| 371 |
missing_items.append("Open Punch Points Detected")
|
| 372 |
checklist_details.append("Punch List: Pending")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
-
|
| 375 |
escalated = score < 70 or open_punch_items > 2
|
|
|
|
|
|
|
|
|
|
| 376 |
status = "Escalated" if escalated else ("Completed" if not missing_items else "In Progress")
|
| 377 |
-
checklist_status = "Escalated" if escalated else ("Completed" if not missing_items else "Pending")
|
| 378 |
|
|
|
|
| 379 |
checklist_summary = "\n".join(checklist_details)
|
| 380 |
missing_summary = "None" if not missing_items else ", ".join(missing_items)
|
| 381 |
|
|
|
|
| 382 |
color_class = "red" if score < 70 else "yellow" if score <= 90 else "green"
|
|
|
|
| 383 |
progress_bar = f"""
|
| 384 |
<div class="progress-container">
|
| 385 |
<div class="progress-bar {color_class}" style="width: {score}%;">
|
|
@@ -388,42 +312,44 @@ def evaluate_readiness(project_id, logs, qa_report, punch_list_text):
|
|
| 388 |
</div>
|
| 389 |
"""
|
| 390 |
|
| 391 |
-
|
| 392 |
-
return (project_id, score, checklist_summary, missing_summary, status, progress_bar, escalated, logs, qa_report, punch_list_text, open_punch_items, checklist_status)
|
| 393 |
except Exception as e:
|
| 394 |
logging.error(f"Error in evaluate_readiness: {str(e)}")
|
| 395 |
raise
|
| 396 |
|
| 397 |
# Generate PDF report with signature slots
|
| 398 |
-
def generate_pdf(
|
| 399 |
try:
|
| 400 |
-
|
|
|
|
| 401 |
score = str(float(score)) if score is not None else "0"
|
| 402 |
-
checklist_summary = str(checklist_summary)
|
| 403 |
-
checklist_status = str(checklist_status)
|
| 404 |
|
|
|
|
| 405 |
checklist_summary = checklist_summary.encode('ascii', 'ignore').decode('ascii')
|
| 406 |
checklist_status = checklist_status.encode('ascii', 'ignore').decode('ascii')
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
|
|
|
| 410 |
|
|
|
|
| 411 |
c = canvas.Canvas(pdf_path, pagesize=letter)
|
| 412 |
c.setFont("Times-Bold", 16)
|
| 413 |
c.drawString(50, 750, "Project Closure Readiness Report")
|
| 414 |
c.setFont("Times-Roman", 12)
|
| 415 |
-
c.drawString(50,
|
| 416 |
-
c.drawString(50,
|
| 417 |
-
c.drawString(50,
|
| 418 |
-
|
| 419 |
-
y = 650
|
| 420 |
BOTTOM_MARGIN = 50
|
| 421 |
for line in checklist_summary.split("\n"):
|
| 422 |
if y < BOTTOM_MARGIN:
|
| 423 |
c.showPage()
|
| 424 |
c.setFont("Times-Roman", 12)
|
| 425 |
y = 750
|
| 426 |
-
c.drawString(50, y, line
|
| 427 |
y -= 20
|
| 428 |
if y - 40 < BOTTOM_MARGIN:
|
| 429 |
c.showPage()
|
|
@@ -437,6 +363,7 @@ def generate_pdf(project_id, score, checklist_summary, missing_summary, checklis
|
|
| 437 |
c.drawString(50, y - 60, "Date: ____________________")
|
| 438 |
c.save()
|
| 439 |
|
|
|
|
| 440 |
if not os.path.exists(pdf_path):
|
| 441 |
logging.error(f"PDF file {pdf_path} was not created")
|
| 442 |
raise FileNotFoundError(f"PDF file {pdf_path} was not created")
|
|
@@ -447,7 +374,7 @@ def generate_pdf(project_id, score, checklist_summary, missing_summary, checklis
|
|
| 447 |
logging.error(f"Error in generate_pdf: {str(e)}")
|
| 448 |
raise
|
| 449 |
|
| 450 |
-
# Gradio interface
|
| 451 |
with gr.Blocks(css="""
|
| 452 |
.progress-container { background-color: #f0f0f0; width: 100%; height: 20px; border-radius: 5px; overflow: hidden; position: relative; }
|
| 453 |
.progress-bar { height: 100%; text-align: center; line-height: 20px; color: #000; font-size: 12px; }
|
|
@@ -463,7 +390,6 @@ with gr.Blocks(css="""
|
|
| 463 |
)
|
| 464 |
with gr.Row():
|
| 465 |
with gr.Column(scale=2):
|
| 466 |
-
project_id_input = gr.Textbox(label="Project ID", placeholder="Enter Project ID (e.g., PRJ-001)", value="PRJ-001")
|
| 467 |
logs_input = gr.Textbox(label="Project Logs", lines=5, placeholder="Enter project logs (e.g., 'Project complete, handover done')")
|
| 468 |
qa_input = gr.Dropdown(
|
| 469 |
label="QA Report",
|
|
@@ -479,7 +405,6 @@ with gr.Blocks(css="""
|
|
| 479 |
)
|
| 480 |
submit_btn = gr.Button("Evaluate and Generate PDF")
|
| 481 |
with gr.Column(scale=3):
|
| 482 |
-
project_id_output = gr.Textbox(label="Project ID")
|
| 483 |
score_output = gr.Number(label="Readiness Score (%)")
|
| 484 |
progress_output = gr.HTML(label="Alert Indicator: Progress")
|
| 485 |
gr.Markdown("Color-coded readiness: Red (<70%), Yellow (70-90%), Green (>90%)")
|
|
@@ -491,26 +416,25 @@ with gr.Blocks(css="""
|
|
| 491 |
open_punch_items_output = gr.Number(label="Open Punch Items (Debug)")
|
| 492 |
pdf_output = gr.File(label="Download PDF Report", type="filepath", interactive=False)
|
| 493 |
pdf_debug = gr.Textbox(label="PDF Debug Output")
|
| 494 |
-
salesforce_output = gr.Textbox(label="Salesforce Status")
|
| 495 |
|
|
|
|
| 496 |
submit_btn.click(
|
| 497 |
fn=evaluate_readiness,
|
| 498 |
-
inputs=[
|
| 499 |
outputs=[
|
| 500 |
-
|
| 501 |
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, status_output
|
| 502 |
]
|
| 503 |
).then(
|
| 504 |
fn=generate_pdf,
|
| 505 |
-
inputs=[
|
| 506 |
outputs=[pdf_output, pdf_debug]
|
| 507 |
).then(
|
| 508 |
fn=create_salesforce_record,
|
| 509 |
inputs=[
|
| 510 |
-
|
| 511 |
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, pdf_output
|
| 512 |
],
|
| 513 |
-
outputs=
|
| 514 |
)
|
| 515 |
-
|
| 516 |
-
demo.launch()
|
|
|
|
| 10 |
import time
|
| 11 |
from datetime import datetime
|
| 12 |
import base64
|
|
|
|
| 13 |
|
| 14 |
# Print statement to confirm script initialization
|
| 15 |
print("Starting Project Closure Readiness Evaluator app...")
|
|
|
|
| 64 |
def init_salesforce():
|
| 65 |
if not SALESFORCE_AVAILABLE:
|
| 66 |
logging.error("Salesforce library not available. Skipping connection.")
|
| 67 |
+
return None, "Salesforce library not available"
|
| 68 |
|
| 69 |
try:
|
| 70 |
+
logging.info("Attempting to connect to Salesforce with the following credentials:")
|
| 71 |
+
logging.info(f"Username: {SF_USERNAME}")
|
| 72 |
+
logging.info(f"Instance URL: {SF_INSTANCE_URL}")
|
| 73 |
sf = Salesforce(
|
| 74 |
username=SF_USERNAME,
|
| 75 |
password=SF_PASSWORD,
|
|
|
|
| 77 |
instance_url=SF_INSTANCE_URL
|
| 78 |
)
|
| 79 |
logging.info("Salesforce connected successfully")
|
|
|
|
| 80 |
# Test read access on Project_Closure_Handover__c
|
| 81 |
test_query = sf.query("SELECT Id FROM Project_Closure_Handover__c LIMIT 1")
|
| 82 |
+
logging.info(f"Test query result (read access): {test_query}")
|
| 83 |
+
# Test create access by attempting to describe the object and check permissions
|
|
|
|
| 84 |
object_description = sf.Project_Closure_Handover__c.describe()
|
| 85 |
+
logging.info(f"Object description: {object_description}")
|
| 86 |
+
return sf, "Salesforce connected successfully"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
except SalesforceError as e:
|
| 88 |
logging.error(f"Salesforce authentication failed: {str(e)}")
|
| 89 |
logging.error("Possible issues: Incorrect credentials, IP restrictions, or insufficient permissions.")
|
|
|
|
| 93 |
logging.error("Check your Salesforce org settings, network restrictions, or API access.")
|
| 94 |
raise
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# Summarize text using Hugging Face Inference API
|
| 97 |
def summarize_text(text, max_length=100, min_length=30):
|
| 98 |
if not HF_AVAILABLE:
|
|
|
|
| 114 |
return text # Fallback to original text
|
| 115 |
|
| 116 |
# Create Salesforce record in custom object Project_Closure_Handover__c
|
| 117 |
+
def create_salesforce_record(score, checklist_summary, missing_summary, status, escalated, logs, qa_report, punch_list_text, open_punch_items, pdf_path=None):
|
| 118 |
if not SALESFORCE_AVAILABLE:
|
| 119 |
logging.error("Salesforce library not available. Skipping record creation.")
|
| 120 |
return "Salesforce library not available"
|
| 121 |
|
| 122 |
try:
|
| 123 |
+
sf, connection_message = init_salesforce()
|
| 124 |
+
if not sf:
|
| 125 |
logging.error(f"Skipping Salesforce record creation due to connection failure: {connection_message}")
|
| 126 |
return connection_message
|
| 127 |
|
| 128 |
+
# Summarize checklist_summary and missing_summary using Hugging Face
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
summarized_checklist = summarize_text(checklist_summary)
|
| 130 |
summarized_missing = summarize_text(missing_summary)
|
| 131 |
|
| 132 |
# Ensure inputs are properly formatted
|
|
|
|
| 133 |
score = float(score) if score is not None else 0.0
|
| 134 |
+
checklist_summary = str(checklist_summary) if checklist_summary else ""
|
| 135 |
+
summarized_checklist = str(summarized_checklist) if summarized_checklist else ""
|
| 136 |
+
missing_summary = str(missing_summary) if missing_summary else ""
|
| 137 |
+
summarized_missing = str(summarized_missing) if summarized_missing else ""
|
| 138 |
+
status = str(status) if status else ""
|
| 139 |
+
logs = str(logs) if logs else ""
|
| 140 |
+
qa_report = str(qa_report) if qa_report else ""
|
| 141 |
+
punch_list_text = str(punch_list_text) if punch_list_text else ""
|
| 142 |
missing_documents = len(missing_summary.split(", ")) if missing_summary and missing_summary != "None" else 0
|
| 143 |
open_punch_items = int(open_punch_items) if open_punch_items is not None else 0
|
| 144 |
+
evaluated_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") # Salesforce Date/Time format
|
| 145 |
+
alert_sent = str(bool(escalated)).lower() # Converts True/False to "true"/"false"
|
| 146 |
+
logging.info(f"Setting Alert_Sent__c to: {alert_sent}")
|
| 147 |
+
escalation_flag = str(bool(escalated)).lower() # Ensure this is also a proper boolean string
|
| 148 |
|
| 149 |
+
# Create the record in Project_Closure_Handover__c without the PDF URL for now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
record = {
|
|
|
|
| 151 |
"Readiness_Score__c": score,
|
| 152 |
"Checklist_Summary__c": checklist_summary,
|
| 153 |
"Missing_Documents__c": missing_documents,
|
| 154 |
"Status__c": status,
|
| 155 |
"Summarized_Missing_Items__c": summarized_missing,
|
| 156 |
"Alert_Sent__c": alert_sent,
|
| 157 |
+
"Client_PDF_Pack_URL__c": "", # Will be updated after attachment is created
|
| 158 |
+
"Closure_Pack_URL__c": "", # Placeholder; update if you have a closure pack URL
|
| 159 |
"Escalation_Flag__c": escalation_flag,
|
| 160 |
"Evaluated_At__c": evaluated_at,
|
| 161 |
"Logs__c": logs,
|
|
|
|
| 164 |
"QA_Report__c": qa_report
|
| 165 |
}
|
| 166 |
|
| 167 |
+
logging.debug(f"Attempting to create Salesforce record in Project_Closure_Handover__c with data: {record}")
|
| 168 |
result = sf.Project_Closure_Handover__c.create(record)
|
| 169 |
+
logging.info(f"Successfully created Salesforce record: {result}")
|
| 170 |
record_id = result.get('id')
|
| 171 |
+
logging.info(f"Record ID: {record_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
# Attach the PDF to the record if pdf_path exists and update the URL
|
| 174 |
pdf_download_url = ""
|
| 175 |
if pdf_path and os.path.exists(pdf_path):
|
| 176 |
+
logging.info(f"Attaching PDF to Salesforce record ID: {record_id}")
|
| 177 |
+
with open(pdf_path, "rb") as pdf_file:
|
| 178 |
+
pdf_content = pdf_file.read()
|
| 179 |
+
pdf_base64 = base64.b64encode(pdf_content).decode('utf-8')
|
| 180 |
+
|
| 181 |
+
attachment = {
|
| 182 |
+
"ParentId": record_id,
|
| 183 |
+
"Name": "Readiness_Report.pdf",
|
| 184 |
+
"Body": pdf_base64,
|
| 185 |
+
"ContentType": "application/pdf"
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
attachment_result = sf.Attachment.create(attachment)
|
| 189 |
+
logging.info(f"Successfully attached PDF to record: {attachment_result}")
|
| 190 |
+
attachment_id = attachment_result.get('id')
|
| 191 |
+
logging.info(f"Attachment ID: {attachment_id}")
|
| 192 |
+
|
| 193 |
+
# Construct the direct download URL for the attachment
|
| 194 |
+
pdf_download_url = f"{SF_INSTANCE_URL}/servlet/servlet.FileDownload?file={attachment_id}"
|
| 195 |
+
logging.info(f"Generated PDF download URL: {pdf_download_url}")
|
| 196 |
+
|
| 197 |
+
# Update the Project_Closure_Handover__c record with the PDF download URL
|
| 198 |
+
update_data = {
|
| 199 |
+
"Client_PDF_Pack_URL__c": pdf_download_url
|
| 200 |
+
}
|
| 201 |
+
sf.Project_Closure_Handover__c.update(record_id, update_data)
|
| 202 |
+
logging.info(f"Updated record {record_id} with Client_PDF_Pack_URL__c: {pdf_download_url}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
else:
|
| 204 |
+
logging.warning(f"No PDF file found at {pdf_path}. Skipping attachment and URL update.")
|
| 205 |
|
| 206 |
+
return f"Record created successfully. Record ID: {record_id}. PDF attached and URL set to: {pdf_download_url}"
|
| 207 |
except SalesforceError as e:
|
| 208 |
+
logging.error(f"Salesforce error while creating Project_Closure_Handover__c record: {str(e)}")
|
| 209 |
logging.error("Possible issues: Object permissions, field-level security, validation rules, or required fields.")
|
| 210 |
+
logging.error("Check the following in your Salesforce org:")
|
| 211 |
+
logging.error("- Ensure the user has Create and Edit permission on Project_Closure_Handover__c.")
|
| 212 |
+
logging.error("- Ensure the user has permission to create and read Attachments.")
|
| 213 |
+
logging.error("- Verify field-level security for all fields in the record.")
|
| 214 |
+
logging.error("- Check for validation rules or required fields that might be failing.")
|
| 215 |
return f"Salesforce error: {str(e)}"
|
| 216 |
except Exception as e:
|
| 217 |
+
logging.error(f"Failed to create Salesforce Project_Closure_Handover__c record: {str(e)}")
|
| 218 |
return f"Error creating record: {str(e)}"
|
| 219 |
|
| 220 |
# Clean input to prevent injection attacks
|
|
|
|
| 224 |
return bleach.clean(text.strip())
|
| 225 |
|
| 226 |
# Rule-based completeness engine with weighted scoring
|
| 227 |
+
def evaluate_readiness(logs, qa_report, punch_list_text):
|
| 228 |
try:
|
| 229 |
+
# Log inputs for debugging
|
| 230 |
+
logging.info(f"Inputs - Logs: {logs}, QA Report: {qa_report}, Punch List: {punch_list_text}")
|
| 231 |
+
|
| 232 |
+
# Initialize score and lists for tracking
|
| 233 |
score = 0
|
| 234 |
missing_items = []
|
| 235 |
checklist_details = []
|
| 236 |
|
| 237 |
+
# Define weights for scoring
|
| 238 |
+
LOGS_WEIGHT = 30 # 30% weight for logs
|
| 239 |
+
QA_WEIGHT = 40 # 40% weight for QA report
|
| 240 |
+
PUNCH_WEIGHT = 30 # 30% weight for punch list
|
| 241 |
|
| 242 |
+
# Sanitize inputs
|
| 243 |
logs = sanitize_input(logs)
|
| 244 |
qa_report = sanitize_input(qa_report)
|
| 245 |
punch_list_text = sanitize_input(punch_list_text)
|
| 246 |
|
| 247 |
+
# Process Project Logs (30% weight)
|
| 248 |
log_keywords = r"complete|handover done|finished|closed|successful"
|
| 249 |
negative_keywords = r"issue|pending|incomplete|problem|delay"
|
| 250 |
logs_pass = logs and re.search(log_keywords, logs.lower())
|
|
|
|
| 253 |
if logs_pass and not logs_negative:
|
| 254 |
score += LOGS_WEIGHT
|
| 255 |
checklist_details.append("Logs: Completed")
|
| 256 |
+
logging.info(f"Logs Check: Pass (positive keywords found, no negative keywords), Score: {score}%")
|
| 257 |
elif logs_pass and logs_negative:
|
| 258 |
+
score += LOGS_WEIGHT // 2 # Partial score for mixed signals
|
| 259 |
missing_items.append("Issues detected in logs")
|
| 260 |
checklist_details.append("Logs: Partially Completed (issues detected)")
|
| 261 |
+
logging.info(f"Logs Check: Partial Pass (positive and negative keywords found), Score: {score}%")
|
| 262 |
else:
|
| 263 |
missing_items.append("Project Logs Incomplete")
|
| 264 |
checklist_details.append("Logs: Pending")
|
| 265 |
+
logging.info(f"Logs Check: Fail (no positive keywords or only negative keywords), Score: {score}%")
|
| 266 |
|
| 267 |
+
# Process QA Report (40% weight)
|
| 268 |
qa_keywords = r"approved|passed|cleared"
|
| 269 |
qa_pass = qa_report and re.search(qa_keywords, qa_report.lower())
|
| 270 |
if qa_pass:
|
|
|
|
| 273 |
else:
|
| 274 |
missing_items.append("QA Approval Missing")
|
| 275 |
checklist_details.append("QA Report: Pending")
|
| 276 |
+
logging.info(f"QA Check: {'Pass' if qa_pass else 'Fail'}, Score so far: {score}%")
|
| 277 |
|
| 278 |
+
# Process Punch List (30% weight)
|
| 279 |
punch_keywords = r"none|resolved|closed|no issues"
|
| 280 |
punch_pass = punch_list_text and re.search(punch_keywords, punch_list_text.lower())
|
| 281 |
if punch_pass:
|
|
|
|
| 284 |
else:
|
| 285 |
missing_items.append("Open Punch Points Detected")
|
| 286 |
checklist_details.append("Punch List: Pending")
|
| 287 |
+
logging.info(f"Punch List Check: {'Pass' if punch_pass else 'Fail'}, Final Score: {score}%")
|
| 288 |
+
|
| 289 |
+
# Calculate open punch items
|
| 290 |
+
open_punch_items = 0 if punch_pass else 1 # Simplified for dropdown input
|
| 291 |
|
| 292 |
+
# Enhanced escalation logic
|
| 293 |
escalated = score < 70 or open_punch_items > 2
|
| 294 |
+
logging.info(f"Escalation Check: Score < 70 ({score < 70}), Open Punch Items > 2 ({open_punch_items > 2}), Escalated: {escalated}")
|
| 295 |
+
|
| 296 |
+
# Map "Pending" to "In Progress" for Salesforce Status__c picklist
|
| 297 |
status = "Escalated" if escalated else ("Completed" if not missing_items else "In Progress")
|
| 298 |
+
checklist_status = "Escalated" if escalated else ("Completed" if not missing_items else "Pending") # For UI display
|
| 299 |
|
| 300 |
+
# Build summaries
|
| 301 |
checklist_summary = "\n".join(checklist_details)
|
| 302 |
missing_summary = "None" if not missing_items else ", ".join(missing_items)
|
| 303 |
|
| 304 |
+
# Generate progress bar HTML
|
| 305 |
color_class = "red" if score < 70 else "yellow" if score <= 90 else "green"
|
| 306 |
+
logging.info(f"Readiness Score: {score}%, Color Class: {color_class}")
|
| 307 |
progress_bar = f"""
|
| 308 |
<div class="progress-container">
|
| 309 |
<div class="progress-bar {color_class}" style="width: {score}%;">
|
|
|
|
| 312 |
</div>
|
| 313 |
"""
|
| 314 |
|
| 315 |
+
return (score, checklist_summary, missing_summary, status, progress_bar, escalated, logs, qa_report, punch_list_text, open_punch_items, checklist_status)
|
|
|
|
| 316 |
except Exception as e:
|
| 317 |
logging.error(f"Error in evaluate_readiness: {str(e)}")
|
| 318 |
raise
|
| 319 |
|
| 320 |
# Generate PDF report with signature slots
|
| 321 |
+
def generate_pdf(score, checklist_summary, missing_summary, checklist_status, logs, qa_report, punch_list_text):
|
| 322 |
try:
|
| 323 |
+
# Sanitize inputs
|
| 324 |
+
logging.info("Sanitizing inputs for PDF generation")
|
| 325 |
score = str(float(score)) if score is not None else "0"
|
| 326 |
+
checklist_summary = str(checklist_summary) if checklist_summary is not None else ""
|
| 327 |
+
checklist_status = str(checklist_status) if checklist_status is not None else ""
|
| 328 |
|
| 329 |
+
# Remove non-ASCII characters
|
| 330 |
checklist_summary = checklist_summary.encode('ascii', 'ignore').decode('ascii')
|
| 331 |
checklist_status = checklist_status.encode('ascii', 'ignore').decode('ascii')
|
| 332 |
|
| 333 |
+
# Define the temporary file path
|
| 334 |
+
pdf_path = "readiness_report.pdf"
|
| 335 |
+
logging.info(f"Creating PDF at {pdf_path}")
|
| 336 |
|
| 337 |
+
# Create the PDF
|
| 338 |
c = canvas.Canvas(pdf_path, pagesize=letter)
|
| 339 |
c.setFont("Times-Bold", 16)
|
| 340 |
c.drawString(50, 750, "Project Closure Readiness Report")
|
| 341 |
c.setFont("Times-Roman", 12)
|
| 342 |
+
c.drawString(50, 720, f"Readiness Score: {score}%")
|
| 343 |
+
c.drawString(50, 700, f"Status: {checklist_status}")
|
| 344 |
+
c.drawString(50, 680, "Checklist Summary:")
|
| 345 |
+
y = 660
|
|
|
|
| 346 |
BOTTOM_MARGIN = 50
|
| 347 |
for line in checklist_summary.split("\n"):
|
| 348 |
if y < BOTTOM_MARGIN:
|
| 349 |
c.showPage()
|
| 350 |
c.setFont("Times-Roman", 12)
|
| 351 |
y = 750
|
| 352 |
+
c.drawString(50, y, line)
|
| 353 |
y -= 20
|
| 354 |
if y - 40 < BOTTOM_MARGIN:
|
| 355 |
c.showPage()
|
|
|
|
| 363 |
c.drawString(50, y - 60, "Date: ____________________")
|
| 364 |
c.save()
|
| 365 |
|
| 366 |
+
# Confirm the file exists
|
| 367 |
if not os.path.exists(pdf_path):
|
| 368 |
logging.error(f"PDF file {pdf_path} was not created")
|
| 369 |
raise FileNotFoundError(f"PDF file {pdf_path} was not created")
|
|
|
|
| 374 |
logging.error(f"Error in generate_pdf: {str(e)}")
|
| 375 |
raise
|
| 376 |
|
| 377 |
+
# Gradio interface with updated UI
|
| 378 |
with gr.Blocks(css="""
|
| 379 |
.progress-container { background-color: #f0f0f0; width: 100%; height: 20px; border-radius: 5px; overflow: hidden; position: relative; }
|
| 380 |
.progress-bar { height: 100%; text-align: center; line-height: 20px; color: #000; font-size: 12px; }
|
|
|
|
| 390 |
)
|
| 391 |
with gr.Row():
|
| 392 |
with gr.Column(scale=2):
|
|
|
|
| 393 |
logs_input = gr.Textbox(label="Project Logs", lines=5, placeholder="Enter project logs (e.g., 'Project complete, handover done')")
|
| 394 |
qa_input = gr.Dropdown(
|
| 395 |
label="QA Report",
|
|
|
|
| 405 |
)
|
| 406 |
submit_btn = gr.Button("Evaluate and Generate PDF")
|
| 407 |
with gr.Column(scale=3):
|
|
|
|
| 408 |
score_output = gr.Number(label="Readiness Score (%)")
|
| 409 |
progress_output = gr.HTML(label="Alert Indicator: Progress")
|
| 410 |
gr.Markdown("Color-coded readiness: Red (<70%), Yellow (70-90%), Green (>90%)")
|
|
|
|
| 416 |
open_punch_items_output = gr.Number(label="Open Punch Items (Debug)")
|
| 417 |
pdf_output = gr.File(label="Download PDF Report", type="filepath", interactive=False)
|
| 418 |
pdf_debug = gr.Textbox(label="PDF Debug Output")
|
|
|
|
| 419 |
|
| 420 |
+
# Chain the evaluation, PDF generation, and Salesforce record creation
|
| 421 |
submit_btn.click(
|
| 422 |
fn=evaluate_readiness,
|
| 423 |
+
inputs=[logs_input, qa_input, punch_input],
|
| 424 |
outputs=[
|
| 425 |
+
score_output, checklist_output, missing_output, status_output, progress_output,
|
| 426 |
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, status_output
|
| 427 |
]
|
| 428 |
).then(
|
| 429 |
fn=generate_pdf,
|
| 430 |
+
inputs=[score_output, checklist_output, missing_output, status_output, gr.State(), gr.State(), gr.State()],
|
| 431 |
outputs=[pdf_output, pdf_debug]
|
| 432 |
).then(
|
| 433 |
fn=create_salesforce_record,
|
| 434 |
inputs=[
|
| 435 |
+
score_output, checklist_output, missing_output, status_output,
|
| 436 |
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, pdf_output
|
| 437 |
],
|
| 438 |
+
outputs=None
|
| 439 |
)
|
| 440 |
+
demo.launch()
|
|
|