-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
171 lines (146 loc) · 6.24 KB
/
Copy pathcli.py
File metadata and controls
171 lines (146 loc) · 6.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""Command-line interface; run with --help for options."""
from __future__ import annotations
import argparse
import os
import sys
from jobpilot import history
from jobpilot.agent import Agent
from jobpilot.export import export
from jobpilot.llm import LLMError, OllamaClient
from jobpilot.pipeline import ats_check, generate_cover_letter, generate_rewrite, rank_jobs
def main() -> int:
ap = argparse.ArgumentParser(description="JobPilot: local AI job-application agent")
ap.add_argument("--resume", help="Path to resume (PDF/DOCX/MD/TXT)")
ap.add_argument("--job-url", default="", help="URL of the job posting")
ap.add_argument("--job-file", default="", help="Text file containing the job description")
ap.add_argument("--ats-only", action="store_true",
help="Standalone ATS test, no agent (job optional)")
ap.add_argument("--rank", nargs="+", metavar="JOB_FILE",
help="Rank the resume against several posting files")
ap.add_argument("--rewrite", action="store_true",
help="Generate the rewritten resume without asking")
ap.add_argument("--cover-letter", action="store_true",
help="Generate a cover letter without asking (needs a job)")
ap.add_argument("--history", action="store_true", help="Show past runs and score changes")
args = ap.parse_args()
if args.history:
return _print_history()
if not args.resume:
ap.error("--resume is required (except with --history)")
if args.rank:
return _rank(args)
job_text = ""
if args.job_file:
with open(args.job_file, "r", encoding="utf-8") as f:
job_text = f.read()
if not args.ats_only and not (job_text or args.job_url):
ap.error("full agent mode needs --job-url or --job-file (or use --ats-only)")
if args.ats_only:
try:
ctx = ats_check(args.resume, job_text=job_text, job_url=args.job_url)
except (ValueError, LLMError) as exc:
print(exc, file=sys.stderr)
return 1
_print_ats(ctx.ats_report)
if ctx.extras.get("resume_review"):
print("\n" + "=" * 60 + "\n" + ctx.extras["resume_review"])
else:
def show(step):
if step["type"] == "tool_call":
print(f"→ [{step['t']}s] calling {step['tool']}({step['args'] or ''})")
elif step["type"] == "tool_result":
print(f" {'✓' if step['ok'] else '✗'} {step['detail'][:160]}")
elif step["type"] == "error":
print(f" ! {step['detail']}", file=sys.stderr)
result = Agent(OllamaClient()).run(
resume_path=args.resume, job_url=args.job_url, job_text=job_text, on_step=show
)
ctx = result["context"]
print("\n" + "=" * 60)
print(result["answer"])
if ctx.report_path:
print(f"\nFull report: {ctx.report_path}")
_offer_rewrite(ctx, args)
_offer_cover_letter(ctx, args)
return 0
def _offer_rewrite(ctx, args) -> None:
question = (
"Want to see your resume rewritten for this job?"
if ctx.job_info
else "Want a modified resume with an improved ATS score?"
)
wants = args.rewrite or (sys.stdin.isatty() and _ask(question))
if not wants:
return
print("\nRewriting resume (never invents experience)...")
md = generate_rewrite(ctx)
print("=" * 60 + "\n" + md)
base = os.path.splitext(ctx.extras["rewritten_resume_path"])[0]
for fmt in ("pdf", "docx"):
data, _ = export(md, fmt)
with open(f"{base}.{fmt}", "wb") as f:
f.write(data)
print(f"\nSaved: {base}.md / .pdf / .docx")
def _offer_cover_letter(ctx, args) -> None:
if not ctx.job_info:
return
wants = args.cover_letter or (sys.stdin.isatty() and _ask("Also want a cover letter for it?"))
if not wants:
return
print("\nWriting cover letter (grounded in your resume)...")
md = generate_cover_letter(ctx)
print("=" * 60 + "\n" + md)
path = os.path.join("reports", "cover-letter")
os.makedirs("reports", exist_ok=True)
for fmt in ("md", "pdf", "docx"):
data, _ = export(md, fmt)
with open(f"{path}.{fmt}", "wb") as f:
f.write(data)
print(f"\nSaved: {path}.md / .pdf / .docx")
def _ask(question: str) -> bool:
return input(f"\n{question} [y/N] ").strip().lower() in ("y", "yes")
def _rank(args) -> int:
texts = []
for path in args.rank:
with open(path, "r", encoding="utf-8") as f:
texts.append(f.read())
def progress(e):
print(f" scored {e['index']}/{e['total']}: {e['title']} — {e['overall']}/100")
try:
ranked = rank_jobs(args.resume, texts, on_progress=progress)
except (ValueError, LLMError) as exc:
print(exc, file=sys.stderr)
return 1
print(f"\n{'#':<3}{'score':<8}{'grade':<12}job")
print("-" * 60)
for i, e in enumerate(ranked, 1):
print(f"{i:<3}{e['overall']:<8}{e['grade']:<12}{e['title']} @ {e['company']}")
if e["missing_required"]:
print(f"{'':>23}missing: {', '.join(e['missing_required'][:6])}")
return 0
def _print_history() -> int:
entries = history.load()
if not entries:
print("No runs recorded yet.")
return 0
print(f"{'when':<20}{'score':<8}{'Δ':<7}{'resume':<28}job")
print("-" * 90)
for e in entries[:25]:
delta = "" if e["delta"] is None else f"{e['delta']:+.1f}"
job = f"{e['job_title']} @ {e['company']}" if e["job_title"] else "(health check)"
print(f"{e['ts']:<20}{e['overall']:<8}{delta:<7}{e['resume'][:26]:<28}{job}")
return 0
def _print_ats(ats) -> None:
print(f"\nATS score: {ats['overall']}/100 ({ats['grade']})")
for name, res in ats["breakdown"].items():
score = res.get("score")
print(f" {name:10s} {'skipped' if score is None else score}")
kw = ats["breakdown"]["keywords"]
if kw.get("missing_required"):
print(" missing required: " + ", ".join(kw["missing_required"]))
if kw.get("missing_keywords"):
print(" missing keywords: " + ", ".join(kw["missing_keywords"][:10]))
for issue in ats["breakdown"]["quality"]["issues"]:
print(f" issue: {issue}")
if __name__ == "__main__":
sys.exit(main())