Skip to main content

Directory Layout

interview-simulator/
├── app/                        # Main application package
│   ├── __init__.py             # Flask app factory
│   ├── config.py               # Configuration management
│   ├── models.py               # SQLAlchemy database models
│   ├── exceptions.py           # Custom exception classes
│   ├── extensions.py           # Flask extensions initialization
│   │
│   ├── routes/                 # HTTP endpoints (Flask blueprints)
│   │   ├── __init__.py         # Blueprint registration
│   │   ├── session_routes.py   # Session CRUD endpoints
│   │   ├── document_routes.py  # File upload endpoints
│   │   ├── interview_routes.py # Interview conversation endpoints
│   │   ├── feedback_routes.py  # Feedback generation endpoints
│   │   └── errors.py           # Error handlers (404, 500)
│   │
│   ├── services/               # Business logic layer
│   │   ├── __init__.py
│   │   ├── session_service.py  # Session lifecycle management
│   │   ├── document_service.py # Document upload & parsing
│   │   ├── interview_service.py# Interview orchestration
│   │   └── feedback_service.py # Feedback generation
│   │
│   └── repositories/           # Data access layer
│       ├── __init__.py
│       ├── session_repository.py   # Session database operations
│       ├── message_repository.py   # Message CRUD
│       ├── feedback_repository.py  # Feedback persistence
│       └── file_repository.py      # File storage operations

├── client/                     # AI provider abstraction
│   ├── ai_client.py            # High-level AI client interface
│   ├── ai_provider.py          # Provider protocol definition
│   ├── gemini_provider.py      # Google Gemini implementation
│   └── openrouter_provider.py  # OpenRouter implementation

├── utils/                      # Shared utilities
│   ├── document_parser.py      # PDF/DOCX/TXT text extraction
│   └── prompt_templates.py     # AI prompt templates

├── templates/                  # Jinja2 HTML templates
│   ├── base.html               # Base template with layout
│   ├── landing.html            # Landing page
│   ├── index.html              # Dashboard with recent sessions
│   ├── upload.html             # Document upload page
│   ├── interview.html          # Interview conversation UI
│   ├── feedback.html           # Feedback results page
│   └── fragments/              # HTMX partial templates
│       ├── message.html        # Single message component
│       └── progress.html       # Progress indicator

├── static/                     # Static assets
│   ├── css/
│   │   └── main.css            # Application styles
│   └── js/
│       └── htmx.min.js         # HTMX library

├── tests/                      # Pytest test suite
│   ├── conftest.py             # Test fixtures and configuration
│   ├── test_session_service.py
│   ├── test_interview_service.py
│   └── test_repositories.py

├── instance/                   # Instance-specific files (gitignored)
│   └── app.db                  # SQLite database file

├── uploads/                    # Uploaded CV files (gitignored)

├── .env                        # Environment variables (gitignored)
├── .env.example                # Example environment configuration
├── requirements.txt            # Python dependencies
├── wsgi.py                     # WSGI entry point for production
├── Dockerfile                  # Docker container definition
├── docker-compose.yml          # Docker Compose configuration
└── README.md                   # Project documentation

Core Modules

Application Package (app/)

The app factory pattern allows for flexible configuration and testing.
from flask import Flask
from .models import db
from .routes import register_routes

def create_app(config_object=None):
    app = Flask(__name__)
    
    # Load configuration
    app.config.from_object(Config)
    
    # Initialize database
    db.init_app(app)
    
    # Initialize AI providers
    init_ai_providers(app)
    
    # Register blueprints
    register_routes(app)
    
    # Create tables
    with app.app_context():
        db.create_all()
    
    return app
Key functions:
  • create_app(config_object=None): Creates and configures Flask app instance
  • Initializes SQLAlchemy database connection
  • Registers all route blueprints
  • Creates upload folder if it doesn’t exist
Location: app/__init__.py:12
Defines all SQLAlchemy ORM models. See Database Schema for details.Models:
  • User - User accounts (optional)
  • Session - Interview sessions
  • Message - Conversation messages
  • Feedback - Interview feedback
Location: app/models.py
Centralized configuration for Flask app.Key settings:
class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
    DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///dev.db')
    SQLALCHEMY_DATABASE_URI = DATABASE_URL
    UPLOAD_FOLDER = 'uploads'
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16MB max file size
    
    # AI Provider Configuration
    GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
    OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
    ACTIVE_PROVIDERS = os.getenv('ACTIVE_PROVIDERS', 'openrouter,gemini').split(',')
Environment-specific configs:
  • DevelopmentConfig
  • ProductionConfig
  • TestConfig
Domain-specific exceptions for better error handling.
class ValidationError(Exception):
    """Raised when input validation fails"""
    pass

class NotFoundError(Exception):
    """Raised when a resource is not found"""
    pass

class AIProviderError(Exception):
    """Raised when AI provider API fails"""
    pass
Usage in routes:
try:
    session = session_service.get_session(session_id)
except NotFoundError:
    abort(404)
