diff --git a/resume-generator/generate_summary.py b/resume-generator/generate_summary.py deleted file mode 100644 index 0c12065..0000000 --- a/resume-generator/generate_summary.py +++ /dev/null @@ -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") diff --git a/resume-generator/rxresume_automation.py b/resume-generator/rxresume_automation.py deleted file mode 100644 index c86c32e..0000000 --- a/resume-generator/rxresume_automation.py +++ /dev/null @@ -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}")