Backend services for an AI-assisted technical interview platform: REST authentication and user APIs, a real-time interview agent over WebSockets, asynchronous CV analysis (PDF from object storage through an LLM into MongoDB), and email notifications driven by RabbitMQ. The repository is structured as a production-ready polyglot backend—environment-driven configuration, durable messaging with separate job and result routes for CV analysis, layered domain/application/infrastructure code in each service, and Compose-based local stacks that mirror deployment boundaries.
- Overview
- Architecture
- Repository layout
- Root tooling and repository hygiene
- Shared library:
jwt_handler - Services
- Observability, billing, privacy
- Data stores
- Prerequisites
- Quick start
- Development
- License
| Component | Type | Responsibility |
|---|---|---|
| user-management-service | HTTP (FastAPI) | Users, JWT auth (RS256), OAuth (Google/GitHub), billing entitlements, GDPR export/delete, refresh-token rotation with Redis, CV upload + RabbitMQ jobs |
| interview-service | HTTP + WebSocket | LangGraph mock interviews (modes, resume, transcripts), JD match, coding sandbox, question bank / learning paths |
| practice-service | HTTP + workers | Personalized practice plans, grading (MCQ/open/flashcard/code_review/scenario), progress, in-app notifications |
| analyze-service | Worker | Consume cv-analyze-stream → S3 PDF → Docling extract → LLM → CVData → MongoDB → cv-analysis-results |
| notification-service | Worker | Transactional email via AWS SES (reset password, verify email, plan ready, digests) |
| libs/jwt_handler | Python package | JWT encode/decode, token DTOs |
| libs/shared_models | Python package | Shared CV/interview/practice/billing/JD DTOs and messaging contracts |
| libs/observability | Python package | Correlation IDs, metrics, rate limits, PII redaction, Langfuse/Sentry hooks |
High-level view of the platform (API gateway, services, RabbitMQ, databases, and S3). This repository ships User Management, Interview, Practice, Analyze, and Notification backends; the frontend lives in a separate Next.js repo.
Synchronous paths. Clients talk HTTP to User Management / Practice and HTTP/WebSocket to Interview (nginx sticky sessions for WS).
Asynchronous paths. Analyze consumes cv-analyze-stream (Docling + LLM). Notification consumes email streams. Practice workers consume interview-completed and practice-plan jobs and publish practice-plan-ready-stream.
| Kind | Service | Typical command | Role |
|---|---|---|---|
| HTTP | user-management-service | uvicorn src.main:app |
Auth, users, billing, privacy |
| HTTP | interview-service | uvicorn src.main:app |
WebSocket interview + coding/JD APIs |
| HTTP | practice-service | uvicorn src.main:app |
Practice plans + notifications inbox |
| Worker | analyze-service | python -m src.main |
CV analysis pipeline (Docling) |
| Worker | notification-service | python -m src.main |
Transactional email |
- Correlation ID + Prometheus-style
/metrics+ in-process rate limits vialibs/observability. - Optional Sentry (
SENTRY_DSN), Langfuse (LANGFUSE_*), OTEL (OTEL_EXPORTER_OTLP_ENDPOINT). - Billing:
GET /api/v1/billing/entitlements,POST /api/v1/billing/checkout-session(Stripe whenFEATURE_STRIPE_BILLING+ keys set). - Privacy:
GET /api/v1/privacy/export, hardenedDELETE /api/v1/user/me/. - Runbooks: docs/runbooks/.
| Path | Role |
|---|---|
| user-management-service/ | FastAPI app, domain/application/infrastructure layers, Alembic migrations, aio-pika producer for cv-analyze-stream and reset-password-stream, Redis refresh-token rotation and reuse detection |
| interview-service/ | FastAPI router, LangGraph workflow, LLM factory (llm.py), static interview_client.html |
| analyze-service/ | dependency-injector composition root, aio-pika consumer on cv-analyze-stream, producer to cv-analysis-results, S3 + Mongo + LangChain CV parser |
| notification-service/ | Blocking pika consumer on reset-password-stream, ResetPasswordUseCase, Mongo repository + SES adapter |
| libs/jwt_handler/ | Reusable JWT handling consumed by user-management (pyproject.toml Git or path dependency) |
| pyproject.toml / poetry.lock | Repository-root Poetry project: pins shared dev tooling and locks root dependency versions |
| .pre-commit-config.yaml | Hooks run Black, Ruff, and scoped mypy |
| .gitignore | Ignores .env, virtualenvs, IDE metadata, bytecode |
pyproject.toml (root)
- Declares
python = "^3.12". - Root
[tool.poetry.dependencies]includes shared packages such asmotorwhere scripts or future shared tooling need MongoDB async access alongside service-local stacks. - Dev dependencies:
pre-commit,black,mypy,ruff. - [tool.black] —
line-length = 120. - [tool.ruff] —
target-version = "py312",line-length = 120, excludes Alembic migrations under user-management and prompt directories under analyze/interview.
| Hook | Scope / notes |
|---|---|
trailing-whitespace, end-of-file-fixer, check-added-large-files |
Whole repo |
Black 25.1.0, language_version: python3.12 |
Whole repo |
Ruff via ruff-pre-commit |
Whole repo |
mypy v1.15.0, hook entries |
Files under ^user-management-service/ and ^interview-service/; additional_dependencies: [types-PyYAML] |
Additional services can opt into the same hooks by extending the configuration as the codebase grows.
Ignores .env, venv/, .idea/, __pycache__/. Service-specific artifacts (for example Compose volumes) are not globally ignored—prefer keeping machine-local paths out of commits manually.
Shared library: jwt_handler
Purpose. Centralize RS256 JWT creation and verification for microservices that share RSA keys.
Package metadata. libs/jwt_handler/pyproject.toml — package name jwt_handler, Python >=3.9 (services use 3.12).
Core types.
JWTTokenHandler—encode_jwt/decode_jwtusing PyJWT; maps PyJWT exceptions to library-specific errors.- Defaults for
algorithm(RS256) and token TTL fields live onJWTSettings. Optional YAML under keyjwt_handlerviaload_from_yaml()complements environment-first configuration for local profiles.
Consumption in user-management. user-management-service/pyproject.toml pulls jwt-handler from Git (branch = dev, subdirectory = libs/jwt_handler). For forks or offline work, switch to a path dependency pointing at libs/jwt_handler.
Imports. Prefer explicit submodule imports (jwt_handler.models, jwt_handler.value_objects, etc.). The package __init__.py does not re-export the public API.
Entrypoints. src/main.py builds FastAPI with lifespan: obtains Redis and async SQLAlchemy engine via dependency helpers, waits for RabbitMQ readiness (wait_for_rabbitmq), then serves requests; on shutdown closes Redis and the engine.
Configuration. src/config.py via pydantic-settings:
| Group | Env prefix / keys | Purpose |
|---|---|---|
| Postgres | POSTGRES_* |
Async URL postgresql+asyncpg://… |
| SQLAlchemy | (defaults on SQLAlchemySettings) |
Pool size, echo |
| Redis | REDIS_* |
Host, port, passwords, decode_responses |
| JWT RSA | PUBLIC_KEY, PRIVATE_KEY |
PEM strings for RS256 |
| RabbitMQ | RABBITMQ_* |
URL builder; cv_analyzer_queue_name defaults to cv-analyze-stream; connection wait timeout |
Layering.
- Domain — entities such as
User(UserRole, timestamps), repository interfaces, Redis/RabbitMQ ports. - Application — use cases (registration, login, refresh, CRUD, delete me, password reset).
RefreshTokenUseCase: decode refresh JWT, verifyTokenType.REFRESH, load user, reject blocked users, reject reuse ifrefresh-key:{token}already exists in Redis, otherwise issue new access + refresh pair andSETEXthe old refresh token key so it cannot be reused until expiry window ends. - API — FastAPI routers under
api/v1, global exception wiringexception_handler, OAuth2 schemesecurity.pywithtokenUrl="/api/v1/auth/token".
Messaging. RabbitMQProducer implements IRabbitMQProducer: aio-pika, durable queues, persistent messages on the default exchange with routing_key = queue_name. Application use cases publish CV analysis jobs to cv-analyze-stream and password-reset payloads to reset-password-stream as users progress through signup, profile updates, and account recovery.
Auth HTTP surface includes POST /api/v1/auth/signup, POST /api/v1/auth/token, POST /api/v1/auth/refresh, POST /api/v1/auth/reset-password, and POST /api/v1/auth/reset-password/{token}. Integration tests cover this surface under test_reset_password.py.
Persistence. Alembic under src/migrations; initial users table with unique email/username/phone constraints.
Containers. docker-compose.yaml: Postgres 12, Redis 8 with ACL file, RabbitMQ 4.1 management image. Dockerfile: multi-stage Poetry install, uvicorn src.main:app.
Entrypoints. src/main.py: mounts /api/v1 and static files at /static; root GET / returns JSON pointing to /static/interview_client.html.
Configuration. src/config.py: YAML-backed logging (config.yaml → logger key), LLM settings via Pydantic Settings:
LLM_PROVIDER—openai(default),openrouter, orgoogle(.env.example).- OpenAI —
OPENAI_API_KEY,OPENAI_LLM__MODEL(defaultgpt-4o-mini),OPENAI_LLM__TEMPERATURE. - OpenRouter —
OPENROUTER_API_KEY,OPENROUTER_LLM__MODEL,OPENROUTER_LLM__API_BASE(defaulthttps://openrouter.ai/api/v1),OPENROUTER_LLM__TEMPERATURE. - Google Gemini —
GOOGLE_API_KEY,GOOGLE_LLM__MODEL(defaultgemini-2.0-flash),GOOGLE_LLM__TEMPERATURE. INTERVIEW_LLM__TEMPERATURE— optional per-service temperature override.
LLM wiring. src/agent/llm.py: LLMFactory.create_llm() selects the provider from LLM_PROVIDER and returns a LangChain BaseChatModel (ChatOpenAI for OpenAI/OpenRouter, ChatGoogleGenerativeAI for Google).
Agent graph. create_interview_workflow(): LangGraph StateGraph over InterviewState. Nodes include greeting, soft/hard question askers, small talk, wrap-up, question routing, and answer evaluation. Conditional edges:
section_router— drives high-level stage transitions (including end).question_router_decision— async LLM classification (SMALLTALKvs continuing soft/hard questioning usingQUESTION_ROUTER_DECISION_PROMPT).
Checkpointing is LangGraph-compatible and configurable: lean defaults for local demos, durable backends where multiple instances share interview state.
WebSocket contract. websocket_endpoint at /api/v1/interview/ws/{user_id} delegates to InterviewConnectionManager.
- Client JSON
type:user_message(requirescontent),end_interview,get_status; unknown types fall back to forwardingcontentas user text. - Server JSON examples:
interview_started,agent_message(includesstage),interview_status,interview_complete,error.
Reference demo vs production profile. SAMPLE_CV backs the static demo. Production deployments bind user_id and CV context to authenticated sessions and structured outputs from Analyze (for example documents keyed by user or correlation id), so the agent tailors dialogue to the candidate’s profile.
Dependencies. interview-service/pyproject.toml: FastAPI with websockets, LangGraph + SQLite extras, LangChain, LangChain OpenAI, LangChain Google GenAI (optional provider), checkpoint SQLite, pydantic-settings.
Entrypoints. src/main.py: wires Container, waits for RabbitMQ, runs await rabbitmq_consumer.process_messages() (async forever).
Configuration. src/config.py: S3_*, RABBITMQ_* (inbound cv-analyze-stream, results cv-analysis-results), MONGODB_* (includes cv_analysis_collection_name), and shared LLM settings (LLM_PROVIDER, provider-specific keys/models, ANALYZE_LLM__TEMPERATURE default 0.2). Logging can be driven from config.yaml via the container’s dictConfig resource.
LLM wiring. src/agent/llm.py: LLMFactory.create_llm() uses the same LLM_PROVIDER switch as interview-service; CVAnalyzer calls structured output via with_structured_output(CVData).
Pipeline. RabbitMQConsumer:
- aio-pika
connect_robust,prefetch_count=5, durable subscription oncv-analyze-stream. - On message: JSON decode; if
routing_keyequals configured analyzer queue name →CVAnalyzeUseCase.
- Parse
CVInitialAnalysisMessage. - Download PDF bytes from S3 using message
url(IS3Client). - Extract text in a thread (
IPDFLoader, PyPDF-based implementation). - Run
CVAnalyzer: LangChain structured output intoCVData. insert_onedocument into Mongo (IMongoRepository).- Publish
CVResultAnalysisMessageas JSON viaRabbitMQProducertocv-analysis-results.
Docker. docker-compose.yaml: app service depends on RabbitMQ, MongoDB (Bitnami 5.0), LocalStack for S3. Dockerfile: worker-oriented image running python3 -m src.main (the default runtime is the async consumer, not an HTTP server).
Entrypoints. src/main.py: synchronous main() — waits for RabbitMQ, starts blocking consumer, closes Mongo on exit.
Packaging. pyproject.toml uses PEP 621 [project] metadata while retaining poetry-core as the build backend; dependencies include pika, pymongo, boto3, dependency-injector.
Configuration. src/config.py:
| Group | Env prefix | Notes |
|---|---|---|
| RabbitMQ | RABBITMQ_* |
Defaults: host rabbitmq, port 5672 |
| MongoDB | MONGODB_* |
Defaults: host mongodb, port 27017; URI construction supports authSource and related cluster settings |
| SES | SES_* |
AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY, region, sender |
Pipeline. RabbitMQConsumer: blocking pika, durable queue reset-password-stream, prefetch_count=5. Successful handling basic_ack; failures basic_nack(..., requeue=False) after logging.
ResetPasswordUseCase: validates ResetPasswordMessageModel, inserts into Mongo via repository context manager, retries SES send up to five times; raises NotSentError if every attempt fails.
Outbound wiring. OutboundAdaptersContainer: singleton MongoClient, factory SESClient wrapping boto3 SES.
There is no .env.example in this service; infer required variables from config.py.
| Technology | Service | Usage |
|---|---|---|
| PostgreSQL | user-management | users table · Alembic revision 0167f6ba46f8 |
| Redis | user-management | Refresh-token reuse prevention / rotation bookkeeping (refresh-key:{token}) |
| MongoDB | analyze-service | CV analysis documents in MONGODB_CV_ANALYSIS_COLLECTION_NAME |
| MongoDB | notification-service | Outbound message records in MONGODB_MESSAGES_COLLECTION_NAME |
| S3 | analyze-service | CV PDF objects (LocalStack or AWS) · vars S3_* |
| AWS SES | notification-service | Email delivery · SES_* + boto3 client |
- Python 3.12 for all runnable services
- Poetry — install per service directory
- Docker / Docker Compose — optional but practical for Postgres, Redis, RabbitMQ, MongoDB, LocalStack
git clone https://github.com/fedorakooo/AI-Interview-Tutor.git
cd AI-Interview-Tutor
poetry install
poetry run pre-commit installInfra: user-management-service/docker-compose.yaml.
cd user-management-service
cp .env.example .env # fill values (Postgres, Redis, JWT keys, RabbitMQ, ...)
poetry install
poetry run alembic upgrade head
poetry run uvicorn src.main:app --reload --host 0.0.0.0 --port 8000cd interview-service
cp .env.example .env # set LLM_PROVIDER and the matching API key (e.g. OPENAI_API_KEY)
poetry install
poetry run uvicorn src.main:app --reload --host 0.0.0.0 --port 8000Open http://localhost:8000/static/interview_client.html (or your chosen host/port).
cd analyze-service && poetry install && poetry run python -m src.maincd notification-service && poetry install && poetry run python -m src.mainLocal infra for analyze (RabbitMQ, MongoDB, LocalStack): analyze-service/docker-compose.yaml.
From the repository root after poetry install:
poetry run pre-commit run --all-filescd user-management-service
poetry install
poetry run pytestTests live under user-management-service/tests/ (unit + integration; integration may require Compose — see docker-compose.test.yaml).
This project is licensed under the Apache License 2.0 — see LICENSE.
