Spaces:
Sleeping
Sleeping
File size: 7,100 Bytes
74711df | 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | """Application module."""
from pathlib import Path
import typer
from src.core.config import get_settings
from src.core.logging import configure_logging, get_run_logger
from src.ingest.service import IngestionService
from src.ranking.workflow import RankingWorkflow
from src.storage.db import get_session
app = typer.Typer(help="thereisnohr ATS CLI")
@app.command()
def ingest(path: Path) -> None:
"""Runs ingest logic.
Args:
path (Path): Filesystem path of the file being parsed or ingested.
"""
settings = get_settings()
configure_logging(settings.log_level)
log = get_run_logger(__name__)
log.info("ingest command received", extra={"path": str(path)})
typer.echo(f"Ingestion placeholder for: {path}")
@app.command()
def index() -> None:
"""Runs index logic."""
settings = get_settings()
configure_logging(settings.log_level)
log = get_run_logger(__name__)
log.info("index command received")
typer.echo("Indexing placeholder")
@app.command("ingest-job")
def ingest_job_cmd(path: Path, title: str | None = typer.Option(None, help="Job title")) -> None:
"""Extracts requirements and persists a new job posting from a text file."""
settings = get_settings()
configure_logging(settings.log_level)
log = get_run_logger(__name__)
if not path.exists():
typer.secho(f"Error: File not found at {path}", fg=typer.colors.RED)
raise typer.Exit(1)
description = path.read_text(encoding="utf-8")
job_title = title or path.stem.replace("_", " ").title()
log.info("ingest-job command received", extra={"path": str(path), "title": job_title})
with get_session() as session:
service = IngestionService()
job_id = service.ingest_job(title=job_title, description=description, session=session)
session.commit()
typer.secho(f"Successfully ingested job posting (ID: {job_id})", fg=typer.colors.GREEN)
@app.command()
def rank(job_id: int, top_k: int = 5) -> None:
"""Retrieves and ranks candidates for a specific job posting.
Args:
job_id (int): Database ID of the job posting to rank against.
top_k (int): Number of top candidates to return.
"""
settings = get_settings()
configure_logging(settings.log_level)
log = get_run_logger(__name__)
log.info("rank command received", extra={"job_id": job_id, "top_k": top_k})
with get_session() as session:
workflow = RankingWorkflow(session)
ranked = workflow.run(job_id=job_id, top_k=top_k)
session.commit()
typer.secho(
f"\nTop {len(ranked[:top_k])} Candidates for Job {job_id}:", fg=typer.colors.CYAN, bold=True
)
for r in ranked[:top_k]:
color = typer.colors.GREEN if r.rank == 1 else typer.colors.WHITE
typer.secho(
f"{r.rank}. Candidate ID: {r.candidate_id} | Score: {r.scores.final_score:.2f}",
fg=color,
)
if r.explanation:
typer.echo(f" Summary: {r.explanation.evidence_based_summary}")
if r.explanation.gaps_and_risks:
typer.echo(" Gaps/Risks:")
for gap in r.explanation.gaps_and_risks:
typer.echo(f" - {gap.missing_requirement}: {gap.impact}")
typer.echo("-" * 40)
@app.command()
def prep(job_id: int, candidate_id: int) -> None:
"""Displays or generates an interview preparation pack for a candidate."""
from src.storage import models
from src.storage.repositories import MatchRepository, ResumeRepository
from src.ranking.service import RankingService
from src.extract.types import CandidateSignals, JobRequirements
from src.ranking.types import InterviewPrepPack, RankExplanation, RankInput
settings = get_settings()
configure_logging(settings.log_level)
with get_session() as session:
match_repo = MatchRepository(session)
match = match_repo.get_by_job_and_candidate(job_id, candidate_id)
if not match:
typer.secho(
f"No match found for Job {job_id} and Candidate {candidate_id}.",
fg=typer.colors.RED,
)
raise typer.Exit(1)
if match.interview_pack_json:
pack = InterviewPrepPack.model_validate(match.interview_pack_json)
else:
typer.echo("Generating interview preparation pack...")
job = session.get(models.JobPosting, job_id)
requirements = JobRequirements.model_validate(job.requirements_json)
resume_repo = ResumeRepository(session)
latest_resume = resume_repo.get_latest_resume_by_candidate_id(candidate_id)
if not latest_resume or not latest_resume.signals_json:
typer.secho("Missing candidate signals.", fg=typer.colors.RED)
raise typer.Exit(1)
signals = CandidateSignals.model_validate(latest_resume.signals_json)
rank_input = RankInput(
candidate_id=candidate_id,
retrieval_score=match.retrieval_score or 0.0,
requirements=requirements,
signals=signals,
)
explanation = None
if (
match.reasons_json
and "explanation" in match.reasons_json
and match.reasons_json["explanation"]
):
explanation = RankExplanation.model_validate(match.reasons_json["explanation"])
if not explanation:
typer.secho(
"No ranking explanation found to base the prep pack on.", fg=typer.colors.RED
)
raise typer.Exit(1)
ranking_service = RankingService()
pack = ranking_service.generate_interview_pack(rank_input, explanation)
if pack:
match.interview_pack_json = pack.model_dump()
session.commit()
typer.secho("Interview pack generated and saved.", fg=typer.colors.GREEN)
else:
typer.secho("Failed to generate interview pack.", fg=typer.colors.RED)
raise typer.Exit(1)
typer.secho(
f"\nInterview Preparation Pack (Job {job_id}, Candidate {candidate_id})",
fg=typer.colors.CYAN,
bold=True,
)
typer.secho("\nTechnical Questions:", fg=typer.colors.YELLOW, bold=True)
for q in pack.technical_questions:
typer.echo(f" - {q}")
typer.secho("\nBehavioral Questions:", fg=typer.colors.YELLOW, bold=True)
for q in pack.behavioral_questions:
typer.echo(f" - {q}")
typer.secho("\nClarification Questions (Gaps/Risks):", fg=typer.colors.YELLOW, bold=True)
for q in pack.clarification_questions:
typer.echo(f" - {q}")
@app.command("ingest-flow-help")
def ingest_flow_help() -> None:
"""Show how to run the Metaflow PDF ingestion pipeline."""
typer.echo(
"Run ingestion flow with: uv run python src/ingest/pdf_ingestion_flow.py run --input-dir data --pattern '*.pdf'"
)
if __name__ == "__main__":
app()
|