""" Provider-specific prompt loader (spec #3). Different models follow instructions differently, so each LLM capability has a family-specific prompt template under `prompts/`: prompts/_.txt e.g. resume_tailor_kimi.txt prompts/_default.txt optional shared fallback Families: claude | kimi | nvidia. Templates use <> placeholders (NOT Python str.format braces) so the literal JSON braces inside the templates never collide with substitution. If no template file exists for a (task, family), `render_prompt` returns None and the caller falls back to its built-in inline prompt — so this layer is purely additive and never breaks existing behaviour. """ from __future__ import annotations import os from typing import Optional _PROMPTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "prompts") FAMILIES = ("claude", "kimi", "nvidia") TASKS = ("jd_analysis", "resume_tailor", "repair", "jobalytics_repair") def family_for(name_or_model: str) -> str: """Map a provider name or model id to a prompt family.""" s = (name_or_model or "").lower() if "claude" in s or "anthropic" in s: return "claude" if "kimi" in s or "moonshot" in s: return "kimi" # GLM / Qwen / DeepSeek / Step / GPT-OSS / MiniMax are all NVIDIA-hosted, # OpenAI-compatible endpoints — they share the strict "nvidia" style. return "nvidia" def load_prompt(task: str, family: str) -> Optional[str]: """Read prompts/_.txt, falling back to _default.txt.""" for fam in (family, "default"): path = os.path.join(_PROMPTS_DIR, f"{task}_{fam}.txt") if os.path.exists(path): try: with open(path, encoding="utf-8") as f: return f.read() except Exception: return None return None def render_prompt(task: str, family: str, **tokens) -> Optional[str]: """Load a template and substitute <> placeholders. tokens are passed as token_name=value; the template placeholder is the UPPER-CASED token name wrapped in << >>, e.g. jd_text -> <>. Returns None if no template exists (caller uses its inline prompt). """ tmpl = load_prompt(task, family) if tmpl is None: return None out = tmpl for key, val in tokens.items(): out = out.replace(f"<<{key.upper()}>>", "" if val is None else str(val)) return out def has_prompt(task: str, family: str) -> bool: return load_prompt(task, family) is not None