Metadata-Version: 2.4
Name: job-hunt-agent
Version: 0.1.0
Summary: Job hunting + tailored CV generation agent built with LangChain deepagents
Project-URL: Homepage, https://github.com/EceDalpolat/job-hunt-agent
Project-URL: Repository, https://github.com/EceDalpolat/job-hunt-agent
Project-URL: Issues, https://github.com/EceDalpolat/job-hunt-agent/issues
Author-email: Ece Dalpolat <ecedlplt9850@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: agent,cv,deepagents,job-search,langchain,llm,resume
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Requires-Python: >=3.11
Requires-Dist: deepagents>=0.2
Requires-Dist: docx2txt>=0.9
Requires-Dist: httpx
Requires-Dist: langchain-anthropic
Requires-Dist: langchain-openai>=1.3.3
Requires-Dist: pypdf>=6.14.2
Requires-Dist: python-dotenv
Requires-Dist: python-jobspy>=1.1.82
Requires-Dist: streamlit>=1.58.0
Description-Content-Type: text/markdown

# 💼 Job Hunt Agent

An end-to-end **job search + tailored CV generation** agent built with
[LangChain deepagents](https://docs.langchain.com/oss/python/deepagents):

1. builds your master profile (`profile/profile.md`) from an uploaded CV,
2. searches postings on LinkedIn/Indeed and more (JobSpy scraper — no API key
   needed; optional JSearch API),
3. ranks them against your profile with honest fit scores,
4. for every job you pick, a **cv-tailor subagent** writes a job-specific CV +
   cover letter (XYZ-formula bullets, ATS-friendly) into `applications/`,
5. starts an application **only with your approval** (human-in-the-loop interrupt).

## What is deepagents and how is it used here?

Deepagents is an agent harness that ships with planning (`write_todos`), a
virtual file system, **subagent** delegation (`task` tool) and **human
approval** (interrupts). The mapping in this project:

| Deepagents feature | Here |
|---|---|
| `create_deep_agent(tools=...)` | `search_jobs`, `get_job_details`, `read_profile`, `submit_application` |
| `subagents=[...]` | `cv-tailor`: writes the CV/cover letter in an isolated context |
| `interrupt_on={...}` + checkpointer | `submit_application` never runs without human approval |
| `write_todos` (built-in) | The agent plans multi-job tasks itself |
| Model | Anthropic, OpenRouter or OpenAI — whichever key you set |

## Setup

```bash
git clone https://github.com/EceDalpolat/job-hunt-agent && cd job-hunt-agent
uv sync                      # or: pip install -e .
cp .env.example .env         # fill in ONE of the API keys (see below)
```

- For the model, set **one** of `ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY` or
  `OPENAI_API_KEY` (priority in that order). Override the default model with
  `JOB_AGENT_MODEL` — e.g. `openai/gpt-5.1` on OpenRouter.
- Job search needs no key: [JobSpy](https://github.com/speedyapply/JobSpy)
  scrapes LinkedIn + Indeed directly. LinkedIn rate-limits aggressively; if it
  blocks you, Indeed results keep coming. Setting `RAPIDAPI_KEY` switches to the
  [JSearch](https://rapidapi.com/letscrape-6bRBa3QguO5/api/jsearch) API instead.
  If both fail, the agent falls back to 6 bundled sample postings.

## Create your profile

```bash
uv run job-agent-profile path/to/your-cv.pdf --notes "linkedin.com/in/you, target: AI engineer roles"
```

This extracts the text from your CV (pdf/docx/md/txt), normalizes it with the
LLM into `profile/profile.md`, and that file becomes the **single source of
truth** — the agent never puts anything on a CV that is not in it. Review it
before applying anywhere. See [profile/profile.example.md](profile/profile.example.md)
for the expected shape. (`profile/profile.md` is gitignored — your personal data
stays local.)

## Run

```bash
uv run job-agent                          # terminal CLI
uv run streamlit run job_agent/ui.py     # web UI
```

In the Streamlit UI: upload your CV in the sidebar to build the profile, chat to
search postings ("find AI/LLM engineer jobs in Istanbul, rank the top 5"), say
"prepare CVs for 1 and 3", approve applications with ✅/❌ buttons, download the
generated CV/cover letters from the sidebar, and watch live token/cost metrics.

Example CLI session:

```
👤 > find python backend jobs that fit me
🤖 (reads your profile, searches postings, returns a fit-scored list)

👤 > I want to apply to 1 and 3
🤖 (cv-tailor subagent writes a CV + cover letter per job into
    applications/2026-07-06-healthtech-labs-senior-backend.../)

⚠️  Pending approval:
   Tool : submit_application
   Args : {'job_id': 'mock-001', ...}
   Approve? [y/n]: y
🤖 Application logged, apply page opened in your browser.
💰 7 LLM calls • 41,203 in + 2,841 out tokens • ~$0.16
```

## Architecture

```
                    ┌─────────────────────────────────────────────┐
 Entry points       │  main.py (CLI)         ui.py (Streamlit)    │
                    │  setup_logging() + UsageTracker + thread_id │
                    └────────────────┬────────────────────────────┘
                                     │ agent.invoke(message, callbacks=[tracker])
                    ┌────────────────▼────────────────────────────┐
 Orchestration      │  agent.py → build_agent()                   │
                    │  _resolve_model(): ANTHROPIC > OPENROUTER > │
                    │                    OPENAI (from config.py)  │
                    │  system prompt: prompts/system.md           │
                    └───────┬──────────────────────┬──────────────┘
                            │ tool calls           │ task (subagent)
                ┌───────────▼──────────┐  ┌────────▼───────────────┐
 Tools          │ tools.py             │  │ cv-tailor subagent     │
                │ search_jobs:         │  │ prompt: cv_tailor.md   │
                │  JSearch>JobSpy>mock │  │ reads profile + the    │
                │ get_job_details      │  │ references/ guides,    │
                │ read_profile         │  │ writes XYZ-format CV,  │
                │ submit_application ←─┼──│ save_application_pkg   │
                │  (human approval!)   │  └────────────────────────┘
                └───────────┬──────────┘
                ┌───────────▼──────────────────────────────────────┐
 Data & output  │ schemas.py (Job, UsageRecord)   config.py (paths)│
                │ applications/  logs/job_agent.log  logs/usage.jsonl│
                └──────────────────────────────────────────────────┘
```

Flow of one "apply to 1 and 3" turn:
1. `main.py`/`ui.py` passes the user message to `agent.invoke(...)`; the
   `UsageTracker` callback captures the tokens of every LLM call (main agent
   AND subagents).
2. The main agent (prompt: `prompts/system.md`) calls `search_jobs` — `tools.py`
   tries JSearch API → JobSpy scraper → sample data in order, normalizes results
   to `schemas.Job` and caches them in `_JOB_CACHE`.
3. For each selected job the main agent spawns the **cv-tailor subagent** via
   the `task` tool (prompt: `prompts/cv_tailor.md`). The subagent reads the
   profile and the writing guides in `references/`, writes the CV/cover letter
   and saves them with `save_application_package` under `applications/`.
4. `submit_application` pauses on `interrupt_on`; y/n in the CLI or ✅/❌ in the
   UI resumes the flow with `Command(resume={"decisions": ...})`.
5. Every step is logged to `logs/job_agent.log`; every LLM call's tokens/cost go
   to `logs/usage.jsonl` (rates in `config.PRICING` — approximate, keep updated).

## File map

```
job_agent/
├── config.py          # paths, model defaults, pricing table (single place)
├── schemas.py         # Pydantic: Job, UsageRecord, ApplicationLogEntry
├── prompts/           # system.md, cv_tailor.md, profile_builder.md (out of code)
├── logging_conf.py    # logs/job_agent.log (rotating) + console warnings
├── usage.py           # UsageTracker callback → tokens/cost → usage.jsonl
├── agent.py           # deep agent wiring, model selection, subagent, HITL
├── tools.py           # search (JSearch>JobSpy>sample), profile, references, save, apply
├── profile_builder.py # CV file → LLM → profile/profile.md
├── main.py            # interactive CLI
├── ui.py              # Streamlit chat UI (approvals, downloads, cost metrics)
└── sample_jobs.py     # sample postings (last-resort fallback)
profile/               # profile.md (gitignored) + profile.example.md
references/            # CV writing guides (XYZ formula, Harvard guide, ...)
logs/                  # job_agent.log + usage.jsonl (gitignored)
applications/          # generated packages (gitignored)
```

## Honesty notes

- **No automated form submission:** neither the job boards' APIs nor their
  terms of service allow auto-filling application forms. The agent prepares the
  package, logs the application and opens the apply page; you press the final
  "Submit".
- **No fabrication on CVs:** the subagent is instructed to use only the real
  facts in `profile/profile.md` — still, read everything before sending it.
