Spaces:
Sleeping
Sleeping
| """ | |
| Self Vision API β core orchestration module. | |
| Implements the 7-step execution flow: | |
| 1. Validate request | |
| 2. Fetch onboarding context from Pinecone | |
| 3. Normalize context | |
| 4. Build LLM input (with truncation) | |
| 5. Generate Self Vision via Gemini | |
| 6. Persist raw transcript to Pinecone | |
| 7. Return response with observability metadata | |
| """ | |
| import os | |
| import time | |
| import logging | |
| from google import genai | |
| from dotenv import load_dotenv | |
| from pinecone_client import PineconeClient, KEY_ONBOARDING, KEY_SELF_VISION, KEY_ROADMAP | |
| from context_processor import build_llm_input, count_words | |
| load_dotenv() | |
| logger = logging.getLogger("self_vision.api") | |
| # ββ Singleton Pinecone client ββ | |
| _pinecone: PineconeClient | None = None | |
| def _get_pinecone() -> PineconeClient: | |
| global _pinecone | |
| if _pinecone is None: | |
| _pinecone = PineconeClient() | |
| return _pinecone | |
| # ββ Singleton Gemini client ββ | |
| _gemini_client = None | |
| def _get_gemini(): | |
| global _gemini_client | |
| if _gemini_client is None: | |
| api_key = os.getenv("GEMINI_API_KEY", "") | |
| if not api_key: | |
| raise RuntimeError("GEMINI_API_KEY not set") | |
| _gemini_client = genai.Client(api_key=api_key) | |
| return _gemini_client | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STEP 1 β Validation | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def validate_request(data: dict) -> dict | None: | |
| """ | |
| Validate the incoming request payload. | |
| Returns an error dict if invalid, or None if valid. | |
| """ | |
| if not data: | |
| return {"error": "request body required", "status_code": 400} | |
| user_id = data.get("user_id") | |
| if user_id is None or (isinstance(user_id, str) and not user_id.strip()): | |
| return {"error": "user_id required", "status_code": 400} | |
| raw = data.get("raw_transcription") | |
| if raw is None: | |
| return {"error": "raw_transcription required", "status_code": 400} | |
| if isinstance(raw, str) and not raw.strip(): | |
| return {"error": "empty transcript", "status_code": 400} | |
| return None | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # STEP 5 β LLM Generation | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SELF_VISION_SYSTEM_PROMPT = """\ | |
| You are a career Self-Vision assistant. | |
| You receive three blocks of context: | |
| 1. **[ONBOARDING_CONTEXT]** β the user's onboarding conversation from a previous POC session. This contains their background, aspirations, and initial career information. | |
| 2. **[ROADMAP_CONTEXT]** β the user's generated career roadmap (including target role, milestones, vision profile, etc.). | |
| 3. **[SELF_VISION_TRANSCRIPT]** β the current Self-Vision session transcript capturing the user's reflections, goals, and career vision. | |
| Optionally you also receive: | |
| 4. **[LESSON_CONTEXT]** β additional lesson or learning context. | |
| Your job: | |
| - Synthesize the onboarding context with the current self-vision transcript. | |
| - Produce a rich, actionable **Self Vision** output that reflects the user's complete journey so far. | |
| - Maintain continuity: reference earlier context naturally. | |
| - Be specific, encouraging, and grounded in what the user has shared. | |
| - Output structured JSON with the following shape: | |
| { | |
| "vision_summary": "A 2-3 paragraph narrative synthesis of the user's self-vision", | |
| "key_themes": ["theme1", "theme2", ...], | |
| "strengths_identified": ["strength1", "strength2", ...], | |
| "growth_areas": ["area1", "area2", ...], | |
| "next_steps": ["step1", "step2", ...], | |
| "continuity_notes": "How this connects to their onboarding journey" | |
| } | |
| """ | |
| def _generate_self_vision(combined_text: str) -> dict: | |
| """ | |
| Call Gemini to generate the Self Vision result. | |
| Returns the parsed JSON result or a fallback dict on failure. | |
| """ | |
| client = _get_gemini() | |
| try: | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[ | |
| {"role": "user", "parts": [{"text": f"{SELF_VISION_SYSTEM_PROMPT}\n\n---\n\n{combined_text}"}]} | |
| ], | |
| ) | |
| # Extract text from response | |
| result_text = "" | |
| if response.text: | |
| result_text = response.text | |
| # Try to parse as JSON | |
| import json | |
| import re | |
| # Strip markdown code fences if present | |
| cleaned = result_text.strip() | |
| cleaned = re.sub(r'^```(?:json)?\s*', '', cleaned) | |
| cleaned = re.sub(r'\s*```$', '', cleaned) | |
| try: | |
| return json.loads(cleaned) | |
| except (json.JSONDecodeError, ValueError): | |
| # Return as unstructured text if not valid JSON | |
| return { | |
| "vision_summary": result_text, | |
| "key_themes": [], | |
| "strengths_identified": [], | |
| "growth_areas": [], | |
| "next_steps": [], | |
| "continuity_notes": "", | |
| } | |
| except Exception as exc: | |
| logger.error("LLM generation failed: %s", exc) | |
| raise RuntimeError(f"self vision generation failed: {exc}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main orchestration | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_self_vision( | |
| user_id: str, | |
| raw_transcription: str, | |
| lesson_context: str = "", | |
| session_metadata: dict | None = None, | |
| ) -> dict: | |
| """ | |
| Execute the full Self Vision pipeline. | |
| Returns a dict with: | |
| - self_vision_result: the LLM-generated output | |
| - raw_transcription: the original transcript (echoed back) | |
| - metadata: observability data | |
| """ | |
| total_start = time.time() | |
| # ββ STEP 1 β Validate (already done by caller, but double-check) ββ | |
| validation_err = validate_request({ | |
| "user_id": user_id, | |
| "raw_transcription": raw_transcription, | |
| }) | |
| if validation_err: | |
| return {"error": validation_err["error"], "status_code": validation_err["status_code"]} | |
| pc = _get_pinecone() | |
| # ββ STEP 2 β Fetch prior context (Onboarding and Roadmap) ββ | |
| logger.info("Fetching onboarding context for user_id=%s", user_id) | |
| fetch_result = pc.fetch_conversation(user_id, KEY_ONBOARDING) | |
| onboarding_text = fetch_result["text"] | |
| fetch_status = fetch_result["status"] | |
| logger.info("Fetching roadmap context for user_id=%s", user_id) | |
| fetch_roadmap_result = pc.fetch_conversation(user_id, KEY_ROADMAP) | |
| roadmap_text = fetch_roadmap_result["text"] | |
| fetch_roadmap_status = fetch_roadmap_result["status"] | |
| # ββ STEP 3 + 4 + 5 β Normalize, build input, apply truncation ββ | |
| llm_input = build_llm_input( | |
| onboarding_text=onboarding_text, | |
| transcript_text=raw_transcription, | |
| roadmap_text=roadmap_text, | |
| lesson_context=lesson_context, | |
| ) | |
| context_word_count = llm_input["total_word_count"] | |
| truncation_applied = llm_input["truncation_applied"] | |
| logger.info( | |
| "LLM input built: words=%d truncated=%s", | |
| context_word_count, | |
| truncation_applied, | |
| ) | |
| # ββ STEP 6 β Generate Self Vision ββ | |
| try: | |
| self_vision_result = _generate_self_vision(llm_input["combined_text"]) | |
| except RuntimeError as exc: | |
| total_elapsed = (time.time() - total_start) * 1000 | |
| return { | |
| "error": "self vision generation failed", | |
| "status_code": 500, | |
| "metadata": { | |
| "user_id": user_id, | |
| "fetch_status": fetch_status, | |
| "fetch_roadmap_status": fetch_roadmap_status, | |
| "upsert_status": "skipped", | |
| "context_word_count": context_word_count, | |
| "truncation_applied": truncation_applied, | |
| "execution_time_ms": round(total_elapsed, 1), | |
| }, | |
| } | |
| # ββ STEP 7 β Persist raw transcript ββ | |
| logger.info("Upserting self_vision_conversation for user_id=%s", user_id) | |
| upsert_result = pc.upsert_conversation( | |
| user_id=user_id, | |
| key=KEY_SELF_VISION, | |
| text=raw_transcription, # Store RAW transcript, not LLM output | |
| ) | |
| upsert_status = upsert_result["status"] | |
| total_elapsed = (time.time() - total_start) * 1000 | |
| # ββ Build response ββ | |
| logger.info( | |
| "Self Vision complete: user_id=%s fetch=%s fetch_roadmap=%s upsert=%s time=%.0fms", | |
| user_id, fetch_status, fetch_roadmap_status, upsert_status, total_elapsed, | |
| ) | |
| return { | |
| "self_vision_result": self_vision_result, | |
| "raw_transcription": raw_transcription, | |
| "metadata": { | |
| "user_id": user_id, | |
| "fetch_status": fetch_status, | |
| "fetch_roadmap_status": fetch_roadmap_status, | |
| "upsert_status": upsert_status, | |
| "context_word_count": context_word_count, | |
| "truncation_applied": truncation_applied, | |
| "execution_time_ms": round(total_elapsed, 1), | |
| }, | |
| } | |