File size: 21,023 Bytes
8295d42 12810cb cd77c9b 240ee7a b6cb32f 4d3e78d 3ce8a35 84c6769 cd77c9b 4b251bd 4d3e78d 4b251bd 4d3e78d 4b251bd d64dce4 4b251bd d64dce4 4b251bd 40ed91d 4b251bd 9172d5c 29109f8 9172d5c 4d3e78d 40ed91d 45e88c4 40ed91d 4d3e78d b6cb32f d64dce4 b6cb32f 4d3e78d 9172d5c 4b251bd d988691 4b251bd 9172d5c d988691 d64dce4 9172d5c 4d3e78d 0d855c4 40ed91d d988691 d64dce4 0d855c4 d988691 d64dce4 4d3e78d 40ed91d 4d3e78d cd77c9b 40ed91d 4d3e78d 9172d5c b6cb32f c61971d 9f6a147 4b251bd d64dce4 4b251bd cd77c9b d988691 cd77c9b 4d3e78d 9172d5c d988691 b6cb32f 1fd7646 3ce8a35 4d3e78d d988691 eb02c5d ea0944f d988691 cdf17ec 31e7955 9172d5c c61971d 3ce8a35 f1c3af4 b6cb32f 8dd5c64 d988691 8dd5c64 3ce8a35 7d14612 9172d5c 3ce8a35 d988691 4d3e78d d988691 84c6769 d988691 cdf17ec d988691 3473329 84c6769 d988691 84c6769 d988691 84c6769 d64dce4 d988691 0d855c4 d988691 d64dce4 d988691 4d3e78d cd77c9b d988691 4d3e78d 223543d a93deaa 12810cb a93deaa 12810cb b1396ef d988691 cd77c9b d988691 240ee7a cd77c9b d988691 240ee7a d988691 cd77c9b d988691 678abc9 62f03de 240ee7a cd77c9b d988691 62f03de d988691 62f03de d988691 cd77c9b 109f65d cd77c9b d988691 cd77c9b d988691 cd77c9b 80608fa 240ee7a cd77c9b d988691 cd77c9b d988691 cd77c9b 80608fa 240ee7a cd77c9b d988691 3ce8a35 d988691 62f03de d988691 62f03de d988691 cd77c9b d988691 cd77c9b d988691 80608fa d988691 80608fa cd77c9b d988691 cd77c9b 240ee7a cd77c9b 12810cb 62f03de d988691 cd77c9b d988691 37b77cd d988691 a93deaa d988691 a93deaa d988691 9582e93 d988691 9582e93 a93deaa cd77c9b a93deaa d988691 ea0944f a93deaa 37b77cd ea0944f d988691 a93deaa 37b77cd a93deaa 37b77cd a93deaa 3ce8a35 6949038 3ce8a35 a93deaa cd77c9b 9582e93 d988691 9582e93 a93deaa cd77c9b d64dce4 12810cb d988691 80608fa b6cb32f 80608fa 12810cb d64dce4 12810cb 62f03de ea0944f 193525a ea0944f d64dce4 12810cb 37b77cd df24de1 12810cb 31c5a4e 29d2a6c ea0944f 12810cb d988691 12810cb d64dce4 d988691 4dd8634 d988691 d64dce4 4dd8634 d64dce4 12810cb d64dce4 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 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 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 |
import gradio as gr
import re
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
import bleach
import logging
import os
from huggingface_hub import InferenceClient
from retry import retry
import time
from datetime import datetime
import base64
# Print statement to confirm script initialization
print("Starting Project Closure Readiness Evaluator app...")
# Set up console logging for debugging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()
logger.handlers = [] # Clear existing handlers
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# Attempt to import Salesforce library with fallback
try:
from simple_salesforce import Salesforce, SalesforceError
SALESFORCE_AVAILABLE = True
except ImportError as e:
logging.error(f"Failed to import simple-salesforce: {str(e)}. Salesforce functionality will be disabled.")
logging.error("Ensure 'simple-salesforce' is included in requirements.txt and installed in your Hugging Face Space.")
SALESFORCE_AVAILABLE = False
# Salesforce configuration (loaded from environment variables for security)
SF_USERNAME = os.getenv("SF_USERNAME")
SF_PASSWORD = os.getenv("SF_PASSWORD")
SF_SECURITY_TOKEN = os.getenv("SF_SECURITY_TOKEN")
SF_INSTANCE_URL = os.getenv("SF_INSTANCE_URL")
# Validate Salesforce environment variables
if not all([SF_USERNAME, SF_PASSWORD, SF_SECURITY_TOKEN, SF_INSTANCE_URL]):
logging.error("One or more Salesforce environment variables are missing. Salesforce functionality will be disabled.")
logging.error(f"SF_USERNAME: {SF_USERNAME if SF_USERNAME else 'Not Set'}")
logging.error(f"SF_PASSWORD: {'Set' if SF_PASSWORD else 'Not Set'}")
logging.error(f"SF_SECURITY_TOKEN: {'Set' if SF_SECURITY_TOKEN else 'Not Set'}")
logging.error(f"SF_INSTANCE_URL: {SF_INSTANCE_URL if SF_INSTANCE_URL else 'Not Set'}")
logging.error("Please set these variables in Hugging Face Space Settings > Secrets.")
SALESFORCE_AVAILABLE = False
# Hugging Face configuration
HF_API_TOKEN = os.getenv("HF_API_TOKEN")
if not HF_API_TOKEN:
logging.error("Hugging Face API token (HF_API_TOKEN) not found. Hugging Face functionality will be disabled.")
logging.error("Please set HF_API_TOKEN in Hugging Face Space Settings > Secrets.")
HF_AVAILABLE = False
else:
HF_AVAILABLE = True
hf_client = InferenceClient(token=HF_API_TOKEN)
# Initialize Salesforce connection with retry mechanism
@retry(tries=3, delay=2, backoff=2, logger=logger)
def init_salesforce():
if not SALESFORCE_AVAILABLE:
logging.error("Salesforce library not available. Skipping connection.")
return None, "Salesforce library not available"
try:
logging.info("Attempting to connect to Salesforce with the following credentials:")
logging.info(f"Username: {SF_USERNAME}")
logging.info(f"Instance URL: {SF_INSTANCE_URL}")
sf = Salesforce(
username=SF_USERNAME,
password=SF_PASSWORD,
security_token=SF_SECURITY_TOKEN,
instance_url=SF_INSTANCE_URL
)
logging.info("Salesforce connected successfully")
# Test read access on Project_Closure_Handover__c
test_query = sf.query("SELECT Id FROM Project_Closure_Handover__c LIMIT 1")
logging.info(f"Test query result (read access): {test_query}")
# Test create access by attempting to describe the object and check permissions
object_description = sf.Project_Closure_Handover__c.describe()
logging.info(f"Object description: {object_description}")
return sf, "Salesforce connected successfully"
except SalesforceError as e:
logging.error(f"Salesforce authentication failed: {str(e)}")
logging.error("Possible issues: Incorrect credentials, IP restrictions, or insufficient permissions.")
raise
except Exception as e:
logging.error(f"Failed to initialize Salesforce connection: {str(e)}")
logging.error("Check your Salesforce org settings, network restrictions, or API access.")
raise
# Summarize text using Hugging Face Inference API
def summarize_text(text, max_length=100, min_length=30):
if not HF_AVAILABLE:
logging.error("Hugging Face API not available. Returning original text.")
return text
if not text or text == "None":
return "No summary available"
try:
summary = hf_client.summarization(
text,
model="facebook/bart-large-cnn",
parameters={"max_length": max_length, "min_length": min_length}
)
return summary
except Exception as e:
logging.error(f"Failed to summarize text with Hugging Face: {str(e)}")
return text # Fallback to original text
# Create Salesforce record in custom object Project_Closure_Handover__c
def create_salesforce_record(score, checklist_summary, missing_summary, status, escalated, logs, qa_report, punch_list_text, open_punch_items, pdf_path=None):
if not SALESFORCE_AVAILABLE:
logging.error("Salesforce library not available. Skipping record creation.")
return "Salesforce library not available"
try:
sf, connection_message = init_salesforce()
if not sf:
logging.error(f"Skipping Salesforce record creation due to connection failure: {connection_message}")
return connection_message
# Summarize checklist_summary and missing_summary using Hugging Face
summarized_checklist = summarize_text(checklist_summary)
summarized_missing = summarize_text(missing_summary)
# Ensure inputs are properly formatted
score = float(score) if score is not None else 0.0
checklist_summary = str(checklist_summary) if checklist_summary else ""
summarized_checklist = str(summarized_checklist) if summarized_checklist else ""
missing_summary = str(missing_summary) if missing_summary else ""
summarized_missing = str(summarized_missing) if summarized_missing else ""
status = str(status) if status else ""
logs = str(logs) if logs else ""
qa_report = str(qa_report) if qa_report else ""
punch_list_text = str(punch_list_text) if punch_list_text else ""
missing_documents = len(missing_summary.split(", ")) if missing_summary and missing_summary != "None" else 0
open_punch_items = int(open_punch_items) if open_punch_items is not None else 0
evaluated_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") # Salesforce Date/Time format
alert_sent = str(bool(escalated)).lower() # Converts True/False to "true"/"false"
logging.info(f"Setting Alert_Sent__c to: {alert_sent}")
escalation_flag = str(bool(escalated)).lower() # Ensure this is also a proper boolean string
# Create the record in Project_Closure_Handover__c without the PDF URL for now
record = {
"Readiness_Score__c": score,
"Checklist_Summary__c": checklist_summary,
"Missing_Documents__c": missing_documents,
"Status__c": status,
"Summarized_Missing_Items__c": summarized_missing,
"Alert_Sent__c": alert_sent,
"Client_PDF_Pack_URL__c": "", # Will be updated after attachment is created
"Closure_Pack_URL__c": "", # Placeholder; update if you have a closure pack URL
"Escalation_Flag__c": escalation_flag,
"Evaluated_At__c": evaluated_at,
"Logs__c": logs,
"Open_Punch_Items__c": open_punch_items,
"Punch_List__c": punch_list_text,
"QA_Report__c": qa_report
}
logging.debug(f"Attempting to create Salesforce record in Project_Closure_Handover__c with data: {record}")
result = sf.Project_Closure_Handover__c.create(record)
logging.info(f"Successfully created Salesforce record: {result}")
record_id = result.get('id')
logging.info(f"Record ID: {record_id}")
# Attach the PDF to the record if pdf_path exists and update the URL
pdf_download_url = ""
if pdf_path and os.path.exists(pdf_path):
logging.info(f"Attaching PDF to Salesforce record ID: {record_id}")
with open(pdf_path, "rb") as pdf_file:
pdf_content = pdf_file.read()
pdf_base64 = base64.b64encode(pdf_content).decode('utf-8')
attachment = {
"ParentId": record_id,
"Name": "Readiness_Report.pdf",
"Body": pdf_base64,
"ContentType": "application/pdf"
}
attachment_result = sf.Attachment.create(attachment)
logging.info(f"Successfully attached PDF to record: {attachment_result}")
attachment_id = attachment_result.get('id')
logging.info(f"Attachment ID: {attachment_id}")
# Construct the direct download URL for the attachment
pdf_download_url = f"{SF_INSTANCE_URL}/servlet/servlet.FileDownload?file={attachment_id}"
logging.info(f"Generated PDF download URL: {pdf_download_url}")
# Update the Project_Closure_Handover__c record with the PDF download URL
update_data = {
"Client_PDF_Pack_URL__c": pdf_download_url
}
sf.Project_Closure_Handover__c.update(record_id, update_data)
logging.info(f"Updated record {record_id} with Client_PDF_Pack_URL__c: {pdf_download_url}")
else:
logging.warning(f"No PDF file found at {pdf_path}. Skipping attachment and URL update.")
return f"Record created successfully. Record ID: {record_id}. PDF attached and URL set to: {pdf_download_url}"
except SalesforceError as e:
logging.error(f"Salesforce error while creating Project_Closure_Handover__c record: {str(e)}")
logging.error("Possible issues: Object permissions, field-level security, validation rules, or required fields.")
logging.error("Check the following in your Salesforce org:")
logging.error("- Ensure the user has Create and Edit permission on Project_Closure_Handover__c.")
logging.error("- Ensure the user has permission to create and read Attachments.")
logging.error("- Verify field-level security for all fields in the record.")
logging.error("- Check for validation rules or required fields that might be failing.")
return f"Salesforce error: {str(e)}"
except Exception as e:
logging.error(f"Failed to create Salesforce Project_Closure_Handover__c record: {str(e)}")
return f"Error creating record: {str(e)}"
# Clean input to prevent injection attacks
def sanitize_input(text):
if not text or not isinstance(text, str):
return ""
return bleach.clean(text.strip())
# Rule-based completeness engine with weighted scoring
def evaluate_readiness(logs, qa_report, punch_list_text):
try:
# Log inputs for debugging
logging.info(f"Inputs - Logs: {logs}, QA Report: {qa_report}, Punch List: {punch_list_text}")
# Initialize score and lists for tracking
score = 0
missing_items = []
checklist_details = []
# Define weights for scoring
LOGS_WEIGHT = 30 # 30% weight for logs
QA_WEIGHT = 40 # 40% weight for QA report
PUNCH_WEIGHT = 30 # 30% weight for punch list
# Sanitize inputs
logs = sanitize_input(logs)
qa_report = sanitize_input(qa_report)
punch_list_text = sanitize_input(punch_list_text)
# Process Project Logs (30% weight)
log_keywords = r"complete|handover done|done|finished|closed|successful"
negative_keywords = r"issue|pending|incomplete|problem|delay"
logs_pass = logs and re.search(log_keywords, logs.lower())
logs_negative = logs and re.search(negative_keywords, logs.lower())
if logs_pass and not logs_negative:
score += LOGS_WEIGHT
checklist_details.append("Logs: Completed")
logging.info(f"Logs Check: Pass (positive keywords found, no negative keywords), Score: {score}%")
elif logs_pass and logs_negative:
score += LOGS_WEIGHT // 2 # Partial score for mixed signals
missing_items.append("Issues detected in logs")
checklist_details.append("Logs: Partially Completed (issues detected)")
logging.info(f"Logs Check: Partial Pass (positive and negative keywords found), Score: {score}%")
else:
missing_items.append("Project Logs Incomplete")
checklist_details.append("Logs: Pending")
logging.info(f"Logs Check: Fail (no positive keywords or only negative keywords), Score: {score}%")
# Process QA Report (40% weight)
qa_keywords = r"approved|passed|cleared"
qa_pass = qa_report and re.search(qa_keywords, qa_report.lower())
if qa_pass:
score += QA_WEIGHT
checklist_details.append("QA Report: Approved")
else:
missing_items.append("QA Approval Missing")
checklist_details.append("QA Report: Pending")
logging.info(f"QA Check: {'Pass' if qa_pass else 'Fail'}, Score so far: {score}%")
# Process Punch List (30% weight)
punch_keywords = r"none|resolved|closed|no issues"
punch_pass = punch_list_text and re.search(punch_keywords, punch_list_text.lower())
if punch_pass:
score += PUNCH_WEIGHT
checklist_details.append("Punch List: Resolved")
else:
missing_items.append("Open Punch Points Detected")
checklist_details.append("Punch List: Pending")
logging.info(f"Punch List Check: {'Pass' if punch_pass else 'Fail'}, Final Score: {score}%")
# Calculate open punch items
open_punch_items = 0 if punch_pass else 1 # Simplified for dropdown input
# Enhanced escalation logic
escalated = score < 70 or open_punch_items > 2
logging.info(f"Escalation Check: Score < 70 ({score < 70}), Open Punch Items > 2 ({open_punch_items > 2}), Escalated: {escalated}")
# Map "Pending" to "In Progress" for Salesforce Status__c picklist
status = "Escalated" if escalated else ("Completed" if not missing_items else "In Progress")
checklist_status = "Escalated" if escalated else ("Completed" if not missing_items else "Pending") # For UI display
# Build summaries
checklist_summary = "\n".join(checklist_details)
missing_summary = "None" if not missing_items else ", ".join(missing_items)
# Generate progress bar HTML
color_class = "red" if score < 70 else "yellow" if score <= 90 else "green"
logging.info(f"Readiness Score: {score}%, Color Class: {color_class}")
progress_bar = f"""
<div class="progress-container">
<div class="progress-bar {color_class}" style="width: {score}%;">
{score}%
</div>
</div>
"""
return (score, checklist_summary, missing_summary, status, progress_bar, escalated, logs, qa_report, punch_list_text, open_punch_items, checklist_status)
except Exception as e:
logging.error(f"Error in evaluate_readiness: {str(e)}")
raise
# Generate PDF report with signature slots
def generate_pdf(score, checklist_summary, missing_summary, checklist_status, logs, qa_report, punch_list_text):
try:
# Sanitize inputs
logging.info("Sanitizing inputs for PDF generation")
score = str(float(score)) if score is not None else "0"
checklist_summary = str(checklist_summary) if checklist_summary is not None else ""
checklist_status = str(checklist_status) if checklist_status is not None else ""
# Remove non-ASCII characters
checklist_summary = checklist_summary.encode('ascii', 'ignore').decode('ascii')
checklist_status = checklist_status.encode('ascii', 'ignore').decode('ascii')
# Define the temporary file path
pdf_path = "readiness_report.pdf"
logging.info(f"Creating PDF at {pdf_path}")
# Create the PDF
c = canvas.Canvas(pdf_path, pagesize=letter)
c.setFont("Times-Bold", 16)
c.drawString(50, 750, "Project Closure Readiness Report")
c.setFont("Times-Roman", 12)
c.drawString(50, 720, f"Readiness Score: {score}%")
c.drawString(50, 700, f"Status: {checklist_status}")
c.drawString(50, 680, "Checklist Summary:")
y = 660
BOTTOM_MARGIN = 50
for line in checklist_summary.split("\n"):
if y < BOTTOM_MARGIN:
c.showPage()
c.setFont("Times-Roman", 12)
y = 750
c.drawString(50, y, line)
y -= 20
if y - 40 < BOTTOM_MARGIN:
c.showPage()
c.setFont("Times-Roman", 12)
y = 750
c.drawString(50, y - 40, "Stakeholder Signature: ____________________")
if y - 60 < BOTTOM_MARGIN:
c.showPage()
c.setFont("Times-Roman", 12)
y = 750
c.drawString(50, y - 60, "Date: ____________________")
c.save()
# Confirm the file exists
if not os.path.exists(pdf_path):
logging.error(f"PDF file {pdf_path} was not created")
raise FileNotFoundError(f"PDF file {pdf_path} was not created")
logging.info(f"PDF generated successfully at {pdf_path}")
return pdf_path, "PDF generation completed. Click the link to download."
except Exception as e:
logging.error(f"Error in generate_pdf: {str(e)}")
raise
# Gradio interface with updated UI
with gr.Blocks(css="""
.progress-container { background-color: #f0f0f0; width: 100%; height: 20px; border-radius: 5px; overflow: hidden; position: relative; }
.progress-bar { height: 100%; text-align: center; line-height: 20px; color: #000; font-size: 12px; }
.progress-bar.red { background-color: #FF0000; }
.progress-bar.yellow { background-color: #FFFF00; }
.progress-bar.green { background-color: #00FF00; }
""") as demo:
gr.Markdown(
"""
# Project Closure Readiness Evaluator
Evaluate project readiness, generate a PDF report with signature slots.
"""
)
with gr.Row():
with gr.Column(scale=2):
logs_input = gr.Textbox(label="Project Logs", lines=5, placeholder="Enter project logs (e.g., 'Project complete, handover done')")
qa_input = gr.Dropdown(
label="QA Report",
choices=["Approved", "Cleared", "Pending", "Not Started", "Rejected"],
value="Pending",
allow_custom_value=False
)
punch_input = gr.Dropdown(
label="Punch List",
choices=["None", "Resolved", "Closed", "No Issues", "Open Items"],
value="Open Items",
allow_custom_value=False
)
submit_btn = gr.Button("Evaluate and Generate PDF")
with gr.Column(scale=3):
score_output = gr.Number(label="Readiness Score (%)")
progress_output = gr.HTML(label="Alert Indicator: Progress")
gr.Markdown("Color-coded readiness: Red (<70%), Yellow (70-90%), Green (>90%)")
status_output = gr.Textbox(label="Overall Status")
with gr.Group():
gr.Markdown("### Handover Summary")
checklist_output = gr.Textbox(label="Checklist Summary")
missing_output = gr.Textbox(label="Missing Items")
open_punch_items_output = gr.Number(label="Open Punch Items (Debug)")
pdf_output = gr.File(label="Download PDF Report", type="filepath", interactive=False)
pdf_debug = gr.Textbox(label="PDF Debug Output")
# Chain the evaluation, PDF generation, and Salesforce record creation
submit_btn.click(
fn=evaluate_readiness,
inputs=[logs_input, qa_input, punch_input],
outputs=[
score_output, checklist_output, missing_output, status_output, progress_output,
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, status_output
]
).then(
fn=generate_pdf,
inputs=[score_output, checklist_output, missing_output, status_output, gr.State(), gr.State(), gr.State()],
outputs=[pdf_output, pdf_debug]
).then(
fn=create_salesforce_record,
inputs=[
score_output, checklist_output, missing_output, status_output,
gr.State(), gr.State(), gr.State(), gr.State(), open_punch_items_output, pdf_output
],
outputs=None
)
demo.launch() |