""" AI Mock Interviewer — FastAPI Backend Powered by Google Gemini + LangGraph Deploy on HuggingFace Spaces (port 7860) """ import base64 import json import os import uuid from io import BytesIO from typing import Annotated, Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union import pandas as pd from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from google import genai from google.genai import types from google.api_core import retry from jinja2 import Template from langchain_core.messages import BaseMessage from langchain_core.messages.ai import AIMessage from langchain_core.messages.human import HumanMessage from langchain_core.messages.system import SystemMessage from langchain_core.messages.tool import ToolMessage from langchain_core.runnables import Runnable from langchain_core.tools import BaseTool, tool from langchain_google_genai import ChatGoogleGenerativeAI from langgraph.graph import END, START, StateGraph from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode from pydantic import BaseModel, Field load_dotenv() def extract_text(content) -> str: if isinstance(content, str): return content if isinstance(content, list): parts = [p["text"] for p in content if isinstance(p, dict) and p.get("type") == "text"] return " ".join(parts) if parts else "" return str(content) app = FastAPI( title="AI Mock Interviewer API", description="Google Gemini + LangGraph powered technical interview simulator", version="1.0.0", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], # Restrict to your Vercel domain in production allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_PATH = os.path.join(BASE_DIR, "data.json") try: df = pd.read_json(DATA_PATH) print(f"Loaded {len(df)} questions from data.json") except Exception as e: print(f"ERROR loading data.json: {e}") df = pd.DataFrame() _client_cache: Dict[str, Any] = {} def get_api_key(user_key: str = "") -> str: key = user_key or os.environ.get("GOOGLE_API_KEY", "") if not key: raise ValueError("No Gemini API key provided. Please enter your API key.") return key def get_client(user_key: str = ""): key = get_api_key(user_key) if key not in _client_cache: _client_cache[key] = genai.Client(api_key=key) is_retriable = lambda e: isinstance(e, genai.errors.APIError) and e.code in {429, 503} if not hasattr(genai.models.Models.generate_content, "__wrapped__"): genai.models.Models.generate_content = retry.Retry(predicate=is_retriable)( genai.models.Models.generate_content ) return _client_cache[key] INTERVIEWER_SYSTEM_PROMPT = """ COMPANY NAME: "Mock Technologie Inc." You are a technical interviewer and an expert in software engineering, technical interviewing, and pedagogical best practices. Your primary goal is to evaluate a candidate's technical skills, problem-solving abilities, and relevant experience. You should keep the candidate actively engaged and progressing through the given problem. You will provide hints, guidance, and ask probing questions to facilitate the candidate's problem-solving process. You are designed to be supportive, encouraging, and focused on helping the candidate demonstrate their abilities. You should ask user to choose question for the technical interview. User can choose specific question or a random one. You CANNOT start the interview if you have not received an interview question. You should take questions ONLY from the question database using tools. Do NOT make up questions! You should ask the candidate to confirm the selected question before starting. You should ask the candidate to confirm if they want to end the interview. Only ask probing questions or give hints if the candidate is struggling. **I. Core Principles:** - Facilitating Problem-Solving: Guide the candidate, don't solve it for them. - Encouraging Communication: Prompt the candidate to explain their thought process. - Providing Strategic Hints: Offer hints in a graduated manner (Level 1 → Level 2 → Level 3). - Positive and Supportive Tone: Create a comfortable environment. **II. Interview Execution:** - Ask clarifying questions before coding begins. - Prompt the candidate to "think out loud." - Hint levels: Level 1 (general), Level 2 (specific), Level 3 (code snippet - sparingly). - If completely stuck, redirect to a simpler sub-problem. **III. Whiteboard Input:** If a whiteboard image description is provided, seamlessly integrate your understanding into your response. Don't create a separate "Whiteboard Analysis" section — weave it naturally into the dialogue. If the whiteboard content is unrelated to the problem, say so clearly. **IV. Output Format:** Respond conversationally. Include probing questions, strategic hints, or guiding suggestions as needed. **Example Interactions:** Example 1 - Candidate slightly stuck: Candidate: "I'm trying to find pairs efficiently. Maybe sort the array first?" Interviewer: "Sorting is interesting. What's the time complexity, and how would you use the sorted array to find the pair?" Example 2 - Small logic error in code: Candidate: (shares hash map code with a bug) Interviewer: "Let's trace it with nums=[3,2,4] and target=6. What happens when i=0 and nums[i]=3? What's checked and what's added?" """ WELCOME_MSG = """Hello! I'm a technical interviewer for Mock Technologie Inc. I'm here to help you demonstrate your software engineering skills. To start, please choose a question for the technical interview. You can either: - Pick a **specific topic and difficulty** (e.g. "Give me a medium array problem") - Ask for a **random question** - Ask me to **list available topics** What would you prefer?""" CANDIDATE_EVALUATION_PROMPT = """ Your Role: You are an experienced Technical Hiring Manager. Evaluate the candidate based solely on the provided interview transcript. Evaluation Criteria: 1. Technical Competence: Problem understanding, algorithm design, coding logic, edge cases, debugging 2. Problem-Solving & Critical Thinking: Systematic approach, adaptability, optimization awareness 3. Communication & Collaboration: Clarity, active listening, asking questions, receiving feedback Required Output (JSON): - overallSummary: 2-3 sentence overview + high-level recommendation - strengths: List of 5 strengths with evidence from transcript - areasForDevelopment: List of 5+ weaknesses with evidence - detailedAnalysis: technicalCompetence, problemSolvingCriticalThinking, communicationCollaboration (5+ sentences each) - finalRecommendation: recommendation (Strong Hire/Hire/Lean Hire/No Hire/Needs Further Discussion) + justification - topicsToLearn: List of areas with descriptions targeting weaknesses Guidelines: - Base evaluation strictly on the transcript. Be objective. Cite specific examples. - Avoid vague evidence like "nums[i]" — use meaningful excerpts. {question} {transcript} {code} """ RESOURCES_SEARCH_PROMPT = """ You are an expert learning advisor providing recommendations based on a technical interview evaluation. Interview Context: - Question Asked: {question} - Language Used: {language} - Expert Evaluation Summary: {analytics} - Key Topics for Learning: {topics} Your Task: Generate a concise, actionable learning plan using search results. - Start DIRECTLY with recommendations. No preamble like "Okay, based on..." - Do NOT list URLs in your text — citations are added automatically. - Use search tool to find current relevant resources. - Structure clearly with bullet points. """ DESCRIBE_IMAGE_PROMPT = """ Given the transcript of a technical interview, analyze the provided whiteboard image. Describe its content and relevance to the ongoing discussion or code. Be concise — only provide what's necessary. {transcript} """ REPORT_TEMPLATE = """ --- {{ evaluation.overall_summary }} --- **Recommendation:** {{ evaluation.final_recommendation.recommendation }} **Justification:** {{ evaluation.final_recommendation.justification }} --- {% if evaluation.strengths %} {% for s in evaluation.strengths %} * **{{ s.point }}** * *Evidence:* {{ s.evidence }} {% endfor %} {% else %} * No specific strengths noted. {% endif %} --- {% if evaluation.areas_for_development %} {% for a in evaluation.areas_for_development %} * **{{ a.point }}** * *Evidence:* {{ a.evidence }} {% endfor %} {% else %} * No specific areas noted. {% endif %} --- {{ evaluation.detailed_analysis.technical_competence }} {{ evaluation.detailed_analysis.problem_solving_critical_thinking }} {{ evaluation.detailed_analysis.communication_collaboration }} --- {{ recommendations | default("No specific learning recommendations were generated.") }} --- """ class StrengthItem(BaseModel): point: str = Field(..., description="Concise strength statement") evidence: str = Field(..., description="Evidence from transcript") class AreaForDevelopmentItem(BaseModel): point: str = Field(..., description="Concise weakness statement") evidence: str = Field(..., description="Evidence from transcript") class DetailedAnalysis(BaseModel): technical_competence: str = Field(..., alias="technicalCompetence") problem_solving_critical_thinking: str = Field(..., alias="problemSolvingCriticalThinking") communication_collaboration: str = Field(..., alias="communicationCollaboration") class Config: validate_by_name = True class FinalRecommendation(BaseModel): recommendation: Literal["Strong Hire", "Hire", "Lean Hire", "No Hire", "Needs Further Discussion"] justification: str class TopicsToLearn(BaseModel): area: str description: str class EvaluationOutput(BaseModel): overall_summary: str = Field(..., alias="overallSummary") strengths: List[StrengthItem] areas_for_development: List[AreaForDevelopmentItem] = Field(..., alias="areasForDevelopment") detailed_analysis: DetailedAnalysis = Field(..., alias="detailedAnalysis") final_recommendation: FinalRecommendation = Field(..., alias="finalRecommendation") topics_to_learn: List[TopicsToLearn] = Field(..., alias="topicsToLearn") class Config: validate_by_name = True class InterviewState(TypedDict): messages: Annotated[list, add_messages] question: str code: str report: str finished: bool api_key: str DIFFICULTY = tuple(df.difficulty.unique().tolist()) if not df.empty else ("Easy", "Medium", "Hard") TOPICS = tuple(df.topic.unique().tolist()) if not df.empty else ("Array Manipulation",) IDS = df.id.apply(str).tolist() if not df.empty else [] class ListQuestionArgs(BaseModel): category: Literal[TOPICS] = Field(description="Topic category to filter by") difficulty: Literal[DIFFICULTY] = Field(description="Difficulty level: Easy, Medium, or Hard") class SelectQuestionArgs(BaseModel): ID: Literal[tuple(IDS)] = Field(description="Unique ID of the question to select") @tool(args_schema=SelectQuestionArgs) def select_question(ID: str) -> str: """Selects a question by ID and loads it for the interview. ALWAYS use this tool when the candidate confirms their question choice. The interview can ONLY begin after this tool is called. """ @tool(args_schema=ListQuestionArgs) def list_questions(category: str, difficulty: str) -> Union[str, List[str]]: """Lists available questions filtered by topic category and difficulty level. Returns up to 5 matching questions with their IDs and names. """ filtered = df[ (df["topic"].str.lower() == category.lower()) & (df["difficulty"].str.lower() == difficulty.lower()) ] if filtered.empty: return f"No questions found for topic='{category}' and difficulty='{difficulty}'. Try different filters." sample = filtered.sample(n=min(len(filtered), 5)) return [f"ID: {row.id} | {row.problem_name}" for _, row in sample[["id", "problem_name"]].iterrows()] @tool def get_random_problem() -> str: """Selects a random question from the database and returns its ID and name.""" try: row = df.sample(n=1).iloc[0] return f"ID: {row.id} | {row.problem_name} | Difficulty: {row.difficulty} | Topic: {row.topic}" except Exception as e: return f"Error selecting random problem: {e}" @tool def get_difficulty_levels() -> List[str]: """Returns all available difficulty levels in the question database.""" return df.difficulty.unique().tolist() @tool def get_topic_categories() -> List[str]: """Returns all available topic categories in the question database.""" return df.topic.unique().tolist() @tool def end_interview() -> bool: """Ends the interview session and triggers the evaluation report generation. Use this ONLY when the candidate confirms they want to end the interview. """ _llm_cache: Dict[str, Any] = {} def get_llm(user_key: str = ""): key = get_api_key(user_key) if key not in _llm_cache: llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=key) _llm_cache[key] = (llm, llm.bind_tools(auto_tools + interview_tools)) return _llm_cache[key] auto_tools: List[BaseTool] = [get_difficulty_levels, get_topic_categories, get_random_problem, list_questions] tool_node = ToolNode(auto_tools) interview_tools: List[BaseTool] = [select_question, end_interview] def get_interview_transcript(messages: List[BaseMessage], api_key: str = "") -> str: """Converts message history to a readable transcript string. Handles text, code, and whiteboard image descriptions. """ transcript = "" for message in messages: if isinstance(message, AIMessage) and message.content: content = extract_text(message.content) transcript += f"Interviewer: {content}\n\n" elif isinstance(message, HumanMessage): text = "" for part in message.content: text += part.get("text", "") + "\n" if image_data := part.get("image_url"): try: response = get_client(api_key).models.generate_content( model="gemini-2.5-flash", contents=[DESCRIBE_IMAGE_PROMPT.format(transcript=transcript), image_data.get("url")], ) text += f"[Whiteboard description: {response.text}]\n" except Exception as e: text += f"[Whiteboard image could not be described: {e}]\n" transcript += f"Candidate: {text}\n\n" return transcript def get_data_for_search(evaluation_response) -> Tuple[str, str]: """Extracts analytics text and topics list from evaluation for the learning plan.""" analytics = "" for theme, desc in evaluation_response.parsed.detailed_analysis: analytics += f"{theme}: {desc}\n\n" topics = "" for item in evaluation_response.parsed.topics_to_learn: topics += f"{item.area}: {item.description}\n\n" return analytics, topics def get_learning_resources(question: str, analytics: str, topics: str, api_key: str = "", language: str = "Python") -> str: """Uses Gemini with Google Search grounding to generate a personalized learning plan.""" config = types.GenerateContentConfig( tools=[types.Tool(google_search=types.GoogleSearch())] ) rc = None for attempt in range(5): try: response = get_client(api_key).models.generate_content( model="gemini-2.5-flash", contents=RESOURCES_SEARCH_PROMPT.format( question=question, analytics=analytics, topics=topics, language=language ), config=config, ) rc = response.candidates[0] if (rc.grounding_metadata and rc.grounding_metadata.grounding_supports and rc.grounding_metadata.grounding_chunks and rc.content.parts and rc.content.parts[0].text): break print(f"Grounding attempt {attempt + 1}: no metadata, retrying...") except Exception as e: print(f"Grounding attempt {attempt + 1} error: {e}") if not rc or not rc.grounding_metadata: fallback_text = "\n".join(p.text for p in rc.content.parts) if rc else "" return f"*Could not retrieve grounded recommendations.*\n\n{fallback_text}" parts = [] generated_text = "\n".join(p.text for p in rc.content.parts) last_idx = 0 for support in sorted(rc.grounding_metadata.grounding_supports, key=lambda s: s.segment.start_index): parts.append(generated_text[last_idx:support.segment.start_index]) parts.append(generated_text[support.segment.start_index:support.segment.end_index]) for i in sorted(set(support.grounding_chunk_indices)): parts.append(f"[{i+1}]") last_idx = support.segment.end_index parts.append(generated_text[last_idx:]) parts.append("\n\n### Sources\n\n") for i, chunk in enumerate(rc.grounding_metadata.grounding_chunks, start=1): title = chunk.web.title or "Reference" uri = chunk.web.uri or "#" parts.append(f"{i}. [{title}]({uri})\n") return "".join(parts) def chatbot_with_tools(state: InterviewState) -> InterviewState: """Main LLM node — generates the next interviewer response or tool call.""" messages = state["messages"] system_and_messages = [SystemMessage(content=INTERVIEWER_SYSTEM_PROMPT)] + messages if not messages: ai_message = AIMessage(content=WELCOME_MSG) else: api_key = state.get("api_key", "") print(f"DEBUG chatbot: api_key_present={bool(api_key)}, msg_count={len(messages)}") _, llm_with_tools = get_llm(api_key) ai_message = llm_with_tools.invoke(system_and_messages) return state | {"messages": [ai_message]} def question_selection_node(state: InterviewState) -> InterviewState: """Handles the select_question tool call — loads question content into state.""" tool_msg: AIMessage = state["messages"][-1] outbound_msgs = [] question_content = state.get("question", "") question_code = state.get("code", "") for tool_call in tool_msg.tool_calls: if tool_call["name"] == "select_question": ID = int(tool_call["args"]["ID"]) row = df[df.id == ID].iloc[0] question_content = row.content question_code = row.code response = ( f"Question loaded. Please present a summarized version of this problem to the candidate:\n\n" f"{question_content}\n\nStarter code:\n{question_code}" ) else: raise NotImplementedError(f"Unknown tool: {tool_call['name']}") outbound_msgs.append(ToolMessage( content=response, name=tool_call["name"], tool_call_id=tool_call["id"], )) return state | {"messages": outbound_msgs, "question": question_content, "code": question_code} def finish_interview_node(state: InterviewState) -> InterviewState: """Handles the end_interview tool call — sets finished flag.""" tool_msg: AIMessage = state["messages"][-1] outbound_msgs = [] for tool_call in tool_msg.tool_calls: if tool_call["name"] == "end_interview": response = "Say goodbye warmly to the candidate and let them know their evaluation report is being prepared." else: raise NotImplementedError(f"Unknown tool: {tool_call['name']}") outbound_msgs.append(ToolMessage( content=response, name=tool_call["name"], tool_call_id=tool_call["id"], )) return state | {"messages": outbound_msgs, "finished": True} def create_report_node(state: InterviewState) -> InterviewState: """Generates the full evaluation report using structured output + grounding.""" question = state.get("question", "") if not question or "not been selected" in question: return state | {"report": "Report cannot be generated — no question was selected."} messages = state.get("messages", []) transcript = get_interview_transcript(messages, api_key=state.get("api_key", "")) code = state.get("code", "") try: eval_response = get_client(state.get("api_key", "")).models.generate_content( model="gemini-2.5-flash", contents=CANDIDATE_EVALUATION_PROMPT.format( question=question, transcript=transcript, code=code ), config={ "response_mime_type": "application/json", "response_schema": EvaluationOutput, }, ) evaluation = eval_response.parsed except Exception as e: print(f"Evaluation generation error: {e}") return state | {"report": f"Error generating evaluation: {e}"} analytics, topics = get_data_for_search(eval_response) recommendations = get_learning_resources(question, analytics, topics, api_key=state.get("api_key", "")) report_md = Template(REPORT_TEMPLATE).render( evaluation=evaluation, recommendations=recommendations, ) return state | {"report": report_md} def maybe_route_to_tools( state: InterviewState, ) -> Literal["tools", "question selection", "end interview", "__end__"]: """Routing function — decides which node to go to after the chatbot.""" messages = state.get("messages", []) if not messages: raise ValueError("No messages in state") last = messages[-1] if not (hasattr(last, "tool_calls") and last.tool_calls): return "__end__" tool_names = [t["name"] for t in last.tool_calls] if any(n in tool_node.tools_by_name for n in tool_names): return "tools" elif "select_question" in tool_names: return "question selection" elif "end_interview" in tool_names: return "end interview" return "__end__" graph_builder = StateGraph(InterviewState) graph_builder.add_node("chatbot", chatbot_with_tools) graph_builder.add_node("tools", tool_node) graph_builder.add_node("question selection", question_selection_node) graph_builder.add_node("end interview", finish_interview_node) graph_builder.add_node("create report", create_report_node) graph_builder.add_edge(START, "chatbot") graph_builder.add_conditional_edges("chatbot", maybe_route_to_tools) graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge("question selection", "chatbot") graph_builder.add_edge("end interview", "create report") graph_builder.add_edge("create report", "__end__") interviewer_graph = graph_builder.compile() print("LangGraph compiled successfully.") sessions: Dict[str, Dict[str, Any]] = {} class StartSessionResponse(BaseModel): session_id: str message: str class SendMessageRequest(BaseModel): session_id: str message: str = "" code: str = "" code_changed: bool = False image_base64: Optional[str] = None api_key: Optional[str] = None # base64-encoded PNG from whiteboard class SendMessageResponse(BaseModel): message: str problem: str code: str finished: bool report: Optional[str] = None class SessionInfoResponse(BaseModel): session_id: str problem: str code: str finished: bool @app.get("/", tags=["Health"]) def root(): """Health check endpoint.""" return { "status": "ok", "service": "AI Mock Interviewer API", "version": "1.0.0", "questions_loaded": len(df), } class StartSessionRequest(BaseModel): api_key: Optional[str] = None @app.post("/api/session/start", response_model=StartSessionResponse, tags=["Session"]) def start_session(req: StartSessionRequest = StartSessionRequest()): """ Start a new interview session. Returns a session_id and the AI's welcome message. """ session_id = str(uuid.uuid4()) initial_state: Dict[str, Any] = { "messages": [], "question": "Problem has not been selected yet", "code": "# Your solution here\n", "report": "", "finished": False, "api_key": req.api_key or "", } try: new_state = interviewer_graph.invoke(initial_state) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to initialize graph: {e}") welcome = WELCOME_MSG for msg in reversed(new_state.get("messages", [])): if isinstance(msg, AIMessage): welcome = extract_text(msg.content) break new_state["api_key"] = req.api_key or "" sessions[session_id] = new_state return StartSessionResponse(session_id=session_id, message=welcome) @app.post("/api/chat", response_model=SendMessageResponse, tags=["Interview"]) def chat(req: SendMessageRequest): """ Send a message, code update, or whiteboard image to the AI interviewer. Returns the AI's response and updated state. """ if req.session_id not in sessions: raise HTTPException(status_code=404, detail="Session not found. Please start a new session.") state = sessions[req.session_id] if state.get("finished"): return SendMessageResponse( message="The interview has ended. Your report is ready below.", problem=state.get("question", ""), code=state.get("code", ""), finished=True, report=state.get("report", ""), ) content: List[Dict[str, Any]] = [] if req.message: content.append({"type": "text", "text": req.message}) if req.code_changed and req.code: content.append({"type": "text", "text": f"\nMy current code:\n```python\n{req.code}\n```"}) if req.image_base64: content.append({"type": "text", "text": "Here is a screenshot of my whiteboard:"}) content.append({ "type": "image_url", "image_url": {"url": f"data:image/png;base64,{req.image_base64}"}, }) if not content: return SendMessageResponse( message="Please provide a message, code update, or whiteboard drawing.", problem=state.get("question", ""), code=state.get("code", ""), finished=False, ) current_messages = list(state.get("messages", [])) current_messages.append(HumanMessage(content=content)) user_key = req.api_key or state.get("api_key", "") print(f"DEBUG: session_id={req.session_id}, user_key_present={bool(user_key)}, message={req.message[:50] if req.message else ''}") graph_input: Dict[str, Any] = { "messages": current_messages, "question": state.get("question", ""), "code": req.code if req.code_changed else state.get("code", ""), "report": state.get("report", ""), "finished": False, "api_key": user_key, } try: new_state = interviewer_graph.invoke(graph_input) except Exception as e: import traceback print("=== CHAT ERROR ===") print(traceback.format_exc()) print("==================") err = str(e) if "429" in err or "RESOURCE_EXHAUSTED" in err: return SendMessageResponse( message="The AI is receiving too many requests right now. Please wait a few seconds and try again.", problem=state.get("question", ""), code=state.get("code", ""), finished=False, ) if "quota" in err.lower(): return SendMessageResponse( message="API quota exceeded. Please wait a minute before sending another message.", problem=state.get("question", ""), code=state.get("code", ""), finished=False, ) raise HTTPException(status_code=500, detail=f"Interview graph error: {traceback.format_exc()}") sessions[req.session_id] = new_state ai_response = "Processing..." for msg in reversed(new_state.get("messages", [])): if isinstance(msg, AIMessage): ai_response = extract_text(msg.content) break elif isinstance(msg, ToolMessage) and msg.name == "end_interview": ai_response = "Thank you for your time! The interview has ended. Your evaluation report is being prepared..." break finished = new_state.get("finished", False) if finished: ai_response += "\n\n**Your evaluation report is ready below.**" return SendMessageResponse( message=ai_response, problem=new_state.get("question", ""), code=new_state.get("code", ""), finished=finished, report=new_state.get("report") if finished else None, ) @app.get("/api/session/{session_id}", response_model=SessionInfoResponse, tags=["Session"]) def get_session(session_id: str): """Get current session state (problem, code, finished status).""" if session_id not in sessions: raise HTTPException(status_code=404, detail="Session not found.") state = sessions[session_id] return SessionInfoResponse( session_id=session_id, problem=state.get("question", ""), code=state.get("code", ""), finished=state.get("finished", False), ) @app.delete("/api/session/{session_id}", tags=["Session"]) def delete_session(session_id: str): """Delete a session and free memory.""" sessions.pop(session_id, None) return {"status": "deleted", "session_id": session_id} @app.get("/api/questions/topics", tags=["Questions"]) def get_topics(): """List all available question topics.""" return {"topics": df.topic.unique().tolist()} @app.get("/api/questions/difficulties", tags=["Questions"]) def get_difficulties(): """List all available difficulty levels.""" return {"difficulties": df.difficulty.unique().tolist()} @app.get("/api/questions", tags=["Questions"]) def list_all_questions(topic: Optional[str] = None, difficulty: Optional[str] = None): """List questions with optional topic and difficulty filters.""" filtered = df.copy() if topic: filtered = filtered[filtered["topic"].str.lower() == topic.lower()] if difficulty: filtered = filtered[filtered["difficulty"].str.lower() == difficulty.lower()] return { "count": len(filtered), "questions": filtered[["id", "problem_name", "topic", "difficulty", "link"]].to_dict(orient="records"), }