Spaces:
Sleeping
Sleeping
| """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") | |
| 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}") | |
| 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") | |
| 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) | |
| 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) | |
| 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}") | |
| 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() | |