Personas

Personas are how you control who is in the room. Both agents are conditioned on a persona object that shapes how they speak, what they remember, and how they reason. Because personas are data rather than code, you can build a pool once and slice it into experimental conditions.

Patient personas

PatientPersona is a Pydantic model with fourteen required string fields and one optional free-text field.

FieldMeaning
age_groupBroad age band, e.g. child, adult, elderly.
genderPatient gender.
ethnicityCultural and linguistic background.
socioeconomic_statuse.g. low, middle, high.
language_proficiencyFluency in the language of the consultation.
recall_accuracyHow reliably the patient remembers onset, timing and history.
cognitive_statee.g. clear, confused, drowsy.
trust_in_healthcareWillingness to disclose fully to a clinician.
pain_expressionHow strongly pain is verbalised — stoic through dramatic.
reactivity_to_clinician_emotionHow much the nurse's tone shifts the patient's behaviour.
emotion_regulatione.g. stable, labile.
disfluency_rateFrequency of hesitations, restarts and fillers.
topic_driftTendency to wander off the clinical question.
verbosityTypical response length.
instructionOptional. Free-text instruction describing how the patient should speak.

verbosity, not response_length

Some older examples show a response_length field for patients. The schema field is verbosity. A file using response_length will not populate the field you expect.

patient.yaml
- age_group: elderly
  gender: male
  ethnicity: Vietnamese-Australian
  socioeconomic_status: low
  language_proficiency: limited
  recall_accuracy: low
  cognitive_state: mildly confused
  trust_in_healthcare: low
  pain_expression: stoic
  reactivity_to_clinician_emotion: high
  emotion_regulation: stable
  disfluency_rate: high
  topic_drift: high
  verbosity: short
  instruction: >-
    Speaks in short sentences with frequent pauses. Occasionally substitutes a
    Vietnamese word when the English one does not come to mind. Downplays pain.

- age_group: adult
  gender: female
  ethnicity: Australian
  socioeconomic_status: middle
  language_proficiency: high
  recall_accuracy: high
  cognitive_state: clear
  trust_in_healthcare: high
  pain_expression: moderate
  reactivity_to_clinician_emotion: low
  emotion_regulation: stable
  disfluency_rate: low
  topic_drift: low
  verbosity: medium

Nurse personas

NursePersona has eight required string fields plus the same optional instruction.

FieldMeaning
genderNurse gender.
ethnicityCultural and linguistic background.
experience_levele.g. graduate, mid-level, senior.
risk_toleranceWillingness to accept diagnostic uncertainty before deciding.
guideline_adherenceHow strictly the triage protocol is followed.
communication_stylee.g. direct, warm, clipped.
verbosityTypical utterance length.
emotional_expressione.g. neutral, reassuring, brusque.
instructionOptional. Free-text instruction describing how the nurse should speak.
nurse.yaml
- gender: female
  ethnicity: Australian
  experience_level: senior
  risk_tolerance: low
  guideline_adherence: high
  communication_style: direct
  verbosity: medium
  emotional_expression: neutral

- gender: male
  ethnicity: Indian-Australian
  experience_level: graduate
  risk_tolerance: high
  guideline_adherence: medium
  communication_style: warm
  verbosity: high
  emotional_expression: reassuring
  instruction: >-
    Checks understanding often and apologises for repeating questions.

Fields are free-form strings

None of these fields is an enum. experience_level: "12 years in a rural ED" is as valid as "senior" — it is interpolated into the prompt as written. That flexibility is deliberate, but it also means typos pass validation silently.

Loading

from triagesim.personas import load_patient_personas, load_nurse_personas

patients = load_patient_personas("patient.yaml")
nurses = load_nurse_personas("nurse.yaml")

Each function reads a YAML file containing a list of mappings and returns a list of validated persona objects.

Missing required fields do not raise. The loader fills any absent required field with a default before validation, so a partial persona still constructs — with the unspecified traits taking a neutral value rather than the one you meant.

Note

Because absent fields are silently defaulted, a misspelled key (say verbosty) will not error. It will be dropped and the real verbosity field will be defaulted. Validate your persona files if they are machine-generated.

Sampling

from triagesim.personas import sample_patient_personas, sample_nurse_personas

chosen = sample_patient_personas(patients, k=4, seed=42)

Sampling is without replacement and uses a local random.Random(seed) instance, so it does not disturb the global RNG. The same seed and the same input list always yield the same selection.

Requesting more personas than exist raises ValueError:

sample_patient_personas(patients, k=100)
# ValueError: Cannot sample k=100 personas from population of size 2

Filtering

filter_patient_personas and filter_nurse_personas take a criteria dict and keep only personas where every field matches exactly:

from triagesim.personas import filter_patient_personas

low_proficiency = filter_patient_personas(
    patients,
    {"language_proficiency": "limited", "trust_in_healthcare": "low"},
)

Matching is exact string equality — "Limited" will not match "limited". Passing a field name that does not exist on the model raises AttributeError, which is a useful early failure when building experimental conditions.

A worked example

Filtering and sampling compose naturally into experimental arms:

from triagesim.personas import (
    load_patient_personas,
    filter_patient_personas,
    sample_patient_personas,
)

patients = load_patient_personas("patients.yaml")

conditions = {
    "high_proficiency": {"language_proficiency": "high"},
    "limited_proficiency": {"language_proficiency": "limited"},
}

arms = {}
for name, criteria in conditions.items():
    pool = filter_patient_personas(patients, criteria)
    if len(pool) < 10:
        raise ValueError(f"Condition {name!r} has only {len(pool)} personas")
    arms[name] = sample_patient_personas(pool, k=10, seed=42)

Holding the seed fixed across arms keeps sampling noise from confounding the comparison, so any difference in outcomes is attributable to the condition rather than to which personas happened to be drawn.

Generating persona pools

Writing hundreds of personas by hand is impractical. The repository includes scripts/generate_personas.py, which produces persona YAML files programmatically — a useful starting point for building a pool large enough to filter meaningfully.

Next: Agents and LLM backends covers how a persona is turned into a prompt.