The modern job search is an overwhelming and repetitive grind. Candidates pour countless hours into tailoring resumes and filling out redundant forms, only to be met with silence or, if they're lucky, the cold efficiency of a templated rejection. It's the kind of email that lands in your inbox just moments after you hit 'submit', making it painfully clear your application was never even seen by human eyes. This cycle consumes valuable time that could be better spent preparing for interviews, developing new skills, or building professional networks.
Chameleon was built to confront this challenge. It's not just an automation tool, but a strategic assistant designed to give you back control. By leveraging the power of Large Language Models (LLMs), Chameleon intelligently handles the most tedious parts of the application process, from resume customization to filling out complex forms.
Our goal is to let you focus your energy on what truly matters: making valuable connections, showcasing your talent in interviews, and landing the role you truly deserve.
A sophisticated, AI-powered framework for automating the job application process across multiple platforms. Chameleon is designed with a robust, maintainable, and scalable architecture, leveraging modern software engineering principles to go far beyond simple scripting.
At its core, Chameleon uses a Large Language Model (LLM) to intelligently tailor your resume and application answers to the specific requirements of each job posting, significantly increasing the relevance and quality of your applications.
- AI-Powered Tailoring: Leverages Google's Gemini models via LangChain to dynamically rewrite resume sections and answer application questions based on job descriptions.
- Multi-Persona & Multi-Platform: Manage multiple professional personas (e.g., "Software Engineer," "Data Analyst") and apply to jobs on different platforms (initially LinkedIn) from a single, unified configuration.
- Hexagonal Architecture (Ports & Adapters): The core application logic is completely decoupled from external services, making the system highly testable, maintainable, and easy to extend with new job boards or LLMs.
- Robust Data Parsing Pipeline: A sophisticated multi-stage pipeline parses complex and volatile API data from LinkedIn's "Easy Apply" forms into a clean, structured format, ensuring reliability even when the source API changes.
- Intelligent Search Filtering: Employs an LLM-based "Crappy Search Mitigator" to filter out irrelevant job listings that often pollute platform search results, ensuring the bot focuses only on relevant opportunities.
- Automated Job Tracking: Integrates with Notion to automatically log every application attempt, its status, the job details, and the submitted answers, creating a powerful job tracking database.
- Dynamic PDF Generation: Creates beautifully formatted, tailored PDF resumes on the fly using XeLaTeX and Jinja2 templates for a professional finish.
- Full Proxy Support: Integrates with HTTP/S and SOCKS proxies for enhanced privacy and network flexibility.
- Configuration-Driven: Define personas, search filters, credentials, and templates in a simple
personas.ymlfile. No code changes are needed to alter search strategies. - Comprehensive Testing: A strong suite of unit and integration tests ensures reliability and correctness across the complex parsing and application pipeline.
Chameleon is built using the Ports and Adapters (Hexagonal) Architecture. This design pattern isolates the core application logic from outside concerns like APIs, databases, or file systems.
- The Core (The Hexagon): Contains the application's business logic (
use_cases) and domain models (domain/models.py). It knows nothing about LinkedIn, Google Gemini, or Notion. - Ports: Simple interfaces defined in the
core/portsdirectory that act as contracts. They define what the application needs to do (e.g.,JobBoardPort,LLMPort,JobTrackerPort). - Adapters: Concrete implementations of the ports that interact with the outside world. These live in the
adaptersdirectory. For example, theLinkedInAdapterimplements theJobBoardPortto talk to the LinkedIn API.
This separation allows us to swap out implementations easily. We could add an "Indeed" adapter or switch to an OpenAI LLM without changing a single line of code in the core application.
graph TD
subgraph User-Side Adapters
A[main.py / CLI]
end
subgraph "Core Application (The Hexagon)"
B(Use Cases e.g., FindAndApplyToJobsUseCase)
subgraph Ports
P1(JobBoardPort)
P2(LLMPort)
P3(JobTrackerPort)
P4(AttachmentImportPort)
P5(IAttachmentBuilderService)
end
end
subgraph Infrastructure-Side Adapters
Ad1(LinkedInAdapter)
Ad2(LangChainAdapter)
Ad3(NotionJobTrackerAdapter)
Ad4(LocalAttachmentImportAdapter)
Ad5(AttachmentBuilderService / PDFProcessor)
end
A --> B
B --> P1
B --> P2
B --> P3
B --> P4
B --> P5
P1 --> Ad1
P2 --> Ad2
P3 --> Ad3
P4 --> Ad4
P5 --> Ad5
Ad1 --> C((LinkedIn API))
Ad2 --> D((Google Gemini API))
Ad3 --> E((Notion API))
Ad4 --> F((File System - Templates))
Ad5 --> G((XeLaTeX Engine))
flowchart TD
subgraph subGraph0["1- Initialization"]
B("Load Persona Configurations from .yml & .env")
A("Start main.py")
end
subgraph subGraph1["2- Persona Workflow"]
C{"For each Persona"}
D["Initialize Adapters"]
end
subgraph subGraph2["3- Job Discovery"]
E["Find Jobs via Job Board Adapter"]
F{"Loop through<br>Job Stream"}
end
subgraph subGraph3["4- Application Cycle (for each job)"]
G{"Already Applied?<br>(Check Job Tracker)"}
H["1- Start Session & Fetch Application Form"]
I["2- Parse Form & Generate LLM Schema"]
J["3- Invoke LLM <br>(Tailor Resume & Answer Questions)"]
K["4- Build Final PDF Attachment"]
L["5- Submit Application via Job Board API (e.g. LinkedIn, Indeed, etc.)"]
M["6- Update Job Tracker in persistence (e.g. Notion)"]
end
subgraph subGraph4["5- Completion"]
Z("End")
end
A --> B
B --> C
C --> D
D --> E
E --> F
F -- Job Found --> G
G -- Yes --> F
G -- No --> H
H --> I
I --> J
J --> K
K --> L
L --> M
M --> F
F -- No More Jobs --> C
C -- All Personas Processed --> Z
- Language: Python 3.10+
- Core Framework: Asyncio for concurrent API operations
- Configuration: Pydantic for validation, PyYAML for parsing
- AI & LLM: LangChain, Google Gemini
- API Communication: httpx, httpx-socks
- Job Tracking: Notion API
- PDF Generation: Jinja2, XeLaTeX
- Testing: unittest, unittest.mock
- Python 3.10 or higher
- Poetry for dependency management
- A working installation of XeLaTeX (e.g., via MiKTeX on Windows, MacTeX on macOS, or TeX Live on Linux).
- API Keys & Credentials:
- Google Gemini API Key
- Notion API Token and a duplicated version of the Job Tracking Database Template
- LinkedIn
li_atandJSESSIONID(csrf-token) cookies
git clone https://github.com/eljaviluki/chameleon.git
cd chameleonpoetry installConfiguration is split into two main files: secrets (.env) and application behavior (config/personas.yml).
a. Create the .env file for secrets:
Create a .env file in the project root. This file stores sensitive information and should never be committed to version control.
# --- LLM Configuration ---
GOOGLE_API_KEY="your_google_ai_api_key"
LLM_MODEL="gemini-2.5-flash" # Or another supported model
# --- Notion Job Tracker Configuration ---
NOTION_API_TOKEN="your_notion_integration_token"
NOTION_JOB_TRACKING_DB_ID="the_id_of_your_notion_database"b. Customize the config/personas.yml file:
This file is the main control panel. It defines your professional personas, job search filters, credentials, and templates. Open config/personas.yml and modify it to your needs.
Here is an example structure:
# This is a dictionary of personas. You can define as many as you like.
SoftwareEngineer_EU:
platform_configs:
- platform: "linkedin"
name: "LinkedIn SWE Search"
job_board_account:
li_at_token: "your_li_at_cookie_value"
csrf_token: "your_JSESSIONID_cookie_value"
password: "your_linkedin_password"
connection_config:
linkedin:
accept_language: "en-US"
user_agent: "Mozilla/5.0 ..."
# Optional: proxy_url: "socks5://user:pass@host:port"
prompt_config:
template: "prompts/machiavellian_prompt.md"
pdf_config:
resume_data_template: "config/resume_data_template.json"
resume_template: "templates/resume.tex.j2"
output_dir: "attachments/software_engineer"
search_filter_set:
filters:
- keywords: "Senior Python Engineer"
location: "SPAIN"
easy_apply_only: True
workplace_types: ["REMOTE", "HYBRID"]
weight: 10 # This search is 10x more likely to be picked than one with weight 1
- keywords: "Backend Developer (Golang)"
location: "SWITZERLAND"
easy_apply_only: True
workplace_types: ["REMOTE"]
weight: 5
personal_data:
# Your personal information for filling out forms and templates
name:
full: "Jane Doe"
first: "Jane"
last: "Doe"
contact:
email: "jane.doe@example.com"
phone:
number: "600123456"
country:
name: "Spain (+34)"
location:
address:
street: "123 Main St"
city:
name: "Madrid, Community of Madrid, Spain"
postal_code: "28001"
state: "Madrid"
county: "Madrid"c. Configure your Resume Data Template:
Edit the file specified in pdf_config.resume_data_template (e.g., config/resume_data_template.json). This file defines the structure and base content of your resume. The LLM will use this as a schema to generate tailored content for each job application. Mark fields you want the AI to fill with {"__schema__": true}.
Once configured, you can start the automation process:
poetry run python src/main.py```
The application will concurrently run the defined workflows for each persona in your `personas.yml`.
## β
Running Tests
To ensure everything is working correctly and to run the test suite:
```bash
poetry run python -m unittest discover -s tests.
βββ config/
β βββ logging.yml # Configuration for logging
β βββ personas.yml # Main application configuration for personas
β βββ resume_data_template.json # Schema and base data for the resume
βββ logs/ # Log files are generated here
βββ src/
β βββ core/ # Application Core (Hexagon)
β β βββ application/ # Use Cases (e.g., FindAndApplyToJobs)
β β βββ domain/ # Domain Models (JobDetails, etc.)
β β βββ ports/ # Port interfaces (the contracts)
β βββ adapters/ # Implementations of Ports
β β βββ job_board/ # Adapters for LinkedIn, Indeed, etc.
β β βββ llm/ # Adapter for LangChain/Gemini
β β βββ pdf_creation/ # Adapter for XeLaTeX PDF generation
β β βββ job_tracker/ # Adapter for Notion
β βββ utils/ # Shared utility functions
β βββ main.py # Application entry point
βββ templates/
β βββ resume.tex.j2 # Jinja2 template for the PDF resume
βββ tests/ # Unit and integration tests
βββ tools/ # Helper scripts for development
βββ poetry.lock # Dependency lock file
One of the most complex parts of this project is reliably parsing LinkedIn's "Easy Apply" forms. The API response is deeply nested and can change. This project solves this with a robust, multi-stage pipeline located in src/adapters/job_board/linkedin/parsers/pipeline/.
- SourceFormParser: Takes the raw, messy JSON from the API and, using Pydantic models as a validation layer, transforms it into a clean, stable Intermediate Representation (IR).
- SchemaGenerator: Reads the IR and generates a formal JSON Schema. This schema describes the form's questions, types, and constraints (e.g., "this field is a number between 1 and 10").
- LlmRelevantSchemaFilter: A clever optimization step that removes Personally Identifiable Information (PII) and other pre-fillable fields (like your name or email) from the JSON Schema. This creates a smaller, cheaper, and safer schema to send to the LLM.
- PayloadSerializer: After the LLM provides answers for the filtered schema, this final step merges the LLM's answers with your PII data and serializes everything into the exact JSON payload format required by the LinkedIn API for submission.
This entire flow is orchestrated by the LinkedInEasyApplyPipeline class, providing a clean facade over this complex process.
The tools/ directory contains several helpful scripts:
debug_pdf_generator.py: Generate a PDF from a JSON data file and your.tex.j2template without running the full application. Perfect for iterating on your resume design.check_proxy.py: Quickly verify if your SOCKS or HTTP proxy is configured correctly and reachable.anonymize_json.py: A script to scrub sensitive information from JSON files, useful for creating safe test fixtures or sharing bug reports.get_schema.py: Connects to the Notion API to fetch and print the schema of your job tracking database, which is useful for debugging.
Contributions are welcome! Please feel free to open an issue or submit a pull request.
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
