🔥 0
0
Lesson 7 of 8 30 min +150 XP

Task 6: Build the Complete Application

Let's combine everything into a polished, production-ready interview coach.

Final Project Structure

interview-coach/
├── main.py                 # CLI entry point
├── app.py                  # Streamlit web interface
├── config.py               # Configuration
├── requirements.txt
├── .env
│
├── chains/
│   ├── __init__.py
│   ├── interviewer.py      # Interview chain with memory
│   └── evaluator.py        # Structured feedback
│
├── agents/
│   ├── __init__.py
│   ├── tools.py            # Agent tools
│   └── coach.py            # Main interview agent
│
├── rag/
│   ├── __init__.py
│   ├── loader.py           # Document loaders
│   └── retriever.py        # Vector store & retrieval
│
└── data/
    └── job_descriptions/   # Sample JDs

Step 1: Configuration

# config.py
from pydantic_settings import BaseSettings
from typing import Literal

class Settings(BaseSettings):
    # API Keys
    openai_api_key: str

    # Model settings
    model_name: str = "gpt-4o-mini"
    temperature: float = 0.7
    max_tokens: int = 1000

    # Interview settings
    max_questions: int = 5
    default_difficulty: Literal["easy", "medium", "hard"] = "medium"

    # RAG settings
    chunk_size: int = 500
    chunk_overlap: int = 50
    retriever_k: int = 3

    class Config:
        env_file = ".env"

settings = Settings()

Step 2: Complete Interview Coach Class

# interview_coach.py
from dataclasses import dataclass, field
from typing import List, Optional
from enum import Enum

from chains.interviewer import create_interviewer_with_history
from chains.evaluator import create_evaluator_simple, create_report_generator, AnswerFeedback, InterviewReport
from rag.setup import setup_interview_rag
from config import settings

class InterviewPhase(Enum):
    NOT_STARTED = "not_started"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"

@dataclass
class InterviewSession:
    """Holds all interview state."""
    session_id: str
    position: str
    level: str
    topics: List[str]
    phase: InterviewPhase = InterviewPhase.NOT_STARTED
    current_question: str = ""
    current_topic_index: int = 0
    questions_asked: List[str] = field(default_factory=list)
    answers: List[str] = field(default_factory=list)
    feedback: List[AnswerFeedback] = field(default_factory=list)
    transcript: List[dict] = field(default_factory=list)

class InterviewCoach:
    """Complete AI Interview Coach."""

    def __init__(
        self,
        job_description: str = None,
        job_description_path: str = None,
        interview_type: str = "technical",
        difficulty: str = "adaptive",
        position: str = "Software Engineer",
        level: str = "senior"
    ):
        self.interview_type = interview_type
        self.difficulty = difficulty
        self.position = position
        self.level = level

        # Initialize chains
        self.interviewer = create_interviewer_with_history()
        self.evaluator = create_evaluator_simple()
        self.report_generator = create_report_generator()

        # Setup RAG if job description provided
        self.rag_enabled = False
        if job_description_path:
            rag_components = setup_interview_rag(job_description_path)
            self.question_generator = rag_components["question_generator"]
            self.retriever = rag_components["retriever"]
            self.rag_enabled = True
        elif job_description:
            # Create in-memory RAG from string
            from rag.loader import create_docs_from_text, split_documents
            from rag.retriever import create_vector_store, create_retriever

            docs = create_docs_from_text(job_description)
            chunks = split_documents(docs)
            vector_store = create_vector_store(chunks)
            self.retriever = create_retriever(vector_store)
            self.rag_enabled = True

        # Session management
        self.sessions: dict[str, InterviewSession] = {}

    def start_interview(self, session_id: str, topics: List[str] = None) -> str:
        """Start a new interview session."""

        if topics is None:
            topics = ["Python fundamentals", "async programming",
                     "system design", "problem solving", "best practices"]

        session = InterviewSession(
            session_id=session_id,
            position=self.position,
            level=self.level,
            topics=topics,
            phase=InterviewPhase.IN_PROGRESS
        )
        self.sessions[session_id] = session

        # Generate first question
        question = self._generate_question(session)
        session.current_question = question
        session.questions_asked.append(question)
        session.transcript.append({"role": "interviewer", "content": question})

        return f"Welcome! Let's begin your {self.level} {self.position} interview.\n\n{question}"

    def submit_answer(self, session_id: str, answer: str) -> dict:
        """Process candidate's answer and get next question."""

        session = self.sessions.get(session_id)
        if not session or session.phase != InterviewPhase.IN_PROGRESS:
            return {"error": "No active interview session"}

        # Save answer
        session.answers.append(answer)
        session.transcript.append({"role": "candidate", "content": answer})

        # Evaluate answer
        feedback = self.evaluator.invoke({
            "question": session.current_question,
            "level": self.level,
            "answer": answer
        })
        session.feedback.append(feedback)

        # Check if interview should end
        if len(session.questions_asked) >= settings.max_questions:
            session.phase = InterviewPhase.COMPLETED
            return {
                "feedback": feedback,
                "is_complete": True,
                "message": "Interview complete! Generating your report..."
            }

        # Adjust difficulty if adaptive
        if self.difficulty == "adaptive":
            self._adjust_difficulty(session)

        # Generate next question
        session.current_topic_index += 1
        next_question = self._generate_question(session, previous_feedback=feedback)
        session.current_question = next_question
        session.questions_asked.append(next_question)
        session.transcript.append({"role": "interviewer", "content": next_question})

        return {
            "feedback": feedback,
            "next_question": next_question,
            "is_complete": False,
            "questions_remaining": settings.max_questions - len(session.questions_asked)
        }

    def _generate_question(
        self,
        session: InterviewSession,
        previous_feedback: AnswerFeedback = None
    ) -> str:
        """Generate the next interview question."""

        topic_index = session.current_topic_index % len(session.topics)
        topic = session.topics[topic_index]

        if self.rag_enabled:
            # Use RAG to generate job-specific question
            return self.question_generator.invoke({
                "topic": topic,
                "difficulty": self.difficulty,
                "previous_questions": ", ".join(session.questions_asked[-3:])
            })
        else:
            # Use standard interviewer chain
            context = f"Ask a {self.difficulty} question about {topic}."
            if previous_feedback and previous_feedback.follow_up_question:
                context += f"\nConsider: {previous_feedback.follow_up_question}"

            return self.interviewer.invoke(
                {
                    "interview_type": self.interview_type,
                    "level": self.level,
                    "focus_area": topic,
                    "input": context
                },
                config={"configurable": {"session_id": session.session_id}}
            )

    def _adjust_difficulty(self, session: InterviewSession):
        """Adjust difficulty based on recent performance."""
        if len(session.feedback) < 2:
            return

        recent_scores = [f.score for f in session.feedback[-2:]]
        avg_score = sum(recent_scores) / len(recent_scores)

        if avg_score >= 8:
            self.difficulty = "hard"
        elif avg_score <= 4:
            self.difficulty = "easy"
        else:
            self.difficulty = "medium"

    def generate_report(self, session_id: str) -> InterviewReport:
        """Generate final interview report."""

        session = self.sessions.get(session_id)
        if not session:
            raise ValueError("Session not found")

        # Format transcript
        transcript_text = "\n\n".join([
            f"{'Q' if t['role'] == 'interviewer' else 'A'}: {t['content']}"
            for t in session.transcript
        ])

        scores = [f.score for f in session.feedback]

        report = self.report_generator.invoke({
            "position": self.position,
            "level": self.level,
            "interview_type": self.interview_type,
            "transcript": transcript_text,
            "scores": scores
        })

        return report

    @property
    def is_complete(self) -> bool:
        """Check if current interview is complete."""
        # For backward compatibility with simple usage
        return False  # Managed per-session now

