Skip to content

Commit 5e5514c

Browse files
authored
Merge pull request #62 from HappyHackingSpace/feat/cli
feat: add CLI for resume analysis via levelup command
2 parents 7f9ee77 + ba9631c commit 5e5514c

3 files changed

Lines changed: 695 additions & 487 deletions

File tree

levelup/cli.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import json
2+
import re
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
import google.generativeai as genai
7+
import pdfplumber
8+
import typer
9+
10+
from levelup import config
11+
from levelup.prompts import get_resume_analysis_prompt
12+
13+
app = typer.Typer(name="levelup", help="AI-powered CV analysis from the command line.")
14+
15+
LANGUAGES = [
16+
"Czech",
17+
"Danish",
18+
"Dutch",
19+
"English",
20+
"Finnish",
21+
"French",
22+
"German",
23+
"Greek",
24+
"Italian",
25+
"Kurdish (Kurmanji)",
26+
"Polish",
27+
"Portuguese",
28+
"Russian",
29+
"Spanish",
30+
"Swedish",
31+
"Turkish",
32+
"Ukrainian",
33+
]
34+
35+
36+
def _extract_text(pdf_path: Path) -> str:
37+
with pdfplumber.open(pdf_path) as pdf:
38+
parts = [page.extract_text() or "" for page in pdf.pages]
39+
return "\n".join(parts).strip()
40+
41+
42+
def _extract_json(raw: str) -> dict | None:
43+
fence = re.search(r"```(?:json)?\s*({[\s\S]*?})\s*```", raw, re.IGNORECASE)
44+
if fence:
45+
block = fence.group(1)
46+
else:
47+
match = re.search(r"\{[\s\S]*\}", raw)
48+
block = match.group(0) if match else None
49+
if not block:
50+
return None
51+
try:
52+
result = json.loads(block)
53+
return result if isinstance(result, dict) else None
54+
except json.JSONDecodeError:
55+
return None
56+
57+
58+
@app.command()
59+
def analyze(
60+
resume: Path = typer.Argument(..., help="Path to the PDF resume file."),
61+
language: str = typer.Option(
62+
"English", "--language", "-l", help="Report language."
63+
),
64+
role: Optional[str] = typer.Option(
65+
None, "--role", "-r", help="Target role for the analysis."
66+
),
67+
output: Optional[Path] = typer.Option(
68+
None, "--output", "-o", help="Save JSON output to a file."
69+
),
70+
) -> None:
71+
if not resume.exists():
72+
typer.echo(f"Error: file not found: {resume}", err=True)
73+
raise typer.Exit(1)
74+
75+
if language not in LANGUAGES:
76+
typer.echo(
77+
f"Error: unsupported language '{language}'.\nAvailable: {', '.join(LANGUAGES)}",
78+
err=True,
79+
)
80+
raise typer.Exit(1)
81+
82+
if not config.GEMINI_API_KEY:
83+
typer.echo("Error: GEMINI_API_KEY is not set.", err=True)
84+
raise typer.Exit(1)
85+
86+
typer.echo("Extracting text from PDF...")
87+
try:
88+
text = _extract_text(resume)
89+
except Exception as e:
90+
typer.echo(f"Error reading PDF: {e}", err=True)
91+
raise typer.Exit(1)
92+
93+
if not text:
94+
typer.echo("Error: could not extract text from the PDF.", err=True)
95+
raise typer.Exit(1)
96+
97+
typer.echo("Analyzing resume...")
98+
genai.configure(api_key=config.GEMINI_API_KEY)
99+
model = genai.GenerativeModel("gemini-2.0-flash-lite")
100+
prompt = get_resume_analysis_prompt(text, language, role)
101+
102+
try:
103+
response = model.generate_content(prompt)
104+
raw = (response.text or "").strip()
105+
except Exception as e:
106+
typer.echo(f"Error calling LLM: {e}", err=True)
107+
raise typer.Exit(1)
108+
109+
result = _extract_json(raw)
110+
if not result:
111+
typer.echo("Error: could not parse the analysis response.", err=True)
112+
raise typer.Exit(1)
113+
114+
output_json = json.dumps(result, indent=2, ensure_ascii=False)
115+
116+
if output:
117+
output.write_text(output_json, encoding="utf-8")
118+
typer.echo(f"Results saved to {output}")
119+
else:
120+
typer.echo(output_json)
121+
122+
123+
def main() -> None:
124+
app()

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ readme = "README.md"
1616
requires-python = ">=3.13"
1717
dependencies = [
1818
"alembic>=1.17.1",
19+
"typer>=0.12.0",
1920
"celery>=5.5.3",
2021
"dotenv>=0.9.9",
2122
"fastapi>=0.120.4",
@@ -33,6 +34,9 @@ dependencies = [
3334
"pdfplumber==0.11.8"
3435
]
3536

37+
[project.scripts]
38+
levelup = "levelup.cli:main"
39+
3640
[dependency-groups]
3741
dev = [
3842
# docs

0 commit comments

Comments
 (0)