Skip to content

Commit fd14658

Browse files
authored
Merge branch 'main' into fix/krippendorff-unpairable-identical-ratings
2 parents dd638d8 + c32546a commit fd14658

109 files changed

Lines changed: 5998 additions & 358 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.pyrit_conf_example‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ operation: op_trash_panda
111111
# Applies only to the pyrit_backend server.
112112
max_concurrent_scenario_runs: 3
113113

114+
# Live Reinitialization
115+
# ---------------------
116+
# Allows administrators to apply saved configuration changes without restarting
117+
# the backend. Enable only when the deployment has exactly one backend process
118+
# and one replica. Known worker and replica settings are checked as an additional
119+
# safeguard. Live apply rejects busy runtimes and requires a restart if
120+
# initialization fails after replacement begins.
121+
enable_live_reinitialization: false
122+
114123
# Custom Initializer Registration (REST API)
115124
# -------------------------------------------
116125
# When true, the REST API accepts POST /api/initializers to register custom

‎build_scripts/validate_docs.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ def find_orphaned_files(toc_files: set[str], doc_root: Path) -> list[str]:
102102
continue
103103
# .py companion files for .ipynb are not orphaned
104104
if file_path.suffix == ".py":
105+
if "supporting_assets" in file_path.parts:
106+
continue
105107
notebook_version = file_path.with_suffix(".ipynb")
106108
if notebook_version.exists():
107109
continue

‎doc/code/memory/8_seed_database.ipynb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@
286286
"\n",
287287
"For the most part these are user errors, but when you want to remove whole groups rather than individual seeds, use `remove_seed_groups_from_memory`. It applies the same filters, but removes every seed that shares a `prompt_group_id` with any match, so groups are never left partial. Note that it only affects seeds that belong to a group: a matching seed added individually (with no `prompt_group_id`) is skipped, so use `remove_seeds_from_memory` for those.\n",
288288
"\n",
289-
"> **Note on deleting by `value`.** For the remove methods, the `value` filter defaults to full-string equality (`exact=True`), so `remove_seeds_from_memory(value=\"the\")` deletes only seeds whose value is exactly `\"the\"` — not everything containing it. This differs from `get_seeds`, which always matches `value` by substring. Pass `exact=False` to opt into substring deletion when you really want it. As a general rule, preview with the same filters via `get_seeds(...)` first and prefer a specific filter (such as `dataset_name` or `value_sha256`) for deletion.\n",
289+
"> **Note on deleting by `value`.** For the remove methods, the `value` filter defaults to full-string equality (`exact=True`), so `remove_seeds_from_memory(value=\"the\")` deletes only seeds whose value is exactly `\"the\"` — not everything containing it. This differs from `get_seeds`, which always matches `value` by substring. The same applies to the list filters: `harm_categories`, `authors`, `groups` and `parameters` must match whole list elements (case-insensitive), so `remove_seeds_from_memory(harm_categories=[\"hate\"])` does not also remove seeds tagged `\"hate_speech\"`. Pass `exact=False` to opt into substring deletion when you really want it. As a general rule, preview with the same filters via `get_seeds(...)` first and prefer a specific filter (such as `dataset_name` or `value_sha256`) for deletion. Because `get_seeds` always matches by substring, the preview is a superset of what the remove methods delete: some previewed seeds will survive the deletion, which is expected.\n",
290290
"\n",
291291
"> **Note on file-backed seeds.** For `image_path`, `audio_path`, and `video_path` seeds, removal deletes only the database record; the serialized file on disk is left in place. Delete those files separately if they are no longer needed."
292292
]

