Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -114,13 +114,9 @@ def _strip_code_fences(text: str) -> str:
|
|
| 114 |
|
| 115 |
def _escape_bare_ampersands(text: str) -> str:
|
| 116 |
# Replace & that aren't valid entities with &
|
| 117 |
-
return re.sub(
|
| 118 |
-
r'&(?!amp;|lt;|gt;|apos;|quot;|#\d+;|#x[0-9a-fA-F]+;)',
|
| 119 |
-
'&',
|
| 120 |
-
text
|
| 121 |
-
)
|
| 122 |
|
| 123 |
-
# ---------- Metadata Validator (
|
| 124 |
def validate_metadata(metadata, admin_id=None):
|
| 125 |
if not metadata.strip():
|
| 126 |
return "No metadata provided.", "", ""
|
|
@@ -129,11 +125,10 @@ def validate_metadata(metadata, admin_id=None):
|
|
| 129 |
issue = "Unknown"
|
| 130 |
recommendation = "No recommendation found."
|
| 131 |
|
| 132 |
-
#
|
| 133 |
preview = metadata[:100].replace("\n", "\\n")
|
| 134 |
log_to_console({"preview": preview}, "Metadata Input Preview")
|
| 135 |
|
| 136 |
-
# Clean typical paste artefacts
|
| 137 |
cleaned = _strip_code_fences(metadata).lstrip("\ufeff").strip()
|
| 138 |
|
| 139 |
# Only treat as XML if the first non-space character is '<'
|
|
@@ -144,16 +139,11 @@ def validate_metadata(metadata, admin_id=None):
|
|
| 144 |
"<CustomField xmlns=\"http://soap.sforce.com/2006/04/metadata\">...</CustomField>"
|
| 145 |
)
|
| 146 |
else:
|
| 147 |
-
# Escape stray ampersands (common XML error)
|
| 148 |
cleaned = _escape_bare_ampersands(cleaned)
|
| 149 |
-
|
| 150 |
try:
|
| 151 |
root = ET.fromstring(cleaned)
|
| 152 |
# Detect <description> regardless of namespace
|
| 153 |
-
has_description = any(
|
| 154 |
-
elem.tag.split('}')[-1].lower() == "description"
|
| 155 |
-
for elem in root.iter()
|
| 156 |
-
)
|
| 157 |
|
| 158 |
if not has_description:
|
| 159 |
issue = "Missing description"
|
|
@@ -161,7 +151,6 @@ def validate_metadata(metadata, admin_id=None):
|
|
| 161 |
else:
|
| 162 |
issue = "Unused field detected"
|
| 163 |
recommendation = "Remove it to improve performance or document its purpose."
|
| 164 |
-
|
| 165 |
except ET.ParseError as pe:
|
| 166 |
issue = "Invalid XML"
|
| 167 |
recommendation = f"Could not parse metadata XML. Error: {pe}."
|
|
@@ -169,7 +158,7 @@ def validate_metadata(metadata, admin_id=None):
|
|
| 169 |
issue = "Validation Error"
|
| 170 |
recommendation = f"Unexpected error while validating metadata: {str(e)}"
|
| 171 |
|
| 172 |
-
# Log to Salesforce
|
| 173 |
log_data = {
|
| 174 |
"Name": f"MetadataLog_{mtype}",
|
| 175 |
"MetadataType__c": mtype,
|
|
@@ -196,34 +185,45 @@ def validate_metadata(metadata, admin_id=None):
|
|
| 196 |
|
| 197 |
return mtype, issue, recommendation
|
| 198 |
|
| 199 |
-
# ---------- Salesforce Chatbot (
|
| 200 |
-
conversation_history = []
|
| 201 |
|
| 202 |
-
def salesforce_chatbot(query,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
global conversation_history
|
| 204 |
-
if not query.strip():
|
| 205 |
return "Please provide a valid Salesforce-related question."
|
| 206 |
|
| 207 |
salesforce_keywords = [
|
| 208 |
"apex", "soql", "trigger", "lwc", "aura", "visualforce", "salesforce", "governor limits",
|
| 209 |
"dml", "metadata", "batch apex", "queueable", "future method", "api", "sfdc", "heap", "limits"
|
| 210 |
]
|
| 211 |
-
|
| 212 |
-
if not any(keyword.lower() in query.lower() for keyword in salesforce_keywords):
|
| 213 |
return "Please ask a Salesforce-related question (e.g., Apex, SOQL, LWC, limits, etc)."
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
query_key = query.lower().strip()
|
| 216 |
for kb_key, kb_answer in salesforce_knowledge_base.items():
|
| 217 |
if kb_key in query_key:
|
| 218 |
conversation_history.append((query, kb_answer))
|
| 219 |
-
conversation_history = conversation_history[-6:]
|
| 220 |
log_to_console({"Question": query, "Answer": kb_answer}, "Chatbot Query")
|
| 221 |
return kb_answer
|
| 222 |
|
| 223 |
-
history_summary = "\n".join([f"User: {q}\nAssistant: {a}" for q, a in conversation_history[-4:]])
|
| 224 |
-
|
| 225 |
prompt = f"""
|
| 226 |
-
You are an expert Salesforce developer and certified architect.
|
|
|
|
| 227 |
When answering:
|
| 228 |
- ALWAYS give at least 2 lines of explanation.
|
| 229 |
- Be clear, concise, and technically correct.
|
|
@@ -231,8 +231,10 @@ When answering:
|
|
| 231 |
- Use bullet points or code snippets when helpful.
|
| 232 |
- Avoid speculation — if unknown, say so and suggest Trailhead or official docs.
|
| 233 |
- Examples must be realistic and follow best practices.
|
|
|
|
| 234 |
Conversation History:
|
| 235 |
{history_summary}
|
|
|
|
| 236 |
User: {query.strip()}
|
| 237 |
Assistant:
|
| 238 |
"""
|
|
@@ -243,11 +245,13 @@ Assistant:
|
|
| 243 |
if output.startswith("Assistant:"):
|
| 244 |
output = output.replace("Assistant:", "").strip()
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
| 248 |
|
| 249 |
conversation_history.append((query, output))
|
| 250 |
-
conversation_history = conversation_history[-6:]
|
| 251 |
log_to_console({"Question": query, "Answer": output}, "Chatbot Query")
|
| 252 |
return output
|
| 253 |
except Exception as e:
|
|
@@ -257,6 +261,7 @@ Assistant:
|
|
| 257 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 258 |
gr.Markdown("# 🤖 Advanced Salesforce AI Code Review & Chatbot")
|
| 259 |
|
|
|
|
| 260 |
with gr.Tab("Code Review"):
|
| 261 |
code_input = gr.Textbox(label="Apex / LWC Code", lines=8, placeholder="Enter your Apex or LWC code here")
|
| 262 |
issue_type = gr.Textbox(label="Issue Type")
|
|
@@ -265,6 +270,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 265 |
code_button = gr.Button("Analyze Code")
|
| 266 |
code_button.click(analyze_code, inputs=code_input, outputs=[issue_type, suggestion, severity])
|
| 267 |
|
|
|
|
| 268 |
with gr.Tab("Metadata Validation"):
|
| 269 |
metadata_input = gr.Textbox(label="Metadata XML", lines=8, placeholder="Enter your metadata XML here")
|
| 270 |
mtype = gr.Textbox(label="Type")
|
|
@@ -273,24 +279,30 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 273 |
metadata_button = gr.Button("Validate Metadata")
|
| 274 |
metadata_button.click(validate_metadata, inputs=metadata_input, outputs=[mtype, issue, recommendation])
|
| 275 |
|
|
|
|
| 276 |
with gr.Tab("Salesforce Chatbot"):
|
| 277 |
chatbot_output = gr.Chatbot(label="Conversation History", height=400, type="messages")
|
| 278 |
query_input = gr.Textbox(label="Your Question", placeholder="e.g., What is heap size in Apex?")
|
| 279 |
with gr.Row():
|
| 280 |
chatbot_button = gr.Button("Ask")
|
| 281 |
clear_button = gr.Button("Clear Chat")
|
|
|
|
|
|
|
| 282 |
chat_state = gr.State(value=[])
|
| 283 |
|
| 284 |
-
def update_chatbot(query,
|
| 285 |
-
if not query.strip():
|
| 286 |
-
return
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
def clear_chat():
|
| 292 |
-
global conversation_history
|
| 293 |
-
conversation_history = []
|
| 294 |
return [], ""
|
| 295 |
|
| 296 |
chatbot_button.click(fn=update_chatbot, inputs=[query_input, chat_state], outputs=[chatbot_output, query_input])
|
|
|
|
| 114 |
|
| 115 |
def _escape_bare_ampersands(text: str) -> str:
|
| 116 |
# Replace & that aren't valid entities with &
|
| 117 |
+
return re.sub(r'&(?!amp;|lt;|gt;|apos;|quot;|#\d+;|#x[0-9a-fA-F]+;)', '&', text)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
# ---------- Metadata Validator (handles non-XML gracefully) ----------
|
| 120 |
def validate_metadata(metadata, admin_id=None):
|
| 121 |
if not metadata.strip():
|
| 122 |
return "No metadata provided.", "", ""
|
|
|
|
| 125 |
issue = "Unknown"
|
| 126 |
recommendation = "No recommendation found."
|
| 127 |
|
| 128 |
+
# Preview to help diagnose pastes
|
| 129 |
preview = metadata[:100].replace("\n", "\\n")
|
| 130 |
log_to_console({"preview": preview}, "Metadata Input Preview")
|
| 131 |
|
|
|
|
| 132 |
cleaned = _strip_code_fences(metadata).lstrip("\ufeff").strip()
|
| 133 |
|
| 134 |
# Only treat as XML if the first non-space character is '<'
|
|
|
|
| 139 |
"<CustomField xmlns=\"http://soap.sforce.com/2006/04/metadata\">...</CustomField>"
|
| 140 |
)
|
| 141 |
else:
|
|
|
|
| 142 |
cleaned = _escape_bare_ampersands(cleaned)
|
|
|
|
| 143 |
try:
|
| 144 |
root = ET.fromstring(cleaned)
|
| 145 |
# Detect <description> regardless of namespace
|
| 146 |
+
has_description = any(elem.tag.split('}')[-1].lower() == "description" for elem in root.iter())
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
if not has_description:
|
| 149 |
issue = "Missing description"
|
|
|
|
| 151 |
else:
|
| 152 |
issue = "Unused field detected"
|
| 153 |
recommendation = "Remove it to improve performance or document its purpose."
|
|
|
|
| 154 |
except ET.ParseError as pe:
|
| 155 |
issue = "Invalid XML"
|
| 156 |
recommendation = f"Could not parse metadata XML. Error: {pe}."
|
|
|
|
| 158 |
issue = "Validation Error"
|
| 159 |
recommendation = f"Unexpected error while validating metadata: {str(e)}"
|
| 160 |
|
| 161 |
+
# Log to Salesforce
|
| 162 |
log_data = {
|
| 163 |
"Name": f"MetadataLog_{mtype}",
|
| 164 |
"MetadataType__c": mtype,
|
|
|
|
| 185 |
|
| 186 |
return mtype, issue, recommendation
|
| 187 |
|
| 188 |
+
# ---------- Salesforce Chatbot (messages format, 2+ line answers) ----------
|
| 189 |
+
conversation_history = [] # optional global memory
|
| 190 |
|
| 191 |
+
def salesforce_chatbot(query: str, chat_messages: list):
|
| 192 |
+
"""
|
| 193 |
+
query: latest user question string
|
| 194 |
+
chat_messages: list[{"role":"user"|"assistant","content":str}] from the UI state
|
| 195 |
+
"""
|
| 196 |
global conversation_history
|
| 197 |
+
if not query or not query.strip():
|
| 198 |
return "Please provide a valid Salesforce-related question."
|
| 199 |
|
| 200 |
salesforce_keywords = [
|
| 201 |
"apex", "soql", "trigger", "lwc", "aura", "visualforce", "salesforce", "governor limits",
|
| 202 |
"dml", "metadata", "batch apex", "queueable", "future method", "api", "sfdc", "heap", "limits"
|
| 203 |
]
|
| 204 |
+
if not any(k in query.lower() for k in salesforce_keywords):
|
|
|
|
| 205 |
return "Please ask a Salesforce-related question (e.g., Apex, SOQL, LWC, limits, etc)."
|
| 206 |
|
| 207 |
+
# Build history summary from messages (last few pairs)
|
| 208 |
+
chat_messages = chat_messages or []
|
| 209 |
+
history_summary = []
|
| 210 |
+
for m in chat_messages[-8:]:
|
| 211 |
+
prefix = "User" if m.get("role") == "user" else "Assistant"
|
| 212 |
+
history_summary.append(f"{prefix}: {m.get('content','')}")
|
| 213 |
+
history_summary = "\n".join(history_summary)
|
| 214 |
+
|
| 215 |
+
# Quick KB shortcut
|
| 216 |
query_key = query.lower().strip()
|
| 217 |
for kb_key, kb_answer in salesforce_knowledge_base.items():
|
| 218 |
if kb_key in query_key:
|
| 219 |
conversation_history.append((query, kb_answer))
|
| 220 |
+
conversation_history[:] = conversation_history[-6:]
|
| 221 |
log_to_console({"Question": query, "Answer": kb_answer}, "Chatbot Query")
|
| 222 |
return kb_answer
|
| 223 |
|
|
|
|
|
|
|
| 224 |
prompt = f"""
|
| 225 |
+
You are an expert Salesforce developer and certified architect. Provide accurate, clear, and practical answers for topics like Apex, SOQL, LWC, governor limits, triggers, metadata, and more.
|
| 226 |
+
|
| 227 |
When answering:
|
| 228 |
- ALWAYS give at least 2 lines of explanation.
|
| 229 |
- Be clear, concise, and technically correct.
|
|
|
|
| 231 |
- Use bullet points or code snippets when helpful.
|
| 232 |
- Avoid speculation — if unknown, say so and suggest Trailhead or official docs.
|
| 233 |
- Examples must be realistic and follow best practices.
|
| 234 |
+
|
| 235 |
Conversation History:
|
| 236 |
{history_summary}
|
| 237 |
+
|
| 238 |
User: {query.strip()}
|
| 239 |
Assistant:
|
| 240 |
"""
|
|
|
|
| 245 |
if output.startswith("Assistant:"):
|
| 246 |
output = output.replace("Assistant:", "").strip()
|
| 247 |
|
| 248 |
+
# Enforce 2+ meaningful lines
|
| 249 |
+
if len(output.split()) < 20:
|
| 250 |
+
output = (output + "\n\n"
|
| 251 |
+
"Tip: You can verify limits and examples in the official docs at https://developer.salesforce.com/docs.")
|
| 252 |
|
| 253 |
conversation_history.append((query, output))
|
| 254 |
+
conversation_history[:] = conversation_history[-6:]
|
| 255 |
log_to_console({"Question": query, "Answer": output}, "Chatbot Query")
|
| 256 |
return output
|
| 257 |
except Exception as e:
|
|
|
|
| 261 |
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 262 |
gr.Markdown("# 🤖 Advanced Salesforce AI Code Review & Chatbot")
|
| 263 |
|
| 264 |
+
# Code Review
|
| 265 |
with gr.Tab("Code Review"):
|
| 266 |
code_input = gr.Textbox(label="Apex / LWC Code", lines=8, placeholder="Enter your Apex or LWC code here")
|
| 267 |
issue_type = gr.Textbox(label="Issue Type")
|
|
|
|
| 270 |
code_button = gr.Button("Analyze Code")
|
| 271 |
code_button.click(analyze_code, inputs=code_input, outputs=[issue_type, suggestion, severity])
|
| 272 |
|
| 273 |
+
# Metadata Validation
|
| 274 |
with gr.Tab("Metadata Validation"):
|
| 275 |
metadata_input = gr.Textbox(label="Metadata XML", lines=8, placeholder="Enter your metadata XML here")
|
| 276 |
mtype = gr.Textbox(label="Type")
|
|
|
|
| 279 |
metadata_button = gr.Button("Validate Metadata")
|
| 280 |
metadata_button.click(validate_metadata, inputs=metadata_input, outputs=[mtype, issue, recommendation])
|
| 281 |
|
| 282 |
+
# Salesforce Chatbot (messages format)
|
| 283 |
with gr.Tab("Salesforce Chatbot"):
|
| 284 |
chatbot_output = gr.Chatbot(label="Conversation History", height=400, type="messages")
|
| 285 |
query_input = gr.Textbox(label="Your Question", placeholder="e.g., What is heap size in Apex?")
|
| 286 |
with gr.Row():
|
| 287 |
chatbot_button = gr.Button("Ask")
|
| 288 |
clear_button = gr.Button("Clear Chat")
|
| 289 |
+
|
| 290 |
+
# Messages state: list of {"role","content"}
|
| 291 |
chat_state = gr.State(value=[])
|
| 292 |
|
| 293 |
+
def update_chatbot(query, chat_messages):
|
| 294 |
+
if not query or not query.strip():
|
| 295 |
+
return chat_messages, "Please enter a valid question."
|
| 296 |
+
# Get model answer
|
| 297 |
+
answer = salesforce_chatbot(query, chat_messages)
|
| 298 |
+
# Append messages in OpenAI style
|
| 299 |
+
chat_messages = (chat_messages or []) + [
|
| 300 |
+
{"role": "user", "content": query},
|
| 301 |
+
{"role": "assistant", "content": answer},
|
| 302 |
+
]
|
| 303 |
+
return chat_messages, ""
|
| 304 |
|
| 305 |
def clear_chat():
|
|
|
|
|
|
|
| 306 |
return [], ""
|
| 307 |
|
| 308 |
chatbot_button.click(fn=update_chatbot, inputs=[query_input, chat_state], outputs=[chatbot_output, query_input])
|