talentry-ai / src /talentry /cli /rank.py
williyam's picture
deploy: sync from talentry-ai @ 86a1a30
1f7b170
Raw
History Blame Contribute Delete
4.12 kB
"""``talentry-rank`` - the one command that produces the submission CSV.
This is the entry point pinned in ``submission_metadata.yaml``'s
``reproduce_command`` field:
python -m talentry.cli.rank --candidates ./candidates.jsonl --out ./submission.csv
The command is deliberately tiny - all the engineering lives in the package,
and this file just argparses, dispatches and reports.
"""
from __future__ import annotations
import argparse
import logging
import sys
import time
from pathlib import Path
from talentry import __version__
from talentry.io.candidates import load_candidates
from talentry.io.submission import write_submission, write_submission_xlsx
from talentry.ranker import parse_job_description, rank_candidates
def _build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="talentry-rank",
description="Rank the top 100 candidates from a Redrob JSONL pool against a JD.",
)
p.add_argument(
"--candidates",
required=True,
type=Path,
help="Path to candidates.jsonl (gzip ok).",
)
p.add_argument(
"--jd",
type=Path,
default=None,
help="Path to job description text file. Defaults to the default JD shipped with the package.",
)
p.add_argument(
"--out",
type=Path,
default=Path("submission.csv"),
help=(
"Output path. Format is inferred from the extension: "
".csv -> CSV, .xlsx -> Excel. Default: submission.csv"
),
)
p.add_argument(
"--also-xlsx",
action="store_true",
help="Also emit a sibling .xlsx file next to --out (handy for human review).",
)
p.add_argument(
"--top-k",
type=int,
default=100,
help="Number of candidates to keep. Default: 100 (hackathon requirement).",
)
p.add_argument(
"--limit",
type=int,
default=None,
help="Optional cap on candidates loaded (useful for smoke tests).",
)
p.add_argument(
"--quiet",
action="store_true",
help="Suppress per-stage progress logging.",
)
p.add_argument("--version", action="version", version=f"talentry-ai {__version__}")
return p
def main(argv: list[str] | None = None) -> int:
args = _build_parser().parse_args(argv)
logging.basicConfig(
level=logging.WARNING if args.quiet else logging.INFO,
format="%(message)s",
stream=sys.stderr,
)
log = logging.getLogger("talentry.cli")
log.info("[talentry-rank] v%s", __version__)
log.info("[talentry-rank] loading candidates from %s …", args.candidates)
t0 = time.perf_counter()
candidates = load_candidates(args.candidates, limit=args.limit)
log.info(
"[talentry-rank] loaded %d candidates in %.2fs", len(candidates), time.perf_counter() - t0
)
jd = parse_job_description(args.jd)
log.info("[talentry-rank] JD parsed - title=%r seniority=%s", jd.title, jd.seniority)
ranked = rank_candidates(candidates, jd, top_k=args.top_k, progress=not args.quiet)
if len(ranked) < args.top_k:
log.warning(
"[talentry-rank] only %d candidates ranked (< top_k=%d). "
"Submission requires exactly 100 rows.",
len(ranked),
args.top_k,
)
strict = args.top_k == 100
out_ext = args.out.suffix.lower()
if out_ext == ".xlsx":
out = write_submission_xlsx(ranked, args.out, strict=strict)
else:
out = write_submission(ranked, args.out, strict=strict)
log.info(
"[talentry-rank] wrote %s submission → %s",
"strict-validated" if strict else f"non-strict (top_k={args.top_k})",
out,
)
# Optional sibling .xlsx for human review.
if args.also_xlsx and out_ext != ".xlsx":
sibling = args.out.with_suffix(".xlsx")
write_submission_xlsx(ranked, sibling, strict=strict)
log.info("[talentry-rank] also wrote XLSX → %s", sibling)
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())