‎doc/code/memory/8_seed_database.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,6 @@ def print_group(seed_group):
145145
#
146146
# For the most part these are user errors, but when you want to remove whole groups rather than individual seeds, use `remove_seed_groups_from_memory`. It applies the same filters, but removes every seed that shares a `prompt_group_id` with any match, so groups are never left partial. Note that it only affects seeds that belong to a group: a matching seed added individually (with no `prompt_group_id`) is skipped, so use `remove_seeds_from_memory` for those.
147147
#
148-
# > **Note on deleting by `value`.** For the remove methods, the `value` filter defaults to full-string equality (`exact=True`), so `remove_seeds_from_memory(value="the")` deletes only seeds whose value is exactly `"the"` — not everything containing it. This differs from `get_seeds`, which always matches `value` by substring. Pass `exact=False` to opt into substring deletion when you really want it. As a general rule, preview with the same filters via `get_seeds(...)` first and prefer a specific filter (such as `dataset_name` or `value_sha256`) for deletion.
148+
# > **Note on deleting by `value`.** For the remove methods, the `value` filter defaults to full-string equality (`exact=True`), so `remove_seeds_from_memory(value="the")` deletes only seeds whose value is exactly `"the"` — not everything containing it. This differs from `get_seeds`, which always matches `value` by substring. The same applies to the list filters: `harm_categories`, `authors`, `groups` and `parameters` must match whole list elements (case-insensitive), so `remove_seeds_from_memory(harm_categories=["hate"])` does not also remove seeds tagged `"hate_speech"`. Pass `exact=False` to opt into substring deletion when you really want it. As a general rule, preview with the same filters via `get_seeds(...)` first and prefer a specific filter (such as `dataset_name` or `value_sha256`) for deletion. Because `get_seeds` always matches by substring, the preview is a superset of what the remove methods delete: some previewed seeds will survive the deletion, which is expected.
149149
#
150150
# > **Note on file-backed seeds.** For `image_path`, `audio_path`, and `video_path` seeds, removal deletes only the database record; the serialized file on disk is left in place. Delete those files separately if they are no longer needed.

