From 64b3226027b809f60d33eb836eeaae0fa60b2449 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Wed, 25 Feb 2026 08:25:17 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20wizard=5Fgenerate=20task=20type=20?= =?UTF-8?q?=E2=80=94=208=20LLM=20generation=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/task_runner.py | 86 +++++++++++++++++++++++++++++++++++++++ tests/test_task_runner.py | 85 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/scripts/task_runner.py b/scripts/task_runner.py index 956c1bf..6c817d1 100644 --- a/scripts/task_runner.py +++ b/scripts/task_runner.py @@ -42,6 +42,78 @@ def submit_task(db_path: Path = DEFAULT_DB, task_type: str = "", return task_id, is_new +_WIZARD_PROMPTS: dict[str, str] = { + "career_summary": ( + "Based on the following resume text, write a concise 2-4 sentence professional " + "career summary in first person. Focus on years of experience, key skills, and " + "what makes this person distinctive. Return only the summary text, no labels.\n\n" + "Resume:\n{resume_text}" + ), + "expand_bullets": ( + "Rewrite these rough responsibility notes as polished STAR-format bullet points " + "(Situation/Task, Action, Result). Each bullet should start with a strong action verb. " + "Return a JSON array of bullet strings only.\n\nNotes:\n{bullet_notes}" + ), + "suggest_skills": ( + "Based on these work experience descriptions, suggest additional skills to add to " + "a resume. Return a JSON array of skill strings only — no explanations.\n\n" + "Experience:\n{experience_text}" + ), + "voice_guidelines": ( + "Analyze the writing style and tone of this resume and cover letter corpus. " + "Return 3-5 concise guidelines for maintaining this person's authentic voice in " + "future cover letters (e.g. 'Uses direct, confident statements'). " + "Return a JSON array of guideline strings.\n\nContent:\n{content}" + ), + "job_titles": ( + "Given these job titles and resume, suggest 5-8 additional job title variations " + "this person should search for. Return a JSON array of title strings only.\n\n" + "Current titles: {current_titles}\nResume summary: {resume_text}" + ), + "keywords": ( + "Based on this resume and target job titles, suggest important keywords and phrases " + "to include in job applications. Return a JSON array of keyword strings.\n\n" + "Titles: {titles}\nResume: {resume_text}" + ), + "blocklist": ( + "Based on this resume and job search context, suggest companies, industries, or " + "keywords to blocklist (avoid in job search results). " + "Return a JSON array of strings.\n\nContext: {resume_text}" + ), + "mission_notes": ( + "Based on this resume, write a short personal note (1-2 sentences) about why this " + "person might genuinely care about each of these industries: music, animal_welfare, education. " + "Return a JSON object with those three industry keys and note values. " + "If the resume shows no clear connection to an industry, set its value to empty string.\n\n" + "Resume: {resume_text}" + ), +} + + +def _run_wizard_generate(section: str, input_data: dict) -> str: + """Run LLM generation for a wizard section. Returns result string. + + Raises ValueError for unknown sections. + Raises any LLM exception on failure. + """ + template = _WIZARD_PROMPTS.get(section) + if template is None: + raise ValueError(f"Unknown wizard_generate section: {section!r}") + # Format the prompt, substituting available keys; unknown placeholders become empty string + import re as _re + + def _safe_format(tmpl: str, kwargs: dict) -> str: + """Format template substituting available keys; leaves missing keys as empty string.""" + def replacer(m): + key = m.group(1) + return str(kwargs.get(key, "")) + return _re.sub(r"\{(\w+)\}", replacer, tmpl) + + prompt = _safe_format(template, {k: str(v) for k, v in input_data.items()}) + from scripts.llm_router import LLMRouter + return LLMRouter().complete(prompt) + + def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int, params: str | None = None) -> None: """Thread body: run the generator and persist the result.""" @@ -146,6 +218,20 @@ def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int, error="Email not configured — go to Settings → Email") return + elif task_type == "wizard_generate": + import json as _json + p = _json.loads(params or "{}") + section = p.get("section", "") + input_data = p.get("input", {}) + if not section: + raise ValueError("wizard_generate: 'section' key is required in params") + result = _run_wizard_generate(section, input_data) + update_task_status( + db_path, task_id, "completed", + error=_json.dumps({"section": section, "result": result}), + ) + return + else: raise ValueError(f"Unknown task_type: {task_type!r}") diff --git a/tests/test_task_runner.py b/tests/test_task_runner.py index 3ea5090..e3de98c 100644 --- a/tests/test_task_runner.py +++ b/tests/test_task_runner.py @@ -208,3 +208,88 @@ def test_scrape_url_submits_enrich_craigslist_for_craigslist_job(tmp_path): call_args = mock_submit.call_args assert call_args[0][1] == "enrich_craigslist" assert call_args[0][2] == job_id + + +import json as _json + +def test_wizard_generate_unknown_section_fails(tmp_path): + """wizard_generate with unknown section marks task failed.""" + db = tmp_path / "t.db" + from scripts.db import init_db, insert_task + init_db(db) + + params = _json.dumps({"section": "nonexistent_section", "input": {}}) + task_id, _ = insert_task(db, "wizard_generate", 0, params=params) + + # Call _run_task directly (not via thread) to test synchronously + from scripts.task_runner import _run_task + _run_task(db, task_id, "wizard_generate", 0, params=params) + + import sqlite3 + conn = sqlite3.connect(db) + row = conn.execute("SELECT status, error FROM background_tasks WHERE id=?", (task_id,)).fetchone() + conn.close() + assert row[0] == "failed", f"Expected 'failed', got '{row[0]}'" + + +def test_wizard_generate_missing_section_fails(tmp_path): + """wizard_generate with no section key marks task failed.""" + db = tmp_path / "t.db" + from scripts.db import init_db, insert_task + init_db(db) + + params = _json.dumps({"input": {"resume_text": "some text"}}) # missing section key + task_id, _ = insert_task(db, "wizard_generate", 0, params=params) + + from scripts.task_runner import _run_task + _run_task(db, task_id, "wizard_generate", 0, params=params) + + import sqlite3 + conn = sqlite3.connect(db) + row = conn.execute("SELECT status FROM background_tasks WHERE id=?", (task_id,)).fetchone() + conn.close() + assert row[0] == "failed" + + +def test_wizard_generate_null_params_fails(tmp_path): + """wizard_generate with params=None marks task failed.""" + db = tmp_path / "t.db" + from scripts.db import init_db, insert_task + init_db(db) + + task_id, _ = insert_task(db, "wizard_generate", 0, params=None) + + from scripts.task_runner import _run_task + _run_task(db, task_id, "wizard_generate", 0, params=None) + + import sqlite3 + conn = sqlite3.connect(db) + row = conn.execute("SELECT status FROM background_tasks WHERE id=?", (task_id,)).fetchone() + conn.close() + assert row[0] == "failed" + + +def test_wizard_generate_stores_result_as_json(tmp_path): + """wizard_generate stores result JSON in error field on success.""" + from unittest.mock import patch, MagicMock + db = tmp_path / "t.db" + from scripts.db import init_db, insert_task + init_db(db) + + params = _json.dumps({"section": "career_summary", "input": {"resume_text": "10 years Python"}}) + task_id, _ = insert_task(db, "wizard_generate", 0, params=params) + + # Mock _run_wizard_generate to return a simple string + with patch("scripts.task_runner._run_wizard_generate", return_value="Experienced Python developer."): + from scripts.task_runner import _run_task + _run_task(db, task_id, "wizard_generate", 0, params=params) + + import sqlite3 + conn = sqlite3.connect(db) + row = conn.execute("SELECT status, error FROM background_tasks WHERE id=?", (task_id,)).fetchone() + conn.close() + + assert row[0] == "completed", f"Expected 'completed', got '{row[0]}'" + payload = _json.loads(row[1]) + assert payload["section"] == "career_summary" + assert payload["result"] == "Experienced Python developer."