> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brainworkup.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Reuse prompts across patients with Jinja2 templating

> Add Jinja2 placeholders to PROMPTS.md so a single prompt adapts to every patient, domain, and visit without rewriting clinical instructions.

I built Luria so a single set of prompts could carry me through every evaluation I run — pediatric or adult, one session or three. Jinja2 templating is how I make that possible. Instead of editing patient names and chief complaints into a prompt every time, you write each prompt in `agents/prompts/PROMPTS.md` once with `{{variable}}` placeholders. At runtime, you fill them in from a patient context dictionary.

This guide walks through how the templating system works, which variables are standardized across all prompts, and how to wire the rendered prompts into your agent pipeline.

<Note>
  Templating only changes how prompts are assembled. The clinical instructions —
  tone, methodology, output rules — stay hardcoded in `PROMPTS.md`. You are
  parameterizing the patient-specific content, not the clinical reasoning.
</Note>

## When to use templates

Use a `{{variable}}` when the value changes between patients, domains, or visits:

| Use a template for      | Example                                                            |
| ----------------------- | ------------------------------------------------------------------ |
| Patient-specific fields | `{{patient_first_name}}`, `{{patient_age}}`, `{{chief_complaint}}` |
| Domain-specific fields  | `{{domain_name}}`, `{{domain_scores}}`                             |
| Optional sections       | `{% if has_parent_rater %}...{% endif %}`                          |
| Variable-length lists   | `{% for rec in recommendations %}...{% endfor %}`                  |

Hardcode anything that should stay identical across every evaluation:

| Hardcode instead         | Example                                 |
| ------------------------ | --------------------------------------- |
| Clinical instructions    | "Write in third-person past tense"      |
| Methodology              | "Use a Chain of Density approach"       |
| Output format rules      | "Return clean markdown only"            |
| Fixed domain definitions | The canonical list of cognitive domains |

## Three pieces of syntax

You only need three Jinja2 constructs to template every prompt in Luria.

### 1. Variable substitution

```jinja theme={null}
Patient {{patient_first_name}}, age {{patient_age}}, presented with {{chief_complaint}}.
```

Rendered with `{"patient_first_name": "Sarah", "patient_age": 14, "chief_complaint": "difficulty concentrating"}`, this becomes:

```text theme={null}
Patient Sarah, age 14, presented with difficulty concentrating.
```

### 2. Conditional blocks

Use `{% if %}` / `{% else %}` / `{% endif %}` for content that only applies to some patients:

```jinja theme={null}
{% if has_parent_rater %}
Parent and patient ratings aligned on {{domain_name}} strengths.
{% else %}
Patient self-report on {{domain_name}} unavailable; clinician observation only.
{% endif %}
```

### 3. Loops

Use `{% for %}` / `{% endfor %}` to render a list whose length you do not know in advance — recommendations, history items, validity concerns:

```jinja theme={null}
### School / Academic

{% for rec in academic_recommendations %}
- {{rec}}
{% endfor %}
```

## Standard variables

These variables are loaded from `config.patient.yml` and available to every prompt. Use them consistently so a single context dictionary works across the whole pipeline.

| Variable             | Type    | Example                              |
| -------------------- | ------- | ------------------------------------ |
| `patient_first_name` | string  | `"Sarah"`                            |
| `patient_last_name`  | string  | `"Chen"`                             |
| `patient_full_name`  | string  | `"Sarah Chen"`                       |
| `patient_age`        | integer | `14`                                 |
| `patient_dob`        | string  | `"2010-05-15"`                       |
| `patient_pronouns`   | string  | `"she/her"`                          |
| `he_she`             | string  | `"she"`                              |
| `his_her`            | string  | `"her"`                              |
| `evaluation_date`    | string  | `"May 15, 2026"`                     |
| `referral_source`    | string  | `"PCP Dr. Smith"`                    |
| `chief_complaint`    | string  | `"difficulty concentrating"`         |
| `clinician_name`     | string  | `"Joey Trampush, Ph.D."`             |
| `clinic_name`        | string  | `"Brainworkup Neuropsychology, LLC"` |

## Domain-specific variables

These apply only to domain-level prompts such as `DOMAIN_INTERPRETATION` and `SIRF_SYNTHESIS`. Pass them per call, not from the global patient config.

| Variable            | Type    | Example                           |
| ------------------- | ------- | --------------------------------- |
| `domain_name`       | string  | `"Memory"`                        |
| `domain_type`       | string  | `"neurocog"` or `"neurobehav"`    |
| `domain_scores`     | dict    | `{"test1": 65, "test2": 62}`      |
| `has_parent_rater`  | boolean | `true`                            |
| `has_teacher_rater` | boolean | `true`                            |
| `validity_concerns` | list    | `["low effort", "response bias"]` |

<Tip>
  Use **snake\_case** for every variable name. Jinja2 substitution is
  case-sensitive — `{{patient_first_name}}` and `{{patientFirstName}}` are not
  the same variable.
</Tip>

## Templated example: NSE\_COD\_SUMMARY

This is what a real prompt section in `PROMPTS.md` looks like after templating. Note how the clinical role and methodology remain hardcoded while patient identifiers and history fields use placeholders.