Step 3: CLI Interface

# main.py
import argparse
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress
from interview_coach import InterviewCoach

console = Console()

def run_cli():
    parser = argparse.ArgumentParser(description="AI Interview Coach")
    parser.add_argument("--job", "-j", help="Path to job description file")
    parser.add_argument("--type", "-t", default="technical", help="Interview type")
    parser.add_argument("--level", "-l", default="senior", help="Position level")
    parser.add_argument("--questions", "-q", type=int, default=5, help="Number of questions")
    args = parser.parse_args()

    console.print(Panel.fit(
        "[bold cyan]AI Interview Coach[/bold cyan]\n"
        "Practice technical interviews with AI feedback",
        border_style="cyan"
    ))

    # Initialize coach
    coach = InterviewCoach(
        job_description_path=args.job,
        interview_type=args.type,
        level=args.level
    )

    session_id = "cli_session"
    topics = ["Python", "system design", "algorithms", "best practices", "behavioral"]

    # Start interview
    welcome = coach.start_interview(session_id, topics[:args.questions])
    console.print(f"\n[bold green]Interviewer:[/bold green] {welcome}\n")

    while True:
        answer = console.input("[bold blue]You:[/bold blue] ")

        if answer.lower() in ['quit', 'exit', 'q']:
            console.print("[yellow]Interview ended early.[/yellow]")
            break

        result = coach.submit_answer(session_id, answer)

        # Show feedback
        feedback = result["feedback"]
        console.print(f"\n[dim]Score: {feedback.score}/10 - {feedback.understanding}[/dim]")

        if result["is_complete"]:
            # Generate and display report
            console.print("\n[bold]Generating your interview report...[/bold]\n")

            report = coach.generate_report(session_id)

            console.print(Panel(
                f"[bold]Overall Score: {report.overall_score}/10[/bold]\n"
                f"Recommendation: [cyan]{report.recommendation.upper()}[/cyan]\n\n"
                f"{report.summary}\n\n"
                f"[green]Strengths:[/green]\n" +
                "\n".join(f"  • {s}" for s in report.strengths) + "\n\n"
                f"[yellow]Areas to Improve:[/yellow]\n" +
                "\n".join(f"  • {a}" for a in report.areas_to_improve),
                title="Interview Report",
                border_style="green"
            ))
            break

        console.print(f"\n[bold green]Interviewer:[/bold green] {result['next_question']}\n")
        console.print(f"[dim]({result['questions_remaining']} questions remaining)[/dim]\n")

