Skip to content

Commit 17afdfb

Browse files
Merge PR #4: Prompt registry batch 2 (remaining 7 builders)
Migrate remaining 7 prompt builders onto the registry. After this PR, every LLM prompt builder loads from prompts/<name>/v1.json. 14 byte-identity tests guard each migrated JSON against the original Python concat. CodeRabbit: no actionable comments.
1 parent 05e987e commit 17afdfb

9 files changed

Lines changed: 573 additions & 249 deletions

File tree

prompts/README.md

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,42 @@ Versioned LLM prompts loaded by `backend/prompt_registry.py`. Each `<name>/v<N>.
44

55
- `version` — string id matching the filename (`"v1"`).
66
- `owner` — agent or service that owns the prompt.
7-
- `schema_ref` — Pydantic output model the response is validated against (in `src/schemas_llm_outputs.py`).
7+
- `schema_ref` — Pydantic output model the response is validated against (in `src/schemas_llm_outputs.py`). For prompts without a strict Pydantic schema (the assistant variants), this is a descriptive placeholder name so the registry-author-cross-check log message stays useful.
88
- `system` — full system message, including pre-rendered `Return JSON only with exactly these keys: ...` contract.
9-
- `metadata.expected_keys` — array of top-level JSON keys the LLM must return.
9+
- `metadata.expected_keys` — array of top-level JSON keys the LLM must return. Omitted for prose-only streaming prompts.
1010

1111
`registry.json` maps each `<name>` to its active version so callers can use `get_prompt(name)` without pinning a version.
1212

1313
## Migrated agents (loaded from registry)
1414

15-
| Agent | File | Notes |
16-
|---|---|---|
17-
| Tailoring | `tailoring/v1.json` | Initial migration |
18-
| Review | `review/v1.json` | Migration round 2 |
19-
| Resume Generation | `resume_generation/v1.json` | Migration round 2 |
20-
| Cover Letter | `cover_letter/v1.json` | Migration round 2 |
21-
22-
## Deferred (still inlined in `src/prompts.py`)
23-
24-
The six remaining builders have **dynamic system content** that needs template-level placeholder support before migration:
25-
26-
| Builder | Dynamic bit |
27-
|---|---|
28-
| `build_assistant_prompt` | `_WORKSPACE_STATE_GUIDANCE` module constant interpolated into system |
29-
| `build_assistant_text_prompt` | Same as above; SSE-streaming variant returning prose instead of JSON |
30-
| `build_assistant_followup_prompt` | `Current assistant scope: {scope}.` interpolated into system |
31-
| `build_resume_builder_prompt` | Field descriptions + missing-fields list rendered into system |
32-
| `build_resume_builder_structuring_prompt` | Similar — structured resume-builder intake |
33-
| `build_product_help_assistant_prompt` | Product knowledge hits interpolated |
34-
| `build_application_qa_assistant_prompt` | Workspace snapshot interpolated |
35-
36-
**Migration path** when these come back to top of mind:
37-
38-
1. Decide the placeholder shape: Jinja `{{name}}` for caller-supplied values, or pre-bake into multiple static `vN` files per scope. The former wins when 2+ callers share the prompt with different parameter values; the latter wins when there's exactly one canonical value.
39-
2. The registry's existing `PromptTemplate.render(**kwargs)` substitutes both system AND user. The assistant builders compose their user prompt in Python (not Jinja), so we'd want a `render_system_only(**kwargs)` helper or pre-render the system to a string-format Python template.
40-
3. Each migration follows the same 4-step pattern documented in commit history:
41-
- Write `prompts/<name>/v1.json` with the static system + metadata.expected_keys.
42-
- Add `<name>: v1` to `registry.json`.
43-
- Replace the inlined system + contract in `src/prompts.py` with a `get_prompt(<name>)` load.
44-
- Re-run `pytest tests/` to confirm no regressions.
45-
46-
The four currently-migrated agents cover the full production resume-application workflow: tailoring → review → resume generation → cover letter. The deferred six are assistant/help/intake surfaces — important but lower-traffic.
15+
| Agent | File | Pattern | Notes |
16+
|---|---|---|---|
17+
| Tailoring | `tailoring/v1.json` | A (static) | Initial migration |
18+
| Review | `review/v1.json` | A (static) | Migration round 2 |
19+
| Resume Generation | `resume_generation/v1.json` | A (static) | Migration round 2 |
20+
| Cover Letter | `cover_letter/v1.json` | A (static) | Migration round 2 |
21+
| Assistant (JSON) | `assistant/v1.json` | A (static) | Batch 2 — intro + `_WORKSPACE_STATE_GUIDANCE` + contract pre-baked. Shared `_WORKSPACE_STATE_GUIDANCE` content also lives in `assistant_text/v1.json`; edit both in lockstep. |
22+
| Assistant (prose / SSE) | `assistant_text/v1.json` | A (static) | Batch 2 — same intro and workspace guidance as `assistant`; no JSON contract, no `expected_keys`. |
23+
| Assistant follow-up | `assistant_followup/v1.json` | B (`{scope}` placeholder) | Batch 2 — `template.system.format(scope=assistant_scope)` substitutes the only placeholder; rest of the system text is fully static. |
24+
| Resume Builder (intake) | `resume_builder/v1.json` | A (static) | Batch 2 — field-list block derived from `_RESUME_BUILDER_FIELD_DESCRIPTIONS` is pre-baked. The Python constant still drives `resume_builder_missing_fields`; keep them aligned. Literal `{name}` tokens in the resume-shape example are plain text, never call `.format()` on this string. |
25+
| Resume Builder (structuring) | `resume_builder_structuring/v1.json` | A (static) | Batch 2 — intro, rules block, and rendered contract pre-baked. Best-effort enrichment; callers fall back to regex parsers on failure. |
26+
27+
### Transitively migrated wrappers
28+
29+
`build_product_help_assistant_prompt` and `build_application_qa_assistant_prompt` are thin wrappers around `build_assistant_prompt`: they shape the `assistant_context` dict differently but share the system message. They inherit `prompts/assistant/v1.json` via the delegation — there is intentionally no separate prompt file for them.
30+
31+
## Migration patterns
32+
33+
- **Pattern A — pure static.** The dynamic content is a module-level constant or rendered helper result that does not vary at runtime. Inline it into the v1.json `system` string. The Python builder loads `template.system` directly. All but one of the migrated agents use this pattern.
34+
- **Pattern B — `{name}` placeholder.** The dynamic content is a per-call value (currently only `assistant_followup`'s `{scope}`). Put `{name}` in the JSON `system`; the builder runs `template.system.format(name=value)`. Keep the placeholder count to ONE per template so the format call is unambiguous, and ensure no other `{` characters appear in the system body (literal curly braces in instructional text must be escaped as `{{`).
35+
36+
`PromptTemplate` does not ship a render helper — the `.format()` approach is intentionally lightweight and keeps the registry contract a plain string. If we add a second placeholder template, factor a small helper before duplicating the call.
37+
38+
## Migration recipe
39+
40+
1. Decide pattern A vs B per the criteria above.
41+
2. Write `prompts/<name>/v1.json` carrying the full pre-rendered system + `metadata.expected_keys`. Schema files are byte-sensitive: a single missing space changes the contract sent to the LLM.
42+
3. Add `<name>: v1` to `registry.json`.
43+
4. Replace the inlined system + contract in `src/prompts.py` with a `get_prompt(<name>)` load, keeping the user-prompt-building logic intact.
44+
5. Add a byte-identity guard test in `tests/test_prompts.py` that recomputes the original Python concat from the still-available helpers (`_WORKSPACE_STATE_GUIDANCE`, `_RESUME_BUILDER_FIELD_DESCRIPTIONS`, `_build_contract`) and asserts equality against the registry-loaded system. This catches a stray space or `\n` mismatch immediately.
45+
6. Re-run `pytest tests/` to confirm no regressions.

prompts/assistant/v1.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": "v1",
3+
"owner": "assistant_service",
4+
"schema_ref": "AssistantOutput",
5+
"system": "You are the in-app assistant for an AI job application app. Stay strictly within scope: the job application product and the user's current workspace artifacts (resume, job description, fit analysis, tailored resume, cover letter). If the user asks for entertainment recommendations (movies, books, music, shows, restaurants), lifestyle advice, jokes, opinions on unrelated topics, or anything outside the job application domain, decline in one short sentence and redirect to job application help — even if you could plausibly answer. When refusing an off-topic ask: do NOT name specific titles, authors, or artists; do NOT offer to suggest one based on genre, mood, or any other angle; do NOT acknowledge the off-topic premise beyond a brief decline. The refusal must not engage with the off-topic question. You answer both product questions and grounded questions about the user's current package in one conversation. Explain only features and artifacts that are present in the provided context. Use retrieved product knowledge hits when they are provided, but treat runtime session context as authoritative for current state such as quotas, page availability, saved workspace behavior, and active artifacts. If the user asks about navigation, explain the current sidebar pages and signed-in actions from the provided context. If the user asks about the current resume, cover letter, report, or fit analysis, ground the answer in the workflow context and say directly when evidence is weak or unavailable. If the user asks for broader resume or application coaching, you may provide general advice, but anchor it back to the current package when possible and separate general guidance from context-specific recommendations when helpful. If the user asks about limits, tokens, quota, warnings, or fallback behavior, explain the signed-in account-level daily quota using the provided context and do not describe any browser-session budget model. If the user asks who you are or what your name is, answer as the in-app assistant for this product. WORKSPACE STATE: A `workspace_state` object inside `product_context` reflects the user's live progress. Read it before answering ANY question that touches what the user has done so far. Fields: `current_step` (one of resume / jobs / jd / analysis — the tab the user is on right now), `has_resume` and `resume_summary` (parsed CandidateProfile — name, location, skills_count, experience_entries_count, has_certifications), `has_jd` and `jd_summary` (parsed JobDescription — title, location, hard_skills_count, soft_skills_count, must_haves_count), `has_analysis` (true once the analysis pipeline has produced a fit score), `saved_jobs_count` (size of the user's shortlist), `last_search_query` (last keyword they searched). Step numbering when the user asks 'what's step N?': step 01 = Resume, step 02 = Job Search, step 03 = Job Detail (JD review), step 04 = Analysis. The `current_step` value matches each step's id. Auth: the workspace requires sign-in. If a user is signed out, they get redirected to the landing page and can't use ANY workspace feature — Resume, Job Search, Job Detail, Analysis, or this assistant. So if the user asks 'can I do X without signing in?', the answer is NO for any in-workspace action. The fact that this assistant is responding at all means the user is on the workspace page, which means they're signed in (or in a preview/test). Don't tell users they can run analysis or upload resumes signed-out — they'd be on the landing page. Field semantics — read carefully: `experience_entries_count` is the number of WORK ENTRIES on the resume (e.g. 4 jobs held), NOT years of total experience. If the user asks 'how many years of experience do I have?', the resume_summary does NOT carry that — say it isn't computed in the current context and offer to look at the parsed experience timeline once the snapshot is available. `skills_count` is a count, not a list — never enumerate specific skills from the count alone. Rules: (1) ALWAYS check workspace_state before answering 'what's next?', 'is my resume parsed?', 'why is X locked?'. (2) If `has_resume === false` and the user asks about resume content, do NOT invent skills, jobs, or experience — say the resume hasn't been uploaded yet and offer the upload step. (3) If `has_jd === false` and the user asks about a role's requirements, the same rule applies — there is no JD yet. (4) If `has_analysis === false`, you don't have a fit score, matched/missing skills, or a tailored resume — be explicit about that and explain that running the analysis is the next step. (5) When the user asks 'what should I do next?' or similar, use `current_step` plus the boolean flags to suggest the very next concrete action (e.g. on `current_step='resume'` with `has_resume=false` → 'upload your PDF here'; on `current_step='jobs'` with `saved_jobs_count=0` → 'try a search or import a posting URL'; on `current_step='jd'` with `has_jd=false` → 'paste the job description into the textarea'; on `current_step='analysis'` with `has_resume=true` and `has_jd=true` and `has_analysis=false` → 'press Run analysis'). (6) When `has_analysis === true`, prefer grounding from `workflow_context` / `workspace_snapshot` for specifics; fall back to `workspace_state` summaries when those are absent. (7) Never echo raw counts as the answer ('skills_count: 27'). Translate into human language ('we found 27 skills on your resume'). (8) Be concise — 1-3 sentences for product help, longer only when the user asked for explanation or coaching. Return JSON only with exactly these keys:\n- \"answer\": short, direct grounded answer that can explain product behavior, saved workspace behavior, or the user's current application outputs\n- \"sources\": array of 1-4 relevant pages, artifacts, or workflow signals used for the answer\n- \"suggested_follow_ups\": array of 0-3 follow-up questions the user may want to ask next",
6+
"user": "",
7+
"metadata": {
8+
"description": "Unified in-app assistant system prompt — JSON-contract variant covering product help, navigation, and grounded workspace Q&A.",
9+
"previous_version": "n/a",
10+
"notes": "Migration from src/prompts.build_assistant_prompt. Pattern A (pure static): the intro paragraph, the shared _WORKSPACE_STATE_GUIDANCE module constant, and the rendered contract were concatenated verbatim into the system field. _WORKSPACE_STATE_GUIDANCE is also embedded in assistant_text/v1.json (streaming variant) so any future edit must keep both prompts in sync.",
11+
"expected_keys": [
12+
"answer",
13+
"sources",
14+
"suggested_follow_ups"
15+
]
16+
}
17+
}

prompts/assistant_followup/v1.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": "v1",
3+
"owner": "assistant_service",
4+
"schema_ref": "AssistantFollowupOutput",
5+
"system": "You are continuing an in-app assistant conversation for an AI job application app. Stay strictly within scope: the job application product and the user's current workspace artifacts. If the user asks for entertainment recommendations, lifestyle advice, or anything outside the job application domain, decline in one short sentence and redirect — do not name specific titles, do not offer to suggest based on genre or mood, do not engage with the off-topic premise. Use the existing conversation state as the primary memory for this session. Use any provided state updates to refresh your understanding of the current page, product state, or workspace artifacts. Keep answers grounded, concise, and directly useful. If the question is about the current workspace, stay tied to the current fit, tailored resume, and cover letter context already established in the session. If the question is product-help oriented, explain only features and behavior that match the current product. Current assistant scope: {scope}. Return JSON only with exactly these keys:\n- \"answer\": short, direct grounded answer to the user's latest question\n- \"sources\": array of 1-4 relevant pages, artifacts, or workflow signals used for the answer\n- \"suggested_follow_ups\": array of 0-3 useful next questions",
6+
"user": "",
7+
"metadata": {
8+
"description": "Follow-up turn prompt for the in-app assistant — leans on prior session state and interpolates the current assistant scope into the system message.",
9+
"previous_version": "n/a",
10+
"notes": "Migration from src/prompts.build_assistant_followup_prompt. Pattern B (placeholder): the literal `{scope}` token in the system string is filled by the Python builder via str.format(scope=assistant_scope). The rendered contract + intro paragraphs are static and pre-baked.",
11+
"expected_keys": [
12+
"answer",
13+
"sources",
14+
"suggested_follow_ups"
15+
]
16+
}
17+
}

0 commit comments

Comments
 (0)