|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import faiss |
|
|
import os |
|
|
import re |
|
|
import random |
|
|
from openai import OpenAI |
|
|
|
|
|
|
|
|
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) |
|
|
|
|
|
|
|
|
def row_to_text(row): |
|
|
return ( |
|
|
f"Clinic: {row.get('Account_Name', 'N/A')}\n" |
|
|
f"Interaction Focus: {row.get('Recent_Interaction_Focus', 'N/A')}\n" |
|
|
f"Interaction Notes: {row.get('Recent_Interaction_Notes', 'N/A')}\n" |
|
|
f"Region: {row.get('Region', 'Unknown')}\n" |
|
|
f"Account Manager: {row.get('Account_Manager', 'Unknown')}\n" |
|
|
f"Revenue: €{row.get('Revenue', 'N/A')}\n" |
|
|
f"Last Purchase: {row.get('Last_Purchase_Date', 'N/A')}\n" |
|
|
f"Churn Risk: {row.get('Churn_Risk', 'N/A')}\n" |
|
|
f"Satisfaction Score: {row.get('Satisfaction_Score', 'N/A')}/10\n" |
|
|
f"Product Interest: {row.get('Product_Interest', 'N/A')}\n" |
|
|
f"Notes: {row.get('Account_Notes', 'N/A')}" |
|
|
) |
|
|
|
|
|
|
|
|
def get_embedding(text): |
|
|
response = client.embeddings.create( |
|
|
input=[text], |
|
|
model="text-embedding-3-small" |
|
|
) |
|
|
return response.data[0].embedding |
|
|
|
|
|
|
|
|
def contextual_pitch_assistant(csv_file, query, sender_name): |
|
|
df = pd.read_csv(csv_file.name) |
|
|
text_chunks = df.apply(row_to_text, axis=1).tolist() |
|
|
embeddings = [get_embedding(t) for t in text_chunks] |
|
|
|
|
|
dim = len(embeddings[0]) |
|
|
index = faiss.IndexFlatL2(dim) |
|
|
index.add(np.array(embeddings).astype("float32")) |
|
|
|
|
|
q_emb = np.array([get_embedding(query)]).astype("float32") |
|
|
D, I = index.search(q_emb, 3) |
|
|
retrieved = [df.iloc[i] for i in I[0]] |
|
|
|
|
|
|
|
|
row = retrieved[0] |
|
|
account_manager = row.get("Account_Manager", "team") |
|
|
account_name = row.get("Account_Name", "your clinic") |
|
|
focus = str(row.get("Recent_Interaction_Focus", "")).lower() |
|
|
notes_focus = str(row.get("Recent_Interaction_Notes", "")) |
|
|
notes_background = str(row.get("Account_Notes", "")) |
|
|
|
|
|
if not sender_name.strip(): |
|
|
sender_name = "The Sales Team" |
|
|
|
|
|
|
|
|
if "business" in focus: |
|
|
style_instruction = "Emphasize ROI, efficiency, and competitive advantage." |
|
|
elif "patient" in focus: |
|
|
style_instruction = "Emphasize patient comfort, satisfaction, and outcomes." |
|
|
elif "clinical" in focus: |
|
|
style_instruction = "Emphasize precision, innovation, and clinical quality." |
|
|
else: |
|
|
style_instruction = "Balance clinical, patient, and business value." |
|
|
|
|
|
|
|
|
|
|
|
prompt = f""" |
|
|
You are an expert in B2B sales messaging, inspired by Challenger Sales and Jeb Blount’s prospecting techniques. |
|
|
Your job is to write a short, natural-sounding HTML email pitch to a dental clinic. |
|
|
|
|
|
Rules: |
|
|
- Begin with "Dear {account_manager}," as the greeting. |
|
|
- Anchor immediately in the clinic’s recent concern: "{notes_focus}". |
|
|
- Use background context if relevant: "{notes_background}". |
|
|
- Adapt the pitch style: {style_instruction} |
|
|
- Apply Challenger logic: |
|
|
* Start with the client’s concern in their own terms. |
|
|
* Reframe with an insight (show a broader problem or missed opportunity). |
|
|
* Link features → outcomes → impact (business, patient, or clinical). |
|
|
* Confident but helpful tone. |
|
|
- Keep under 150 words. |
|
|
- End with a strong, specific CTA (e.g. propose a short call or demo, suggest a time). |
|
|
- Close with "Best regards," followed by "{sender_name}". |
|
|
- Return only HTML — no markdown, no code fences. |
|
|
|
|
|
Query: |
|
|
{query} |
|
|
|
|
|
CRM context (for your understanding, do not copy verbatim): |
|
|
{row_to_text(row)} |
|
|
""" |
|
|
|
|
|
|
|
|
response = client.chat.completions.create( |
|
|
model="gpt-4o-mini", |
|
|
messages=[{"role": "user", "content": prompt}], |
|
|
temperature=0.7 |
|
|
) |
|
|
|
|
|
output_text = response.choices[0].message.content |
|
|
|
|
|
|
|
|
image_choices = [ |
|
|
"https://huggingface.co/spaces/nmcamacho/RAGdemo/resolve/main/dental_header_1.png", |
|
|
"https://huggingface.co/spaces/nmcamacho/RAGdemo/resolve/main/dental_header_2.png", |
|
|
"https://huggingface.co/spaces/nmcamacho/RAGdemo/resolve/main/dental_header_3.png" |
|
|
] |
|
|
image_url = random.choice(image_choices) |
|
|
|
|
|
|
|
|
output_text = re.sub(r"(?is)dall[-·]e.*?```", "", output_text) |
|
|
output_text = re.sub(r"###.*?HTML Email Pitch", "", output_text) |
|
|
output_text = re.sub(r"```html|```", "", output_text) |
|
|
output_text = output_text.strip() |
|
|
|
|
|
clinic_note = f""" |
|
|
<div style='margin-top:20px;padding:10px;font-size:13px;color:gray; |
|
|
border-top:1px solid #ddd;'> |
|
|
📌 Target: <b>{account_name}</b> (Region: {row.get('Region', 'N/A')}, |
|
|
Focus: {row.get('Recent_Interaction_Focus', 'N/A')}) |
|
|
</div> |
|
|
""" |
|
|
|
|
|
html = f""" |
|
|
<div style='font-family:Arial,sans-serif;max-width:700px;margin:auto;padding:24px;background:#ffffff; |
|
|
border-radius:12px;box-shadow:0 3px 10px rgba(0,0,0,0.1);'> |
|
|
<img src="{image_url}" style="width:100%;border-radius:8px;margin-bottom:20px;"> |
|
|
{output_text} |
|
|
{clinic_note} |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="Contextual Pitch Assistant for Dental Sales", |
|
|
css=""" |
|
|
body { background-color: #f7f9f9; font-family: 'Inter', sans-serif; } |
|
|
#output_html { min-height: 450px; } |
|
|
.gradio-container { max-width: 90% !important; margin: auto; } |
|
|
h1, h2, h3, h4, h5 { color: #00857C; font-weight: 600; } |
|
|
.gr-button { |
|
|
background-color: #00857C !important; |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
font-weight: 600; |
|
|
padding: 10px 18px; |
|
|
border-radius: 8px; |
|
|
} |
|
|
.gr-button:hover { background-color: #006e67 !important; } |
|
|
.gr-file, .gr-textbox { |
|
|
border: 1px solid #d1d5db !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
.gr-box { |
|
|
background: white !important; |
|
|
border-radius: 12px !important; |
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
|
|
padding: 20px !important; |
|
|
} |
|
|
""" |
|
|
) as app: |
|
|
gr.Markdown( |
|
|
""" |
|
|
# 🦷 Contextual Pitch Assistant for Dental Sales |
|
|
*Powered by contextual CRM data and generative AI* |
|
|
--- |
|
|
Upload a CRM file and enter a sales question — get a personalized email pitch with a contextual image. |
|
|
""" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
csv_file = gr.File(label="📂 Upload CRM CSV (with Recent_Interaction_Focus/Notes)", file_types=[".csv"]) |
|
|
sender_name = gr.Textbox( |
|
|
label="✍️ Who signs the email?", |
|
|
placeholder="e.g. Nuno Camacho, Sales Director", |
|
|
value="Nuno Camacho" |
|
|
) |
|
|
|
|
|
query = gr.Textbox( |
|
|
label="💬 Sales Query", |
|
|
placeholder="e.g. Which clinic is best for our imaging subscription?", |
|
|
lines=2 |
|
|
) |
|
|
|
|
|
run_btn = gr.Button("🚀 Generate Pitch", variant="primary") |
|
|
output = gr.HTML(label="✨ Email Pitch Preview", elem_id="output_html") |
|
|
|
|
|
run_btn.click(fn=contextual_pitch_assistant, inputs=[csv_file, query, sender_name], outputs=output) |
|
|
|
|
|
run_btn.click( |
|
|
lambda: "<p style='color:gray;'>⏳ Processing... please wait 30–60 seconds.</p>", |
|
|
inputs=None, |
|
|
outputs=output, |
|
|
queue=False |
|
|
) |
|
|
|
|
|
app.launch() |
|
|
|