if __name__ == "__main__":
    run_cli()

Step 4: Streamlit Web Interface

# app.py
import streamlit as st
from interview_coach import InterviewCoach
import uuid

st.set_page_config(page_title="AI Interview Coach", page_icon="🎯", layout="wide")

# Initialize session state
if "coach" not in st.session_state:
    st.session_state.coach = None
if "session_id" not in st.session_state:
    st.session_state.session_id = None
if "messages" not in st.session_state:
    st.session_state.messages = []
if "interview_complete" not in st.session_state:
    st.session_state.interview_complete = False

# Sidebar configuration
with st.sidebar:
    st.header("🎯 Interview Setup")

    position = st.text_input("Position", "Senior Python Developer")
    level = st.selectbox("Level", ["junior", "mid", "senior", "staff"])
    interview_type = st.selectbox("Type", ["technical", "behavioral", "system_design"])

    job_desc = st.text_area(
        "Job Description (optional)",
        placeholder="Paste job description for targeted questions..."
    )

    num_questions = st.slider("Number of Questions", 3, 10, 5)

    if st.button("Start Interview", type="primary"):
        st.session_state.coach = InterviewCoach(
            job_description=job_desc if job_desc else None,
            interview_type=interview_type,
            level=level,
            position=position
        )
        st.session_state.session_id = str(uuid.uuid4())
        st.session_state.messages = []
        st.session_state.interview_complete = False

        # Get first question
        topics = ["core skills", "system design", "problem solving", "experience", "culture fit"]
        welcome = st.session_state.coach.start_interview(
            st.session_state.session_id,
            topics[:num_questions]
        )
        st.session_state.messages.append({"role": "assistant", "content": welcome})
        st.rerun()

# Main content
st.title("🎯 AI Interview Coach")

if st.session_state.coach is None:
    st.info("👈 Configure your interview in the sidebar and click 'Start Interview'")
else:
    # Display chat messages
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.write(message["content"])
            if "feedback" in message:
                with st.expander("View Feedback"):
                    fb = message["feedback"]
                    st.metric("Score", f"{fb.score}/10")
                    st.write(f"**Understanding:** {fb.understanding}")
                    if fb.improvements:
                        st.write("**Tips:**")
                        for tip in fb.improvements:
                            st.write(f"- {tip}")

    # Chat input
    if not st.session_state.interview_complete:
        if prompt := st.chat_input("Your answer..."):
            # Add user message
            st.session_state.messages.append({"role": "user", "content": prompt})

            # Get response
            result = st.session_state.coach.submit_answer(
                st.session_state.session_id,
                prompt
            )

            if result["is_complete"]:
                st.session_state.interview_complete = True
                # Generate report
                report = st.session_state.coach.generate_report(st.session_state.session_id)

                report_content = f"""
## Interview Complete! 🎉

**Overall Score: {report.overall_score}/10**
**Recommendation: {report.recommendation.upper()}**

### Summary
{report.summary}

### Strengths
{chr(10).join('- ' + s for s in report.strengths)}

### Areas to Improve
{chr(10).join('- ' + a for a in report.areas_to_improve)}

### Suggested Topics to Study
{chr(10).join('- ' + t for t in report.suggested_topics_to_study)}
"""
                st.session_state.messages.append({
                    "role": "assistant",
                    "content": report_content
                })
            else:
                st.session_state.messages.append({
                    "role": "assistant",
                    "content": result["next_question"],
                    "feedback": result["feedback"]
                })

            st.rerun()
    else:
        st.success("Interview complete! Check the report above.")
        if st.button("Start New Interview"):
            st.session_state.coach = None
            st.session_state.session_id = None
            st.session_state.messages = []
            st.session_state.interview_complete = False
            st.rerun()

Step 5: Requirements

# requirements.txt
langchain>=0.2.0
langchain-openai>=0.1.0
langchain-community>=0.2.0
chromadb>=0.4.0
python-dotenv>=1.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
pypdf>=4.0.0
docx2txt>=0.8
tiktoken>=0.5.0
rich>=13.0.0        # For CLI
streamlit>=1.30.0   # For web UI

Running the Application

# CLI mode
python main.py --job data/job_descriptions/senior_python.txt --questions 5

# Web mode
streamlit run app.py

Key Takeaways

  • Modular design - Separate chains, agents, and RAG components
  • Session management - Track multiple interviews
  • Configuration - Use environment variables and settings
  • Multiple interfaces - CLI and web UI from same core
  • Error handling - Graceful degradation when components fail

Congratulations! 🎉

You've built a complete AI Interview Coach that:

  • Conducts multi-turn interviews with memory
  • Provides structured feedback and scoring
  • Generates job-specific questions using RAG
  • Adapts difficulty based on performance
  • Produces comprehensive reports

Next Steps

  • Add more interview types (coding, system design with diagrams)
  • Implement voice interface with speech-to-text
  • Add company-specific question banks
  • Build analytics dashboard for progress tracking
  • Deploy as a SaaS product!