Dineshpopuri's picture
Update app.py
d64dce4 verified
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()