Spaces:
Sleeping
Sleeping
| import os | |
| import io | |
| import time | |
| import json | |
| import base64 | |
| import random | |
| import logging | |
| import traceback | |
| import requests | |
| from datetime import datetime | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| import gradio as gr | |
| from google import genai | |
| from google.genai import types | |
| from google.genai.errors import APIError | |
| from PIL import Image | |
| # Load dotenv if running locally | |
| load_dotenv() | |
| # Self Vision API imports | |
| from self_vision_api import run_self_vision, validate_request | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", | |
| ) | |
| def parse_input_json(roadmap: dict, profile: dict) -> dict: | |
| """Parse roadmap + user_profile JSONs into run_generation() inputs.""" | |
| icp_raw = roadmap.get("icp_type", "high") | |
| icp = "low_wage" if icp_raw == "low" else "high_wage" | |
| # Derive gender from user_name | |
| name = profile.get("user_name", "") | |
| female_names = ["Priya", "Priyanka", "Ananya", "Pooja", "Shreya", | |
| "Divya", "Anjali", "Neha", "Riya", "Fatima", "Ayesha", | |
| "Lakshmi", "Meera", "Nandini", "Tanvi", "Deepika"] | |
| gender = "female" if any(n in name for n in female_names) else "male" | |
| vision = roadmap.get("vision_profile", {}) | |
| user_goal = ( | |
| f"{roadmap.get('target_role', '')} — " | |
| f"{vision.get('top_motivation', '')} " | |
| f"{vision.get('vision_12mo', '')}" | |
| ) | |
| milestones = [] | |
| for m in roadmap.get("milestones", []): | |
| milestones.append({ | |
| "milestone_id": m["milestone_id"], | |
| "identity_statement": m.get("identity_statement", ""), | |
| "market_value_display": m.get("market_value_display", ""), | |
| }) | |
| return { | |
| "user_id": roadmap.get("user_id", ""), | |
| "user_name": profile.get("user_name", ""), | |
| "user_goal": user_goal, | |
| "domain": "Software Development", | |
| "icp": icp, | |
| "gender": gender, | |
| "milestones": milestones, | |
| "ai_roadmap_id": roadmap.get("ai_roadmap_id", ""), | |
| "ai_session_id": roadmap.get("ai_session_id", ""), | |
| } | |
| IMAGE_MODEL = "gemini-3.1-flash-image" | |
| # Shared client — initialised once, reused for all generation calls | |
| _api_key = os.environ.get("GEMINI_API_KEY", "") | |
| _genai_client = genai.Client(api_key=_api_key) if _api_key else None | |
| # --------------------------------------------------------------------------- | |
| # Load user personas from JSON | |
| # --------------------------------------------------------------------------- | |
| PERSONAS_PATH = Path(__file__).parent / "user_personas.json" | |
| PERSONAS = [] | |
| PERSONA_MAP = {} # "PERS_01 — B.Tech CS Student → Software Engineering" -> persona dict | |
| if PERSONAS_PATH.exists(): | |
| with open(PERSONAS_PATH, "r", encoding="utf-8") as f: | |
| PERSONAS = json.load(f) | |
| for p in PERSONAS: | |
| career_path = p.get('Associated Career Path', '') | |
| label = f"{p['Persona ID']} — {p['Current Role']} ➔ {career_path}" | |
| PERSONA_MAP[label] = p | |
| PERSONA_CHOICES = ["-- Select an Example Persona --"] + list(PERSONA_MAP.keys()) | |
| # --------------------------------------------------------------------------- | |
| # Professional Domain to Career Path mapping | |
| # --------------------------------------------------------------------------- | |
| PROF_DOMAIN_TO_CAREER_PATH = { | |
| "Software Development": "Software Engineering", | |
| "UI/UX Design": "Software Engineering", | |
| "Finance": "Data Science", | |
| "Human Resources": "Data Analytics", | |
| "Manufacturing": "Data Analytics", | |
| "Artificial Intelligence": "Data Science", | |
| "Robotics": "ML/AI Engineering", | |
| "Data Science": "Data Science", | |
| "Consulting": "Product Management", | |
| "Cybersecurity": "DevOps/Cloud", | |
| "Cloud Computing": "DevOps/Cloud", | |
| "E-commerce": "Mobile Development", | |
| "Telecommunications": "Mobile Development", | |
| "Education Technology": "Product Management", | |
| "Operations / BPO": "BPO / Team Lead Path" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Core Data Models: Domains & Market Value Parameters | |
| # --------------------------------------------------------------------------- | |
| CAREER_MILESTONES_PATH = Path(__file__).parent / "career_milestones.json" | |
| CAREER_MILESTONES = {} | |
| if CAREER_MILESTONES_PATH.exists(): | |
| with open(CAREER_MILESTONES_PATH, "r", encoding="utf-8") as f: | |
| CAREER_MILESTONES = json.load(f) | |
| # --------------------------------------------------------------------------- | |
| # Career path to domain mapping (matches DOMAIN_PROPS keys) | |
| # --------------------------------------------------------------------------- | |
| CAREER_PATH_TO_DOMAIN = { | |
| "Software Engineering": "Software Engineering", | |
| "Data Analytics": "Data Analytics", | |
| "Data Science": "Data Science", | |
| "Machine Learning & AI Engineering": "ML/AI Engineering", | |
| "DevOps & Cloud Engineering": "DevOps/Cloud", | |
| "Mobile Development": "Mobile Development", | |
| "Product Management": "Product Management", | |
| "BPO / Team Lead Path": "BPO / Team Lead Path", | |
| } | |
| # --------------------------------------------------------------------------- | |
| # ICP scene contexts (same as lambda_handler.py) | |
| # --------------------------------------------------------------------------- | |
| # Per-milestone visual metadata: attire, lighting mood, and expression tone. | |
| # These complement the scene description and drive visual differentiation. | |
| MILESTONE_VISUAL_META = { | |
| "L1": { | |
| "attire": "smart casual — clean chinos, a well-fitted solid-colour shirt, new white sneakers", | |
| "lighting": "warm golden-hour morning light streaming through large east-facing office windows, soft lens flare", | |
| "expression": "open-eyed curiosity mixed with quiet excitement — a small natural smile, fully present", | |
| }, | |
| "L2": { | |
| "attire": "business-casual — pressed dark trousers, a collared half-sleeve shirt in a muted tone, smart leather shoes", | |
| "lighting": "soft diffused afternoon light from a north-facing window, no harsh shadows, slight warm colour cast", | |
| "expression": "calm, engaged confidence — slightly raised eyebrow while making a point, commanding but approachable", | |
| }, | |
| "L3": { | |
| "attire": "elevated business-casual — tailored trousers, a structured blazer over a crisp shirt, premium watch visible", | |
| "lighting": "cool neutral office lighting supplemented by natural overcast daylight through glass walls, clean shadows", | |
| "expression": "focused authority — leaning slightly forward, measured gaze, the expression of someone who has already solved this problem", | |
| }, | |
| "L4": { | |
| "attire": "premium business formal — a sharply tailored dark suit, subtle tie, polished Oxford shoes, confident posture", | |
| "lighting": "dramatic directional sunlight from large floor-to-ceiling windows hitting at 45 degrees, deep bokeh background", | |
| "expression": "calm gravitas — poised, still, the expression of someone in complete control of the room", | |
| }, | |
| "P1": { | |
| "attire": "professional formal — neat tucked-in formal shirt, pressed trousers or salwar, polished formal shoes", | |
| "lighting": "bright even fluorescent office lighting with a warm overhead fill, cheerful and energetic atmosphere", | |
| "expression": "proud and determined — upright posture, bright eyes, the unmistakable energy of someone on their first day at a real job", | |
| }, | |
| "P2": { | |
| "attire": "professional formal — slightly more confident styling than P1, a well-fitted formal shirt, ID badge on lanyard", | |
| "lighting": "warm soft overhead office light, clean and professional, slightly warmer tone than P1", | |
| "expression": "patient and confident — a helpful half-smile while demonstrating something, the ease of genuine expertise", | |
| }, | |
| "P3": { | |
| "attire": "professional formal — structured formal shirt or kurta, team-lead badge prominently visible, neat and authoritative", | |
| "lighting": "warm professional lighting with a slight directional key light from the side, giving a natural sense of presence", | |
| "expression": "calm authority — standing tall, one hand near the laptop or whiteboard, the stance of someone who runs the meeting", | |
| }, | |
| } | |
| ICP_SCENE_CONTEXTS = { | |
| "high_wage": { | |
| "L1": { | |
| "scene": "seated at a clean standing desk in a bright modern tech startup office in Bangalore, fresh employee ID card clipped to a smart casual shirt, open floor plan with friendly colleagues working in background, a strategy diagram on a nearby whiteboard", | |
| "identity": "You've landed your first tech job — your engineering journey begins here", | |
| "market": "6-12 LPA", # Naukri 2026: Freshers 6-12 across SWE/DS/AI/Cloud | |
| }, | |
| "L2": { | |
| "scene": "standing at a whiteboard in a modern product-team war room, drawing a clean diagram outlining a solution, three colleagues seated around the table with laptops open and nodding, post-it notes and sprint cards covering the wall behind", | |
| "identity": "You think and ship like an engineer — your team trusts your technical judgment", | |
| "market": "12-22 LPA", # Naukri 2026: upper-fresher to early mid-level | |
| }, | |
| "L3": { | |
| "scene": "at a glass-walled breakout room in a modern tech campus, seated across from a small group of 3 senior peers in a focused technical review and strategy discussion, laptop open, coffee cups on the table, city skyline visible through the glass", | |
| "identity": "You own features end-to-end — peers come to you when things get hard", | |
| "market": "22-40 LPA", # Naukri 2026: 5+ yrs avg 18-35 across roles | |
| }, | |
| "L4": { | |
| "scene": "standing at the front of a large sunlit boardroom in a premium Indian tech company, presenting to 10 senior leaders and managers, a large wall-mounted screen behind showing a high-level strategic roadmap and OKR dashboard with green metrics, one hand gesturing toward the screen", | |
| "identity": "You set the technical direction — leaders look to you to define what gets built next", | |
| "market": "40-75 LPA", # Naukri 2026: Sr 40 LPA-1.5 Cr (conservative band) | |
| }, | |
| }, | |
| "low_wage": { | |
| "P1": { | |
| "scene": "seated upright at a clean modern workstation in a well-lit BPO or digital services office, a desktop computer showing a data entry or CRM screen, a shiny new employee ID card on the desk, colleagues at nearby workstations", | |
| "identity": "You have your first digital job — you show up, you deliver, you grow", | |
| "market": "12,000-18,000 per month", | |
| }, | |
| "P2": { | |
| "scene": "leaning toward a colleague at an adjacent workstation, patiently showing them something on the screen, two monitors visible, the surrounding office is busy and modern", | |
| "identity": "You are the reliable one — your team comes to you when they're stuck", | |
| "market": "18,000-28,000 per month", | |
| }, | |
| "P3": { | |
| "scene": "standing at the head of a small conference table presenting from a laptop to a team of 5 direct reports, a whiteboard behind with a weekly targets chart, everyone looking at the presenter with attention and respect, team lead badge visible", | |
| "identity": "You lead the team — your name is attached to the team's results", | |
| "market": "28,000-45,000 per month", | |
| }, | |
| }, | |
| } | |
| DOMAIN_PROPS = { | |
| "Software Engineering": "laptop screen shows a code editor with Python or Java code", | |
| "Data Analytics": "laptop screen shows data dashboards with charts and graphs", | |
| "Data Science": "laptop screen shows a Jupyter notebook with graphs and ML outputs", | |
| "ML/AI Engineering": "laptop screen shows neural network diagrams and model metrics", | |
| "DevOps/Cloud": "multiple monitors show server dashboards and terminal windows", | |
| "Product Management": "product roadmap visible on screen with sticky notes on wall", | |
| "Mobile Development": "phone connected to a laptop showing a mobile app interface being debugged", | |
| "BPO / Team Lead Path": "workstation with CRM software or communication dashboards open on screen", | |
| "Default": "laptop open with professional work visible on screen", | |
| } | |
| DOMAIN_IDENTITIES = { | |
| "Software Engineering": { | |
| "L1": "You've landed your first tech job — your engineering journey begins here", | |
| "L2": "You think and ship like an engineer — your team trusts your technical judgment", | |
| "L3": "You own features end-to-end — peers come to you when things get hard", | |
| "L4": "You set the technical direction — leaders look to you to define what gets built next" | |
| }, | |
| "Data Analytics": { | |
| "L1": "You've landed your first data role — your analytical journey begins here", | |
| "L2": "You uncover actionable insights — your team relies on your data storytelling", | |
| "L3": "You own critical metrics end-to-end — peers come to you for data truth", | |
| "L4": "You define the data strategy — leaders look to you to drive business decisions" | |
| }, | |
| "Data Science": { | |
| "L1": "You've landed your first DS role — your modeling journey begins here", | |
| "L2": "You build and deploy models — your team trusts your predictive accuracy", | |
| "L3": "You own complex ML pipelines end-to-end — peers come to you for model architecture", | |
| "L4": "You set the algorithmic vision — leaders look to you for AI-driven transformation" | |
| }, | |
| "ML/AI Engineering": { | |
| "L1": "You've landed your first AI role — your journey into intelligent systems begins here", | |
| "L2": "You engineer robust ML systems — your team trusts your technical execution", | |
| "L3": "You own scalable AI solutions end-to-end — peers look to you to solve hard architectural problems", | |
| "L4": "You define the AI architecture — leaders rely on you to build the intelligent future" | |
| }, | |
| "DevOps/Cloud": { | |
| "L1": "You've landed your first Cloud role — your infrastructure journey begins here", | |
| "L2": "You automate and scale — your team trusts your operational excellence", | |
| "L3": "You own infrastructure end-to-end — peers come to you when systems break", | |
| "L4": "You set the cloud architecture — leaders look to you for scalable, resilient platforms" | |
| }, | |
| "Product Management": { | |
| "L1": "You've landed your first PM role — your product journey begins here", | |
| "L2": "You ship user value — your team trusts your prioritization and execution", | |
| "L3": "You own product areas end-to-end — peers rely on you for cross-functional alignment", | |
| "L4": "You set the product vision — leaders look to you to define the roadmap and market strategy" | |
| }, | |
| "Mobile Development": { | |
| "L1": "You've landed your first mobile role — your app development journey begins here", | |
| "L2": "You build seamless mobile experiences — your team trusts your front-end expertise", | |
| "L3": "You own complex app features end-to-end — peers come to you for architecture advice", | |
| "L4": "You set the mobile strategy — leaders look to you for the future of the platform" | |
| }, | |
| "BPO / Team Lead Path": { | |
| "P1": "You have your first digital job — you show up, you deliver, you grow", | |
| "P2": "You are the reliable one — your team comes to you when they're stuck", | |
| "P3": "You lead the team — your name is attached to the team's results" | |
| }, | |
| "Default": { | |
| "L1": "You've landed your first professional role — your journey begins here", | |
| "L2": "You execute with confidence — your team trusts your professional judgment", | |
| "L3": "You manage cross-functional projects — peers rely on you for problem resolution", | |
| "L4": "You set the operational standard — leaders look to you for efficiency and growth" | |
| } | |
| } | |
| DOMAIN_CHOICES = list(PROF_DOMAIN_TO_CAREER_PATH.keys()) | |
| ICP_CHOICES = ["high_wage", "low_wage"] | |
| # --------------------------------------------------------------------------- | |
| # Prompt builder (same as lambda_handler.py) | |
| # --------------------------------------------------------------------------- | |
| def build_prompt( | |
| milestone_id, | |
| icp, | |
| domain, | |
| user_goal, | |
| has_reference_photo, | |
| gender="male", | |
| milestone_label=None, | |
| identity_statement=None, | |
| market_value_display=None, | |
| skills_context=None, | |
| ): | |
| icp_scenes = ICP_SCENE_CONTEXTS.get(icp, ICP_SCENE_CONTEXTS["high_wage"]) | |
| # Resolve correct milestone key mapping if mismatch occurs (e.g. low-wage roadmap using "L" keys) | |
| resolved_mid = milestone_id | |
| if milestone_id not in icp_scenes: | |
| import re | |
| digits = re.findall(r'\d+', milestone_id) | |
| if digits: | |
| idx = int(digits[0]) - 1 | |
| keys = list(icp_scenes.keys()) | |
| if 0 <= idx < len(keys): | |
| resolved_mid = keys[idx] | |
| else: | |
| resolved_mid = keys[-1] | |
| else: | |
| resolved_mid = list(icp_scenes.keys())[-1] | |
| ctx = icp_scenes.get(resolved_mid, list(icp_scenes.values())[-1]) | |
| visual = MILESTONE_VISUAL_META.get(resolved_mid, {}) | |
| career_path = PROF_DOMAIN_TO_CAREER_PATH.get(domain, "Default") | |
| # Case-insensitive domain prop selection | |
| domain_key = next((k for k in DOMAIN_PROPS if k.lower() == career_path.lower()), "Default") | |
| domain_prop = DOMAIN_PROPS[domain_key] | |
| # scene always comes from hardcoded ctx — identity_statement is career copy, not a scene | |
| scene = ctx.get("scene", "") | |
| if skills_context: | |
| scene += f" In the background on a screen or whiteboard, you can clearly see text, diagrams, or dashboards displaying topics about: {skills_context}." | |
| fallback_identity = DOMAIN_IDENTITIES.get(domain_key, DOMAIN_IDENTITIES["Default"]).get(resolved_mid, ctx.get("identity", resolved_mid)) | |
| label = milestone_label or fallback_identity | |
| identity = identity_statement or fallback_identity | |
| market = market_value_display or ctx.get("market", "") | |
| attire = visual.get("attire", "smart professional attire appropriate to the career stage") | |
| if gender == "female": | |
| # Replace male-oriented clothing terms with professional female equivalents | |
| attire = attire.replace("clean chinos, a well-fitted solid-colour shirt", "clean trousers or a sleek skirt, a well-fitted solid-colour blouse or shirt") | |
| attire = attire.replace("collared half-sleeve shirt", "collared blouse or half-sleeve shirt") | |
| attire = attire.replace("crisp shirt", "crisp blouse or shirt") | |
| attire = attire.replace("subtle tie, polished Oxford shoes", "polished formal shoes") | |
| attire = attire.replace("dark suit", "dark trouser suit or elegant skirt suit") | |
| attire = attire.replace("tucked-in formal shirt", "tucked-in formal blouse or elegant salwar kameez") | |
| attire = attire.replace("formal shirt, ID badge", "formal shirt or elegant top, ID badge") | |
| attire = attire.replace("formal shirt or kurta", "formal shirt or elegant kurta") | |
| lighting = visual.get("lighting", "soft natural office lighting") | |
| expression = visual.get("expression", "confident and focused professional expression") | |
| if has_reference_photo: | |
| subject_line = ( | |
| f"The subject is a real young Indian {gender}, the exact person from the reference photo provided. " | |
| "You MUST preserve: their face structure, skin tone, eye shape, hair style, and all distinctive facial features. " | |
| "The face must be immediately recognisable as the same person. Do not idealise, smooth, or alter their appearance." | |
| ) | |
| else: | |
| if gender == "female": | |
| subject_line = ( | |
| "The subject is a real young Indian woman, mid-to-late 20s. " | |
| "Natural skin tone, authentic features — not a model or stock photo face." | |
| ) | |
| else: | |
| subject_line = ( | |
| "The subject is a real young Indian man, mid-to-late 20s. " | |
| "Natural skin tone, authentic features — not a model or stock photo face." | |
| ) | |
| return f"""A high-end cinematic editorial portrait photograph. A candid, authentic moment captured in natural lighting. | |
| SUBJECT | |
| {subject_line} | |
| Attire: {attire}. | |
| Expression: {expression}. | |
| SETTING | |
| {scene}. | |
| {domain_prop}. The setting should feel premium, contemporary, and distinctly Indian (e.g. situated in modern tech hubs like Bangalore, Hyderabad, or Mumbai). | |
| COMPOSITION & DEPTH | |
| Vertical 9:16 portrait orientation, optimized for phone screens. | |
| The subject is positioned naturally in the frame using the rule of thirds, occupying the upper two-thirds of the image. | |
| Cinematic depth of field with a clean subject separation and beautiful, soft background bokeh (f/1.8 lens emulation). | |
| No empty space or awkward cropping. | |
| LIGHTING & ATMOSPHERE | |
| {lighting}. | |
| Cinematic soft wrap lighting on the face with gentle gradient transitions, avoiding harsh direct light or flat studio lighting. | |
| Subtle volumetric atmosphere with soft shadows that add dimension and volume to the features. | |
| PHOTOGRAPHY STYLE & TECHNICAL SPECS | |
| Shot on a professional camera system like a Sony A7R V or Canon EOS R5 with an 85mm f/1.4 prime lens. | |
| Highly detailed and crisp subject sharpness focusing on the eyes. | |
| Natural skin texture showing realistic pores, skin grain, and individual hair strands without any artificial smoothing or airbrushing. | |
| A professional color grade with rich midtones, warm highlights, and subtle shadow roll-off. | |
| CAREER CONTEXT (to guide mood, environment details, and visual energy) | |
| Milestone stage: {label} | |
| Professional identity: {identity} | |
| Current market standing: {market} | |
| Ultimate career goal: {user_goal} | |
| STRICT NEGATIVE PROMPT RULES (DO NOT INCLUDE ANY OF THESE VISUALLY) | |
| - No text, watermarks, signatures, logos, or overlaid text of any kind | |
| - No generic, cheesy stock photo poses (e.g. forced smiles, arms crossed, pointing at screens) | |
| - No CGI, 3D renders, digital paintings, illustrations, or synthetic artificial textures | |
| - No airbrushed, plastic-like skin or doll-like facial features | |
| - No double chin, distorted hands, or anatomy issues | |
| - No oversaturated colors, flat lighting, or HDR effects | |
| - Do not place any legible or stylized text on screens in the image""" | |
| # --------------------------------------------------------------------------- | |
| # Image generation | |
| # --------------------------------------------------------------------------- | |
| MAX_RETRIES = 2 # number of retry attempts on transient failures | |
| def _extract_image(response): | |
| """Pull the first IMAGE part out of a Gemini response and return a PIL Image.""" | |
| if not response.parts: | |
| return None | |
| for part in response.parts: | |
| if part.inline_data is not None: | |
| raw = part.inline_data.data | |
| if isinstance(raw, str): | |
| raw = base64.b64decode(raw) | |
| return Image.open(io.BytesIO(raw)) | |
| return None | |
| def generate_single_image(prompt, reference_photo, api_key): | |
| """Generate a single milestone image. Returns PIL Image or None.""" | |
| # Prefer the module-level shared client; fall back to per-call if key differs | |
| client = _genai_client if (_api_key and api_key == _api_key) else genai.Client(api_key=api_key) | |
| gen_config = types.GenerateContentConfig(response_modalities=["IMAGE"]) | |
| # --- Attempt 1: with face reference --- | |
| if reference_photo is not None: | |
| for attempt in range(1, MAX_RETRIES + 1): | |
| try: | |
| print(f" [ref] attempt {attempt}/{MAX_RETRIES}") | |
| response = client.models.generate_content( | |
| model=IMAGE_MODEL, | |
| contents=[prompt, reference_photo], | |
| config=gen_config, | |
| ) | |
| img = _extract_image(response) | |
| if img: | |
| return img | |
| print(f" [ref] attempt {attempt}: no image in response") | |
| except Exception as exc: | |
| print(f" [ref] attempt {attempt} failed: {exc}") | |
| print(" [ref] all attempts failed — falling back to scene-only") | |
| # --- Attempt 2: scene-only (no face reference) --- | |
| for attempt in range(1, MAX_RETRIES + 1): | |
| try: | |
| print(f" [scene] attempt {attempt}/{MAX_RETRIES}") | |
| response = client.models.generate_content( | |
| model=IMAGE_MODEL, | |
| contents=[prompt], | |
| config=gen_config, | |
| ) | |
| img = _extract_image(response) | |
| if img: | |
| return img | |
| print(f" [scene] attempt {attempt}: no image in response") | |
| except Exception as exc: | |
| print(f" [scene] attempt {attempt} failed: {exc}") | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Persona auto-fill logic | |
| # --------------------------------------------------------------------------- | |
| def on_gender_change(current_text, target_gender): | |
| if not current_text: | |
| return current_text | |
| import re | |
| text = current_text | |
| if target_gender == "male": | |
| # female -> male | |
| text = re.sub(r'\bShe\b', 'He', text) | |
| text = re.sub(r'\bshe\b', 'he', text) | |
| text = re.sub(r'\bHerself\b', 'Himself', text) | |
| text = re.sub(r'\bherself\b', 'himself', text) | |
| # known objective cases | |
| text = re.sub(r'\bmakes her\b', 'makes him', text, flags=re.IGNORECASE) | |
| text = re.sub(r'\bpositions her\b', 'positions him', text, flags=re.IGNORECASE) | |
| text = re.sub(r'\bHer\b', 'His', text) | |
| text = re.sub(r'\bher\b', 'his', text) | |
| elif target_gender == "female": | |
| # male -> female | |
| text = re.sub(r'\bHe\b', 'She', text) | |
| text = re.sub(r'\bhe\b', 'she', text) | |
| text = re.sub(r'\bHimself\b', 'Herself', text) | |
| text = re.sub(r'\bhimself\b', 'herself', text) | |
| text = re.sub(r'\bHis\b', 'Her', text) | |
| text = re.sub(r'\bhis\b', 'her', text) | |
| text = re.sub(r'\bHim\b', 'Her', text) | |
| text = re.sub(r'\bhim\b', 'her', text) | |
| return text | |
| def on_persona_change(persona_label): | |
| """When a persona is selected, auto-fill the form fields.""" | |
| if persona_label == "-- Select an Example Persona --" or persona_label not in PERSONA_MAP: | |
| return ( | |
| "", # user_goal | |
| "Software Development", # domain | |
| "high_wage", # icp | |
| "", # gender | |
| "" # milestone text | |
| ) | |
| p = PERSONA_MAP[persona_label] | |
| # Derive user_goal from persona | |
| user_goal = p.get("Target Career Ambition", "") | |
| reason = p.get("Reason for Pursuing This Career Path", "") | |
| if reason: | |
| user_goal = f"{user_goal} — {reason}" | |
| # Derive domain from professional domain | |
| domain = p.get("Professional Domain", "Software Development") | |
| # Derive ICP from Market Value Anchor | |
| market_anchor = p.get("Market Value Anchor", "") | |
| if market_anchor.startswith("P"): | |
| icp = "low_wage" | |
| else: | |
| icp = "high_wage" | |
| # Derive gender from Persona ID since names are now generic | |
| persona_id = p.get("Persona ID", "") | |
| female_persona_ids = ["PERS_02", "PERS_04", "PERS_06", "PERS_07", "PERS_09", "PERS_11", "PERS_13", "PERS_15", "PERS_17", "PERS_19", "PERS_21"] | |
| if persona_id in female_persona_ids: | |
| gender = "female" | |
| elif persona_id: | |
| gender = "male" | |
| else: | |
| name = p.get("Name", "") | |
| female_indicators = [ | |
| "Fatima", "Pooja", "Lakshmi", "Ayesha", "Shreya", "Divya", | |
| "Anjali", "Tanya", "Nandini", "Sunita", "Priya", "Ananya", | |
| "Meera", "Kavitha", "Deepika", "Tanvi", "Riya", "Neha" | |
| ] | |
| gender = "female" if any(ind in name for ind in female_indicators) else "male" | |
| milestone_md = CAREER_MILESTONES.get(persona_id, "") | |
| return (user_goal, domain, icp, gender, milestone_md) | |
| # --------------------------------------------------------------------------- | |
| # Milestone defaults per ICP | |
| # --------------------------------------------------------------------------- | |
| def calculate_market_value(domain, icp, milestone): | |
| career_path = PROF_DOMAIN_TO_CAREER_PATH.get(domain, "Default") | |
| MARKET_RATES = { | |
| "high_wage": { | |
| "Software Engineering": {"L1": "6-12 LPA", "L2": "12-24 LPA", "L3": "24-45 LPA", "L4": "45-80 LPA"}, | |
| "Data Analytics": {"L1": "5-8 LPA", "L2": "8-14 LPA", "L3": "14-22 LPA", "L4": "22-35 LPA"}, | |
| "Data Science": {"L1": "8-14 LPA", "L2": "14-25 LPA", "L3": "25-45 LPA", "L4": "45-75 LPA"}, | |
| "Machine Learning & AI Engineering": {"L1": "12-18 LPA", "L2": "18-35 LPA", "L3": "35-65 LPA", "L4": "65-120 LPA"}, | |
| "DevOps/Cloud": {"L1": "8-14 LPA", "L2": "14-26 LPA", "L3": "26-45 LPA", "L4": "45-80 LPA"}, | |
| "DevOps & Cloud Engineering": {"L1": "8-14 LPA", "L2": "14-26 LPA", "L3": "26-45 LPA", "L4": "45-80 LPA"}, | |
| "Product Management": {"L1": "10-16 LPA", "L2": "16-28 LPA", "L3": "28-55 LPA", "L4": "55-100 LPA"}, | |
| "Mobile Development": {"L1": "6-10 LPA", "L2": "10-18 LPA", "L3": "18-32 LPA", "L4": "32-55 LPA"}, | |
| "BPO / Team Lead Path": {"L1": "4-6 LPA", "L2": "6-10 LPA", "L3": "10-15 LPA", "L4": "15-25 LPA"}, | |
| "Default": {"L1": "6-12 LPA", "L2": "12-22 LPA", "L3": "22-40 LPA", "L4": "40-75 LPA"} | |
| }, | |
| "low_wage": { | |
| "BPO / Team Lead Path": {"P1": "15k-25k / mo", "P2": "25k-40k / mo", "P3": "40k-70k / mo", "P4": "70k-1.2L / mo"}, | |
| "Data Analytics": {"P1": "15k-25k / mo", "P2": "25k-40k / mo", "P3": "40k-65k / mo", "P4": "65k-90k / mo"}, | |
| "Software Engineering": {"P1": "20k-30k / mo", "P2": "30k-50k / mo", "P3": "50k-80k / mo", "P4": "80k-1.2L / mo"}, | |
| "Default": {"P1": "15k-22k / mo", "P2": "22k-35k / mo", "P3": "35k-50k / mo", "P4": "50k-80k / mo"} | |
| } | |
| } | |
| path_rates = MARKET_RATES.get(icp, MARKET_RATES["high_wage"]) | |
| rates = path_rates.get(career_path, path_rates["Default"]) | |
| return rates.get(milestone, rates.get("L1" if icp == "high_wage" else "P1", "N/A")) | |
| def get_milestone_defaults(icp_value, domain_value, persona_label=None): | |
| """Return milestone defaults based on ICP and domain selection.""" | |
| custom_milestones = None | |
| if persona_label and persona_label in PERSONA_MAP: | |
| p = PERSONA_MAP[persona_label] | |
| if "Custom Milestones" in p: | |
| custom_milestones = p["Custom Milestones"] | |
| results = [] | |
| for i in range(4): | |
| if custom_milestones is not None: | |
| if i < len(custom_milestones): | |
| m = custom_milestones[i] | |
| results.extend([ | |
| gr.update(visible=True), | |
| m.get("milestone_id", ""), | |
| m.get("identity_statement", ""), | |
| m.get("market_value_display", ""), | |
| ]) | |
| else: | |
| results.extend([ | |
| gr.update(visible=False), | |
| "", | |
| "", | |
| "", | |
| ]) | |
| else: | |
| career_path = PROF_DOMAIN_TO_CAREER_PATH.get(domain_value, "Default") | |
| icp_scenes = ICP_SCENE_CONTEXTS.get(icp_value, ICP_SCENE_CONTEXTS["high_wage"]) | |
| milestone_ids = list(icp_scenes.keys()) | |
| # Grab identity statements from the selected domain (Career Path map) or Default | |
| identities = DOMAIN_IDENTITIES.get(career_path, DOMAIN_IDENTITIES["Default"]) | |
| if i < len(milestone_ids): | |
| mid = milestone_ids[i] | |
| ctx = icp_scenes[mid] | |
| identity = DOMAIN_IDENTITIES.get(career_path, DOMAIN_IDENTITIES["Default"]).get(mid, ctx.get("identity", "")) | |
| dynamic_market = calculate_market_value(domain_value, icp_value, mid) | |
| results.extend([ | |
| gr.update(visible=True), # group visibility | |
| mid, # milestone_id | |
| identity, # description | |
| dynamic_market, # dynamic market value | |
| ]) | |
| else: | |
| results.extend([ | |
| gr.update(visible=False), # group visibility | |
| "", # milestone_id | |
| "", # description | |
| "", # market value | |
| ]) | |
| return results | |
| def get_milestones_from_roadmap_or_defaults(roadmap_dict, icp_value, domain_value): | |
| """ | |
| Extract milestones from roadmap_dict if available. | |
| Otherwise, fall back to default milestones for the given icp and domain. | |
| """ | |
| results = [] | |
| milestones = roadmap_dict.get("milestones", []) if roadmap_dict else [] | |
| for i in range(4): | |
| if i < len(milestones): | |
| m = milestones[i] | |
| mid = m.get("milestone_id") or m.get("label") or f"M{i+1}" | |
| desc = m.get("identity_statement") or m.get("identity_label") or "" | |
| val = m.get("market_value_display") or "" | |
| results.extend([ | |
| gr.update(visible=True), | |
| mid, | |
| desc, | |
| val, | |
| ]) | |
| else: | |
| if roadmap_dict: | |
| # Hide unused milestone slots | |
| results.extend([ | |
| gr.update(visible=False), | |
| "", | |
| "", | |
| "", | |
| ]) | |
| else: | |
| # No roadmap at all: use standard defaults | |
| career_path = PROF_DOMAIN_TO_CAREER_PATH.get(domain_value, "Default") | |
| icp_scenes = ICP_SCENE_CONTEXTS.get(icp_value, ICP_SCENE_CONTEXTS["high_wage"]) | |
| milestone_ids = list(icp_scenes.keys()) | |
| if i < len(milestone_ids): | |
| mid = milestone_ids[i] | |
| ctx = icp_scenes[mid] | |
| identity = DOMAIN_IDENTITIES.get(career_path, DOMAIN_IDENTITIES["Default"]).get(mid, ctx.get("identity", "")) | |
| dynamic_market = calculate_market_value(domain_value, icp_value, mid) | |
| results.extend([ | |
| gr.update(visible=True), | |
| mid, | |
| identity, | |
| dynamic_market, | |
| ]) | |
| else: | |
| results.extend([ | |
| gr.update(visible=False), | |
| "", | |
| "", | |
| "", | |
| ]) | |
| return results | |
| def load_data_from_url(request: gr.Request): | |
| """ | |
| Load data from the URL query parameters. | |
| e.g. ?user_id=test_user_fresh_001 or ?roadmap_id=ai_roadmap_... | |
| """ | |
| user_id = "" | |
| roadmap_id_val = "" | |
| if request is not None and request.query_params: | |
| user_id = request.query_params.get("user_id", "") | |
| roadmap_id_val = request.query_params.get("roadmap_id", "") | |
| # If roadmap_id provided but no user_id, resolve it | |
| if roadmap_id_val and not user_id: | |
| try: | |
| from pinecone_client import PineconeClient | |
| pc = PineconeClient() | |
| resolve_result = pc.resolve_user_from_roadmap(roadmap_id_val) | |
| if resolve_result["status"] == "success": | |
| user_id = resolve_result["user_id"] | |
| except Exception: | |
| pass | |
| if not user_id: | |
| milestone_defaults = get_milestones_from_roadmap_or_defaults(None, "high_wage", "Software Development") | |
| return ( | |
| roadmap_id_val, # roadmap_id_input | |
| "", # user_id_input | |
| "", # raw_transcription_input | |
| "", # user_name | |
| "", # user_goal | |
| "Software Development", # domain | |
| "high_wage", # icp | |
| "male", # gender_input | |
| "📋 Awaiting user configuration...", # status_box | |
| *milestone_defaults, | |
| "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| ) | |
| # Fetch onboarding context and roadmap context | |
| from pinecone_client import PineconeClient, KEY_ONBOARDING, KEY_ROADMAP, KEY_SELF_VISION | |
| pc = PineconeClient() | |
| fetch_res = pc.fetch_conversation(user_id, KEY_ONBOARDING) | |
| onboarding_text = fetch_res.get("text", "") | |
| fetch_roadmap_res = pc.fetch_conversation(user_id, KEY_ROADMAP) | |
| roadmap_text = fetch_roadmap_res.get("text", "") | |
| fetch_sv_res = pc.fetch_conversation(user_id, KEY_SELF_VISION) | |
| self_vision_text = fetch_sv_res.get("text", "") | |
| user_name_val = "" | |
| user_goal_val = "" | |
| gender_val = "male" | |
| domain_val = "Software Development" | |
| icp_val = "high_wage" | |
| roadmap_dict = None | |
| status_msg = f"📥 Automatically loaded User ID '{user_id}' from URL query parameters." | |
| if onboarding_text: | |
| status_msg += "\n📥 Found prior onboarding conversation in Pinecone." | |
| try: | |
| profile = json.loads(onboarding_text) | |
| status_msg += " Parsed profile data." | |
| user_name_val = profile.get("user_name", "") | |
| # Extract primary goal | |
| objectives = profile.get("learning_objectives", {}) | |
| user_goal_val = objectives.get("primary_goal", "") | |
| motivation = objectives.get("key_motivation", "") | |
| if motivation and user_goal_val: | |
| user_goal_val = f"{user_goal_val} — {motivation}" | |
| elif motivation: | |
| user_goal_val = motivation | |
| # Extract gender from user_name | |
| female_indicators = ["Priya", "Priyanka", "Ananya", "Pooja", "Shreya", "Divya", "Anjali", "Neha", "Riya", "Fatima", "Ayesha", "Lakshmi", "Meera", "Nandini", "Tanvi", "Deepika", "Priyambada"] | |
| if any(ind in user_name_val for ind in female_indicators): | |
| gender_val = "female" | |
| # Extract domain if present | |
| dev_areas = profile.get("development_areas", {}) | |
| focused = dev_areas.get("focused_topics", []) | |
| if focused: | |
| topic = focused[0].lower() | |
| if "data science" in topic: | |
| domain_val = "Data Science" | |
| elif "data analytics" in topic or "analytics" in topic: | |
| domain_val = "Data Science" | |
| elif "machine learning" in topic or "ml" in topic or "ai" in topic: | |
| domain_val = "Artificial Intelligence" | |
| elif "devops" in topic or "cloud" in topic or "infrastructure" in topic: | |
| domain_val = "Cloud Computing" | |
| elif "mobile" in topic or "app" in topic or "android" in topic or "ios" in topic: | |
| domain_val = "E-commerce" | |
| elif "product" in topic: | |
| domain_val = "Education Technology" | |
| elif "bpo" in topic or "operations" in topic: | |
| domain_val = "Operations / BPO" | |
| elif "software" in topic or "web" in topic or "full stack" in topic: | |
| domain_val = "Software Development" | |
| except Exception: | |
| pass | |
| else: | |
| status_msg += "\n⚠️ No prior onboarding conversation found in Pinecone for this User ID." | |
| if roadmap_text: | |
| status_msg += "\n📥 Found career roadmap in Pinecone. Parsed roadmap data." | |
| try: | |
| roadmap_dict = json.loads(roadmap_text) | |
| target_role = roadmap_dict.get("target_role", "") | |
| vision = roadmap_dict.get("vision_profile", {}) | |
| top_motivation = vision.get("top_motivation", "") | |
| vision_12mo = vision.get("vision_12mo", "") | |
| roadmap_goal = f"{target_role}" | |
| if top_motivation or vision_12mo: | |
| details_parts = [] | |
| if top_motivation: | |
| details_parts.append(top_motivation) | |
| if vision_12mo: | |
| details_parts.append(vision_12mo) | |
| roadmap_goal += " — " + " ".join(details_parts) | |
| if roadmap_goal.strip(): | |
| user_goal_val = roadmap_goal | |
| # Extract ICP from roadmap | |
| icp_raw = roadmap_dict.get("icp_type", "high") | |
| icp_val = "low_wage" if icp_raw == "low" else "high_wage" | |
| # Try to map target role to domain | |
| if target_role: | |
| role_lower = target_role.lower() | |
| if "data science" in role_lower or "analytics" in role_lower or "analyst" in role_lower: | |
| domain_val = "Data Science" | |
| elif "machine learning" in role_lower or "ml" in role_lower or "ai" in role_lower or "artificial intelligence" in role_lower: | |
| domain_val = "Artificial Intelligence" | |
| elif "devops" in role_lower or "cloud" in role_lower or "infrastructure" in role_lower: | |
| domain_val = "Cloud Computing" | |
| elif "mobile" in role_lower or "app" in role_lower or "android" in role_lower or "ios" in role_lower: | |
| domain_val = "E-commerce" | |
| elif "product" in role_lower: | |
| domain_val = "Education Technology" | |
| elif "bpo" in role_lower or "operations" in role_lower: | |
| domain_val = "Operations / BPO" | |
| elif "software" in role_lower or "web" in role_lower or "full stack" in role_lower or "developer" in role_lower or "engineer" in role_lower: | |
| domain_val = "Software Development" | |
| except Exception as e: | |
| status_msg += f" (Error parsing roadmap: {e})" | |
| else: | |
| status_msg += "\n⚠️ No career roadmap found in Pinecone for this User ID." | |
| if domain_val not in DOMAIN_CHOICES: | |
| domain_val = "Software Development" | |
| milestone_updates = get_milestones_from_roadmap_or_defaults(roadmap_dict, icp_val, domain_val) | |
| milestone_details_val = "*No detailed milestones found in roadmap.*" | |
| if roadmap_dict and roadmap_dict.get("milestones"): | |
| combined_md = [] | |
| milestones_list = roadmap_dict.get("milestones", [])[:4] | |
| for i, m in enumerate(milestones_list): | |
| m_ui = { | |
| "milestone_id": m.get("milestone_id", ""), | |
| "identity_statement": m.get("identity_statement", ""), | |
| "market_value_display": m.get("market_value_display", "") | |
| } | |
| detail = build_rich_milestone_detail( | |
| milestone_ui=m_ui, | |
| roadmap_dict=roadmap_dict, | |
| onboarding_profile=profile if 'profile' in locals() else None, | |
| milestone_index=i, | |
| total_milestones=len(milestones_list) | |
| ) | |
| combined_md.append(detail) | |
| milestone_details_val = "\n\n---\n\n".join(combined_md) | |
| else: | |
| milestone_details_val = "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| return ( | |
| roadmap_id_val if roadmap_id_val else "", | |
| user_id, | |
| self_vision_text, | |
| user_name_val, | |
| user_goal_val, | |
| domain_val, | |
| icp_val, | |
| gender_val, | |
| status_msg, | |
| *milestone_updates, | |
| milestone_details_val | |
| ) | |
| def on_user_id_change(user_id): | |
| """ | |
| Called when the User ID input is submitted or loses focus (blur). | |
| Fetches the onboarding context and roadmap context from Pinecone and auto-populates the form. | |
| """ | |
| user_id = (user_id or "").strip() | |
| if not user_id: | |
| milestone_defaults = get_milestones_from_roadmap_or_defaults(None, "high_wage", "Software Development") | |
| return ( | |
| "", # raw_transcription_input | |
| "", # user_name | |
| "", # user_goal | |
| "Software Development", # domain | |
| "high_wage", # icp | |
| "male", # gender_input | |
| "📋 Awaiting user configuration...", # status_box | |
| *milestone_defaults, | |
| "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| ) | |
| # Fetch onboarding context and roadmap context | |
| from pinecone_client import PineconeClient, KEY_ONBOARDING, KEY_ROADMAP, KEY_SELF_VISION | |
| pc = PineconeClient() | |
| fetch_res = pc.fetch_conversation(user_id, KEY_ONBOARDING) | |
| onboarding_text = fetch_res.get("text", "") | |
| fetch_roadmap_res = pc.fetch_conversation(user_id, KEY_ROADMAP) | |
| roadmap_text = fetch_roadmap_res.get("text", "") | |
| fetch_sv_res = pc.fetch_conversation(user_id, KEY_SELF_VISION) | |
| self_vision_text = fetch_sv_res.get("text", "") | |
| user_name_val = "" | |
| user_goal_val = "" | |
| gender_val = "male" | |
| domain_val = "Software Development" | |
| icp_val = "high_wage" | |
| roadmap_dict = None | |
| status_msg = f"📥 Automatically loaded onboarding context and roadmap for User ID '{user_id}' from Pinecone." | |
| if onboarding_text: | |
| try: | |
| profile = json.loads(onboarding_text) | |
| user_name_val = profile.get("user_name", "") | |
| # Extract primary goal | |
| objectives = profile.get("learning_objectives", {}) | |
| user_goal_val = objectives.get("primary_goal", "") | |
| motivation = objectives.get("key_motivation", "") | |
| if motivation and user_goal_val: | |
| user_goal_val = f"{user_goal_val} — {motivation}" | |
| elif motivation: | |
| user_goal_val = motivation | |
| # Extract gender from user_name | |
| female_indicators = ["Priya", "Priyanka", "Ananya", "Pooja", "Shreya", "Divya", "Anjali", "Neha", "Riya", "Fatima", "Ayesha", "Lakshmi", "Meera", "Nandini", "Tanvi", "Deepika", "Priyambada"] | |
| if any(ind in user_name_val for ind in female_indicators): | |
| gender_val = "female" | |
| # Extract domain if present | |
| dev_areas = profile.get("development_areas", {}) | |
| focused = dev_areas.get("focused_topics", []) | |
| if focused: | |
| topic = focused[0].lower() | |
| if "data science" in topic: | |
| domain_val = "Data Science" | |
| elif "data analytics" in topic or "analytics" in topic: | |
| domain_val = "Data Science" | |
| elif "machine learning" in topic or "ml" in topic or "ai" in topic: | |
| domain_val = "Artificial Intelligence" | |
| elif "devops" in topic or "cloud" in topic or "infrastructure" in topic: | |
| domain_val = "Cloud Computing" | |
| elif "mobile" in topic or "app" in topic or "android" in topic or "ios" in topic: | |
| domain_val = "E-commerce" | |
| elif "product" in topic: | |
| domain_val = "Education Technology" | |
| elif "bpo" in topic or "operations" in topic: | |
| domain_val = "Operations / BPO" | |
| elif "software" in topic or "web" in topic or "full stack" in topic: | |
| domain_val = "Software Development" | |
| except Exception: | |
| pass | |
| else: | |
| status_msg += "\n⚠️ No prior onboarding conversation found in Pinecone for this User ID." | |
| if roadmap_text: | |
| try: | |
| roadmap_dict = json.loads(roadmap_text) | |
| target_role = roadmap_dict.get("target_role", "") | |
| vision = roadmap_dict.get("vision_profile", {}) | |
| top_motivation = vision.get("top_motivation", "") | |
| vision_12mo = vision.get("vision_12mo", "") | |
| roadmap_goal = f"{target_role}" | |
| if top_motivation or vision_12mo: | |
| details_parts = [] | |
| if top_motivation: | |
| details_parts.append(top_motivation) | |
| if vision_12mo: | |
| details_parts.append(vision_12mo) | |
| roadmap_goal += " — " + " ".join(details_parts) | |
| if roadmap_goal.strip(): | |
| user_goal_val = roadmap_goal | |
| # Extract ICP from roadmap | |
| icp_raw = roadmap_dict.get("icp_type", "high") | |
| icp_val = "low_wage" if icp_raw == "low" else "high_wage" | |
| # Try to map target role to domain | |
| if target_role: | |
| role_lower = target_role.lower() | |
| if "data science" in role_lower or "analytics" in role_lower or "analyst" in role_lower: | |
| domain_val = "Data Science" | |
| elif "machine learning" in role_lower or "ml" in role_lower or "ai" in role_lower or "artificial intelligence" in role_lower: | |
| domain_val = "Artificial Intelligence" | |
| elif "devops" in role_lower or "cloud" in role_lower or "infrastructure" in role_lower: | |
| domain_val = "Cloud Computing" | |
| elif "mobile" in role_lower or "app" in role_lower or "android" in role_lower or "ios" in role_lower: | |
| domain_val = "E-commerce" | |
| elif "product" in role_lower: | |
| domain_val = "Education Technology" | |
| elif "bpo" in role_lower or "operations" in role_lower: | |
| domain_val = "Operations / BPO" | |
| elif "software" in role_lower or "web" in role_lower or "full stack" in role_lower or "developer" in role_lower or "engineer" in role_lower: | |
| domain_val = "Software Development" | |
| except Exception as e: | |
| status_msg += f" (Error parsing roadmap: {e})" | |
| else: | |
| status_msg += "\n⚠️ No career roadmap found in Pinecone for this User ID." | |
| if domain_val not in DOMAIN_CHOICES: | |
| domain_val = "Software Development" | |
| milestone_updates = get_milestones_from_roadmap_or_defaults(roadmap_dict, icp_val, domain_val) | |
| milestone_details_val = "*No detailed milestones found in roadmap.*" | |
| if roadmap_dict and roadmap_dict.get("milestones"): | |
| combined_md = [] | |
| milestones_list = roadmap_dict.get("milestones", [])[:4] | |
| for i, m in enumerate(milestones_list): | |
| m_ui = { | |
| "milestone_id": m.get("milestone_id", ""), | |
| "identity_statement": m.get("identity_statement", ""), | |
| "market_value_display": m.get("market_value_display", "") | |
| } | |
| detail = build_rich_milestone_detail( | |
| milestone_ui=m_ui, | |
| roadmap_dict=roadmap_dict, | |
| onboarding_profile=profile if 'profile' in locals() else None, | |
| milestone_index=i, | |
| total_milestones=len(milestones_list) | |
| ) | |
| combined_md.append(detail) | |
| milestone_details_val = "\n\n---\n\n".join(combined_md) | |
| else: | |
| milestone_details_val = "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| return ( | |
| self_vision_text, | |
| user_name_val, | |
| user_goal_val, | |
| domain_val, | |
| icp_val, | |
| gender_val, | |
| status_msg, | |
| *milestone_updates, | |
| milestone_details_val | |
| ) | |
| def on_roadmap_id_change(roadmap_id): | |
| """ | |
| Called when the Roadmap ID input is submitted or loses focus. | |
| Resolves the user_id from the roadmap_id via Pinecone reverse index, | |
| then triggers the full data load flow. | |
| """ | |
| roadmap_id = (roadmap_id or "").strip() | |
| if not roadmap_id: | |
| milestone_defaults = get_milestones_from_roadmap_or_defaults(None, "high_wage", "Software Development") | |
| return ( | |
| "", # user_id_input | |
| "", # raw_transcription_input | |
| "", # user_name | |
| "", # user_goal | |
| "Software Development", # domain | |
| "high_wage", # icp | |
| "male", # gender_input | |
| "📋 Awaiting user configuration...", # status_box | |
| *milestone_defaults, | |
| "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| ) | |
| from pinecone_client import PineconeClient | |
| pc = PineconeClient() | |
| # Resolve roadmap_id → user_id | |
| resolve_result = pc.resolve_user_from_roadmap(roadmap_id) | |
| user_id = resolve_result.get("user_id", "") | |
| if not user_id: | |
| milestone_defaults = get_milestones_from_roadmap_or_defaults(None, "high_wage", "Software Development") | |
| return ( | |
| "", # user_id_input | |
| "", # raw_transcription_input | |
| "", # user_name | |
| "", # user_goal | |
| "Software Development", # domain | |
| "high_wage", # icp | |
| "male", # gender_input | |
| f"⚠️ No user found for Roadmap ID '{roadmap_id}'. Try entering the User ID directly.", # status_box | |
| *milestone_defaults, | |
| "*Select a persona or enter a User ID to see detailed career trajectory.*" | |
| ) | |
| # We have the user_id — now do the full data load via on_user_id_change | |
| result = on_user_id_change(user_id) | |
| # Prepend the resolved user_id to the result | |
| return (user_id, *result) | |
| def on_dev_upsert(key_selection, user_id, content): | |
| """ | |
| Manually upsert a roadmap, onboarding, or self-vision conversation to Pinecone. | |
| """ | |
| user_id = (user_id or "").strip() | |
| content = (content or "").strip() | |
| if not user_id: | |
| return "❌ Error: Target User ID is required." | |
| if not content: | |
| return "❌ Error: Content is empty." | |
| from pinecone_client import PineconeClient, KEY_ONBOARDING, KEY_ROADMAP, KEY_SELF_VISION | |
| # Map selection to key | |
| key = KEY_ROADMAP | |
| if "onboarding" in key_selection.lower(): | |
| key = KEY_ONBOARDING | |
| elif "self-vision" in key_selection.lower(): | |
| key = KEY_SELF_VISION | |
| # Try to validate JSON if it's roadmap or onboarding | |
| if key in (KEY_ROADMAP, KEY_ONBOARDING): | |
| try: | |
| import json | |
| json.loads(content) | |
| except Exception as e: | |
| return f"❌ Error: Content is not valid JSON ({e}). Please check format." | |
| try: | |
| pc = PineconeClient() | |
| res = pc.upsert_conversation(user_id, key, content) | |
| if res.get("status") == "success": | |
| msg = f"✅ Success: Stored '{key}' under User ID '{user_id}'." | |
| if key == KEY_ROADMAP: | |
| msg += " (Reverse mapping registered.)" | |
| return f"{msg}\nElapsed time: {res.get('elapsed_ms', 0):.1f}ms" | |
| else: | |
| return "❌ Error: Failed to upsert to Pinecone." | |
| except Exception as e: | |
| return f"❌ Error: {e}" | |
| # --------------------------------------------------------------------------- | |
| # Rich milestone detail builder (onboarding + roadmap) | |
| # --------------------------------------------------------------------------- | |
| def build_rich_milestone_detail( | |
| milestone_ui, | |
| roadmap_dict=None, | |
| onboarding_profile=None, | |
| milestone_index=0, | |
| total_milestones=1, | |
| ): | |
| """ | |
| Build rich milestone detail text by combining onboarding profile | |
| and roadmap milestone data. | |
| Args: | |
| milestone_ui: dict with milestone_id, identity_statement, market_value_display | |
| (from the Gradio UI inputs) | |
| roadmap_dict: parsed roadmap JSON (full dict), or None | |
| onboarding_profile: parsed onboarding/user_profile JSON, or None | |
| milestone_index: 0-based index of this milestone | |
| total_milestones: total number of milestones being generated | |
| Returns: | |
| Markdown string with enriched milestone detail. | |
| """ | |
| mid = milestone_ui.get("milestone_id", "") | |
| mdesc = milestone_ui.get("identity_statement", "") | |
| mval = milestone_ui.get("market_value_display", "") | |
| # ── Find matching roadmap milestone (by milestone_id) ── | |
| roadmap_milestone = None | |
| if roadmap_dict: | |
| for rm in roadmap_dict.get("milestones", []): | |
| if rm.get("milestone_id") == mid: | |
| roadmap_milestone = rm | |
| break | |
| # ── Header with label ── | |
| identity_label = "" | |
| if roadmap_milestone: | |
| identity_label = roadmap_milestone.get("identity_label", "") | |
| header = f"#### 🏁 {mid}" | |
| if identity_label: | |
| header += f" — {identity_label}" | |
| sections = [header, ""] | |
| # ── Identity Statement ── | |
| if mdesc: | |
| sections.append(f"**🎯 Your Identity at This Stage**") | |
| sections.append(mdesc) | |
| sections.append("") | |
| # ── Market Value ── | |
| if mval: | |
| sections.append(f"**💰 Market Value:** {mval}") | |
| sections.append("") | |
| # ── Where You Are Now (from roadmap vision_profile + onboarding) ── | |
| where_now_parts = [] | |
| if roadmap_dict: | |
| vision = roadmap_dict.get("vision_profile", {}) | |
| current_state = vision.get("current_state", "") | |
| main_blocker = vision.get("main_blocker", "") | |
| if current_state and milestone_index == 0: | |
| where_now_parts.append(current_state) | |
| if main_blocker and milestone_index == 0: | |
| where_now_parts.append(f"**Key challenge to overcome:** {main_blocker}") | |
| if onboarding_profile: | |
| bg = onboarding_profile.get("background_skills", {}) | |
| exp_level = bg.get("experience_level", "") | |
| tech_skills = bg.get("technical_skills", []) | |
| soft_skills = bg.get("soft_skills", []) | |
| if exp_level and milestone_index == 0: | |
| where_now_parts.append(f"**Experience level:** {exp_level}") | |
| if tech_skills and milestone_index == 0: | |
| where_now_parts.append(f"**Existing technical skills:** {', '.join(tech_skills)}") | |
| if soft_skills and milestone_index == 0: | |
| where_now_parts.append(f"**Soft skills:** {', '.join(soft_skills)}") | |
| if where_now_parts: | |
| sections.append("**📍 Where You Are Now**") | |
| sections.extend(where_now_parts) | |
| sections.append("") | |
| # ── What You'll Master (from roadmap modules + skills) ── | |
| if roadmap_milestone: | |
| modules = roadmap_milestone.get("modules", []) | |
| if modules: | |
| sections.append("**📚 What You'll Master**") | |
| total_hours = 0 | |
| for mod in modules: | |
| mod_title = mod.get("title", "Module") | |
| mod_desc = mod.get("description", "") | |
| skills = mod.get("skills", []) | |
| # Module header | |
| mod_line = f"- **{mod_title}**" | |
| if mod_desc: | |
| mod_line += f" — {mod_desc}" | |
| sections.append(mod_line) | |
| # Skills under module | |
| for skill in skills: | |
| s_title = skill.get("title", "") | |
| s_hours = skill.get("estimated_hours", 0) | |
| s_diff = skill.get("difficulty", 0) | |
| total_hours += s_hours | |
| skill_line = f" - {s_title}" | |
| skill_meta = [] | |
| if s_hours: | |
| skill_meta.append(f"{s_hours} hrs") | |
| if s_diff: | |
| # Convert 0-1 difficulty to a label | |
| if s_diff <= 0.25: | |
| skill_meta.append("beginner") | |
| elif s_diff <= 0.5: | |
| skill_meta.append("intermediate") | |
| elif s_diff <= 0.75: | |
| skill_meta.append("advanced") | |
| else: | |
| skill_meta.append("expert") | |
| if skill_meta: | |
| skill_line += f" *({', '.join(skill_meta)})*" | |
| sections.append(skill_line) | |
| sections.append("") | |
| # Effort summary | |
| if total_hours > 0: | |
| sections.append(f"**⏱️ Estimated Effort:** ~{total_hours} hours across {len(modules)} module{'s' if len(modules) != 1 else ''}") | |
| sections.append("") | |
| # Checkpoint | |
| checkpoint = roadmap_milestone.get("checkpoint_rule", {}) | |
| cp_type = checkpoint.get("checkpoint_type", "") | |
| cp_mastery = checkpoint.get("required_mastery", 0) | |
| if cp_type: | |
| mastery_pct = int(cp_mastery * 100) if cp_mastery else 0 | |
| cp_display = cp_type.replace("_", " ").title() | |
| sections.append(f"**🎓 Checkpoint:** {cp_display} | Required mastery: {mastery_pct}%") | |
| sections.append("") | |
| # ── Personalized For You (from onboarding) ── | |
| personalized_parts = [] | |
| if onboarding_profile: | |
| # Learning style | |
| ls = onboarding_profile.get("learning_style", {}) | |
| pref_style = ls.get("preferred_style", "") | |
| pace = ls.get("pace", "") | |
| if pref_style and pref_style != "not specified": | |
| personalized_parts.append(f"**Learning style:** {pref_style}") | |
| if pace and pace != "not specified": | |
| personalized_parts.append(f"**Pace:** {pace}") | |
| # Time commitment | |
| pi = onboarding_profile.get("personalized_insights", {}) | |
| time_commit = pi.get("time_commitment", "") | |
| goal_timeline = pi.get("goal_timeline", "") | |
| familiar_tools = pi.get("familiar_tools", []) | |
| if time_commit: | |
| personalized_parts.append(f"**Your time:** {time_commit}") | |
| if goal_timeline: | |
| personalized_parts.append(f"**Target timeline:** {goal_timeline}") | |
| if familiar_tools: | |
| personalized_parts.append(f"**Tools you know:** {', '.join(familiar_tools)}") | |
| # Challenges | |
| barriers = onboarding_profile.get("learning_barriers", {}) | |
| challenges = barriers.get("challenges", []) | |
| if challenges: | |
| personalized_parts.append(f"**Watch out for:** {', '.join(challenges)}") | |
| # Focused topics | |
| dev_areas = onboarding_profile.get("development_areas", {}) | |
| topics = dev_areas.get("focused_topics", []) | |
| if topics: | |
| personalized_parts.append(f"**Your interests:** {', '.join(topics)}") | |
| # Primary goal + motivation | |
| objectives = onboarding_profile.get("learning_objectives", {}) | |
| primary_goal = objectives.get("primary_goal", "") | |
| motivation = objectives.get("key_motivation", "") | |
| if primary_goal: | |
| goal_line = f"**Your goal:** {primary_goal}" | |
| if motivation: | |
| goal_line += f" — {motivation}" | |
| personalized_parts.append(goal_line) | |
| if personalized_parts: | |
| sections.append("**🔗 Personalized For You**") | |
| sections.extend(personalized_parts) | |
| sections.append("") | |
| # ── Progress indicator ── | |
| sections.append(f"*Milestone {milestone_index + 1} of {total_milestones}*") | |
| return "\n".join(sections) | |
| # --------------------------------------------------------------------------- | |
| # Main generation function (unified: images + Pinecone persistence) | |
| # --------------------------------------------------------------------------- | |
| def run_generation( | |
| user_id_input, raw_transcription_input, | |
| user_goal, domain, icp, gender_input, reference_photo, | |
| m1_visible, m1_id, m1_desc, m1_val, | |
| m2_visible, m2_id, m2_desc, m2_val, | |
| m3_visible, m3_id, m3_desc, m3_val, | |
| m4_visible, m4_id, m4_desc, m4_val, | |
| ): | |
| """Generate milestone images + run Pinecone persistence pipeline.""" | |
| api_key = os.environ.get("GEMINI_API_KEY", "") | |
| empty_details = ["", "", "", ""] | |
| pinecone_empty = "" # empty pinecone status | |
| sv_empty = { | |
| "status": "Awaiting generation...", | |
| "message": "Click 'Generate Self-Vision' to run the pipeline." | |
| } | |
| if not api_key: | |
| yield (None, None, None, None, | |
| *empty_details, | |
| gr.update(interactive=True), | |
| "❌ No API key provided. Set the GEMINI_API_KEY environment variable.", | |
| None, pinecone_empty, sv_empty) | |
| return | |
| # ── Pinecone: resolve user_id ── | |
| uid = (user_id_input or "").strip() | |
| raw_transcript = (raw_transcription_input or "").strip() | |
| use_pinecone = bool(uid) | |
| # ── Pinecone STEP: Fetch onboarding context ── | |
| pinecone_status_lines = [] | |
| onboarding_context = "" | |
| self_vision_result = None | |
| if use_pinecone: | |
| pinecone_status_lines.append(f"🔑 User ID: {uid}") | |
| try: | |
| from pinecone_client import PineconeClient, KEY_ONBOARDING, KEY_ROADMAP | |
| pc = PineconeClient() | |
| # Fetch onboarding context | |
| fetch_result = pc.fetch_conversation(uid, KEY_ONBOARDING) | |
| onboarding_context = fetch_result["text"] | |
| fetch_status = fetch_result["status"] | |
| pinecone_status_lines.append(f"📥 Onboarding fetch: {fetch_status}") | |
| if onboarding_context: | |
| from context_processor import count_words | |
| pinecone_status_lines.append(f" Context: {count_words(onboarding_context)} words") | |
| # Fetch roadmap context | |
| fetch_roadmap = pc.fetch_conversation(uid, KEY_ROADMAP) | |
| roadmap_context = fetch_roadmap["text"] | |
| roadmap_status = fetch_roadmap["status"] | |
| pinecone_status_lines.append(f"🗺️ Roadmap fetch: {roadmap_status}") | |
| if roadmap_context: | |
| from context_processor import count_words | |
| pinecone_status_lines.append(f" Roadmap: {count_words(roadmap_context)} words") | |
| try: | |
| roadmap_data = json.loads(roadmap_context) | |
| roadmap_id = roadmap_data.get("roadmap_id", "") | |
| if roadmap_id: | |
| pinecone_status_lines.append(f" Roadmap ID: {roadmap_id}") | |
| except Exception: | |
| roadmap_data = None | |
| else: | |
| pinecone_status_lines.append(" ⚠️ No roadmap found for this User ID") | |
| except Exception as e: | |
| pinecone_status_lines.append(f"⚠️ Fetch error: {e}") | |
| # Resolve gender | |
| gender = "male" | |
| gl = str(gender_input).lower() | |
| if "female" in gl or "woman" in gl or "girl" in gl: | |
| gender = "female" | |
| elif "male" in gl or "man" in gl or "boy" in gl: | |
| gender = "male" | |
| else: | |
| gender = random.choice(["male", "female"]) | |
| milestones = [] | |
| for mid, mdesc, mval, mvis in [ | |
| (m1_id, m1_desc, m1_val, m1_visible), | |
| (m2_id, m2_desc, m2_val, m2_visible), | |
| (m3_id, m3_desc, m3_val, m3_visible), | |
| (m4_id, m4_desc, m4_val, m4_visible), | |
| ]: | |
| if mid and mid.strip(): | |
| milestones.append({ | |
| "milestone_id": mid.strip(), | |
| "identity_statement": mdesc, | |
| "market_value_display": mval, | |
| }) | |
| if not milestones: | |
| yield (None, None, None, None, | |
| *empty_details, | |
| gr.update(interactive=True), | |
| "❌ No milestones configured.", | |
| None, "\n".join(pinecone_status_lines), sv_empty) | |
| return | |
| ref_photo = None | |
| if reference_photo is not None: | |
| if isinstance(reference_photo, Image.Image): | |
| ref_photo = reference_photo | |
| else: | |
| try: | |
| ref_photo = Image.open(reference_photo) | |
| except Exception: | |
| ref_photo = None | |
| if ref_photo and ref_photo.mode != "RGB": | |
| ref_photo = ref_photo.convert("RGB") | |
| images = [None, None, None, None] | |
| details = ["", "", "", ""] | |
| transcriptions = ["", "", "", ""] | |
| total_start = time.time() | |
| # ── Parse onboarding + roadmap for rich milestone details ── | |
| parsed_onboarding = None | |
| parsed_roadmap = None | |
| if use_pinecone: | |
| # Parse onboarding profile | |
| if onboarding_context: | |
| try: | |
| parsed_onboarding = json.loads(onboarding_context) | |
| except Exception: | |
| parsed_onboarding = None | |
| # roadmap_data was already parsed above (L1137); make it available | |
| # If not fetched from Pinecone, try to see if we have it | |
| try: | |
| parsed_roadmap = roadmap_data # set at L1137 inside the Pinecone block | |
| except NameError: | |
| parsed_roadmap = None | |
| # ── Override prompt variables from Pinecone roadmap/onboarding if available ── | |
| if use_pinecone: | |
| if parsed_roadmap: | |
| # Override ICP | |
| icp_raw = parsed_roadmap.get("icp_type", "high") | |
| icp = "low_wage" if icp_raw == "low" else "high_wage" | |
| # Override milestones list directly from the roadmap JSON | |
| roadmap_milestones = parsed_roadmap.get("milestones", []) | |
| if roadmap_milestones: | |
| milestones = [] | |
| for rm in roadmap_milestones[:4]: # limit to at most 4 | |
| rmid = rm.get("milestone_id") or rm.get("label") or "" | |
| rmdesc = rm.get("identity_statement") or rm.get("identity_label") or "" | |
| rmval = rm.get("market_value_display") or "" | |
| rmlabel = rm.get("identity_label") or "" | |
| if rmid: | |
| milestones.append({ | |
| "milestone_id": rmid.strip(), | |
| "identity_statement": rmdesc, | |
| "market_value_display": rmval, | |
| "identity_label": rmlabel, | |
| }) | |
| # Override domain | |
| target_role = parsed_roadmap.get("target_role", "") | |
| if target_role: | |
| role_lower = target_role.lower() | |
| if "data science" in role_lower or "analytics" in role_lower or "analyst" in role_lower: | |
| domain = "Data Science" | |
| elif "machine learning" in role_lower or "ml" in role_lower or "ai" in role_lower or "artificial intelligence" in role_lower: | |
| domain = "Artificial Intelligence" | |
| elif "devops" in role_lower or "cloud" in role_lower or "infrastructure" in role_lower: | |
| domain = "Cloud Computing" | |
| elif "mobile" in role_lower or "app" in role_lower or "android" in role_lower or "ios" in role_lower: | |
| domain = "E-commerce" | |
| elif "product" in role_lower: | |
| domain = "Education Technology" | |
| elif "bpo" in role_lower or "operations" in role_lower: | |
| domain = "Operations / BPO" | |
| elif "software" in role_lower or "web" in role_lower or "full stack" in role_lower or "developer" in role_lower or "engineer" in role_lower: | |
| domain = "Software Development" | |
| # Override user_goal | |
| vision = parsed_roadmap.get("vision_profile", {}) | |
| top_motivation = vision.get("top_motivation", "") | |
| vision_12mo = vision.get("vision_12mo", "") | |
| roadmap_goal = f"{target_role}" | |
| if top_motivation or vision_12mo: | |
| details_parts = [] | |
| if top_motivation: | |
| details_parts.append(top_motivation) | |
| if vision_12mo: | |
| details_parts.append(vision_12mo) | |
| roadmap_goal += " — " + " ".join(details_parts) | |
| if roadmap_goal.strip(): | |
| user_goal = roadmap_goal | |
| if parsed_onboarding: | |
| # Override Gender | |
| user_name_val = parsed_onboarding.get("user_name", "") | |
| female_indicators = ["Priya", "Priyanka", "Ananya", "Pooja", "Shreya", "Divya", "Anjali", "Neha", "Riya", "Fatima", "Ayesha", "Lakshmi", "Meera", "Nandini", "Tanvi", "Deepika", "Priyambada"] | |
| if any(ind in user_name_val for ind in female_indicators): | |
| gender = "female" | |
| else: | |
| gender = "male" | |
| # Pre-build detail text for all milestones using both sources | |
| for i, milestone in enumerate(milestones): | |
| details[i] = build_rich_milestone_detail( | |
| milestone_ui=milestone, | |
| roadmap_dict=parsed_roadmap, | |
| onboarding_profile=parsed_onboarding, | |
| milestone_index=i, | |
| total_milestones=len(milestones), | |
| ) | |
| for i, milestone in enumerate(milestones): | |
| mid = milestone["milestone_id"] | |
| status_msg = f"⏳ Generating milestone {mid} ({i+1}/{len(milestones)})..." | |
| yield (*images, | |
| *details, | |
| gr.update(interactive=False, value="⏳ Generating..."), | |
| status_msg, | |
| None, "\n".join(pinecone_status_lines), sv_empty) | |
| # Extract skills/modules context from roadmap milestone to customize the image setting | |
| skills_list = [] | |
| if parsed_roadmap: | |
| for rm in parsed_roadmap.get("milestones", []): | |
| if rm.get("milestone_id") == mid: | |
| for mod in rm.get("modules", []): | |
| mod_title = mod.get("title", "") | |
| if mod_title: | |
| skills_list.append(mod_title) | |
| for skill in mod.get("skills", []): | |
| s_title = skill.get("title", "") | |
| if s_title: | |
| skills_list.append(s_title) | |
| break | |
| skills_context = ", ".join(skills_list[:4]) if skills_list else "" | |
| prompt_with_ref = build_prompt( | |
| milestone_id=mid, | |
| icp=icp, | |
| domain=domain, | |
| user_goal=user_goal, | |
| has_reference_photo=True, | |
| gender=gender, | |
| milestone_label=milestone.get("identity_label"), | |
| identity_statement=milestone.get("identity_statement"), | |
| market_value_display=milestone.get("market_value_display"), | |
| skills_context=skills_context, | |
| ) | |
| transcriptions[i] = prompt_with_ref | |
| prompt_scene_only = build_prompt( | |
| milestone_id=mid, | |
| icp=icp, | |
| domain=domain, | |
| user_goal=user_goal, | |
| has_reference_photo=False, | |
| gender=gender, | |
| milestone_label=milestone.get("identity_label"), | |
| identity_statement=milestone.get("identity_statement"), | |
| market_value_display=milestone.get("market_value_display"), | |
| skills_context=skills_context, | |
| ) | |
| try: | |
| result_img = generate_single_image( | |
| prompt_with_ref if ref_photo else prompt_scene_only, | |
| ref_photo, | |
| api_key, | |
| ) | |
| images[i] = result_img | |
| except Exception as e: | |
| print(f"Error generating {mid}: {e}") | |
| traceback.print_exc() | |
| elapsed = time.time() - total_start | |
| successful = sum(1 for img in images if img is not None) | |
| total_attempted = len(milestones) | |
| # ── Pinecone STEP: Run Self Vision LLM + persist ── | |
| if use_pinecone and raw_transcript: | |
| pinecone_status_lines.append("") | |
| pinecone_status_lines.append("🧠 Running Self Vision LLM...") | |
| try: | |
| sv_result = run_self_vision( | |
| user_id=uid, | |
| raw_transcription=raw_transcript, | |
| ) | |
| if "self_vision_result" in sv_result: | |
| self_vision_result = sv_result["self_vision_result"] | |
| m = sv_result.get("metadata", {}) | |
| pinecone_status_lines.append(f"✅ LLM generation complete") | |
| pinecone_status_lines.append(f"📤 Upsert: {m.get('upsert_status', '?')}") | |
| pinecone_status_lines.append(f"📊 Context words: {m.get('context_word_count', '?')}") | |
| pinecone_status_lines.append(f"📥 Onboarding used: {m.get('fetch_status', '?')}") | |
| pinecone_status_lines.append(f"🗺️ Roadmap used: {m.get('fetch_roadmap_status', '?')}") | |
| pinecone_status_lines.append(f"✂️ Truncation: {m.get('truncation_applied', False)}") | |
| pinecone_status_lines.append(f"⏱️ Pipeline: {m.get('execution_time_ms', '?')}ms") | |
| else: | |
| pinecone_status_lines.append(f"⚠️ LLM error: {sv_result.get('error', 'unknown')}") | |
| except Exception as e: | |
| pinecone_status_lines.append(f"⚠️ Pipeline error: {e}") | |
| elif use_pinecone and not raw_transcript: | |
| # No transcript provided — just store the prompts as raw transcript | |
| combined_prompts = "\n\n---\n\n".join(t for t in transcriptions if t) | |
| try: | |
| from pinecone_client import PineconeClient, KEY_SELF_VISION | |
| pc = PineconeClient() | |
| upsert_result = pc.upsert_conversation(uid, KEY_SELF_VISION, combined_prompts) | |
| pinecone_status_lines.append(f"📤 Stored prompts as transcript: {upsert_result['status']}") | |
| self_vision_result = { | |
| "status": "Stored prompts only", | |
| "message": "Prompts were stored to Pinecone as raw transcript. LLM synthesis was skipped because no raw transcription was provided." | |
| } | |
| except Exception as e: | |
| pinecone_status_lines.append(f"⚠️ Upsert error: {e}") | |
| self_vision_result = { | |
| "status": "Upsert error", | |
| "error": str(e) | |
| } | |
| else: | |
| self_vision_result = { | |
| "status": "Inactive", | |
| "message": "Pinecone persistence and LLM synthesis were not run because no User ID was provided." | |
| } | |
| summary = ( | |
| f"✅ Done! {successful}/{total_attempted} images generated in {elapsed:.1f}s\n" | |
| f"Face reference: {'used' if ref_photo else 'not provided'}\n" | |
| f"Gender: {gender} | Domain: {domain} | ICP: {icp}" | |
| ) | |
| if use_pinecone: | |
| summary += f"\nPinecone: namespace={uid}" | |
| # Build output JSON | |
| output_json = { | |
| "service": "self-vision", | |
| "version": "v1.0", | |
| "input_source": "roadmap", | |
| "user_id": uid, | |
| "generated_at": datetime.utcnow().isoformat(), | |
| "domain": domain, | |
| "icp": icp, | |
| "gender": gender, | |
| "face_reference_used": ref_photo is not None, | |
| "milestones": [ | |
| { | |
| "milestone_id": milestones[i]["milestone_id"], | |
| "identity_statement": milestones[i].get("identity_statement", ""), | |
| "market_value_display": milestones[i].get("market_value_display", ""), | |
| "raw_prompt": transcriptions[i], | |
| "success": images[i] is not None, | |
| "image_base64": ( | |
| __import__('base64').b64encode( | |
| (lambda img: (img.save(buf := __import__('io').BytesIO(), format='JPEG'), buf)[1])(images[i]).getvalue() | |
| ).decode() if images[i] is not None else None | |
| ), | |
| } | |
| for i in range(len(milestones)) | |
| ], | |
| "summary": { | |
| "total_milestones": len(milestones), | |
| "successful": sum(1 for img in images if img is not None), | |
| "face_reference_used": ref_photo is not None, | |
| } | |
| } | |
| # Save JSON to file so user can download it | |
| output_json_path = "/tmp/self_vision_output.json" | |
| os.makedirs(os.path.dirname(output_json_path), exist_ok=True) | |
| with open(output_json_path, "w") as f: | |
| json.dump(output_json, f, indent=2) | |
| yield (*images, | |
| *details, | |
| gr.update(interactive=True, value="🚀 Generate Self-Vision"), | |
| summary, | |
| output_json_path, | |
| "\n".join(pinecone_status_lines), | |
| self_vision_result) | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| /* ── Global dark overrides ── */ | |
| .gradio-container { | |
| max-width: 1400px !important; | |
| margin: 0 auto !important; | |
| background: #0a0a1a !important; | |
| } | |
| .dark, body, .main, .app { | |
| background: #0a0a1a !important; | |
| } | |
| /* ── Typography ── */ | |
| h1, h2, h3, h4, h5, .markdown h1, .markdown h2, .markdown h3, .markdown h4 { | |
| color: #e2e8f0 !important; | |
| } | |
| p, span, label, .label-wrap span, .markdown p { | |
| color: #cbd5e1 !important; | |
| } | |
| /* ── Section headers ── */ | |
| .section-header { | |
| background: linear-gradient(135deg, #7c3aed 0%, #6366f1 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-weight: 700; | |
| } | |
| /* ── Input fields & dropdowns ── */ | |
| input, textarea, select, | |
| .wrap input, .wrap textarea, | |
| input[type="text"], input[type="password"], | |
| .border-none input, .border-none textarea { | |
| background: #131328 !important; | |
| border: 1px solid #2d2d5e !important; | |
| color: #e2e8f0 !important; | |
| border-radius: 10px !important; | |
| transition: border-color 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| input:focus, textarea:focus { | |
| border-color: #7c3aed !important; | |
| box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2) !important; | |
| outline: none !important; | |
| } | |
| /* ── Dropdown menus ── */ | |
| .dropdown-container, .options, ul[role="listbox"] { | |
| background: #131328 !important; | |
| border: 1px solid #2d2d5e !important; | |
| border-radius: 10px !important; | |
| } | |
| ul[role="listbox"] li { | |
| color: #cbd5e1 !important; | |
| padding: 10px 14px !important; | |
| transition: background 0.15s ease; | |
| } | |
| ul[role="listbox"] li:hover, ul[role="listbox"] li[aria-selected="true"] { | |
| background: rgba(124, 58, 237, 0.2) !important; | |
| color: #f1f5f9 !important; | |
| } | |
| /* ── Groups / Accordion panels ── */ | |
| .form, .block, .panel, fieldset, .group { | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| .gr-group { | |
| background: #0f0f25 !important; | |
| border: 1px solid #1e1e48 !important; | |
| border-radius: 14px !important; | |
| padding: 16px !important; | |
| margin-bottom: 8px !important; | |
| } | |
| /* ── Milestone output cards ── */ | |
| .milestone-card { | |
| background: linear-gradient(145deg, #0f0f25 0%, #141432 100%) !important; | |
| border: 1px solid #2d2d5e !important; | |
| border-radius: 16px !important; | |
| padding: 14px !important; | |
| transition: border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .milestone-card:hover { | |
| border-color: #7c3aed !important; | |
| box-shadow: 0 0 20px rgba(124, 58, 237, 0.15) !important; | |
| } | |
| /* ── Milestone detail text panels ── */ | |
| .milestone-detail { | |
| background: linear-gradient(145deg, #0d0d22 0%, #12122e 100%) !important; | |
| border: 1px solid #1e1e48 !important; | |
| border-left: 3px solid #7c3aed !important; | |
| border-radius: 12px !important; | |
| padding: 18px 16px !important; | |
| display: flex; | |
| align-items: flex-start; | |
| } | |
| .milestone-detail h4, .milestone-detail .markdown h4 { | |
| color: #a78bfa !important; | |
| font-size: 1.05em !important; | |
| margin: 0 0 10px 0 !important; | |
| } | |
| .milestone-detail p, .milestone-detail .markdown p { | |
| color: #94a3b8 !important; | |
| line-height: 1.65 !important; | |
| margin: 6px 0 !important; | |
| font-size: 0.92em !important; | |
| } | |
| .milestone-detail strong, .milestone-detail .markdown strong { | |
| color: #c4b5fd !important; | |
| } | |
| /* ── Generate button ── */ | |
| .gen-btn { | |
| background: linear-gradient(135deg, #7c3aed 0%, #6366f1 50%, #818cf8 100%) !important; | |
| border: none !important; | |
| font-size: 1.15em !important; | |
| font-weight: 700 !important; | |
| padding: 14px 28px !important; | |
| border-radius: 14px !important; | |
| color: #ffffff !important; | |
| letter-spacing: 0.3px; | |
| box-shadow: 0 4px 20px rgba(124, 58, 237, 0.35) !important; | |
| transition: transform 0.15s ease, box-shadow 0.2s ease !important; | |
| } | |
| .gen-btn:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 6px 28px rgba(124, 58, 237, 0.5) !important; | |
| } | |
| .gen-btn:active { | |
| transform: translateY(0px) !important; | |
| } | |
| /* ── Status box ── */ | |
| .status-box textarea { | |
| font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace !important; | |
| font-size: 0.88em !important; | |
| background: #0a0a1a !important; | |
| color: #a78bfa !important; | |
| border: 1px solid #1e1e48 !important; | |
| border-radius: 10px !important; | |
| line-height: 1.6 !important; | |
| } | |
| /* ── Image upload area ── */ | |
| .image-container, .upload-container { | |
| background: #0f0f25 !important; | |
| border: 2px dashed #2d2d5e !important; | |
| border-radius: 14px !important; | |
| transition: border-color 0.2s ease; | |
| } | |
| .image-container:hover, .upload-container:hover { | |
| border-color: #7c3aed !important; | |
| } | |
| /* ── Checkbox hide ── */ | |
| .checkbox-container { display: none; } | |
| /* ── Image output styling ── */ | |
| .milestone-card img { | |
| border-radius: 12px !important; | |
| } | |
| /* ── Labels ── */ | |
| .label-wrap { | |
| color: #a78bfa !important; | |
| } | |
| .label-wrap span { | |
| color: #a78bfa !important; | |
| font-weight: 600 !important; | |
| font-size: 0.9em !important; | |
| } | |
| /* ── Separator lines ── */ | |
| hr { | |
| border-color: #1e1e48 !important; | |
| opacity: 0.5; | |
| } | |
| /* ── Title area ── */ | |
| .title-area { | |
| text-align: center; | |
| padding: 8px 0 20px 0; | |
| } | |
| .title-area h1 { | |
| background: linear-gradient(135deg, #a78bfa 0%, #818cf8 40%, #c084fc 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| font-size: 2em !important; | |
| } | |
| /* ── Scrollbar ── */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: #0a0a1a; } | |
| ::-webkit-scrollbar-thumb { background: #2d2d5e; border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #7c3aed; } | |
| """ | |
| with gr.Blocks( | |
| title="Vidya Self-Vision — POC", | |
| theme=gr.themes.Base( | |
| primary_hue=gr.themes.colors.violet, | |
| secondary_hue=gr.themes.colors.indigo, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=gr.themes.GoogleFont("Inter"), | |
| font_mono=gr.themes.GoogleFont("JetBrains Mono"), | |
| ).set( | |
| body_background_fill="#0a0a1a", | |
| body_background_fill_dark="#0a0a1a", | |
| background_fill_primary="#0f0f25", | |
| background_fill_primary_dark="#0f0f25", | |
| background_fill_secondary="#131328", | |
| background_fill_secondary_dark="#131328", | |
| block_background_fill="#0f0f25", | |
| block_background_fill_dark="#0f0f25", | |
| block_border_color="#1e1e48", | |
| block_border_color_dark="#1e1e48", | |
| block_label_text_color="#a78bfa", | |
| block_label_text_color_dark="#a78bfa", | |
| block_label_background_fill="#0a0a1a", | |
| block_label_background_fill_dark="#0a0a1a", | |
| block_title_text_color="#e2e8f0", | |
| block_title_text_color_dark="#e2e8f0", | |
| body_text_color="#cbd5e1", | |
| body_text_color_dark="#cbd5e1", | |
| body_text_color_subdued="#64748b", | |
| body_text_color_subdued_dark="#64748b", | |
| input_background_fill="#131328", | |
| input_background_fill_dark="#131328", | |
| input_border_color="#2d2d5e", | |
| input_border_color_dark="#2d2d5e", | |
| input_border_color_focus="#7c3aed", | |
| input_border_color_focus_dark="#7c3aed", | |
| input_placeholder_color="#64748b", | |
| input_placeholder_color_dark="#64748b", | |
| panel_background_fill="#0d0d22", | |
| panel_background_fill_dark="#0d0d22", | |
| panel_border_color="#1e1e48", | |
| panel_border_color_dark="#1e1e48", | |
| button_primary_background_fill="linear-gradient(135deg, #7c3aed 0%, #6366f1 100%)", | |
| button_primary_background_fill_dark="linear-gradient(135deg, #7c3aed 0%, #6366f1 100%)", | |
| button_primary_text_color="#ffffff", | |
| button_primary_text_color_dark="#ffffff", | |
| button_primary_border_color="transparent", | |
| button_primary_border_color_dark="transparent", | |
| button_secondary_background_fill="#1e1e48", | |
| button_secondary_background_fill_dark="#1e1e48", | |
| button_secondary_text_color="#cbd5e1", | |
| button_secondary_text_color_dark="#cbd5e1", | |
| border_color_primary="#2d2d5e", | |
| border_color_primary_dark="#2d2d5e", | |
| border_color_accent="#7c3aed", | |
| border_color_accent_dark="#7c3aed", | |
| color_accent="#7c3aed", | |
| color_accent_soft="rgba(124, 58, 237, 0.15)", | |
| color_accent_soft_dark="rgba(124, 58, 237, 0.15)", | |
| link_text_color="#a78bfa", | |
| link_text_color_dark="#a78bfa", | |
| loader_color="#7c3aed", | |
| loader_color_dark="#7c3aed", | |
| shadow_drop="0 4px 12px rgba(0, 0, 0, 0.4)", | |
| shadow_drop_lg="0 8px 24px rgba(0, 0, 0, 0.5)", | |
| ), | |
| css=CUSTOM_CSS, | |
| ) as demo: | |
| gr.Markdown( | |
| """ | |
| <div class="title-area"> | |
| # 🔮 Vidya Self-Vision — POC | |
| Milestone Image Generation · Cross-POC Conversation Persistence | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(): | |
| # ── LEFT COLUMN: Controls ── | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 🔗 Cross-POC Persistence") | |
| roadmap_id_input = gr.Textbox( | |
| label="🗺️ Roadmap ID", | |
| placeholder="e.g. ai_roadmap_20260611162618_d40b3260", | |
| lines=1, | |
| info="Enter a Roadmap ID to auto-resolve the User ID", | |
| ) | |
| user_id_input = gr.Textbox( | |
| label="🔑 User ID (UUID from Onboarding)", | |
| placeholder="e.g. 550e8400-e29b-41d4-a716-446655440000", | |
| lines=1, | |
| ) | |
| with gr.Accordion("📝 Raw Transcription (optional)", open=False): | |
| raw_transcription_input = gr.Textbox( | |
| label="Self-Vision session transcript", | |
| placeholder="Paste the raw self-vision transcript here for Pinecone storage + LLM analysis…", | |
| lines=6, | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown("### 👤 Persona & Configuration") | |
| persona_selector = gr.Dropdown( | |
| choices=PERSONA_CHOICES, | |
| value="-- Select an Example Persona --", | |
| label="🎯 Select Example Learner Persona (Auto-fills inputs)", | |
| interactive=True, | |
| ) | |
| with gr.Accordion("Detailed Career Milestones", open=False): | |
| milestone_details_md = gr.Markdown(value="*Select a persona to see detailed career trajectory.*") | |
| user_name = gr.Textbox( | |
| label="✏️ Your Name", | |
| placeholder="Enter your name here", | |
| lines=1, | |
| ) | |
| user_goal = gr.Textbox( | |
| label="🎯 Target Career Ambition", | |
| placeholder="e.g., get an engineering role at a product company like Swiggy", | |
| lines=2, | |
| ) | |
| domain = gr.Dropdown( | |
| choices=DOMAIN_CHOICES, | |
| value="Software Development", | |
| label="💼 Professional Domain", | |
| interactive=True, | |
| ) | |
| icp = gr.Dropdown( | |
| choices=ICP_CHOICES, | |
| value="high_wage", | |
| label="📊 ICP (Income Customer Profile)", | |
| interactive=True, | |
| ) | |
| gender_input = gr.Dropdown( | |
| choices=["male", "female"], | |
| value="male", | |
| label="👤 Gender", | |
| interactive=True, | |
| ) | |
| reference_photo = gr.Image( | |
| label="📸 Reference Photo (optional — enables face personalization)", | |
| type="pil", | |
| height=200, | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown("### 🏁 Milestone Configuration") | |
| # Milestone 1 | |
| with gr.Group(visible=True) as m1_group: | |
| gr.Markdown("**Milestone 1**") | |
| m1_visible = gr.Checkbox(value=True, visible=False) | |
| m1_id = gr.Textbox(label="ID", value="L1", lines=1) | |
| m1_desc = gr.Textbox( | |
| label="Identity Statement", | |
| value="You've landed your first tech job — your engineering journey begins here", | |
| lines=2, | |
| ) | |
| m1_val = gr.Textbox(label="Market Value", value="6-12 LPA", lines=1) | |
| # Milestone 2 | |
| with gr.Group(visible=True) as m2_group: | |
| gr.Markdown("**Milestone 2**") | |
| m2_visible = gr.Checkbox(value=True, visible=False) | |
| m2_id = gr.Textbox(label="ID", value="L2", lines=1) | |
| m2_desc = gr.Textbox( | |
| label="Identity Statement", | |
| value="You think and ship like an engineer — your team trusts your technical judgment", | |
| lines=2, | |
| ) | |
| m2_val = gr.Textbox(label="Market Value", value="12-22 LPA", lines=1) | |
| # Milestone 3 | |
| with gr.Group(visible=True) as m3_group: | |
| gr.Markdown("**Milestone 3**") | |
| m3_visible = gr.Checkbox(value=True, visible=False) | |
| m3_id = gr.Textbox(label="ID", value="L3", lines=1) | |
| m3_desc = gr.Textbox( | |
| label="Identity Statement", | |
| value="You own features end-to-end — peers come to you when things get hard", | |
| lines=2, | |
| ) | |
| m3_val = gr.Textbox(label="Market Value", value="22-40 LPA", lines=1) | |
| # Milestone 4 | |
| with gr.Group(visible=True) as m4_group: | |
| gr.Markdown("**Milestone 4**") | |
| m4_visible = gr.Checkbox(value=True, visible=False) | |
| m4_id = gr.Textbox(label="ID", value="L4", lines=1) | |
| m4_desc = gr.Textbox( | |
| label="Identity Statement", | |
| value="You set the technical direction — leaders look to you to define what gets built next", | |
| lines=2, | |
| ) | |
| m4_val = gr.Textbox(label="Market Value", value="40-75 LPA", lines=1) | |
| gr.Markdown("---") | |
| # --- Developer tools accordion --- | |
| with gr.Accordion("🛠️ Pinecone Developer Utilities", open=False): | |
| gr.Markdown( | |
| "Manually upload/upsert context data (Onboarding profile or Roadmap JSON) to Pinecone." | |
| ) | |
| dev_key = gr.Dropdown( | |
| choices=[ | |
| "Roadmap (roadmap_output)", | |
| "Onboarding (onboarding_conversation)", | |
| "Self-Vision (self_vision_conversation)" | |
| ], | |
| value="Roadmap (roadmap_output)", | |
| label="Context Type", | |
| ) | |
| dev_user_id = gr.Textbox( | |
| label="Target User ID", | |
| placeholder="Enter User ID", | |
| lines=1, | |
| ) | |
| dev_content = gr.Textbox( | |
| label="JSON or Text Content", | |
| placeholder="Paste the raw JSON or text content here...", | |
| lines=8, | |
| ) | |
| dev_upsert_btn = gr.Button("Upsert to Pinecone", variant="secondary") | |
| dev_status = gr.Textbox( | |
| label="Upsert Status", | |
| interactive=False, | |
| lines=2, | |
| ) | |
| gr.Markdown("---") | |
| gen_btn = gr.Button( | |
| "🚀 Generate Self-Vision", | |
| variant="primary", | |
| elem_classes=["gen-btn"], | |
| ) | |
| status_box = gr.Textbox( | |
| label="📋 Status", | |
| lines=4, | |
| interactive=False, | |
| elem_classes=["status-box"], | |
| ) | |
| output_json_file = gr.File( | |
| label="📥 Download Output JSON", | |
| visible=True | |
| ) | |
| # ── RIGHT COLUMN: Output images & Pinecone status ── | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 🖼️ Milestone Visualizations (9:16 portrait)") | |
| with gr.Row(): | |
| with gr.Column(scale=2, elem_classes=["milestone-card"]): | |
| m1_image = gr.Image(label="Milestone 1", type="pil", height=400) | |
| with gr.Column(scale=1, elem_classes=["milestone-detail"]): | |
| m1_detail = gr.Markdown(value="") | |
| with gr.Column(scale=2, elem_classes=["milestone-card"]): | |
| m2_image = gr.Image(label="Milestone 2", type="pil", height=400) | |
| with gr.Column(scale=1, elem_classes=["milestone-detail"]): | |
| m2_detail = gr.Markdown(value="") | |
| with gr.Row(): | |
| with gr.Column(scale=2, elem_classes=["milestone-card"]): | |
| m3_image = gr.Image(label="Milestone 3", type="pil", height=400) | |
| with gr.Column(scale=1, elem_classes=["milestone-detail"]): | |
| m3_detail = gr.Markdown(value="") | |
| with gr.Column(scale=2, elem_classes=["milestone-card"]): | |
| m4_image = gr.Image(label="Milestone 4", type="pil", height=400) | |
| with gr.Column(scale=1, elem_classes=["milestone-detail"]): | |
| m4_detail = gr.Markdown(value="") | |
| gr.Markdown("### 🧠 Pinecone Integration & Self-Vision Synthesis") | |
| with gr.Row(): | |
| pinecone_status = gr.Textbox( | |
| label="📡 Pinecone & LLM Pipeline Status", | |
| lines=6, | |
| interactive=False, | |
| elem_classes=["status-box"], | |
| ) | |
| with gr.Row(): | |
| self_vision_result_box = gr.JSON( | |
| label="🔮 Self Vision LLM Result", | |
| value={ | |
| "status": "Awaiting generation...", | |
| "message": "Enter your User ID and Raw Transcription on the left column, then click 'Generate Self-Vision' to run the pipeline." | |
| } | |
| ) | |
| # ── Image Gen Event handlers ── | |
| # Persona selector auto-fills form fields | |
| persona_selector.change( | |
| fn=on_persona_change, | |
| inputs=[persona_selector], | |
| outputs=[user_goal, domain, icp, gender_input, milestone_details_md], | |
| ).then( | |
| fn=get_milestone_defaults, | |
| inputs=[icp, domain, persona_selector], | |
| outputs=[ | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| ], | |
| ) | |
| # Gender selector dynamically updates pronouns in the user goal | |
| gender_input.change( | |
| fn=on_gender_change, | |
| inputs=[user_goal, gender_input], | |
| outputs=[user_goal], | |
| ) | |
| # Auto-fetch from Pinecone when user_id is changed/submitted/unfocused | |
| user_id_input.blur( | |
| fn=on_user_id_change, | |
| inputs=[user_id_input], | |
| outputs=[ | |
| raw_transcription_input, | |
| user_name, | |
| user_goal, | |
| domain, | |
| icp, | |
| gender_input, | |
| status_box, | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| milestone_details_md, | |
| ], | |
| ) | |
| user_id_input.submit( | |
| fn=on_user_id_change, | |
| inputs=[user_id_input], | |
| outputs=[ | |
| raw_transcription_input, | |
| user_name, | |
| user_goal, | |
| domain, | |
| icp, | |
| gender_input, | |
| status_box, | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| milestone_details_md, | |
| ], | |
| ) | |
| # Auto-resolve User ID when Roadmap ID is entered | |
| roadmap_id_outputs = [ | |
| user_id_input, | |
| raw_transcription_input, | |
| user_name, | |
| user_goal, | |
| domain, | |
| icp, | |
| gender_input, | |
| status_box, | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| milestone_details_md, | |
| ] | |
| roadmap_id_input.blur( | |
| fn=on_roadmap_id_change, | |
| inputs=[roadmap_id_input], | |
| outputs=roadmap_id_outputs, | |
| ) | |
| roadmap_id_input.submit( | |
| fn=on_roadmap_id_change, | |
| inputs=[roadmap_id_input], | |
| outputs=roadmap_id_outputs, | |
| ) | |
| # ICP or Domain change updates milestone defaults | |
| gr.on( | |
| triggers=[icp.change, domain.change], | |
| fn=get_milestone_defaults, | |
| inputs=[icp, domain, persona_selector], | |
| outputs=[ | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| ], | |
| ) | |
| # Generate button | |
| gen_btn.click( | |
| fn=run_generation, | |
| inputs=[ | |
| user_id_input, raw_transcription_input, | |
| user_goal, domain, icp, gender_input, reference_photo, | |
| m1_visible, m1_id, m1_desc, m1_val, | |
| m2_visible, m2_id, m2_desc, m2_val, | |
| m3_visible, m3_id, m3_desc, m3_val, | |
| m4_visible, m4_id, m4_desc, m4_val, | |
| ], | |
| outputs=[ | |
| m1_image, m2_image, m3_image, m4_image, | |
| m1_detail, m2_detail, m3_detail, m4_detail, | |
| gen_btn, status_box, | |
| output_json_file, | |
| pinecone_status, | |
| self_vision_result_box, | |
| ], | |
| ) | |
| # Developer utilities callbacks | |
| dev_upsert_btn.click( | |
| fn=on_dev_upsert, | |
| inputs=[dev_key, dev_user_id, dev_content], | |
| outputs=[dev_status], | |
| ) | |
| user_id_input.change( | |
| fn=lambda uid: uid, | |
| inputs=[user_id_input], | |
| outputs=[dev_user_id], | |
| ) | |
| demo.load( | |
| fn=load_data_from_url, | |
| inputs=None, | |
| outputs=[ | |
| roadmap_id_input, | |
| user_id_input, | |
| raw_transcription_input, | |
| user_name, | |
| user_goal, | |
| domain, | |
| icp, | |
| gender_input, | |
| status_box, | |
| m1_group, m1_id, m1_desc, m1_val, | |
| m2_group, m2_id, m2_desc, m2_val, | |
| m3_group, m3_id, m3_desc, m3_val, | |
| m4_group, m4_id, m4_desc, m4_val, | |
| milestone_details_md, | |
| ] | |
| ) | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Startup — HF Spaces compatible + local FastAPI REST endpoint | |
| # ───────────────────────────────────────────────────────────────────── | |
| # HF Spaces (sdk: gradio) discovers the `demo` variable at module level | |
| # and calls demo.launch() automatically. The REST API endpoint is only | |
| # available when running locally via `python app.py`. | |
| # ───────────────────────────────────────────────────────────────────── | |
| demo.queue() | |
| if __name__ == "__main__": | |
| import os as _os | |
| # Detect Hugging Face Spaces environment | |
| if _os.getenv("SPACE_ID"): | |
| # Running on HF Spaces — use Gradio's built-in launcher | |
| demo.launch(server_name="0.0.0.0", server_port=7860) | |
| else: | |
| # Running locally — mount Gradio inside FastAPI for REST endpoint | |
| import uvicorn | |
| import fastapi | |
| _app = fastapi.FastAPI() | |
| async def api_self_vision(request: fastapi.Request): | |
| """POST /api/self_vision — programmatic Self Vision pipeline.""" | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| return fastapi.responses.JSONResponse( | |
| status_code=400, | |
| content={"error": "invalid JSON body"}, | |
| ) | |
| err = validate_request(body) | |
| if err: | |
| return fastapi.responses.JSONResponse( | |
| status_code=err["status_code"], | |
| content={"error": err["error"]}, | |
| ) | |
| result = run_self_vision( | |
| user_id=body["user_id"].strip(), | |
| raw_transcription=body["raw_transcription"].strip(), | |
| lesson_context=body.get("lesson_context", ""), | |
| session_metadata=body.get("session_metadata"), | |
| ) | |
| if "error" in result and "self_vision_result" not in result: | |
| status_code = result.get("status_code", 500) | |
| return fastapi.responses.JSONResponse( | |
| status_code=status_code, | |
| content={"error": result["error"]}, | |
| ) | |
| return fastapi.responses.JSONResponse( | |
| status_code=200, | |
| content=result, | |
| ) | |
| async def api_upsert_context(request: fastapi.Request): | |
| """POST /api/upsert_context — programmatic upsert of context to Pinecone.""" | |
| try: | |
| body = await request.json() | |
| except Exception: | |
| return fastapi.responses.JSONResponse( | |
| status_code=400, | |
| content={"error": "invalid JSON body"}, | |
| ) | |
| user_id = body.get("user_id", "").strip() | |
| key = body.get("key", "").strip() | |
| text = body.get("text", "").strip() | |
| if not user_id or not key or not text: | |
| return fastapi.responses.JSONResponse( | |
| status_code=400, | |
| content={"error": "user_id, key, and text are required fields"}, | |
| ) | |
| from pinecone_client import KEY_ONBOARDING, KEY_ROADMAP, KEY_SELF_VISION | |
| key_map = { | |
| "onboarding": KEY_ONBOARDING, | |
| "roadmap": KEY_ROADMAP, | |
| "self_vision": KEY_SELF_VISION, | |
| KEY_ONBOARDING: KEY_ONBOARDING, | |
| KEY_ROADMAP: KEY_ROADMAP, | |
| KEY_SELF_VISION: KEY_SELF_VISION | |
| } | |
| resolved_key = key_map.get(key.lower()) | |
| if not resolved_key: | |
| return fastapi.responses.JSONResponse( | |
| status_code=400, | |
| content={"error": f"invalid key '{key}'. Must be 'onboarding', 'roadmap', or 'self_vision'"}, | |
| ) | |
| # Try to validate JSON if it's roadmap or onboarding | |
| if resolved_key in (KEY_ROADMAP, KEY_ONBOARDING): | |
| try: | |
| json.loads(text) | |
| except Exception as e: | |
| return fastapi.responses.JSONResponse( | |
| status_code=400, | |
| content={"error": f"text is not valid JSON ({e})"}, | |
| ) | |
| try: | |
| from pinecone_client import PineconeClient | |
| pc = PineconeClient() | |
| res = pc.upsert_conversation(user_id, resolved_key, text) | |
| if res.get("status") == "success": | |
| return fastapi.responses.JSONResponse( | |
| status_code=200, | |
| content={ | |
| "status": "success", | |
| "message": f"Successfully upserted context for key '{resolved_key}' under user '{user_id}'", | |
| "elapsed_ms": res.get("elapsed_ms") | |
| }, | |
| ) | |
| else: | |
| return fastapi.responses.JSONResponse( | |
| status_code=500, | |
| content={"error": "Failed to upsert context to Pinecone"}, | |
| ) | |
| except Exception as e: | |
| return fastapi.responses.JSONResponse( | |
| status_code=500, | |
| content={"error": str(e)}, | |
| ) | |
| _app = gr.mount_gradio_app(_app, demo, path="/") | |
| uvicorn.run(_app, host="0.0.0.0", port=7860) | |