except ValidationError as e:
    flash(str(e), 'error')

Routes Layer (app/routes/)

Flask blueprints that handle HTTP requests. Each blueprint focuses on a specific domain.
Endpoints:
  • GET / - Landing page
  • GET /dashboard - Dashboard with recent sessions
  • POST /session/create - Create new session
Example:
@bp.route("/session/create", methods=["POST"])
def create_session():
    job_title = request.form.get("job_title")
    company_name = request.form.get("company_name")
    
    session_service = _get_session_service()
    new_session = session_service.create_session(job_title, company_name)
    
    return redirect(url_for("document.upload_page", session_id=new_session.id))
Location: app/routes/session_routes.py:29
Endpoints:
  • GET /session/<id>/upload - Upload page
  • POST /session/<id>/upload-cv - Upload CV file
  • POST /session/<id>/upload-job - Submit job description text
File handling:
  • Validates file types (PDF, DOCX, TXT)
  • Extracts text using DocumentParser
  • Stores extracted text in Session.cv_text
Supported formats:
  • PDF (via pdfplumber)
  • DOCX (via python-docx)
  • TXT (plain text)
Endpoints:
  • GET /session/<id>/interview - Interview UI
  • POST /session/<id>/start - Start interview (first question)
  • POST /session/<id>/message - Submit answer (HTMX endpoint)
  • POST /session/<id>/complete - Finish interview and generate feedback
HTMX integration:
<form hx-post="/session/{{ session.id }}/message" 
      hx-target="#messages" 
      hx-swap="beforeend">
    <textarea name="answer"></textarea>
    <button type="submit">Submit</button>
</form>
Returns HTML fragment for progressive enhancement.
Endpoints:
  • GET /session/<id>/feedback - View feedback results
Data displayed:
  • Interview score (1-10)
  • Strengths (markdown list)
  • Weaknesses (areas for improvement)
  • CV optimization suggestions

Services Layer (app/services/)

Business logic and workflow orchestration.
Key methods:
class SessionService:
    def create_session(self, job_title: str, company_name: str) -> Session:
        # Validates input
        # Creates session via repository
        
    def get_session(self, session_id: int) -> Session:
        # Fetches session or raises NotFoundError
        
    def is_ready_for_interview(self, session_id: int) -> bool:
        # Checks if CV and job description are uploaded
Validation rules:
  • Job title must be non-empty and ≤ 200 characters
  • Company name must be non-empty
Location: app/services/session_service.py:6
Key methods:
class InterviewService:
    MAX_QUESTIONS = 8
    
    def start_interview(self, session_id: int) -> str:
        # Generates first question using AI
        # Stores in messages table
        
    def submit_answer(self, session_id: int, answer: str) -> dict:
        # Stores user answer
        # Checks if interview is complete
        # Generates next question if not complete
        
    def is_interview_complete(self, session_id: int) -> bool:
        # Returns True if 8 questions have been asked
Flow:
  1. User starts interview
  2. AI generates first question based on CV and job description
  3. User submits answer
  4. Service checks if MAX_QUESTIONS reached
  5. If not, AI generates follow-up question
  6. Repeat until 8 questions answered
Location: app/services/interview_service.py:6
Key methods:
class DocumentService:
    def process_cv_upload(self, session_id: int, file) -> str:
        # Validates file type
        # Extracts text using DocumentParser
        # Updates session.cv_text
        
    def process_job_description(self, session_id: int, text: str) -> None:
        # Stores job description text
        # Updates session.job_description_text
Uses:
  • utils.document_parser.DocumentParser for text extraction
  • repositories.file_repository.FileRepository for file storage
Key methods:
class FeedbackService:
    def generate_feedback(self, session_id: int) -> Feedback:
        # Fetches all messages from session
        # Calls AI to analyze performance
        # Parses AI response into structured feedback
        # Stores in feedback table
AI prompt includes:
  • All conversation messages
  • Original CV text
  • Job description
  • Request for score, strengths, weaknesses, and CV tips

Repositories Layer (app/repositories/)

Data access abstractions for database operations.
Key methods:
class SessionRepository:
    def create(self, job_title: str, company_name: str) -> Session:
        # Creates new session record
        
    def get_by_id(self, session_id: int) -> Session | None:
        # Fetches session by ID
        
    def get_session_with_messages(self, session_id: int) -> Session:
        # Eager loads messages relationship
        return Session.query.options(db.joinedload(Session.messages)).get(session_id)
        
    def update_cv_text(self, session_id: int, cv_text: str) -> Session:
        # Updates CV text field
Performance optimization:
  • Uses db.joinedload() for eager loading
  • Avoids N+1 query problems
Location: app/repositories/session_repository.py:5
Key methods:
class MessageRepository:
    def create_message(self, session_id: int, role: str, content: str) -> Message:
        # Creates new message
        
    def get_messages(self, session_id: int) -> list[Message]:
        # Fetches all messages for session, ordered by timestamp
        
    def count_messages(self, session_id: int, role: str = None) -> int:
        # Counts messages, optionally filtered by role
        
    def conversation_to_history(self, session_id: int) -> list[dict]:
        # Converts messages to AI provider format
        return [{"role": msg.role, "content": msg.content} for msg in messages]
