From Monolith to Modularity: Streamlining the Job-Hunter-Assistant with Python and Robust CI
The 'job-hunter-assistant' project is designed to streamline the job application process by automating tasks like job scraping and generating personalized CVs and cover letters. Initially, our approach to integrating large language models (LLMs) for content creation faced challenges, particularly concerning the project's architecture and the efficiency of our continuous integration pipeline. This post details our journey to a more modular, efficient, and reliable system.
The Situation
Our initial plan for the job-hunter-assistant involved a single, heavy Python script attempting to handle both job scraping and LLM interactions for CV and cover letter generation. This monolithic approach, coupled with a base Docker image like nvidia-cuda (intended for self-hosted LLMs or heavy GPU tasks), presented immediate roadblocks. It was akin to using a powerful supercomputer for simple arithmetic – overkill, inefficient, and resource-intensive, especially when our strategy shifted to using external LLM APIs.
The Descent into Modularity
Recognizing the inefficiencies, we pivoted to a more modular architecture. Instead of one script doing everything, we refactored the functionality into three distinct, focused Python scripts:
scraper.py: Dedicated solely to scraping job postings from various sources.cv_creator.py: Responsible for generating CV content by interacting with an external LLM API.cover_letter_creator.py: Handles cover letter generation, also via an external LLM API.
This separation allowed us to replace the bulky nvidia-cuda base image with a lightweight python:3.11-slim image, significantly reducing image size and build times. We removed all torch and CUDA-related code, as well as GPU resource reservations. This change was crucial for aligning our infrastructure with our new strategy of leveraging cloud-based LLM services.
Here's a conceptual Python snippet demonstrating how cv_creator.py or cover_letter_creator.py might interact with a generic LLM API:
import requests
import json
def generate_text_with_llm(prompt: str, api_endpoint: str, api_key: str) -> str:
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {"model": "generic-llm-model", "messages": [{"role": "user", "content": prompt}]}
try:
response = requests.post(api_endpoint, headers=headers, json=payload)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except requests.exceptions.RequestException as e:
return f"LLM API error: {e}"
This generate_text_with_llm function abstracts the details of calling an external LLM API, making the content generation scripts clean and focused on their core logic. It takes a prompt, the api_endpoint, and an api_key, then returns the generated text.
The Wake-Up Call: CI/CD Challenges
With our application architecture refined, attention shifted to the development pipeline itself. We encountered several issues with our GitHub Actions workflows: builds were failing due to missing dependencies and formatting errors, and overall build times were unacceptably slow. These issues created friction in our development process, delaying feedback and deployments.
What We Changed: Optimizing CI/CD
To address the CI/CD bottlenecks, we implemented a series of targeted fixes and optimizations:
- Frontend Dependency Fixes: A critical error was the absence of
package-lock.jsonfor frontend dependencies, which causednpm cito fail. Generating this file ensured consistent and reliable frontend builds. - Python Code Quality: We integrated and enforced code quality standards by fixing Python Black formatting issues and Flake8 linting errors (such as unused imports and trailing whitespace) across all Python scripts. This improved code readability and maintainability.
- Comprehensive Caching: To drastically reduce build times, we introduced robust caching strategies:
- NPM cache: Utilized
setup-node@v4to cache frontend dependencies. - Pip cache: Leveraged
setup-python@v5for caching Python dependencies (for both backend and scraper components). - Docker build cache: Implemented GitHub Actions cache for Docker image layers (
type=gha), ensuring that subsequent builds could reuse existing layers instead of rebuilding everything from scratch.
- NPM cache: Utilized
These changes ensured that all workflows now pass successfully with significantly faster build times, providing a smoother developer experience.
The Technical Lesson
Our journey with the job-hunter-assistant highlighted several key lessons in software engineering:
- Modularity over Monoliths: Breaking down complex functionality into smaller, specialized services (or scripts) improves maintainability, simplifies resource management, and enhances scalability. It allows for independent evolution and better resource allocation.
- Right Tool for the Job: Choosing the correct base image and dependencies is crucial. Opting for a slim Python image when interacting with external APIs is far more efficient than a heavy GPU-accelerated image if local GPU processing isn't required.
- CI/CD as a First-Class Citizen: A robust and efficient CI/CD pipeline is not an afterthought but a foundational element of any successful project. Investing in reliable builds, code quality checks, and intelligent caching pays dividends in developer productivity and faster feedback loops. Think of it like a well-oiled assembly line: each step must be efficient and reliable for the final product to be delivered quickly and correctly.
The Takeaway
Through architectural refactoring and rigorous CI/CD optimization, the job-hunter-assistant project evolved into a more performant and maintainable application. Embracing modular design principles and prioritizing CI/CD health are paramount for building resilient software systems that are both powerful and pleasant to work with. These changes not only streamlined our development but also prepared the project for future enhancements with a solid, efficient foundation.
Generated with Gitvlg.com