| import gradio as gr |
| import openai |
| import json |
| import re |
| import os |
| from datetime import datetime, timedelta |
| import uuid |
| import random |
| from typing import Dict |
|
|
| from config import ( |
| OPENAI_API_KEY, DB_PATH, EMBED_MODEL, |
| GEN_MODEL, FAST_MODEL, |
| EMOTIONAL_KEYWORDS, ACTION_KEYWORDS, POLICY_KEYWORDS, |
| EMAIL_ONLY_KEYWORDS, DETAIL_SYNONYMS, PERSONA_INSTRUCTION |
| ) |
| from utils import ( |
| get_embedding, cosine_similarity, find_top_k_matches, |
| classify_intent, should_include_email, classify_user_type |
| ) |
| from database import ( |
| fetch_all_embeddings, |
| fetch_row_by_id, |
| fetch_all_faq_embeddings, |
| get_session_state, |
| update_session_state, |
| log_question, |
| get_recent_history |
| ) |
| from scraper import scrape_workshops_from_squarespace |
|
|
| |
| |
| |
|
|
| if not OPENAI_API_KEY: |
| raise ValueError("OPENAI_API_KEY not found in .env file") |
|
|
| openai.api_key = OPENAI_API_KEY |
|
|
|
|
| |
|
|
| |
| workshop_cache = { |
| 'data': [], |
| 'embeddings': [], |
| 'last_updated': None, |
| 'cache_duration': timedelta(hours=24) |
| } |
|
|
| |
| STRUCTURED_KNOWLEDGE = {} |
| try: |
| knowledge_path = os.path.join(os.path.dirname(__file__), "structured_knowledge.json") |
| with open(knowledge_path, "r") as f: |
| STRUCTURED_KNOWLEDGE = json.load(f) |
| except Exception as e: |
| print(f"Error loading structured_knowledge.json: {e}") |
|
|
| def get_structured_knowledge_snippet(preference=None): |
| """Formats structured knowledge into a text snippet for the prompt, filtering by preference if provided.""" |
| if not STRUCTURED_KNOWLEDGE: |
| return "" |
| |
| snippet = "--- STRUCTURED TRUTH SHEET (VERIFIED KNOWLEDGE) ---\n" |
| |
| |
| free_online = STRUCTURED_KNOWLEDGE.get('free_online_class', {}).get('link', '') |
| if not preference or preference.lower() == 'online': |
| snippet += f"Free Online Class: {free_online}\n" |
| |
| kids = STRUCTURED_KNOWLEDGE.get('kids_classes', {}) |
| if not preference or preference.lower() == 'online': |
| snippet += f"Kids Classes (Online): {kids.get('online_link', '')}\n" |
| if not preference or preference.lower() == 'instudio': |
| snippet += f"Kids Classes (Atlanta): {kids.get('atlanta_link', '')}\n" |
| |
| summit = STRUCTURED_KNOWLEDGE.get('summit', {}) |
| snippet += f"Summit: {summit.get('link', '')} - {summit.get('description', '')}\n" |
| |
| |
| instructors = STRUCTURED_KNOWLEDGE.get('instructors', []) |
| if instructors: |
| snippet += "Instructors & Roles (STRICT):\n" |
| for inst in instructors: |
| not_roles = inst.get('not_roles', []) |
| not_str = f" [NOT: {', '.join(not_roles)}]" if not_roles else "" |
| snippet += f"- {inst['name']}: {inst['role']}{not_str}\n" |
| |
| |
| paths = STRUCTURED_KNOWLEDGE.get('paths', {}) |
| if not preference or preference.lower() == 'online': |
| snippet += f"Online Path: {paths.get('online', '')}\n" |
| if not preference or preference.lower() == 'instudio': |
| snippet += f"Atlanta Path: {paths.get('atlanta', '')}\n" |
| |
| snippet += "--------------------------------------------------\n" |
| return snippet |
|
|
| |
| |
| |
|
|
| def calculate_workshop_confidence(w: Dict) -> float: |
| """Calculate confidence score of retrieved workshop data""" |
| score = 0.0 |
| if w.get('title'): score += 0.3 |
| if w.get('instructor_name'): score += 0.3 |
| if w.get('date'): score += 0.2 |
| if w.get('time'): score += 0.1 |
| if w.get('source_url'): score += 0.1 |
| return round(score, 2) |
|
|
| |
| |
| |
|
|
| def get_current_workshops(): |
| """Get current workshops with caching""" |
| global workshop_cache |
| |
| now = datetime.now() |
| |
| |
| if (workshop_cache['last_updated'] and |
| now - workshop_cache['last_updated'] < workshop_cache['cache_duration'] and |
| workshop_cache['data']): |
| print("Using cached workshop data") |
| return workshop_cache['data'], workshop_cache['embeddings'] |
| |
| print("Fetching fresh workshop data...") |
| |
| |
| online_workshops = scrape_workshops_from_squarespace("https://www.getscenestudios.com/online") |
| instudio_workshops = scrape_workshops_from_squarespace("https://www.getscenestudios.com/instudio") |
| |
| all_workshops = online_workshops + instudio_workshops |
| |
| |
| valid_workshops = [] |
| total_score = 0 |
| for w in all_workshops: |
| conf = calculate_workshop_confidence(w) |
| if conf >= 0.8: |
| valid_workshops.append(w) |
| total_score += conf |
| else: |
| print(f"β οΈ Rejecting weak record (Confidence: {conf}): {w.get('title', 'Unknown')}", flush=True) |
| |
| avg_conf = total_score / len(valid_workshops) if valid_workshops else 0 |
| print(f"π DATA INTEGRITY: Found {len(all_workshops)} total, {len(valid_workshops)} valid (Confidence >= 0.8)", flush=True) |
| print(f"π Retrieval Confidence: {avg_conf:.2f} (Average)", flush=True) |
| |
| all_workshops = valid_workshops |
| |
| if not all_workshops: |
| if workshop_cache['data']: |
| print("Scraping failed, using cached data") |
| return workshop_cache['data'], workshop_cache['embeddings'] |
| else: |
| print("No workshop data available") |
| return [], [] |
| |
| |
| workshop_embeddings = [] |
| for workshop in all_workshops: |
| try: |
| embedding = get_embedding(workshop['full_text']) |
| workshop_embeddings.append(embedding) |
| except Exception as e: |
| print(f"Error generating embedding for workshop: {e}") |
| workshop_embeddings.append([0] * 1536) |
| |
| |
| workshop_cache['data'] = all_workshops |
| workshop_cache['embeddings'] = workshop_embeddings |
| workshop_cache['last_updated'] = now |
| |
| print(f"Cached {len(all_workshops)} workshops") |
| return all_workshops, workshop_embeddings |
|
|
| def find_top_workshops(user_embedding, k=3): |
| """Find top matching workshops using real-time data""" |
| workshops, workshop_embeddings = get_current_workshops() |
| |
| if not workshops: |
| return [] |
| |
| scored = [] |
| for i, (workshop, emb) in enumerate(zip(workshops, workshop_embeddings)): |
| try: |
| score = cosine_similarity(user_embedding, emb) |
| scored.append((score, i, workshop['full_text'], workshop)) |
| except Exception as e: |
| print(f"Error calculating similarity: {e}") |
| continue |
| |
| scored.sort(reverse=True) |
| return scored[:k] |
|
|
| |
| |
| |
|
|
| def generate_enriched_links(row): |
| base_url = row.get("youtube_url") |
| guest_name = row.get("guest_name", "") |
| highlights = json.loads(row.get("highlight_json", "[]")) |
| summary = highlights[0]["summary"] if highlights else "" |
| |
| |
| if summary: |
| first_sentence = summary.split('.')[0] + '.' |
| |
| if len(first_sentence) > 120: |
| short_summary = first_sentence[:117] + "..." |
| else: |
| short_summary = first_sentence |
| else: |
| short_summary = "Industry insights for actors" |
| |
| markdown = f"π§ [Watch {guest_name}'s episode here]({base_url}) - {short_summary}" |
| return [markdown] |
|
|
| def build_enhanced_prompt(user_question, context_results, top_workshops, user_preference=None, user_type='unknown', enriched_podcast_links=None, wants_details=False, current_topic=None, mode="Mode B", is_low_confidence=False, is_faq_match=False, is_policy_query=False): |
| """Builds the system prompt with strict formatting rules.""" |
| |
| |
| free_class_url = STRUCTURED_KNOWLEDGE.get('free_online_class', {}).get('link', "https://www.getscenestudios.com/online") |
| if user_preference and user_preference.lower() == 'instudio': |
| free_class_url = STRUCTURED_KNOWLEDGE.get('paths', {}).get('atlanta', "https://www.getscenestudios.com/instudio") |
| |
| atlanta_link = STRUCTURED_KNOWLEDGE.get('paths', {}).get('atlanta', "https://www.getscenestudios.com/instudio") |
| online_link = STRUCTURED_KNOWLEDGE.get('paths', {}).get('online', "https://www.getscenestudios.com/online") |
| |
| truth_sheet_snippet = get_structured_knowledge_snippet(preference=user_preference) |
| |
| single_podcast = "" |
| |
| |
| def format_workshop(w): |
| |
| if not w.get('title') or not w.get('instructor_name') or not w.get('date'): |
| return None |
| |
| |
| link = "https://www.getscenestudios.com/instudio" if "/instudio" in w.get('source_url', '') else "https://www.getscenestudios.com/online" |
| |
| |
| w_type = "Online" if "online" in w.get('source_url', '') else "In-Studio" |
| if user_preference: |
| if user_preference.lower() != w_type.lower(): |
| return None |
|
|
| |
| confidence = calculate_workshop_confidence(w) |
| if confidence < 0.70: |
| return None |
|
|
| return f"- [{w['title']}]({link}) with {w['instructor_name']} ({w_type}) on {w['date']} at {w.get('time', '')}" |
|
|
| |
| workshop_lines = [] |
| if top_workshops: |
| for _, _, _, w_data in top_workshops[:5]: |
| formatted = format_workshop(w_data) |
| if formatted: |
| workshop_lines.append(formatted) |
| |
|
|
| workshop_text = "" |
| if workshop_lines: |
| workshop_text = "\n".join(workshop_lines[:3]) |
| else: |
| |
| label = f"{user_preference.capitalize()} " if user_preference else "" |
| link = online_link if user_preference == 'online' else atlanta_link if user_preference == 'instudio' else online_link |
| |
| workshop_text = f"We are constantly updating our schedule! You can view and [register for upcoming {label}workshops here]({link})." |
| |
| |
| podcast_options = "" |
| if not enriched_podcast_links: |
| podcast_options = "Our latest industry insights are available on YouTube: https://www.youtube.com/@GetSceneStudios" |
| else: |
| |
| podcast_options = "\n".join(enriched_podcast_links[:3]) |
| |
| |
| is_emotional = detect_response_type(user_question) == "support" |
| |
| if is_emotional: |
| prompt = f"""{PERSONA_INSTRUCTION} |
| |
| You are acting in SUPPORT MODE. |
| |
| CRITICAL INSTRUCTIONS: |
| 1. ACKNOWLEDGE their feelings first (e.g., "I hear how frustrating it is to feel stuck..."). |
| 2. Provide SUPPORTIVE language (2-3 sentences max). |
| 3. Offer EXACTLY ONE gentle follow-up resource: either the podcast OR the free class. |
| 4. DO NOT suggest paid workshops or upsell in this response. |
| 5. KEEP IT BRIEF (β€150 words). |
| |
| USER'S QUESTION: {user_question} |
| |
| REQUIRED RESPONSE FORMAT: |
| [Your empathetic, supportive acknowledgment] |
| |
| Here's a free resource that might help you move forward: |
| [Pick ONE: {single_podcast} OR Free Class at {free_class_url}] |
| |
| Questions? Contact info@getscenestudios.com""" |
| return prompt |
| |
| |
| if is_policy_query: |
| prompt = f"""{PERSONA_INSTRUCTION} |
| |
| You are acting as a helpful mentor assisting with a policy-related inquiry (refund, cancellation, or billing). |
| |
| CRITICAL INSTRUCTIONS: |
| 1. Be extremely polite, warm, and professional. |
| 2. If the user is asking for a refund or to cancel, you MUST ask them for a reason if they haven't provided one yet. |
| 3. If they haven't mentioned which class or podcast they are referring to, ask them politely to specify. |
| 4. Let them know it's no problem at all and that we want to make sure they are taken care of. |
| 5. Always provide the contact email: info@getscenestudios.com for them to finalize their request. |
| 6. Do NOT suggest new workshops or podcasts in this response. |
| 7. ACKNOWLEDGE their situation first. |
| |
| USER'S QUESTION: {user_question} |
| |
| REQUIRED RESPONSE FORMAT: |
| [Your polite, mentor-like response asking for missing details or acknowledging the reason] |
| |
| Questions? Contact info@getscenestudios.com""" |
| return prompt |
|
|
| |
| question_lower = user_question.lower() |
| context_snippet = "" |
| |
| |
| detected_topic = None |
| if any(word in question_lower for word in ['agent', 'representation', 'rep', 'manager']): |
| detected_topic = 'agent' |
| elif any(word in question_lower for word in ['beginner', 'new', 'start', 'beginning']): |
| detected_topic = 'beginner' |
| elif any(word in question_lower for word in ['callback', 'audition', 'tape', 'self-tape', 'booking']): |
| detected_topic = 'audition' |
| elif any(word in question_lower for word in ['mentorship', 'coaching']): |
| detected_topic = 'mentorship' |
| elif any(word in question_lower for word in ['price', 'cost', 'how much']): |
| detected_topic = 'pricing' |
| elif any(word in question_lower for word in ['class', 'workshop', 'training', 'learn']): |
| detected_topic = 'classes' |
| elif any(word in question_lower for word in ['membership', 'gsp', 'plus']): |
| detected_topic = 'membership' |
| |
| |
| if not detected_topic and current_topic: |
| topic_map = { |
| 'agent_seeking': 'agent', |
| 'beginner': 'beginner', |
| 'audition_help': 'audition', |
| 'mentorship': 'mentorship', |
| 'pricing': 'pricing', |
| 'classes': 'classes', |
| 'membership': 'membership' |
| } |
| detected_topic = topic_map.get(current_topic) |
|
|
| |
| if detected_topic == 'agent': |
| context_snippet = "Get Scene Studios has helped 1000+ actors land representation. Total Agent Prep offers live practice with working agents (age 16+, limited to 12 actors)." |
| elif detected_topic == 'beginner': |
| context_snippet = "Get Scene Studios specializes in getting actors audition-ready fast with camera technique and professional self-tape skills." |
| elif detected_topic == 'audition': |
| context_snippet = "Get Scene offers Crush the Callback (Zoom simulation) and Perfect Submission (self-tape mastery) for actors refining their technique." |
| elif detected_topic == 'mentorship': |
| context_snippet = "Working Actor Mentorship is a 6-month program ($3,000) with structured feedback and industry access." |
| elif detected_topic == 'pricing': |
| context_snippet = "Get Scene Studios pricing varies by program. Most workshops cap at 12-14 actors for personalized feedback." |
| elif detected_topic == 'classes': |
| link = online_link if user_preference == 'online' else atlanta_link |
| context_snippet = f"Get Scene Studios offers world-class {user_preference or ''} acting workshops. Our sessions focus on camera technique and industry readiness. Full details at {link}." |
| elif detected_topic == 'membership': |
| context_snippet = "Get Scene Plus (GSP) is our membership program that provides ongoing access to industry pros and audition insights." |
| elif 'summit' in question_lower: |
| context_snippet = "The Get Scene Summit is a premier special event featuring massive line-ups of agents, managers, and casting directors. It is NOT a recursive workshop." |
| else: |
| context_snippet = "Get Scene Studios (founded by Jesse Malinowski) offers training for TV/film actors at all levels." |
|
|
| preference_instruction = "" |
| if not user_preference: |
| preference_instruction = """ |
| IMPORTANT: We need to know if the user prefers "Online" or "In-Studio" workshops. |
| If their question is broad (e.g., "starting acting", "kids classes", "workshops", "training", "classes") and they haven't specified a format, you MUST START your response with this exact question: "Are you looking for online training or in-studio in Atlanta?" |
| NO PREFIXES, NO "WARM" TRANSITIONS, NO PARAPHRASING. |
| |
| FEW-SHOT EXAMPLES: |
| User: "I want to start acting" |
| Response: "Are you looking for online training or in-studio in Atlanta? That's a fantastic decision! With Get Scene Studios..." |
| |
| User: "Do you have kids classes?" |
| Response: "Are you looking for online training or in-studio in Atlanta? Absolutely, we offer kids classes in both formats..." |
| """ |
| else: |
| preference_instruction = f""" |
| USER PREFERENCE KNOWN: {user_preference.upper()} |
| 1. DO NOT ask "Online or In-Studio" again. |
| 2. Ensure your recommendations align with {user_preference.upper()} where possible. |
| """ |
|
|
| BUSINESS_RULES_INSTRUCTION = f""" |
| TOP-PRIORITY BUSINESS RULES (NO EXCEPTIONS): |
| 1. **NO AUDITING**: Workshops can NEVER be audited. Do not reason about this. Tell the user "We do not allow auditing for our workshops" and immediately redirect them to the Free Online Class. |
| 2. **FREE CLASS FIRST**: The Free Online Class is the MANDATORY first step for ALL new users. If a user is "starting out", "new to acting", or asking "how to begin", you MUST route them to the Free Online Class link below as their primary next step. |
| 3. **NO IMMEDIATE PAID RECOMMENDATIONS**: For new or unclear users, do NOT recommend specific paid workshops yet. Focus entirely on the Free Online Class as the entry point. |
| 4. **KIDS CLASSES**: We offer kids classes both Online and in Atlanta (In-Studio). |
| 5. **SUMMIT**: The Summit is a special event offering, NOT a regular workshop. |
| {"6. **STRICT LINK FILTERING**: User prefers " + user_preference.upper() + ". You MUST ONLY provide links for " + user_preference.upper() + " training. OMIT any " + ("In-Studio" if user_preference.lower() == 'online' else "Online") + " links entirely." if user_preference else ""} |
| 7. **ROLE INTEGRITY (STRICT)**: |
| - **THE TRUTH SHEET IS THE ABSOLUTE AND FINAL AUTHORITY.** It overrides ANY information found in podcast descriptions, workshop titles, or suggested by the user. |
| - ONLY use the roles explicitly defined in the Truth Sheet. |
| - **NEVER infer a role** from the context of a workshop or podcast. |
| - If someone is teaching a class, do NOT assume they are an "Instructor" unless the Truth Sheet says so. |
| - If someone is labeled as an "Agent", do NOT call them an "Instructor" or "Mentor" unless explicitly listed as such in the TRUTH SHEET. |
| - Pay attention to the "[NOT: ...]" list for each person in the Truth Sheet. For example, if someone is listed as "[NOT: Instructor]", NEVER call them an instructor, even if they are described as one in a podcast or workshop description. |
| - **NEVER** guess or invent a role for anyone. |
| """ |
| |
| detail_instruction = "Answer the user's question briefly (2-3 sentences max, β€150 words total)." |
| if wants_details: |
| target = f" regarding {detected_topic or 'the current recommendations'}" |
| detail_instruction = f"Provide a detailed and thorough explanation for the user's request{target}. Focus on being helpful and providing deep value as a mentor." |
|
|
| |
| email_contact = "" |
| if should_include_email(user_question): |
| email_contact = "\n \nQuestions? Contact info@getscenestudios.com" |
|
|
| |
| retrieved_info = "" |
| if context_results: |
| retrieved_info = f"\nRELEVANT INFORMATION FROM KNOWLEDGE BASE:\n{context_results}\n" |
|
|
| is_beginner = (detected_topic == 'beginner') |
| beginner_enforcement = "" |
| if is_beginner: |
| beginner_enforcement = """ |
| CRITICAL: The user is a BEGINNER. You MUST prioritize the Free Online Class above all else. |
| 1. Do NOT recommend specific paid workshops in your numbered list. |
| 2. Instead, provide the Free Online Class as your primary recommendation. |
| 3. Your numbered list should be: |
| 1. Free Online Class (The mandatory first step) |
| 2. The Get Scene Podcast (For industry mindset) |
| 3. [Choose a very general resource or a 1:1 consultation if available, but NOT a specific workshop] |
| """ |
|
|
| user_type_instruction = "" |
| if user_type == 'new_actor': |
| user_type_instruction = "USER TYPE: NEW ACTOR. Focus heavily on foundation, the Free Online Class, and beginner-friendly mindset. Avoid advanced industry jargon." |
| elif user_type == 'experienced_actor': |
| user_type_instruction = "USER TYPE: EXPERIENCED ACTOR. Focus on advanced technique, Agent Prep, mentorship, and industry networking. Use professional terminology." |
| elif user_type == 'parent': |
| user_type_instruction = "USER TYPE: PARENT. Focus on kids/teen programs, safety, youth training paths, and parent-specific concerns." |
| elif user_type == 'current_student': |
| user_type_instruction = "USER TYPE: EXISTING STUDENT. Focus on GSP membership benefits, advanced mentorships (WAM), and specialized recurring workshops." |
|
|
| |
| if is_faq_match: |
| prompt = f"""{PERSONA_INSTRUCTION} |
| |
| {truth_sheet_snippet} |
| |
| {BUSINESS_RULES_INSTRUCTION} |
| |
| {user_type_instruction} |
| |
| {context_snippet}{retrieved_info} |
| |
| CRITICAL INSTRUCTIONS (FAQ MODE): |
| - You are answering a question that has a direct match in our FAQ. |
| - Answer the user's question directly and punchily using ONLY the provided information. |
| - **DO NOT** use the structured 1. 2. 3. format. |
| - **DO NOT** ask a routing question. |
| - **MANDATORY: Use direct hyperlinks.** For ANY mention of signing up, classes, kids programs, or the free class, you MUST include the direct [Title](Link) format. |
| - Focus on being a helpful guide. {preference_instruction} |
| |
| CRITICAL ROLE GUARD (FINAL AUTHORITY): |
| - Corey Lawson: Instructor/Actor [NOT an Agent] |
| - Jacob Lawson: Agent/Owner [NOT an Instructor] |
| - Jesse Malinowski: Founder/Mentor [NOT an Agent] |
| - Alex White: Agent [NOT an Instructor/Mentor] |
| - THE TRUTH SHEET IS THE ABSOLUTE AUTHORITY. |
| |
| USER'S QUESTION: {user_question} |
| |
| REQUIRED RESPONSE FORMAT: |
| [Punchy, helpful answer based on FAQ with relevant links]{email_contact}""" |
| return prompt |
|
|
| if mode == "Mode A": |
| |
| prompt = f"""{PERSONA_INSTRUCTION} |
| |
| {truth_sheet_snippet} |
| |
| {BUSINESS_RULES_INSTRUCTION} |
| |
| {user_type_instruction} |
| |
| {beginner_enforcement} |
| |
| {context_snippet}{retrieved_info} |
| |
| CRITICAL INSTRUCTIONS (RECOMMENDATION MODE): |
| - {detail_instruction} |
| - Use natural, human transitions between your answer and the recommendations. |
| - For each recommendation, add a tiny bit of "mentor advice" on why it helps. |
| - Use ONLY the provided links - do not invent recommendations. |
| - **MANDATORY: Use direct hyperlinks.** For ANY mention of signing up, classes, kids programs, the Summit, or the free class, you MUST include the direct [Title](Link) format. |
| - **CRITICAL: PRESERVE URLS.** You MUST include the full URL in parentheses `(https://...)`. DO NOT output just the bracketed text `[Title]`. If you fail to include the URL, the link will be broken. |
| - **NEVER say "check our website"** or "visit the link below". Embed the link directly into the relevant part of your mentor advice. |
| - Focus on clean, readable formatting.{preference_instruction} |
| |
| CRITICAL ROLE GUARD (FINAL AUTHORITY): |
| - Corey Lawson: Instructor/Actor [NOT an Agent] |
| - Jacob Lawson: Agent/Owner [NOT an Instructor] |
| - Jesse Malinowski: Founder/Mentor [NOT an Agent] |
| - Alex White: Agent [NOT an Instructor/Mentor] |
| - THE TRUTH SHEET IS THE ABSOLUTE AUTHORITY. It overrides ALL other info. |
| - NEVER call Corey Lawson an agent. They are brothers with different roles. |
| |
| USER'S QUESTION: {user_question} |
| |
| REQUIRED RESPONSE FORMAT (STRICT): |
| [Helpful, mentor-like answer] |
| |
| Here's your path forward: |
| 1. Free Online Class (Mandatory First Step): {free_class_url} |
| 2. Recommended Podcast Episode (For Industry Mindset): |
| {podcast_options} |
| 3. Recommended Workshop/Next Step: |
| {workshop_text}{email_contact} |
| |
| CRITICAL: YOU MUST USE THE ABOVE "1. 2. 3." STRUCTURE EXACTLY. DO NOT RENAME THE STEPS. DO NOT SKIP THE PODCAST. |
| """ |
| else: |
| |
| prompt = f"""{PERSONA_INSTRUCTION} |
| |
| {truth_sheet_snippet} |
| |
| {BUSINESS_RULES_INSTRUCTION} |
| |
| {context_snippet}{retrieved_info} |
| |
| CRITICAL INSTRUCTIONS (FRONT DESK MODE): |
| - You are acting as the warm and helpful Front Desk Mentor. |
| - **MANDATORY: Ask a routing question AT THE BEGINNING** of your response (e.g., "Are you looking to start your journey or refine existing skills?"). |
| - Answer the user's question directly using the provided information but keep it punchyβ**no essays**. |
| - **MANDATORY: Provide direct hyperlinks** for ANY mention of registration, classes, kids programs, the Summit, or more information. Use EXACTLY these links as relevant: |
| - Free Online Class: [{free_class_url}]({free_class_url}) |
| - Recommended for you: {podcast_options} |
| - Upcoming Workshops: {workshop_text} |
| - Southeast Actor Summit: [Southeast Actor Summit Registration](https://www.getscenestudios.com/southeast-actor-summit) |
| - **NEVER say "go to the website"** or "check our site". Always provide the specific hyperlink directly in your answer. |
| - **NEVER guess** or invent information. If it's not in the context, guide the user to clarify. |
| - **MANDATORY: Guide the user to the next step** at the end of your response (e.g., "A great next step for you would be to sign up for our free class"). |
| - {detail_instruction} |
| - Focus on being a helpful guide.{preference_instruction} |
| {"MANDATORY: We don't have a high-confidence match for this specific question. Provide the CLOSEST possible link from our verified knowledge above for their general query." if is_low_confidence else ""} |
| |
| CRITICAL ROLE GUARD (FINAL AUTHORITY): |
| - Corey Lawson: Instructor/Actor [NOT an Agent] |
| - Jacob Lawson: Agent/Owner [NOT an Instructor] |
| - Jesse Malinowski: Founder/Mentor [NOT an Agent] |
| - Alex White: Agent [NOT an Instructor/Mentor] |
| - THE TRUTH SHEET IS THE ABSOLUTE AUTHORITY. It overrides ALL other info. |
| - NEVER call Corey Lawson an agent. They are brothers with different roles. |
| |
| USER'S QUESTION: {user_question} |
| |
| [Routing Question] |
| [Helpful, punchy response with links] |
| **IMPORTANT: You MUST choose the most relevant podcast from the list provided above and include its FULL Markdown link including the URL in your response.** |
| [Next step guidance]{email_contact}""" |
| |
| return prompt |
|
|
| |
| |
| |
|
|
| def detect_question_category(question): |
| """Categorize user questions for better context injection""" |
| question_lower = question.lower() |
| |
| categories = { |
| 'agent_seeking': ['agent', 'representation', 'rep', 'manager', 'get an agent'], |
| 'beginner': ['beginner', 'new', 'start', 'beginning', 'first time', 'never acted'], |
| 'audition_help': ['audition', 'callback', 'tape', 'self-tape', 'submission'], |
| 'mentorship': ['mentorship', 'coaching', 'intensive', 'mentor', 'one-on-one'], |
| 'pricing': ['price', 'cost', 'pricing', '$', 'money', 'payment', 'fee'], |
| 'classes': ['class', 'workshop', 'training', 'course', 'learn'], |
| 'membership': ['membership', 'join', 'member', 'gsp', 'plus'], |
| 'podcast': ['podcast', 'podcasts', 'youtube', 'watch', 'listen', 'episode', 'episodes'], |
| 'technical': ['self-tape', 'equipment', 'lighting', 'editing', 'camera'] |
| } |
| |
| detected = [] |
| for category, keywords in categories.items(): |
| if any(keyword in question_lower for keyword in keywords): |
| detected.append(category) |
| |
| return detected |
|
|
| def detect_response_type(question): |
| """Detect if question is emotional/support vs action/results oriented""" |
| question_lower = question.lower() |
| |
| emotional_count = sum(1 for word in EMOTIONAL_KEYWORDS if word in question_lower) |
| action_count = sum(1 for word in ACTION_KEYWORDS if word in question_lower) |
| |
| if emotional_count > 0 and emotional_count >= action_count: |
| return "support" |
| return "standard" |
|
|
| def detect_policy_issue(question): |
| """Detect if question violates hard policy rules (refunds, attendance, etc.) using word boundaries""" |
| question_lower = question.lower() |
| for word in POLICY_KEYWORDS: |
| |
| pattern = rf'\b{re.escape(word)}\b' |
| if re.search(pattern, question_lower): |
| return True |
| return False |
|
|
| def detect_preference(question): |
| """Detect if user is stating a preference""" |
| q_lower = question.lower() |
| if 'online' in q_lower and 'studio' not in q_lower: |
| return 'online' |
| if ('studio' in q_lower or 'person' in q_lower or 'atlanta' in q_lower) and 'online' not in q_lower: |
| return 'instudio' |
| return None |
|
|
| def get_contextual_business_info(categories): |
| """Return relevant business information based on detected question categories""" |
| |
| context_map = { |
| 'agent_seeking': { |
| 'programs': ['Total Agent Prep', 'Working Actor Mentorship'], |
| 'key_info': 'Live pitch practice with real agents, Actors Access optimization', |
| 'journey': 'Total Agent Prep β GSP β Mentorship for sustained progress' |
| }, |
| 'beginner': { |
| 'programs': ['Free Classes', 'Get Scene 360', 'Get Scene Plus'], |
| 'key_info': 'Start with holistic foundation, build consistency', |
| 'journey': 'Free class β Get Scene 360 β GSP membership' |
| }, |
| 'audition_help': { |
| 'programs': ['Perfect Submission', 'Crush the Callback', 'Audition Insight'], |
| 'key_info': 'Self-tape mastery, callback simulation, pro feedback', |
| 'journey': 'Perfect Submission β GSP for ongoing Audition Insight' |
| }, |
| 'mentorship': { |
| 'programs': ['Working Actor Mentorship'], |
| 'key_info': '6-month intensive with structured feedback and accountability', |
| 'journey': 'Ready for commitment β WAM β Advanced workshops' |
| } |
| } |
| |
| relevant_info = {} |
| for category in categories: |
| if category in context_map: |
| relevant_info[category] = context_map[category] |
| |
| return relevant_info |
|
|
| |
| |
| |
|
|
| def update_knowledge_from_question(session_id: str, question: str): |
| """Extract attributes and update knowledge dictionary""" |
| updates = {} |
| |
| |
| pref = detect_preference(question) |
| if pref: |
| updates['format'] = pref |
| |
| |
| cats = detect_question_category(question) |
| if cats: |
| |
| priority_topics = ['agent_seeking', 'beginner', 'audition_help', 'mentorship', 'pricing'] |
| for topic in priority_topics: |
| if topic in cats: |
| updates['topic'] = topic |
| break |
| if 'topic' not in updates and cats: |
| updates['topic'] = cats[0] |
|
|
| if updates: |
| update_session_state(session_id, knowledge_update=updates, increment_count=False) |
| return updates |
| return {} |
|
|
| def process_question(question: str, current_session_id: str): |
| """Main function to process user questions - replaces Flask /ask endpoint""" |
| |
| try: |
| if not question: |
| return "Question is required" |
|
|
| |
| activated_mode = "Mode B" |
|
|
| |
| is_policy_query = False |
| if detect_policy_issue(question) and should_include_email(question): |
| q_lower = question.lower() |
| |
| |
| is_late_miss = any(re.search(rf'\b{re.escape(word)}\b', q_lower) for word in ['late', 'miss', 'missed', 'attendance', 'attend']) |
| |
| if is_late_miss: |
| ans = "Don't worry! You can email info@getscenestudios.com and our team will help you with any attendance or scheduling issues." |
| log_question( |
| question=question, |
| session_id=current_session_id, |
| category="policy_late_miss", |
| answer=ans, |
| detected_mode="Mode B", |
| routing_question=None, |
| rule_triggered="late_miss_polite", |
| link_provided=False |
| ) |
| return ans |
|
|
| |
| is_policy_query = True |
|
|
| |
| update_knowledge_from_question(current_session_id, question) |
| |
| session_state = get_session_state(current_session_id) |
| |
| try: |
| knowledge = json.loads(session_state.get('knowledge_context', '{}')) |
| except: |
| knowledge = {} |
|
|
| user_type = knowledge.get('user_type', 'unknown') |
|
|
| |
| if user_type == 'unknown' or session_state.get('msg_count', 0) % 3 == 0: |
| new_user_type = classify_user_type(question) |
| if new_user_type != 'unknown': |
| user_type = new_user_type |
| knowledge['user_type'] = user_type |
| update_session_state(current_session_id, knowledge_update=knowledge, increment_count=False) |
|
|
| user_preference = knowledge.get('format') |
| current_topic = knowledge.get('topic') |
| |
| if not user_preference: |
| user_preference = session_state.get('preference') |
| |
| update_session_state(current_session_id, increment_count=True) |
|
|
| |
| activated_mode = classify_intent(question) |
| last_mode = knowledge.get('last_mode') |
| |
| if session_state.get('clarification_count', 0) > 0 and last_mode: |
| if len(question.split()) < 5 or any(k in question.lower() for k in ['yes', 'no', 'sure', 'not sure', 'dont know']): |
| activated_mode = last_mode |
| |
| |
| knowledge['last_mode'] = activated_mode |
| print(f"DEBUG: [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Activated Mode for session {current_session_id}: {activated_mode}") |
| update_session_state(current_session_id, knowledge_update=knowledge, increment_count=False) |
|
|
| |
| user_embedding = get_embedding(question) |
|
|
| |
| faq_data = fetch_all_faq_embeddings() |
| top_faqs = [] |
|
|
| for entry_id, question_text, answer_text, emb in faq_data: |
| score = cosine_similarity(user_embedding, emb) |
| top_faqs.append((score, entry_id, question_text, answer_text)) |
| top_faqs.sort(reverse=True) |
|
|
| faq_threshold = 0.50 |
| ambiguous_threshold = 0.60 |
|
|
| is_low_confidence = False |
| context_results = None |
| is_faq_match = False |
|
|
| if top_faqs and top_faqs[0][0] >= faq_threshold: |
| best_score, faq_id, question_text, answer_text = top_faqs[0] |
| print(f"DEBUG: Processing FAQ match through LLM and Truth Sheet rules...") |
| context_results = answer_text |
| is_faq_match = True |
|
|
| elif activated_mode == "Mode A": |
| |
| |
| is_recommendation_query = any(k in question.lower() for k in ['podcast', 'reccomend', 'recommend', 'path', 'help', 'advice', 'guide']) |
| |
| clarification_count = session_state.get('clarification_count', 0) |
| if clarification_count == 0 and not is_recommendation_query: |
| update_session_state(current_session_id, increment_clarification=True, increment_count=False) |
| return "I want to make sure I give you the best advice. Are you looking for classes in [Atlanta](https://www.getscenestudios.com/instudio), [Online](https://www.getscenestudios.com/online), or something else like getting an agent? You can also start right now with our [Free Online Class](https://www.getscenestudios.com/online)!" |
| elif clarification_count > 0 and not is_recommendation_query: |
| update_session_state(current_session_id, reset_clarification=True) |
| return "I'm still not quite sure, and I want to make sure you get the right answer! Please email our team at info@getscenestudios.com and we'll help you directly. In the meantime, you can explore or [register for our Online Path](https://www.getscenestudios.com/online) or [In-Studio classes in Atlanta](https://www.getscenestudios.com/instudio)." |
| |
|
|
| elif top_faqs and top_faqs[0][0] >= ambiguous_threshold: |
| |
| |
| best_score, faq_id, question_text, answer_text = top_faqs[0] |
| print(f"DEBUG: Ambiguous FAQ match (score={best_score:.2f}), using as LLM context: {question_text[:60]}...") |
| context_results = answer_text |
| is_faq_match = True |
|
|
| else: |
| |
| categories = detect_question_category(question) |
| |
| has_session_context = (current_topic is not None) or (user_preference is not None) |
| |
| FOLLOWUP_KEYWORDS = ['yes', 'no', 'sure', 'okay', 'thanks', 'thank you', 'please', 'go ahead', 'continue', 'more'] |
| ACTING_KEYWORDS = ['class', 'workshop', 'coaching', 'studio', 'acting', 'online', 'person', 'atlanta', 'training', 'prefer', 'preference', 'format', 'recommendation', 'online class', 'online workshop','instudio class','instudio workshop', 'actor', 'scene', 'audition', 'theatre', 'film', 'tv', 'commercial', 'agent', 'rep', 'manager', 'instructor', 'role', 'auditing', 'audit', 'representation', 'summit', 'sign up', 'sign-up', 'register', 'enroll', 'schedule', 'cancel', 'reschedule', 'how do i', 'podcast', 'podcasts', 'youtube', 'episode', 'episodes', 'watch', 'refund'] |
| |
| is_acting_related = ( |
| is_policy_query or |
| len(categories) > 0 or |
| detect_response_type(question) == "support" or |
| any(re.search(rf'\b{re.escape(k)}\b', question.lower()) for k in ACTION_KEYWORDS) or |
| any(re.search(rf'\b{re.escape(k)}\b', question.lower()) for k in DETAIL_SYNONYMS) or |
| any(re.search(rf'\b{re.escape(k)}\b', question.lower()) for k in ACTING_KEYWORDS) or |
| (has_session_context and any(re.search(rf'\b{re.escape(k)}\b', question.lower().strip('.!')) for k in FOLLOWUP_KEYWORDS)) or |
| (session_state.get('clarification_count', 0) > 0 and len(question.split()) < 5) |
| ) |
| |
| if not is_acting_related: |
| return "I'm not exactly sure about that. Could you clarify your question?" |
|
|
| |
| is_low_confidence = (activated_mode == "Mode B" and not context_results) |
| |
| |
| update_session_state(current_session_id, reset_clarification=True, increment_count=False) |
| |
| |
| podcast_data = fetch_all_embeddings("podcast_episodes") |
| top_workshops = find_top_workshops(user_embedding, k=3) |
| top_podcasts = find_top_k_matches(user_embedding, podcast_data, k=3) |
|
|
| |
| chat_history = get_recent_history(current_session_id, limit=5) |
| history_text = " ".join([m['content'] for m in chat_history]).lower() |
|
|
| enriched_podcast_links = [] |
| for _, podcast_id, _ in top_podcasts: |
| row = fetch_row_by_id("podcast_episodes", podcast_id) |
| links = generate_enriched_links(row) |
| enriched_podcast_links.extend(links) |
|
|
| if not enriched_podcast_links: |
| fallback = fetch_row_by_id("podcast_episodes", podcast_data[0][0]) |
| enriched_podcast_links = generate_enriched_links(fallback) |
| |
| |
| random.shuffle(enriched_podcast_links) |
| seen_links = [] |
| unseen_links = [] |
| |
| for link in enriched_podcast_links: |
| |
| |
| match = re.search(r'Watch (.*)\'s episode', link) |
| if match: |
| guest_name = match.group(1).lower() |
| if guest_name in history_text: |
| seen_links.append(link) |
| else: |
| unseen_links.append(link) |
| else: |
| unseen_links.append(link) |
| |
| |
| final_podcast_options = unseen_links + seen_links |
|
|
| |
| wants_details = any(syn in question.lower() for syn in DETAIL_SYNONYMS) |
| |
| |
| final_prompt = build_enhanced_prompt( |
| question, |
| context_results, |
| top_workshops, |
| user_preference=user_preference, |
| user_type=user_type, |
| enriched_podcast_links=final_podcast_options, |
| wants_details=wants_details, |
| current_topic=current_topic, |
| mode=activated_mode, |
| is_low_confidence=is_low_confidence, |
| is_faq_match=is_faq_match, |
| is_policy_query=is_policy_query |
| ) |
|
|
| |
| messages = [{"role": "system", "content": final_prompt}] |
| messages.extend(chat_history) |
| messages.append({"role": "user", "content": question}) |
| |
| response = openai.chat.completions.create( |
| model=GEN_MODEL, |
| messages=messages |
| ) |
| |
| answer_text = response.choices[0].message.content.strip() |
|
|
| |
| routing_q = "Are you looking for online training or in-studio in Atlanta?" |
| broad_triggers = ['start acting', 'beginner', 'new actor', 'kids class', 'workshops', 'training', 'classes'] |
| is_broad = any(t in question.lower() for t in broad_triggers) |
| |
| if is_broad and not user_preference: |
| if not answer_text.lower().startswith(routing_q.lower()): |
| if routing_q.lower() in answer_text.lower(): |
| answer_text = re.sub(rf'{re.escape(routing_q)}[?!.]*', '', answer_text, flags=re.IGNORECASE).strip() |
| |
| answer_text = f"{routing_q} {answer_text}" |
|
|
| |
| routing_q_asked = routing_q if (is_broad and not user_preference and routing_q in answer_text) else None |
| |
| |
| has_links = bool(re.search(r'\[.*?\]\(http', answer_text)) |
| |
| |
| log_question( |
| question=question, |
| session_id=current_session_id, |
| category="llm_generated", |
| answer=answer_text, |
| detected_mode=activated_mode, |
| routing_question=routing_q_asked, |
| rule_triggered=None, |
| link_provided=has_links |
| ) |
|
|
| return answer_text |
|
|
| except Exception as e: |
| import traceback |
| print(f"β CRITICAL ERROR in process_question: {e}") |
| traceback.print_exc() |
| return f"I apologize, but I encountered an error processing your question. Please try again or email info@getscenestudios.com for assistance." |
|
|
| |
| |
| |
|
|
| def chat_with_bot(message, history, session_id): |
| """ |
| Process message directly without Flask API |
| |
| Args: |
| message: User's current message |
| history: Chat history |
| session_id: Per-user session ID state |
| |
| Returns: |
| Updated history and session_id |
| """ |
| if not session_id: |
| session_id = str(uuid.uuid4()) |
| |
| if not message.strip(): |
| return history, session_id |
| |
| try: |
| |
| bot_reply = process_question(message, session_id) |
| except Exception as e: |
| bot_reply = f"β Error: {str(e)}" |
| |
| |
| history.append({"role": "user", "content": message}) |
| history.append({"role": "assistant", "content": bot_reply}) |
| return history, session_id |
|
|
| def reset_session(): |
| """Reset session ID for new conversation""" |
| new_id = str(uuid.uuid4()) |
| return [], new_id |
|
|
| |
| with gr.Blocks(title="Get Scene Studios Chatbot") as demo: |
| |
| gr.Markdown( |
| """ |
| # π¬ Get Scene Studios AI Chatbot |
| |
| Ask questions about acting classes, workshops and more! |
| """ |
| ) |
| |
| |
| chatbot = gr.Chatbot( |
| label="Conversation", |
| height=500 |
| ) |
| |
| |
| with gr.Row(): |
| msg = gr.Textbox( |
| label="Your Message", |
| lines=2, |
| scale=4 |
| ) |
| submit_btn = gr.Button("Send π€", scale=1, variant="primary") |
| |
| |
| with gr.Row(): |
| clear_btn = gr.Button("Clear Chat ποΈ", scale=1) |
| reset_btn = gr.Button("New Session π", scale=1) |
| |
| |
| session_state = gr.State("") |
| |
| |
| submit_btn.click( |
| fn=chat_with_bot, |
| inputs=[msg, chatbot, session_state], |
| outputs=[chatbot, session_state] |
| ).then( |
| fn=lambda: "", |
| inputs=None, |
| outputs=[msg] |
| ) |
| |
| msg.submit( |
| fn=chat_with_bot, |
| inputs=[msg, chatbot, session_state], |
| outputs=[chatbot, session_state] |
| ).then( |
| fn=lambda: "", |
| inputs=None, |
| outputs=[msg] |
| ) |
| |
| clear_btn.click( |
| fn=lambda: [], |
| inputs=None, |
| outputs=[chatbot] |
| ) |
| |
| reset_btn.click( |
| fn=reset_session, |
| inputs=None, |
| outputs=[chatbot, session_state] |
| ) |
|
|
| |
| if __name__ == "__main__": |
| print("\n" + "="*60) |
| print("π¬ Get Scene Studios Chatbot") |
| print("="*60) |
| print("\nβ
No Flask API needed - all processing is done directly!") |
| print("π Gradio interface will open in your browser") |
| print("="*60 + "\n") |
| |
| demo.launch() |
|
|