SS-2005's picture
Upload 17 files
8490649 verified
Raw
History Blame Contribute Delete
17.6 kB
import os
import json
import logging
import boto3
import openai
from typing import Optional
from src.state import VideoGenerationState
from src.nodes.slide_creation.Slide_Creation_Node_refactor import run
from src.nodes.system_design.system_design_pipeline import SystemDesignPipelineNode
from tracker_utils_tl.tracker import update_session, save_session, save_course
# Initialize boto3 client
s3_client = boto3.client("s3")
BUCKET_NAME = "tech-learn-state"
STATE_FILE_PREFIX = "video_states/"
# LLM CONFIGURATION
openai.api_key = os.environ.get("OPENAI_API_KEY")
LLM_CONFIDENCE_THRESHOLD = 0.6
# Logger Setup
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# LLM TOPIC ANALYZER
def analyze_topic_with_llm(topic: str) -> Optional[str]:
"""
LLM analyzes ONLY the topic to decide routing.
Returns: SYSTEM_DESIGN | SLIDE_CREATION | None
NOTE: This should ONLY be called as a last resort fallback.
Metadata from UI should always take priority.
"""
if not openai.api_key:
logger.warning("OPENAI_API_KEY not set, skipping LLM analysis")
return None
prompt = f"""
You are an AI routing agent.
Classify the topic into one of the following:
- SYSTEM_DESIGN → architecture, scalability, components, distributed systems,
"Design X", load balancer, database, cache, microservices
- SLIDE_CREATION → programming tutorials, syntax, concepts, algorithms
Respond ONLY in valid JSON.
Topic:
{topic}
Output:
{{
"intent": "SYSTEM_DESIGN" | "SLIDE_CREATION",
"confidence": 0.0 to 1.0
}}
"""
try:
# Use new OpenAI SDK v1.0+ format
from openai import OpenAI
client = OpenAI() # Uses OPENAI_API_KEY from environment
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
result = json.loads(response.choices[0].message.content)
intent = result.get("intent")
confidence = float(result.get("confidence", 0))
logger.info(f"LLM decision: {intent} (confidence={confidence})")
if confidence >= LLM_CONFIDENCE_THRESHOLD:
return intent
except Exception as e:
logger.warning(f"LLM topic analysis failed: {e}")
return None
def detect_design_level_with_ai(state: VideoGenerationState) -> Optional[str]:
"""
Use AI to determine if topic is HLD (High-Level Design) or LLD (Low-Level Design).
Returns: "HLD" | "LLD" (defaults to "HLD" on failure)
"""
if not openai.api_key:
logger.warning("OPENAI_API_KEY not set, defaulting to HLD")
return "HLD"
topic = state.topic or ""
title = ""
if state.optional_params:
title = state.optional_params.get("chapter_title", "")
prompt = f"""
You are an expert system design educator. Analyze the following topic and classify it as EITHER:
- **HLD (High-Level Design)**: System architecture, scalability, distributed systems, APIs, data flow, infrastructure
- **LLD (Low-Level Design)**: Class design, code implementation, design patterns, algorithms, SOLID principles, data structures
IMPORTANT: You MUST choose ONLY ONE - either "HLD" or "LLD". Do NOT return "BOTH".
TOPIC: {topic}
TITLE: {title}
CLASSIFICATION RULES:
1. If topic mentions "Design [System]" (e.g., "Design Twitter", "Design Uber") → HLD
2. If topic mentions a design pattern name (e.g., "Singleton", "Factory", "Observer") → LLD
3. If topic mentions algorithms or data structures (e.g., "LRU Cache", "Trie") → LLD
4. If topic mentions system components or architecture → HLD
5. If topic mentions class diagrams, OOP, or code structure → LLD
6. When in doubt, prefer HLD
EXAMPLES:
- "Design Twitter" → HLD (system architecture)
- "Factory Pattern" → LLD (design pattern implementation)
- "Design URL Shortener" → HLD (distributed system)
- "Singleton Pattern" → LLD (code implementation)
- "Design Uber" → HLD (distributed system)
- "Observer Pattern" → LLD (design pattern)
- "Design Instagram" → HLD (scalable architecture)
- "LRU Cache Implementation" → LLD (algorithm + data structure)
- "Adapter Pattern" → LLD (design pattern)
- "Strategy Pattern" → LLD (design pattern)
- "Load Balancing" → HLD (system component)
- "Database Sharding" → HLD (architecture)
Respond ONLY in valid JSON (choose ONLY "HLD" or "LLD"):
{{
"design_level": "HLD" | "LLD",
"confidence": 0.0 to 1.0,
"reasoning": "Brief explanation"
}}
"""
try:
from openai import OpenAI
client = OpenAI() # Uses OPENAI_API_KEY from environment
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are a system design expert. Output ONLY valid JSON without markdown. You MUST choose either HLD or LLD, never BOTH."
},
{"role": "user", "content": prompt}
],
temperature=0.2,
)
result = json.loads(response.choices[0].message.content)
design_level = result.get("design_level", "HLD")
confidence = float(result.get("confidence", 0))
reasoning = result.get("reasoning", "")
# Validate that it's only HLD or LLD
if design_level not in ["HLD", "LLD"]:
logger.warning(f"AI returned invalid design_level '{design_level}', defaulting to HLD")
design_level = "HLD"
logger.info(f"🎯 AI Design Level Detection: {design_level} (confidence={confidence:.2f})")
logger.info(f" Reasoning: {reasoning}")
# Only use AI result if confidence is high
if confidence >= 0.7:
return design_level
else:
logger.warning(f"Low confidence ({confidence:.2f}), defaulting to HLD")
return "HLD"
except Exception as e:
logger.error(f"AI design level detection failed: {e}")
return "HLD"
# AGENT ROUTER
def select_pipeline_node(state: VideoGenerationState) -> str:
logger.info(f"DEBUG: Entering select_pipeline_node. state.topic: {state.topic}")
"""
Agent routing logic with METADATA-FIRST approach.
STRONG SYSTEM DESIGN CHECK:
Routes to SYSTEM_DESIGN pipeline if ANY of these conditions are met:
- programming_language in ["system_design", "system design"]
- optional_params.course_type == "system_design"
- topic_analysis.type == "system_design"
- course_name contains "system design"
This ensures UI metadata is ALWAYS trusted over LLM classification.
"""
script_type = (state.optional_params or {}).get("script_type", "")
if script_type == "consultation":
logger.info("Routing to CONSULTATION pipeline")
return "CONSULTATION"
# Normalize and extract metadata
course_name = (state.course_name or "Unknown Course").strip().lower()
language = (state.programming_language or "").strip().lower()
optional_params = state.optional_params or {}
logger.info(f"🔍 Routing Analysis:")
logger.info(f" Course: '{course_name}'")
logger.info(f" Language: '{language}'")
logger.info(f" Optional Params: {optional_params}")
# ========== STRONG SYSTEM DESIGN CHECK ==========
# Extract all system design indicators
topic_analysis = optional_params.get("topic_analysis", {})
topic_analysis_type = ""
if isinstance(topic_analysis, dict):
topic_analysis_type = (topic_analysis.get("type") or "").strip().lower().replace("_", " ")
course_type = (optional_params.get("course_type") or "").strip().lower().replace("_", " ")
language_normalized = language.replace("_", " ")
# CONSOLIDATED CHECK: If ANY system design indicator is present -> SYSTEM_DESIGN
if (
language in ["system_design", "system design"]
or language_normalized == "system design"
or course_type == "system design"
or topic_analysis_type == "system design"
or "system design" in course_name
):
# Determine which condition matched
matched_conditions = []
if language in ["system_design", "system design"]:
matched_conditions.append(f"programming_language='{language}'")
if course_type == "system design":
matched_conditions.append(f"course_type='{course_type}'")
if topic_analysis_type == "system design":
matched_conditions.append(f"topic_analysis.type='{topic_analysis_type}'")
if "system design" in course_name:
matched_conditions.append(f"course_name contains 'system design'")
logger.info(f"✅ SYSTEM DESIGN DETECTED: {', '.join(matched_conditions)}")
# Detect design level (HLD vs LLD)
design_level = detect_design_level_with_ai(state)
if not state.optional_params:
state.optional_params = {}
state.optional_params["design_level"] = design_level
logger.info(f" Design Level: {design_level}")
logger.info(f" Routing to: SYSTEM_DESIGN")
return "SYSTEM_DESIGN"
# ========== CODING COURSE CHECKS ==========
if "dsa" in course_name or "language" in course_name:
logger.info("✅ Routing Rule: course_name implies coding -> SLIDE_CREATION")
return "SLIDE_CREATION"
# Check for explicit coding languages
coding_languages = {
"python", "java", "javascript", "typescript", "cpp", "c++", "c",
"c#", "csharp", "go", "golang", "ruby", "rust", "swift", "kotlin", "php"
}
if language in coding_languages:
logger.info(f"✅ Routing Rule: Known coding language '{language}' -> SLIDE_CREATION")
return "SLIDE_CREATION"
# ========== LLM FALLBACK (LAST RESORT) ==========
logger.warning("⚠️ No metadata match found. Falling back to LLM classification...")
llm_intent = analyze_topic_with_llm(state.topic)
if llm_intent:
logger.info(f"🤖 LLM routing decision: {llm_intent}")
return llm_intent
# ========== FINAL FALLBACK ==========
logger.warning("⚠️ All routing methods failed. Defaulting to SLIDE_CREATION")
return "SLIDE_CREATION"
def main():
logger.info("Starting Video Generation Agent")
# 1. Load input from environment variable
input_payload = os.environ.get("INPUT_PAYLOAD")
if not input_payload:
logger.error("Missing INPUT_PAYLOAD")
exit(1)
try:
event = json.loads(input_payload)
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in INPUT_PAYLOAD: {e}")
exit(1)
# 2. Extract session_id
session_id = extract_session_id(event)
if not session_id:
logger.error("session_id not found")
exit(1)
# 3. Load state (from event body first, then S3 fallback)
session_id = extract_session_id(event)
if not session_id:
logger.error("session_id not found in input.")
exit(1)
# 4. Load state from event body if available
state_data = None
if "body" in event:
try:
body_data = json.loads(event["body"]) if isinstance(event["body"], str) else event["body"]
if "topic" in body_data and "session_id" in body_data:
state_data = body_data
logger.info("Loaded state data from event body")
except Exception:
pass
# 5. Build S3 key and fallback to S3 if body state is missing
state_s3_key = f"{STATE_FILE_PREFIX}{session_id}/{session_id}.json"
if not state_data:
logger.info(f"Loading state from S3: {state_s3_key}")
try:
state_data = load_state_from_s3(state_s3_key)
except Exception as e:
logger.error(f"Failed to load state from S3: {e}")
exit(1)
# 6. Extract optional course and topic metadata
optional_params = state_data.get("optional_params") or {}
course_id = optional_params.get("course_id", 0)
topic_id = optional_params.get("topic_id", 0)
chapter_id = optional_params.get("chapter_id", 0)
topic_title = optional_params.get("chapter_title", "")
course_name = optional_params.get("course_name", f"Course {course_id}")
# Ensure course_name always has a valid string value (never null/empty)
if not course_name or course_name.strip() == "":
course_name = f"Course {course_id}"
# Inject course_name into state data if missing
if "course_name" not in state_data or not state_data.get("course_name"):
state_data["course_name"] = course_name
# 7. Persist course information for tracking and analytics
try:
save_course(
course_detail={
"course_id": int(course_id),
"topic_id": int(topic_id),
"chapter_id": int(chapter_id),
"total_videos": int(optional_params.get("total_videos", 0)),
},
course_name=course_name,
)
except Exception as e:
logger.warning(f"Course save skipped: {e}")
# 8. Create strongly-typed state object
state = VideoGenerationState(**state_data)
logger.info(f"Topic: {state.topic}")
logger.info(f"Programming Language: {state.programming_language}")
# 9. Let agent decide which pipeline node to run
selected_node = select_pipeline_node(state)
logger.info(f"Agent selected node: {selected_node}")
# 10. Execute the selected pipeline node
try:
if selected_node == "SYSTEM_DESIGN":
node_name = "SystemDesign"
# Mark system design node as started
update_session(session_id, node=node_name, status="STARTED")
# Run system design pipeline
node = SystemDesignPipelineNode()
updated_state = node.generate_system_design_video(state)
elif selected_node == "SLIDE_CREATION":
node_name = "SlideCreation"
# Mark slide creation node as started
update_session(session_id, node=node_name, status="STARTED")
# Run slide creation pipeline
updated_state = run(state)
elif selected_node == "CONSULTATION":
node_name = "Consultation"
update_session(session_id, node=node_name, status="STARTED")
from src.nodes.consultation.consultation_pipeline import ConsultationPipelineNode
node = ConsultationPipelineNode()
updated_state = node.generate_consultation_video(state)
else:
raise ValueError(f"Unknown node selected: {selected_node}")
# 11. Save updated execution state back to S3
save_state_to_s3(state_s3_key, updated_state.model_dump())
# 12. Mark pipeline execution as completed
update_session(session_id, node=node_name, status="COMPLETED")
save_session(
session_id=session_id,
course_id=course_id,
topic_id=topic_id,
topic_title=topic_title,
node=node_name,
status="COMPLETED",
)
# 13. Exit successfully if terminal status is reached
if updated_state.status in [
"slide_video_generated",
"system_design_video_generated",
]:
logger.info(f"{node_name} pipeline completed successfully")
exit(0)
# 14. Exit with failure if unexpected status is returned
logger.warning(f"{node_name} finished with status: {updated_state.status}")
exit(1)
except Exception as e:
# 15. Handle pipeline failure and update tracker
logger.error(f"Pipeline failed: {e}")
try:
update_session(session_id, node=node_name, status="FAILED")
save_session(
session_id=session_id,
course_id=course_id,
topic_id=topic_id,
topic_title=topic_title,
node=node_name,
status="FAILED",
)
except Exception:
pass
exit(1)
def extract_session_id(event):
if "session_id" in event:
return event["session_id"]
if "headers" in event and "session_id" in event["headers"]:
return event["headers"]["session_id"]
if "body" in event:
try:
body = json.loads(event["body"]) if isinstance(event["body"], str) else event["body"]
if "session_id" in body:
return body["session_id"]
except Exception:
pass
return None
def load_state_from_s3(key: str):
obj = s3_client.get_object(Bucket=BUCKET_NAME, Key=key)
return json.loads(obj["Body"].read().decode("utf-8"))
def save_state_to_s3(key: str, data: dict):
s3_client.put_object(
Bucket=BUCKET_NAME,
Key=key,
Body=json.dumps(data, indent=2),
ContentType="application/json",
)
if __name__ == "__main__":
main()