remove unused folder
This commit is contained in:
parent
fa13709738
commit
65a139a5ae
@ -1,139 +0,0 @@
|
||||
"""
|
||||
Generate a tailored résumé summary using AI (OpenRouter API).
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import pyperclip
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def load_profile(path: str = "./base.json") -> dict:
|
||||
"""Load the user's profile from a JSON file."""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_job_description(from_clipboard: bool = True, path: str = None) -> str:
|
||||
"""
|
||||
Load the job description from clipboard or a file.
|
||||
|
||||
Args:
|
||||
from_clipboard: If True, read from system clipboard
|
||||
path: If from_clipboard is False, read from this file path
|
||||
|
||||
Returns:
|
||||
The job description text
|
||||
"""
|
||||
if from_clipboard:
|
||||
return pyperclip.paste().strip()
|
||||
if path:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
raise ValueError("No job description source provided.")
|
||||
|
||||
|
||||
def _build_prompt(profile: dict, jd: str) -> str:
|
||||
"""Build the prompt for the AI model."""
|
||||
return f"""
|
||||
You are generating a tailored résumé summary for me.
|
||||
|
||||
Requirements:
|
||||
- Use keywords found in the job description.
|
||||
- Keep it concise but meaningful. Avoid fluff. Avoid long-winded text.
|
||||
- Include just enough detail to feel real and grounded.
|
||||
- Gently convey that I care about helping people and doing good work.
|
||||
- Do NOT invent experience or skills I don't have.
|
||||
- Maintain a warm, confident, human tone.
|
||||
- Target THIS specific job directly, so use ATS keywords, while remaining natural.
|
||||
- Use the profile to add context and details.
|
||||
|
||||
My profile (JSON fields merged):
|
||||
{json.dumps(profile, indent=2)}
|
||||
|
||||
Job description:
|
||||
{jd}
|
||||
|
||||
Write the résumé summary now.
|
||||
"""
|
||||
|
||||
|
||||
def _call_openrouter(prompt: str, model: str, api_key: str) -> str:
|
||||
"""Call OpenRouter API to generate text."""
|
||||
url = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"HTTP-Referer": "http://localhost",
|
||||
"X-Title": "ResumeSummaryScript",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": False,
|
||||
"plugins": [{"id": "response-healing"}],
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f"OpenRouter error {response.status_code}: {response.text}")
|
||||
|
||||
data = response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
def generate_resume_summary(
|
||||
profile_path: str = "./base.json",
|
||||
job_description: str = None,
|
||||
from_clipboard: bool = True,
|
||||
copy_to_clipboard: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a tailored résumé summary using AI.
|
||||
|
||||
Uses the user's profile and a job description to generate a personalized
|
||||
summary section for a résumé, targeting the specific job.
|
||||
|
||||
Args:
|
||||
profile_path: Path to the profile JSON file
|
||||
job_description: Job description text (if None, uses from_clipboard/path)
|
||||
from_clipboard: If job_description is None, read JD from clipboard
|
||||
copy_to_clipboard: If True, copy the generated summary to clipboard
|
||||
|
||||
Returns:
|
||||
The generated résumé summary text
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.getenv("OPENROUTER_API_KEY")
|
||||
model = os.getenv("MODEL", "google/gemini-3-flash-preview")
|
||||
|
||||
if not api_key:
|
||||
raise RuntimeError("Missing OPENROUTER_API_KEY in .env")
|
||||
|
||||
profile = load_profile(profile_path)
|
||||
|
||||
if job_description is None:
|
||||
jd = load_job_description(from_clipboard=from_clipboard)
|
||||
else:
|
||||
jd = job_description
|
||||
|
||||
prompt = _build_prompt(profile, jd)
|
||||
summary = _call_openrouter(prompt, model, api_key)
|
||||
|
||||
if copy_to_clipboard:
|
||||
pyperclip.copy(summary)
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
summary = generate_resume_summary()
|
||||
|
||||
print("\n=== Generated Summary ===\n")
|
||||
print(summary)
|
||||
print("\n[Summary copied to clipboard]\n")
|
||||
@ -1,184 +0,0 @@
|
||||
"""
|
||||
Automate RXResume (rxresu.me) to import resume and export PDF using Playwright.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
# Configuration
|
||||
RXRESUME_EMAIL = os.getenv("RXRESUME_EMAIL", "")
|
||||
RXRESUME_PASSWORD = os.getenv("RXRESUME_PASSWORD", "")
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
# Allow override via environment variables (used by orchestrator)
|
||||
_custom_json_path = os.getenv("RESUME_JSON_PATH")
|
||||
RESUME_JSON_PATH = (
|
||||
Path(_custom_json_path) if _custom_json_path else BASE_DIR / "base.json"
|
||||
)
|
||||
|
||||
_custom_output_filename = os.getenv("OUTPUT_FILENAME")
|
||||
OUTPUT_FILENAME = _custom_output_filename if _custom_output_filename else "resume.pdf"
|
||||
|
||||
# Output directory - can be overridden by orchestrator
|
||||
_custom_output_dir = os.getenv("OUTPUT_DIR")
|
||||
OUTPUT_DIR = Path(_custom_output_dir) if _custom_output_dir else BASE_DIR / "resumes"
|
||||
|
||||
|
||||
def login(page):
|
||||
"""Log in to RXResume."""
|
||||
page.goto("https://v4.rxresu.me/auth/login")
|
||||
page.fill('input[placeholder="john.doe@example.com"]', RXRESUME_EMAIL)
|
||||
page.fill('input[type="password"]', RXRESUME_PASSWORD)
|
||||
page.click('button:has-text("Sign in")')
|
||||
page.wait_for_url("**/dashboard/resumes", timeout=15000)
|
||||
page.click('button:has-text("List")')
|
||||
|
||||
|
||||
def import_resume(page, json_path: Path):
|
||||
"""Import a resume JSON file."""
|
||||
# Log the JSON file size for debugging
|
||||
try:
|
||||
import json
|
||||
with open(json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
print(f" 📋 JSON keys: {list(data.keys())}")
|
||||
if 'basics' in data:
|
||||
print(f" 📋 Headline: {data['basics'].get('headline', 'N/A')[:50]}...")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not read JSON for logging: {e}")
|
||||
|
||||
page.click('h4:has-text("Import")')
|
||||
page.set_input_files('input[type="file"]', str(json_path))
|
||||
page.click('button:has-text("Validate")')
|
||||
|
||||
# Wait for validation to complete - check for either success (Import button) or error
|
||||
try:
|
||||
# Wait for the Import button to become visible (validation succeeded)
|
||||
page.wait_for_selector('button:has-text("Import"):not([disabled])', timeout=10000)
|
||||
except Exception as e:
|
||||
# Save debug files to errors folder (accessible outside Docker)
|
||||
errors_dir = OUTPUT_DIR.parent / "errors"
|
||||
errors_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Take a screenshot for debugging
|
||||
try:
|
||||
screenshot_path = errors_dir / f"debug_{json_path.stem}.png"
|
||||
page.screenshot(path=str(screenshot_path))
|
||||
print(f" 📸 Debug screenshot saved: {screenshot_path}")
|
||||
except Exception as screenshot_err:
|
||||
print(f" ⚠️ Could not save screenshot: {screenshot_err}")
|
||||
|
||||
# Copy the failed JSON to errors folder for inspection
|
||||
try:
|
||||
import shutil
|
||||
failed_json_path = errors_dir / f"{json_path.stem}.json"
|
||||
shutil.copy(str(json_path), str(failed_json_path))
|
||||
print(f" 📋 Failed JSON saved: {failed_json_path}")
|
||||
except Exception as copy_err:
|
||||
print(f" ⚠️ Could not save failed JSON: {copy_err}")
|
||||
|
||||
# Check for validation error messages in the dialog
|
||||
error_selectors = [
|
||||
'text=/error|invalid|failed/i',
|
||||
'[class*="error"]',
|
||||
'[class*="destructive"]',
|
||||
'.text-red-500',
|
||||
'.text-destructive',
|
||||
'[role="alert"]',
|
||||
]
|
||||
for selector in error_selectors:
|
||||
error_element = page.query_selector(selector)
|
||||
if error_element:
|
||||
error_text = error_element.inner_text().strip()
|
||||
if error_text:
|
||||
print(f" ❌ RXResume validation error: {error_text}")
|
||||
raise RuntimeError(f"RXResume validation failed: {error_text}")
|
||||
|
||||
# Log what's visible in the dialog for debugging
|
||||
dialog = page.query_selector('[role="dialog"]')
|
||||
if dialog:
|
||||
dialog_text = dialog.inner_text()[:500]
|
||||
print(f" 📋 Dialog content: {dialog_text}")
|
||||
|
||||
raise RuntimeError(f"Import button not found after validation (timeout): {e}")
|
||||
|
||||
page.click('button:has-text("Import")')
|
||||
|
||||
|
||||
def navigate_to_top_resume(page):
|
||||
"""Navigate to the first resume in the editor."""
|
||||
if "/dashboard/resumes" not in page.url:
|
||||
page.goto("https://v4.rxresu.me/dashboard/resumes")
|
||||
page.wait_for_load_state("networkidle")
|
||||
|
||||
# wait a beat for the list to update
|
||||
page.wait_for_timeout(1000)
|
||||
page.click('span[data-state="closed"]:first-of-type div:first-of-type')
|
||||
page.wait_for_url("**/builder/**", timeout=10000)
|
||||
|
||||
|
||||
def export_pdf(page, output_path: Path) -> Path:
|
||||
"""Export the resume as PDF."""
|
||||
page.wait_for_timeout(1500) # Wait for builder to fully load
|
||||
|
||||
selector = "div.inline-flex.items-center.justify-center.rounded-full.bg-background.px-4.shadow-xl button:last-of-type"
|
||||
|
||||
with page.expect_download(timeout=30000) as download_info:
|
||||
page.click(selector)
|
||||
|
||||
download = download_info.value
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
download.save_as(str(output_path))
|
||||
return output_path
|
||||
|
||||
|
||||
def generate_resume_pdf(
|
||||
output_filename: str = None,
|
||||
import_json: bool = True,
|
||||
json_path: Path = None,
|
||||
) -> Path:
|
||||
"""
|
||||
Import resume and export PDF.
|
||||
|
||||
Args:
|
||||
output_filename: Name of the output PDF file (defaults to OUTPUT_FILENAME env var)
|
||||
import_json: Whether to import a JSON file first (default True)
|
||||
json_path: Path to JSON file (defaults to RESUME_JSON_PATH env var)
|
||||
|
||||
Returns:
|
||||
Path to the generated PDF
|
||||
"""
|
||||
# Use environment-provided defaults
|
||||
actual_filename = output_filename or OUTPUT_FILENAME
|
||||
actual_json_path = json_path or RESUME_JSON_PATH
|
||||
output_path = OUTPUT_DIR / actual_filename
|
||||
|
||||
print(f"📄 Generating PDF: {actual_filename}")
|
||||
print(f" JSON source: {actual_json_path}")
|
||||
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.firefox.launch(headless=True)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
|
||||
try:
|
||||
login(page)
|
||||
|
||||
if import_json:
|
||||
import_resume(page, actual_json_path)
|
||||
|
||||
navigate_to_top_resume(page)
|
||||
export_pdf(page, output_path)
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
print(f"✅ PDF saved: {output_path}")
|
||||
return output_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# When run directly, use environment variables or defaults
|
||||
pdf_path = generate_resume_pdf()
|
||||
print(f"Done! PDF saved: {pdf_path}")
|
||||
Loading…
x
Reference in New Issue
Block a user