Self-Vision-Space / self_vision_api.py
banao-tech's picture
Upload 5 files
f62ed76 verified
Raw
History Blame Contribute Delete
9.86 kB
"""
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),
},
}