‎doc/code/scoring/3_combining_scorers.ipynb‎

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
" INV[\"TrueFalseInverterScorer<br/>negates one result\"]\n",
5959
" CONV[\"create_conversation_scorer()<br/>scores concatenated history\"]\n",
6060
" THRESH[\"FloatScaleThresholdScorer<br/>score ≥ threshold\"]\n",
61+
" FSFALL[\"FloatScaleFallbackScorer<br/>alternative float-scale judge\"]\n",
62+
" TFFALL[\"TrueFalseFallbackScorer<br/>alternative true/false judge\"]\n",
6163
" CONV ~~~ THRESH\n",
6264
" end\n",
6365
"\n",
@@ -70,20 +72,24 @@
7072
" TF -->|\"1+ via scorers=\"| COMP\n",
7173
" TF -->|\"1 via scorer=\"| INV\n",
7274
" FS -->|\"1 via scorer=\"| THRESH\n",
75+
" FS -->|\"2 comparable single-result scorers\"| FSFALL\n",
76+
" TF -->|\"2 comparable single-result scorers\"| TFFALL\n",
7377
" TF -->|\"1 scorer supporting text content\"| CONV\n",
7478
" FS -->|\"1 scorer supporting text content\"| CONV\n",
7579
"\n",
7680
" COMP -. is a .-> TFOUT\n",
7781
" INV -. is a .-> TFOUT\n",
7882
" THRESH -. is a .-> TFOUT\n",
83+
" FSFALL -. is a .-> FSOUT\n",
84+
" TFFALL -. is a .-> TFOUT\n",
7985
" CONV -. \"for true/false input\" .-> TFOUT\n",
8086
" CONV -. \"for float-scale input\" .-> FSOUT\n",
8187
"\n",
8288
" classDef input fill:#e8f0fe,stroke:#4285f4,color:#15233a;\n",
8389
" classDef wrapper fill:#fff4e5,stroke:#f29900,color:#3d2600;\n",
8490
" classDef output fill:#e6f4ea,stroke:#34a853,color:#17351f;\n",
8591
" class TF,FS input;\n",
86-
" class COMP,INV,THRESH,CONV wrapper;\n",
92+
" class COMP,INV,THRESH,CONV,FSFALL,TFFALL wrapper;\n",
8793
" class TFOUT,FSOUT output;\n",
8894
"```"
8995
]
@@ -99,8 +105,11 @@
99105
"`TrueFalseCompositeScorer` requires at least one `TrueFalseScorer` and combines their\n",
100106
"single results with `AND`, `OR`, or `MAJORITY`; `TrueFalseInverterScorer` accepts one\n",
101107
"`TrueFalseScorer`. `FloatScaleThresholdScorer` is the cross-kind adapter: it accepts one\n",
102-
"`FloatScaleScorer` and produces a `TrueFalseScorer`. These generic wrappers forward the\n",
103-
"same `Scorable` to their children, so each child must support that evidence kind.\n",
108+
"`FloatScaleScorer` and produces a `TrueFalseScorer`. `FloatScaleFallbackScorer` accepts two\n",
109+
"`FloatScaleScorer`s and produces a `FloatScaleScorer`. `TrueFalseFallbackScorer` does the\n",
110+
"same for two `TrueFalseScorer`s. Each fallback wrapper tries the primary first and calls\n",
111+
"the fallback only when the primary returns an undetermined score. These wrappers forward the same\n",
112+
"`Scorable` to their children, so each child must support that evidence kind.\n",
104113
"\n",
105114
"`create_conversation_scorer()` accepts a true/false or float-scale scorer that supports\n",
106115
"text `ContentScorable` evidence. It returns a dynamic wrapper that remains the same scorer\n",
@@ -280,6 +289,58 @@
280289
{
281290
"cell_type": "markdown",
282291
"id": "11",
292+
"metadata": {},
293+
"source": [
294+
"## Routing abstentions to a fallback scorer\n",
295+
"\n",
296+
"Both float-scale and true/false scorers can return `ScoreStatus.UNDETERMINED`.\n",
297+
"A fallback wrapper evaluates the same evidence with a second scorer only when the\n",
298+
"primary returns that status. A completed `False` or `0.0` is a valid judgment and does\n",
299+
"not trigger fallback. This lets a fast primary handle most inputs without calling an\n",
300+
"expensive second judge each time.\n",
301+
"\n",
302+
"Both children must belong to the same result family, support the same condition types,\n",
303+
"and return exactly one score when applicable. The wrapper validates both children\n",
304+
"before scoring and passes each its supported expectation. If fallback runs, the results\n",
305+
"must refer to the same evidence and expectation; categories must match when both\n",
306+
"results supply them. An unreadable judgment can have no category labels. Configure equivalent\n",
307+
"criteria and, for float-scale scorers, comparable numeric meanings: matching types and\n",
308+
"categories does not prove that two rubrics measure the same thing.\n",
309+
"\n",
310+
"With preconfigured scorers that meet these requirements:\n",
311+
"\n",
312+
"```python\n",
313+
"from pyrit.score import FloatScaleFallbackScorer, TrueFalseFallbackScorer\n",
314+
"\n",
315+
"harm_scorer = FloatScaleFallbackScorer(\n",
316+
" scorer=primary_harm_scorer,\n",
317+
" fallback_scorer=secondary_harm_scorer,\n",
318+
")\n",
319+
"objective_scorer = TrueFalseFallbackScorer(\n",
320+
" scorer=primary_objective_scorer,\n",
321+
" fallback_scorer=secondary_objective_scorer,\n",
322+
")\n",
323+
"```\n",
324+
"\n",
325+
"A non-applicable primary (`[]`) returns `[]` without calling the fallback. A non-applicable\n",
326+
"fallback leaves the primary's undetermined judgment in place. If both abstain, the result\n",
327+
"remains undetermined. Exceptions propagate; they are not treated as abstentions.\n",
328+
"\n",
329+
"Multiple child results are rejected rather than paired by position or discarded.\n",
330+
"Aggregate them explicitly before using fallback if a single aggregate is meaningful.\n",
331+
"\n",
332+
"Only the root score is persisted. The wrapper creates a new score without modifying\n",
333+
"either child result and retains their observation links. Metadata records `resolved_by`\n",
334+
"as `\"primary\"` or `\"fallback\"`. Child metadata keys are prefixed with `primary.` and\n",
335+
"`fallback.`, so duplicate keys and nested fallback details are not overwritten.\n",
336+
"When fallback is attempted, `primary_rationale` is retained and `fallback_status` records\n",
337+
"`\"complete\"`, `\"undetermined\"`, or `\"not_applicable\"`. A fallback judgment also supplies\n",
338+
"`fallback_rationale`; the combined rationale explains both attempts."
339+
]
340+
},
341+
{
342+
"cell_type": "markdown",
343+
"id": "12",
283344
"metadata": {
284345
"lines_to_next_cell": 0
285346
},
@@ -299,7 +360,7 @@
299360
{
300361
"cell_type": "code",
301362
"execution_count": null,
302-
"id": "12",
363+
"id": "13",
303364
"metadata": {},
304365
"outputs": [
305366
{
@@ -341,7 +402,7 @@
341402
},
342403
{
343404
"cell_type": "markdown",
344-
"id": "13",
405+
"id": "14",
345406
"metadata": {},
346407
"source": [
347408
"For a richer, real-world example, wrap a `SelfAskLikertScorer` with the\n",

‎doc/code/scoring/3_combining_scorers.py‎

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
# INV["TrueFalseInverterScorer<br/>negates one result"]
4949
# CONV["create_conversation_scorer()<br/>scores concatenated history"]
5050
# THRESH["FloatScaleThresholdScorer<br/>score ≥ threshold"]
51+
# FSFALL["FloatScaleFallbackScorer<br/>alternative float-scale judge"]
52+
# TFFALL["TrueFalseFallbackScorer<br/>alternative true/false judge"]
5153
# CONV ~~~ THRESH
5254
# end
5355
#
@@ -60,20 +62,24 @@
6062
# TF -->|"1+ via scorers="| COMP
6163
# TF -->|"1 via scorer="| INV
6264
# FS -->|"1 via scorer="| THRESH
65+
# FS -->|"2 comparable single-result scorers"| FSFALL
66+
# TF -->|"2 comparable single-result scorers"| TFFALL
6367
# TF -->|"1 scorer supporting text content"| CONV
6468
# FS -->|"1 scorer supporting text content"| CONV
6569
#
6670
# COMP -. is a .-> TFOUT
6771
# INV -. is a .-> TFOUT
6872
# THRESH -. is a .-> TFOUT
73+
# FSFALL -. is a .-> FSOUT
74+
# TFFALL -. is a .-> TFOUT
6975
# CONV -. "for true/false input" .-> TFOUT
7076
# CONV -. "for float-scale input" .-> FSOUT
7177
#
7278
# classDef input fill:#e8f0fe,stroke:#4285f4,color:#15233a;
7379
# classDef wrapper fill:#fff4e5,stroke:#f29900,color:#3d2600;
7480
# classDef output fill:#e6f4ea,stroke:#34a853,color:#17351f;
7581
# class TF,FS input;
76-
# class COMP,INV,THRESH,CONV wrapper;
82+
# class COMP,INV,THRESH,CONV,FSFALL,TFFALL wrapper;
7783
# class TFOUT,FSOUT output;
7884
# ```
7985

@@ -82,8 +88,11 @@
8288
# `TrueFalseCompositeScorer` requires at least one `TrueFalseScorer` and combines their
8389
# single results with `AND`, `OR`, or `MAJORITY`; `TrueFalseInverterScorer` accepts one
8490
# `TrueFalseScorer`. `FloatScaleThresholdScorer` is the cross-kind adapter: it accepts one
85-
# `FloatScaleScorer` and produces a `TrueFalseScorer`. These generic wrappers forward the
86-
# same `Scorable` to their children, so each child must support that evidence kind.
91+
# `FloatScaleScorer` and produces a `TrueFalseScorer`. `FloatScaleFallbackScorer` accepts two
92+
# `FloatScaleScorer`s and produces a `FloatScaleScorer`. `TrueFalseFallbackScorer` does the
93+
# same for two `TrueFalseScorer`s. Each fallback wrapper tries the primary first and calls
94+
# the fallback only when the primary returns an undetermined score. These wrappers forward the same
95+
# `Scorable` to their children, so each child must support that evidence kind.
8796
#
8897
# `create_conversation_scorer()` accepts a true/false or float-scale scorer that supports
8998
# text `ContentScorable` evidence. It returns a dynamic wrapper that remains the same scorer
@@ -163,6 +172,53 @@
163172
print(f"[threshold] near-copy -> {near_copy.get_value()}")
164173
print(f"[threshold] independent -> {original.get_value()}")
165174

175+
# %% [markdown]
176+
# ## Routing abstentions to a fallback scorer
177+
#
178+
# Both float-scale and true/false scorers can return `ScoreStatus.UNDETERMINED`.
179+
# A fallback wrapper evaluates the same evidence with a second scorer only when the
180+
# primary returns that status. A completed `False` or `0.0` is a valid judgment and does
181+
# not trigger fallback. This lets a fast primary handle most inputs without calling an
182+
# expensive second judge each time.
183+
#
184+
# Both children must belong to the same result family, support the same condition types,
185+
# and return exactly one score when applicable. The wrapper validates both children
186+
# before scoring and passes each its supported expectation. If fallback runs, the results
187+
# must refer to the same evidence and expectation; categories must match when both
188+
# results supply them. An unreadable judgment can have no category labels. Configure equivalent
189+
# criteria and, for float-scale scorers, comparable numeric meanings: matching types and
190+
# categories does not prove that two rubrics measure the same thing.
191+
#
192+
# With preconfigured scorers that meet these requirements:
193+
#
194+
# ```python
195+
# from pyrit.score import FloatScaleFallbackScorer, TrueFalseFallbackScorer
196+
#
197+
# harm_scorer = FloatScaleFallbackScorer(
198+
# scorer=primary_harm_scorer,
199+
# fallback_scorer=secondary_harm_scorer,
200+
# )
201+
# objective_scorer = TrueFalseFallbackScorer(
202+
# scorer=primary_objective_scorer,
203+
# fallback_scorer=secondary_objective_scorer,
204+
# )
205+
# ```
206+
#
207+
# A non-applicable primary (`[]`) returns `[]` without calling the fallback. A non-applicable
208+
# fallback leaves the primary's undetermined judgment in place. If both abstain, the result
209+
# remains undetermined. Exceptions propagate; they are not treated as abstentions.
210+
#
211+
# Multiple child results are rejected rather than paired by position or discarded.
212+
# Aggregate them explicitly before using fallback if a single aggregate is meaningful.
213+
#
214+
# Only the root score is persisted. The wrapper creates a new score without modifying
215+
# either child result and retains their observation links. Metadata records `resolved_by`
216+
# as `"primary"` or `"fallback"`. Child metadata keys are prefixed with `primary.` and
217+
# `fallback.`, so duplicate keys and nested fallback details are not overwritten.
218+
# When fallback is attempted, `primary_rationale` is retained and `fallback_status` records
219+
# `"complete"`, `"undetermined"`, or `"not_applicable"`. A fallback judgment also supplies
220+
# `fallback_rationale`; the combined rationale explains both attempts.
221+
166222
# %% [markdown]
167223
# ## Scoring a whole conversation
168224
#

‎doc/code/scoring/4_scorer_metrics.ipynb‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,19 @@
132132
"Three alpha values are reported:\n",
133133
"- **`krippendorff_alpha_humans`**: Agreement among human evaluators (baseline quality of labels)\n",
134134
"- **`krippendorff_alpha_model`**: Agreement across multiple model scoring trials (model consistency)\n",
135-
"- **`krippendorff_alpha_combined`**: Overall agreement between humans and model"
135+
"- **`krippendorff_alpha_combined`**: Overall agreement between humans and model\n",
136+
"\n",
137+
"### Error Split by Rater Agreement\n",
138+
"\n",
139+
"When a gold set has more than one human rater, the harm metrics also report the mean absolute error\n",
140+
"separately for **unanimous** responses (every rater on the same side of `contested_threshold`, 0.5) and\n",
141+
"**contested** responses (the raters split across it, so the gold label rests on a 2-1 vote rather than a\n",
142+
"consensus). The aggregate MAE spends part of the scorer's error budget on the contested rows, so a scorer\n",
143+
"can look strong overall while sitting near chance on exactly the responses humans found hard.\n",
144+
"- **`mean_absolute_error_unanimous`** and **`num_unanimous_responses`**\n",
145+
"- **`mean_absolute_error_contested`** and **`num_contested_responses`**\n",
146+
"\n",
147+
"These are `None` for single-rater gold sets, where there is no disagreement to measure."
136148
]
137149
},
138150
{

‎doc/code/scoring/4_scorer_metrics.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@
9292
# - **`krippendorff_alpha_humans`**: Agreement among human evaluators (baseline quality of labels)
9393
# - **`krippendorff_alpha_model`**: Agreement across multiple model scoring trials (model consistency)
9494
# - **`krippendorff_alpha_combined`**: Overall agreement between humans and model
95+
#
96+
# ### Error Split by Rater Agreement
97+
#
98+
# When a gold set has more than one human rater, the harm metrics also report the mean absolute error
99+
# separately for **unanimous** responses (every rater on the same side of `contested_threshold`, 0.5) and
100+
# **contested** responses (the raters split across it, so the gold label rests on a 2-1 vote rather than a
101+
# consensus). The aggregate MAE spends part of the scorer's error budget on the contested rows, so a scorer
102+
# can look strong overall while sitting near chance on exactly the responses humans found hard.
103+
# - **`mean_absolute_error_unanimous`** and **`num_unanimous_responses`**
104+
# - **`mean_absolute_error_contested`** and **`num_contested_responses`**
105+
#
106+
# These are `None` for single-rater gold sets, where there is no disagreement to measure.
95107

96108
# %% [markdown]
97109
# ## Retrieving Scorer Metrics

0 commit comments

Comments
 (0)