feat: wizard_generate task type — 8 LLM generation sections
This commit is contained in:
parent
f7b12a9f98
commit
64b3226027
2 changed files with 171 additions and 0 deletions
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Reference in a new issue