````markdown theme={null}
## NSE_COD_SUMMARY

**Role:** Summarize NSE transcript using Chain of Density (CoD) approach.

**Worker:** `Qwen3.6-35B-A3B-oQ4`

**Input:** `{{transcript_file}}`

**Output:** `data/intake/nse_summary_redacted.md` (post-Presidio redaction)

You are an experienced, board-certified clinical neuropsychologist with expertise
in psychodiagnostic assessment and neurobehavioral examination.

```markdown
# NEUROBEHAVIORAL STATUS EXAM SUMMARY

## Identifying Information and Reason for Referral

**Patient:** {{patient_full_name}}
**Age:** {{patient_age}} years
**Date of Evaluation:** {{evaluation_date}}
**Referral Source:** {{referral_source}}
**Reason for Evaluation:** {{chief_complaint}}

## Background and History

### Medical History
{{medical_history}}

### Psychiatric History
{{psychiatric_history}}
```
````

## Rendering a prompt from Python

Install `jinja2` in your Luria environment (it is already a dependency of the agent pipeline). Then load `PROMPTS.md` as a template and render it against a context dictionary.

```python theme={null}
from jinja2 import Environment, FileSystemLoader

env = Environment(
    loader=FileSystemLoader("agents/prompts")
)

template = env.get_template("PROMPTS.md")

context = {
    "patient_first_name": "Sarah",
    "patient_full_name": "Sarah Chen",
    "patient_age": 14,
    "chief_complaint": "difficulty concentrating",
    "referral_source": "PCP Dr. Smith",
    "evaluation_date": "May 15, 2026",
    "he_she": "she",
    "his_her": "her",
}

filled_prompt = template.render(context)
```

### Rendering a single prompt section

If you only need one section — say, `NSE_COD_SUMMARY` — extract it first with a regex, then render the snippet directly with `jinja2.Template`:

```python theme={null}
import re
from jinja2 import Template

with open("agents/prompts/PROMPTS.md") as f:
    full_file = f.read()

match = re.search(
    r"## NSE_COD_SUMMARY\n(.*?)(?=\n## |\Z)",
    full_file,
    re.DOTALL,
)

if match:
    section = match.group(1)
    filled = Template(section).render(context)
```

## A reusable helper for the agent pipeline

In practice, every agent in the pipeline ends up calling the same render step. Wrap it in a helper so each agent only needs to pass its context:

```python theme={null}
# agents/pipeline/phase_a_nse/nse_cod_summary.py

from jinja2 import Template

NSE_COD_SUMMARY_TEMPLATE = """\
You are an experienced clinical neuropsychologist...

**Patient:** {{patient_full_name}}
**Age:** {{patient_age}} years
**Date of Evaluation:** {{evaluation_date}}
**Referral Source:** {{referral_source}}
**Reason for Evaluation:** {{chief_complaint}}

### Medical History
{{medical_history}}

### Psychiatric History
{{psychiatric_history}}
"""


def fill_nse_cod_prompt(patient_context: dict) -> str:
    """Render NSE_COD_SUMMARY with patient-specific data."""
    return Template(NSE_COD_SUMMARY_TEMPLATE).render(patient_context)
```

Then in the agent:

```python theme={null}
context = {
    "patient_full_name": "Sarah Chen",
    "patient_age": 14,
    "evaluation_date": "May 15, 2026",
    "referral_source": "PCP Dr. Smith",
    "chief_complaint": "difficulty concentrating and organizing",
    "medical_history": "No significant medical history.",
    "psychiatric_history": "No prior psychiatric diagnoses.",
}

prompt = fill_nse_cod_prompt(context)
response = model.generate(prompt)
```

## Common mistakes

<AccordionGroup>
  <Accordion title="Forgetting to render the template">
    Printing a string that contains `{{patient_name}}` does not substitute
    anything. You must construct a `Template` (or load it through an
    `Environment`) and call `.render(context)` for substitution to happen.
  </Accordion>

  <Accordion title="Missing a variable in the context dict">
    Jinja2 silently renders missing variables as an empty string. The prompt
    still runs, but you get sentences like `"Patient Sarah is  years old"`.
    Validate your context dictionary against the variables used in each prompt
    before sending it to a model.
  </Accordion>

  <Accordion title="Mixing snake_case and camelCase">
    A template that reads `{{patientName}}` will not be filled by a context
    with key `patient_name`. Pick snake\_case and stick with it everywhere —
    in `config.patient.yml`, in `PROMPTS.md`, and in your Python code.
  </Accordion>
</AccordionGroup>

## Workflow recommendation

1. Add `{{variable}}` placeholders to `PROMPTS.md` in `agents/prompts/`, starting with patient identifiers and chief complaint.
2. Build a context dictionary from `config.patient.yml` once per evaluation.
3. For each agent in the pipeline, pass that context (plus any domain-specific keys) into the render helper.
4. Send the rendered prompt to the model. Patient data never lives in `PROMPTS.md` itself, which keeps the prompt library reusable and PHI-free.

For deeper integration patterns and per-prompt variable maps, see `agents/prompts/PROMPTS_JINJA_INTEGRATION.md` in the Luria source tree.
