Adritbgh's picture
Update app.py
edf888e verified
Raw
History Blame Contribute Delete
13.1 kB
# -*- coding: utf-8 -*-
import os
import re
import urllib.parse
import time
import random
import gradio as gr
import requests
from dotenv import load_dotenv
from openai import OpenAI
from quiz_data import QUESTIONS, PROFILES
from utils import (
start_state,
compute_winning_profile,
build_profile_summary_prompt,
)
# ------------------------------------------------------
# Config
# ------------------------------------------------------
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
WELCOME_TITLE = "CX Management Maturity Navigator"
WELCOME_INTRO = (
"Where Does Your CX Stand? Find Out in 5 Minutes.\n"
"Discover your CX Management Maturity level – and your next move."
)
PARDOT_FORM_HANDLER_URL = "http://go.demo.pardot.com/l/718473/2026-02-27/3mwk"
# Fallback responses (used only if LLM call fails)
BOT_ACKS_FALLBACK = [
"That's really helpful context, thank you.",
"Interesting — that tells me a lot about where you're at.",
"Got it, that makes sense given what you're working with.",
"Thanks for being so candid about that.",
"That's a common challenge at this stage, actually.",
"Good to know — it shapes the picture quite a bit.",
]
# ------------------------------------------------------
# Helpers
# ------------------------------------------------------
def welcome_messages():
return [
{"role": "assistant", "content": f"Hi there! 👋\n\n{WELCOME_INTRO}"},
{"role": "assistant", "content": QUESTIONS[0]["text"]},
]
def get_current_options(state):
idx = state["current_question_index"]
if idx >= len(QUESTIONS):
return []
return [opt[0] for opt in QUESTIONS[idx]["options"]]
def generate_gpt_summary(profile_key, answers):
prompt = build_profile_summary_prompt(profile_key, answers)
try:
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a concise, professional Merkle CX expert."},
{"role": "user", "content": prompt},
],
temperature=0.6,
)
return completion.choices[0].message.content.strip()
except Exception as e:
print("Error calling OpenAI:", e)
return "Your answers show a clear vision for your CX journey. Let's look at the details below."
def generate_ack(question_text: str, user_answer: str) -> str:
"""
Uses the LLM to generate a short, natural, contextual acknowledgment
based on the question and the user's answer.
Falls back to a static response if the API call fails.
"""
try:
completion = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a warm, sharp CX consultant at Merkle having a real conversation. "
"The user just answered a maturity assessment question. "
"React briefly and naturally to their specific answer — 1 to 2 sentences max. "
"Be conversational, empathetic and human. Show you actually understood what they said. "
"Vary your tone and openings. Never start with 'Great!' or 'Absolutely!'. "
"No bullet points. Minimal emojis. Don't parrot the answer back verbatim."
)
},
{
"role": "user",
"content": f"Question: {question_text}\nTheir answer: {user_answer}"
}
],
temperature=0.85,
max_tokens=60,
)
return completion.choices[0].message.content.strip()
except Exception as e:
print("Error generating ack:", e)
return random.choice(BOT_ACKS_FALLBACK)
def format_answers_for_pardot(answers):
parts = [f"{a['id']}: {a['answer']}" for a in answers]
return " | ".join(parts)
def send_to_pardot_backend(state):
if not state.get("final_profile_key"):
return
profile = PROFILES[state["final_profile_key"]]
payload = {
"email": state["email"],
"firstname": state["first_name"],
"Lastname": state["last_name"],
"Company": state["company"],
"Jobtitle": state["job_title"],
"agentic_ai_personality": profile["label"],
"agentic_ai_profile_key": profile["key"],
"agentic_ai_quiz_answers": format_answers_for_pardot(state["answers"]),
}
try:
resp = requests.post(PARDOT_FORM_HANDLER_URL, data=payload, timeout=5)
print("Pardot response:", resp.status_code)
except Exception as e:
print("Error sending to Pardot:", e)
def is_professional_email(email: str) -> bool:
email = email.strip().lower()
if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email):
return False
personal_domains = {
"gmail.com", "yahoo.com", "yahoo.fr", "hotmail.com",
"outlook.com", "live.com", "icloud.com", "me.com",
"wanadoo.fr", "orange.fr"
}
domain = email.split("@", 1)[1]
return domain not in personal_domains
# ------------------------------------------------------
# Logic: Quiz avec effet Typewriter
# ------------------------------------------------------
def chatbot_step(selected_option, state, history):
if state is None:
state = start_state()
history = welcome_messages()
yield history, state, gr.update(choices=get_current_options(state), value=None, visible=True), gr.update(visible=False)
return
if not selected_option:
yield history, state, gr.update(), gr.update()
return
idx = state["current_question_index"]
q = QUESTIONS[idx]
profile_key = None
for opt_text, prof in q["options"]:
if opt_text == selected_option:
profile_key = prof
break
state["answers"].append({
"id": q["id"],
"question": q["text"],
"answer": selected_option,
"profile": profile_key,
})
if profile_key != "none":
state["profile_counts"][profile_key] += 1
# 1. Affiche la réponse de l'utilisateur
history.append({"role": "user", "content": selected_option})
yield history, state, gr.update(value=None, interactive=False), gr.update(visible=False)
# 2. BULLE 1 — Acquiescement LLM (typewriter)
ack_msg = generate_ack(q["text"], selected_option)
history.append({"role": "assistant", "content": ""})
for i in range(0, len(ack_msg), 2):
history[-1]["content"] += ack_msg[i:i+2]
time.sleep(0.01)
yield history, state, gr.update(value=None, interactive=False), gr.update(visible=False)
# Yield explicite pour que Gradio commite la bulle avant d'en ouvrir une nouvelle
yield history, state, gr.update(value=None, interactive=False), gr.update(visible=False)
time.sleep(0.4)
state["current_question_index"] += 1
# 3. BULLE 2 — Question suivante, texte brut sans numérotation
if state["current_question_index"] < len(QUESTIONS):
next_q = QUESTIONS[state["current_question_index"]]
history.append({"role": "assistant", "content": ""}) # nouvelle bulle
for i in range(0, len(next_q["text"]), 2):
history[-1]["content"] += next_q["text"][i:i+2]
time.sleep(0.01)
yield history, state, gr.update(value=None, interactive=False), gr.update(visible=False)
yield history, state, gr.update(choices=[opt[0] for opt in next_q["options"]], value=None, interactive=True), gr.update(visible=False)
else:
state["phase"] = "lead_gen"
transition_msg = "That's everything I need! Based on your answers, I've built your CX Maturity Profile. Fill in your details below and I'll reveal your results."
history.append({"role": "assistant", "content": ""}) # nouvelle bulle
for i in range(0, len(transition_msg), 2):
history[-1]["content"] += transition_msg[i:i+2]
time.sleep(0.01)
yield history, state, gr.update(visible=False), gr.update(visible=False)
yield history, state, gr.update(visible=False), gr.update(visible=False)
time.sleep(0.3)
yield history, state, gr.update(visible=False), gr.update(visible=True)
# ------------------------------------------------------
# Logic: Lead Gen Form Submission (Typewriter)
# ------------------------------------------------------
def handle_form_submit(fname, lname, email, company, job, consent, state, history):
if not fname or not lname or not email or not company:
history.append({"role": "assistant", "content": "⚠️ Please fill in all required fields (First Name, Last Name, Email, Company)."})
yield history, state, gr.update(visible=True)
return
if not consent:
history.append({"role": "assistant", "content": "⚠️ Please agree to the data usage policy to see your results."})
yield history, state, gr.update(visible=True)
return
if not is_professional_email(email):
history.append({"role": "assistant", "content": "⚠️ Please enter a valid professional email address (no Gmail, Outlook, etc.)."})
yield history, state, gr.update(visible=True)
return
state["first_name"] = fname
state["last_name"] = lname
state["email"] = email
state["company"] = company
state["job_title"] = job
history.append({"role": "user", "content": "My details are submitted!"})
yield history, state, gr.update(visible=False)
winning_key = compute_winning_profile(state["profile_counts"])
state["final_profile_key"] = winning_key
profile = PROFILES[winning_key]
summary = generate_gpt_summary(winning_key, state["answers"])
send_to_pardot_backend(state)
subject = f"I just took the CX Maturity Quiz – I'm a {profile['label']}"
body = f"Hi,\n\n{profile['email_body_template']}\n\n{state['first_name']} {state['last_name']} | {state['company']} | {state['job_title']}"
mailto_link = f"mailto:enquiries-dach@merkle.com?subject={urllib.parse.quote(subject)}&body={urllib.parse.quote(body)}"
cta_html = f'<br><a href="{mailto_link}" target="_blank" style="display:inline-block; padding:12px 24px; background-color:#1a1a1a; color:#ffffff; text-decoration:none; border-radius:4px; font-weight:bold; font-size:16px;">✉️ {profile["cta_text"]}</a><br>'
combined = (
f"**Your Profile: {profile['label']}**\n\n"
f"{summary}\n\n"
f"**{profile['result_title']}**\n\n"
f"{profile['result_body']}\n\n"
f"Feel free to get in contact to schedule an appointment to discuss your specific challenges and provide further inspiration.\n\n"
f"{cta_html}\n\n"
"*(I've also logged your result for the Merkle team so they can prepare tailored ideas for you!)*"
)
history.append({"role": "assistant", "content": ""})
for i in range(0, len(combined), 5):
history[-1]["content"] += combined[i:i+5]
time.sleep(0.01)
yield history, state, gr.update(visible=False)
state["phase"] = "done"
yield history, state, gr.update(visible=False)
# ------------------------------------------------------
# UI
# ------------------------------------------------------
def build_interface():
with gr.Blocks(fill_height=True) as demo:
gr.Markdown(f"### {WELCOME_TITLE}")
state = gr.State(start_state())
chatbot = gr.Chatbot(
value=welcome_messages(),
height=500,
label="CX Guide",
show_label=False
)
options = gr.Radio(
choices=[opt[0] for opt in QUESTIONS[0]["options"]],
label="Choose your answer:",
interactive=True,
)
with gr.Group(visible=False) as lead_form:
gr.Markdown("#### Where should we send your full maturity report?")
with gr.Row():
fname = gr.Textbox(label="First Name *")
lname = gr.Textbox(label="Last Name *")
with gr.Row():
email = gr.Textbox(label="Business Email *")
company = gr.Textbox(label="Company *")
job = gr.Textbox(label="Job Title")
consent = gr.Checkbox(
label="I agree that my data may be used by Merkle to follow up on this quiz. I can revoke my consent at any time.",
value=False
)
submit_btn = gr.Button("See My Results 🚀", variant="primary")
options.change(
fn=chatbot_step,
inputs=[options, state, chatbot],
outputs=[chatbot, state, options, lead_form],
)
submit_btn.click(
fn=handle_form_submit,
inputs=[fname, lname, email, company, job, consent, state, chatbot],
outputs=[chatbot, state, lead_form],
)
return demo
demo = build_interface()
if __name__ == "__main__":
demo.launch(ssr_mode=False)