Dineshpopuri's picture
Update app.py
d64dce4 verified
Raw History Blame Contribute Delete
21 kB
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()