Usage:
  • count_messages(session_id, role='assistant') used to track interview progress
Key methods:
class FeedbackRepository:
    def create_feedback(self, session_id: int, **feedback_data) -> Feedback:
        # Stores feedback record
        
    def get_by_session_id(self, session_id: int) -> Feedback | None:
        # Fetches feedback for a session
One-to-one relationship:
  • Each session has at most one feedback record
  • Creating second feedback for same session would overwrite (not currently handled)

AI Client (client/)

Abstraction layer for AI providers.
High-level methods:
class AIClient:
    def __init__(self, providers: list[AIProvider]):
        self.providers = providers
        
    def generate_first_question(self, cv_text, job_desc, job_title, company_name) -> str:
        # Uses prompt template
        # Calls provider.generate_response()
        # Returns first interview question
        
    def generate_followup_question(self, convo_history, cv_text, job_desc, ...) -> str:
        # Continues conversation
        # Adapts questions based on previous answers
        
    def generate_feedback(self, messages, cv_text, job_desc) -> dict:
        # Analyzes entire conversation
        # Returns structured feedback
Provider fallback:
  • Tries primary provider (e.g., Gemini)
  • Falls back to secondary provider (e.g., OpenRouter) on failure
Defines interface for AI providers.
class AIProvider(Protocol):
    def generate_response(self, prompt: str, context: list[dict] = None) -> str:
        ...
Any class implementing this protocol can be used as an AI provider.
Concrete implementations for specific AI services.Common features:
  • API key authentication
  • Request formatting
  • Response parsing
  • Retry logic with exponential backoff (via Tenacity)
Example Gemini:
class GeminiProvider:
    def generate_response(self, prompt: str, context: list[dict] = None) -> str:
        model = genai.GenerativeModel('gemini-1.5-flash')
        response = model.generate_content(prompt)
        return response.text

Utilities (utils/)

Main class:
class DocumentParser:
    @staticmethod
    def parse(file_path: str) -> str:
        # Detects file type from extension
        # Calls appropriate parser
        # Returns extracted text
        
    @staticmethod
    def parse_pdf(file_path: str) -> str:
        # Uses pdfplumber
        
    @staticmethod
    def parse_docx(file_path: str) -> str:
        # Uses python-docx
        
    @staticmethod
    def parse_txt(file_path: str) -> str:
        # Reads plain text
Error handling:
  • Gracefully handles corrupted files
  • Returns empty string on parse failure
  • Logs errors for debugging
Centralized prompt templates for consistency.Templates:
FIRST_QUESTION_TEMPLATE = """
You are an expert interviewer for {company_name}.
Role: {job_title}

Candidate's CV:
{cv_text}

Job Description:
{job_desc}

Generate the first interview question.
"""

FOLLOWUP_QUESTION_TEMPLATE = """..."""
FEEDBACK_TEMPLATE = """..."""
Benefits:
  • Easy to update prompts without changing code
  • Consistent tone across all AI interactions
  • Version control for prompt engineering

Entry Points

Development Server

flask run
# Runs on http://127.0.0.1:5000
Uses Flask’s built-in development server.

Production WSGI

# wsgi.py
from app import create_app

app = create_app()

if __name__ == "__main__":
    app.run()
Run with Gunicorn:
gunicorn wsgi:app

Docker Container

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-b", "0.0.0.0:8000", "wsgi:app"]
docker-compose up --build
# Access at http://localhost:8000

Testing Structure

Test Organization

tests/
├── conftest.py                 # Shared fixtures
├── test_session_service.py     # Service layer tests
├── test_interview_service.py
├── test_repositories.py        # Repository tests
└── test_routes.py              # Integration tests

Running Tests

# All tests
pytest

# With coverage
pytest --cov=app

# Specific test file
pytest tests/test_interview_service.py

# Specific test
pytest tests/test_session_service.py::test_create_session

Test Fixtures (conftest.py)

@pytest.fixture
def app():
    app = create_app(TestConfig)
    yield app

@pytest.fixture
def client(app):
    return app.test_client()

@pytest.fixture
def db(app):
    with app.app_context():
        _db.create_all()
        yield _db
        _db.drop_all()

Configuration Files

.env.example

GEMINI_API_KEY=your_gemini_api_key_here
OPENROUTER_API_KEY=your_openrouter_api_key_here
ACTIVE_PROVIDERS=openrouter,gemini
SECRET_KEY=your_random_secret_key
DATABASE_URL=sqlite:///instance/app.db

requirements.txt

Flask==3.0.0
Flask-SQLAlchemy==3.0.5
gunicorn==21.2.0
pdfplumber==0.10.3
python-docx==1.1.0
tenacity==8.2.3
python-dotenv==1.0.0

Build docs developers (and LLMs) love