boothpy 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boothpy
3
- Version: 0.4.2
4
- Summary: A lightweight checkpoint layer for LLM outputs — Path B (self-reported confidence with reconsideration retries, sync and async) plus Path A's evidence-agreement gate.
3
+ Version: 0.4.4
4
+ Summary: A lightweight checkpoint library for LLM outputs.
5
5
  Author: Vedant Brahmbhatt
6
6
  License: MIT License
7
7
 
@@ -57,7 +57,7 @@ The name comes from the idea of a **ticket booth, toll booth, or parking/payment
57
57
 
58
58
  ## Current Status
59
59
 
60
- **v0.4.2**
60
+ **v0.4.4**
61
61
 
62
62
  BOOTH currently provides:
63
63
 
@@ -69,7 +69,7 @@ BOOTH currently provides:
69
69
  * synchronous and asynchronous LLM checkpoint functions
70
70
  * evidence-agreement checking against evidence supplied by the caller
71
71
  * configurable confidence and evidence thresholds
72
- * structured result objects, including `result.method` to identify which mechanism produced a result
72
+ * structured result objects, including `result.method` to identify which mechanism produced a result, and `result.parsed` to expose the model's raw JSON response
73
73
  * attempt history
74
74
  * explicit `VERIFIED`, `REPAIRED`, `AMBIGUOUS`, `UNCERTAIN`, and `BLOCKED` statuses
75
75
 
@@ -125,6 +125,8 @@ If BOOTH cannot obtain an acceptable result, it returns `UNCERTAIN`.
125
125
 
126
126
  BOOTH also provides `check_with_evidence()` for applications that already have evidence from their own RAG, search, database, or tool pipeline.
127
127
 
128
+ **On robustness to malformed model output:** every field BOOTH extracts from the model's JSON response is validated against its expected type before being trusted, not just parsed and used as-is. A model returning the JSON string `"false"` instead of the boolean `false` for `ambiguous`, or the boolean `true` instead of a number for `confidence`, is rejected and treated as an unparseable attempt rather than silently coerced into the wrong value — the same discipline BOOTH has always applied to an out-of-range confidence score, now applied consistently across every coerced field. See the [Changelog](CHANGELOG.md)'s `v0.4.4` entry for the specific cases this covers.
129
+
128
130
  ---
129
131
 
130
132
  ## Features
@@ -253,7 +255,7 @@ result = booth.check(
253
255
  )
254
256
  ```
255
257
 
256
- `validator` receives the attempt's answer and returns either:
258
+ `validator` receives the attempt's answer and returns one of:
257
259
 
258
260
  * `True` / `False` — a plain pass/fail. `False` produces a generic failure message.
259
261
  * `(bool, str)` — pass/fail plus a specific reason, shown to the model verbatim on the retry prompt:
@@ -267,14 +269,19 @@ def validate_amount(answer: str):
267
269
  result = booth.check(call_llm, prompt, validator=validate_amount)
268
270
  ```
269
271
 
272
+ * `(bool, None)`, or the equivalent as a **list** rather than a tuple (`[bool, str]` / `[bool, None]`) — accepted with the identical contract as the tuple form above (0.4.4+). `return True, None` is a natural way to write "passed, no message needed," and `[passed, message]` is an easy habit to fall into; both previously triggered an "invalid return type" rejection even though the intent was clear. A `False` with no message gets a generic fallback message, same as bare `False`.
273
+ * `numpy.bool_` (and other duck-typed booleans) are accepted anywhere a plain `bool` is (0.4.4+), recognized by type identity rather than by importing `numpy` — BOOTH stays zero-dependency.
274
+
270
275
  **Ordering:** an attempt is only run through `validator` if it parsed successfully **and** was not flagged ambiguous — a question that's ambiguous as asked isn't something a validator should be judging, and there is nothing to validate if the response never parsed. A validation failure is checked *before* the confidence gate: an answer that fails your validator does not get a chance to pass purely on high self-reported confidence.
271
276
 
272
- An exception raised inside `validator`, or a return value that isn't `bool` or `(bool, str)`, is treated as a failed validation — it never propagates out of `check()`/`acheck()`.
277
+ An exception raised inside `validator`, or a return value that isn't one of the accepted shapes above, is treated as a failed validation — it never propagates out of `check()`/`acheck()`.
273
278
 
274
279
  `validator=None` (the default) is a true no-op: every code path this parameter introduces is unreachable if you never pass it, so existing calls are unaffected.
275
280
 
276
281
  `validator` must be **synchronous**, for both `check()` and `acheck()`. If your validation logic needs to await something (an API call, a DB lookup), resolve it yourself first and pass a plain sync closure in.
277
282
 
283
+ The type alias for a validator function, `ValidatorFn`, is importable from the top-level package (`from booth import ValidatorFn`) for type-hinting your own validator functions.
284
+
278
285
  ---
279
286
 
280
287
  ### `result.method`
@@ -293,7 +300,7 @@ result.method
293
300
  # attempt that did parse and did pass validation
294
301
  ```
295
302
 
296
- This is most useful for `UNCERTAIN` results, where it distinguishes three genuinely different problems that call for different fixes:
303
+ This is most useful for `UNCERTAIN` results, where it distinguishes genuinely different problems that call for different fixes:
297
304
 
298
305
  ```python
299
306
  if result.status == booth.UNCERTAIN:
@@ -309,6 +316,40 @@ if result.status == booth.UNCERTAIN:
309
316
 
310
317
  ---
311
318
 
319
+ ### `result.parsed`
320
+
321
+ Exposes the model's **raw JSON response object**, exactly as returned — before any of BOOTH's own coercion (`str(answer)`, `float(confidence)`, forcing `interpretations` into a list of strings, and so on).
322
+
323
+ ```python
324
+ result = booth.check(call_llm, "What is the order ID for this request?")
325
+
326
+ result.answer # BOOTH's normalized answer, e.g. "ORD-4471"
327
+ result.parsed # the raw dict the model returned, unmodified
328
+ ```
329
+
330
+ This is a **transparency layer, not a second validation layer** — BOOTH already decided `answer`/`confidence`/`ambiguous` from this same object; `parsed` exists so you can see exactly what the model said, including fields BOOTH itself doesn't use:
331
+
332
+ ```python
333
+ def call_llm(prompt: str) -> str:
334
+ # imagine the model, prompted to also include a source field,
335
+ # returns:
336
+ # {"answer": "30 days", "confidence": "0.95", "ambiguous": false,
337
+ # "source_document": "policy.pdf"}
338
+ ...
339
+
340
+ result = booth.check(call_llm, prompt)
341
+
342
+ result.confidence # 0.95 (float — BOOTH coerced it)
343
+ result.parsed["confidence"] # "0.95" (str — the raw value, untouched)
344
+ result.parsed["source_document"] # "policy.pdf" — a field BOOTH never asked for or uses
345
+ ```
346
+
347
+ That type divergence between `result.confidence` and `result.parsed["confidence"]` is deliberate, not a bug — `parsed` is meant to show you precisely what came back, even where BOOTH normalized it for its own use.
348
+
349
+ `parsed` reflects the attempt that determined the result: the winning attempt for `VERIFIED`/`REPAIRED`/`AMBIGUOUS`, the last *successfully parsed* attempt for `UNCERTAIN`, or `None` if no attempt ever parsed. `check_with_evidence()` results always have `parsed=None` — there is no LLM JSON parse involved on that path at all.
350
+
351
+ ---
352
+
312
353
  ### Synchronous and asynchronous APIs
313
354
 
314
355
  BOOTH provides both:
@@ -360,7 +401,7 @@ async def main():
360
401
  asyncio.run(main())
361
402
  ```
362
403
 
363
- Both APIs use the same decision logic, including `validator`. The difference is how the supplied LLM function is called.
404
+ Both APIs use the same decision logic, including `validator` and `parsed`. The difference is how the supplied LLM function is called.
364
405
 
365
406
  ---
366
407
 
@@ -416,7 +457,7 @@ A score of `0.87` passes. A score of `0.62` does not.
416
457
 
417
458
  Boolean comparison results are treated as strict pass/fail values. `evidence_threshold` is not applied to boolean results.
418
459
 
419
- `check_with_evidence()` has no `validator` concept — it is a standalone, single-purpose comparison gate, untouched by the `validator` addition in this release.
460
+ `check_with_evidence()` has no `validator` concept, and its results always have `parsed=None` — it is a standalone, single-purpose comparison gate, untouched by either the `validator` or `parsed` additions.
420
461
 
421
462
  ---
422
463
 
@@ -484,7 +525,7 @@ Optional callback invoked after each attempt.
484
525
 
485
526
  #### `validator` (keyword-only, 0.4.2+)
486
527
 
487
- Optional `Callable[[str], bool | tuple[bool, str]]`. Runs on an attempt's answer only if that attempt parsed successfully and was not ambiguous. See [Custom validation with `validator`](#custom-validation-with-validator) above for the full contract. Must be synchronous. Default `None` — a true no-op.
528
+ Optional `ValidatorFn` — `Callable[[str], bool | tuple[bool, str]]`. Runs on an attempt's answer only if that attempt parsed successfully and was not ambiguous. See [Custom validation with `validator`](#custom-validation-with-validator) above for the full accepted-shape contract (widened in 0.4.4 to also accept `(bool, None)`, list forms, and `numpy.bool_`). Must be synchronous. Default `None` — a true no-op.
488
529
 
489
530
  ---
490
531
 
@@ -530,6 +571,7 @@ Checks an answer against caller-supplied evidence. It:
530
571
  * performs no retries
531
572
  * does not modify a previous `BoothResult`
532
573
  * has no `validator` parameter — it is a standalone comparison gate
574
+ * always returns `parsed=None` — there is no LLM JSON response involved
533
575
  * uses the caller's `compare_fn`
534
576
 
535
577
  #### `answer`
@@ -568,6 +610,7 @@ result.ambiguous
568
610
  result.interpretations
569
611
  result.all_parse_failed
570
612
  result.method
613
+ result.parsed
571
614
  ```
572
615
 
573
616
  ### `answer`
@@ -588,7 +631,7 @@ The comparison score produced by `check_with_evidence()`. `None` for normal `che
588
631
 
589
632
  ### `attempts`
590
633
 
591
- The full history of LLM attempts made by `check()` or `acheck()`, each including per-attempt `passed_validation` / `validation_error` (always `True` / `None` if no `validator` was supplied). Evidence checks do not make attempts, so their attempt list is empty.
634
+ The full history of LLM attempts made by `check()` or `acheck()`, each including per-attempt `passed_validation` / `validation_error` (always `True` / `None` if no `validator` was supplied) and `parsed` (the raw JSON object for that specific attempt, `None` if it failed to parse). Evidence checks do not make attempts, so their attempt list is empty.
592
635
 
593
636
  ### `n_attempts`
594
637
 
@@ -614,6 +657,10 @@ The interpretations reported when the model marks a question as ambiguous.
614
657
 
615
658
  Which mechanism produced the result — `"ambiguity"`, `"evidence"`, `"parse_failure"`, `"validation"`, or `"confidence"`. See [`result.method`](#resultmethod) above.
616
659
 
660
+ ### `parsed` (0.4.3+)
661
+
662
+ The model's raw, uncoerced JSON response object. See [`result.parsed`](#resultparsed) above.
663
+
617
664
  ---
618
665
 
619
666
  ## Result Statuses
@@ -666,6 +713,7 @@ BOOTH currently does **not**:
666
713
  * provide calibrated confidence probabilities
667
714
  * guarantee that retrieved evidence is correct, complete, relevant, or current
668
715
  * guarantee that a custom `validator` is itself correct — a validator can pass a wrong answer or reject a correct one, same as any other application-supplied rule
716
+ * validate or enforce a schema on `result.parsed` — it is exposed as-is, entirely unvalidated
669
717
  * replace application-specific validation or safety systems (though `validator` gives you a documented hook to plug your own logic into BOOTH's retry loop rather than reimplementing that loop yourself)
670
718
 
671
719
  BOOTH is a **checkpoint library**, not an LLM framework, search engine, RAG framework, or autonomous verification system.
@@ -686,6 +734,10 @@ Ambiguity detection depends on the model recognizing the ambiguity. BOOTH can de
686
734
 
687
735
  `validator` is exactly as reliable as the logic you give it. BOOTH enforces that a validator's decision is respected consistently in the retry loop — it does not, and cannot, check whether the validator's own logic is actually correct for your use case.
688
736
 
737
+ ### `result.parsed` is unvalidated
738
+
739
+ `result.parsed` is the model's raw JSON object, exposed as-is. BOOTH does not validate its shape, enforce a schema on it, or guarantee any field beyond the five it extracts for itself (`answer`, `confidence`, `ambiguous`, `interpretations`, `chosen_interpretation`) is present or well-typed. Reading extra fields from `parsed` is entirely the caller's responsibility, including handling missing keys or unexpected types.
740
+
689
741
  ### Evidence quality
690
742
 
691
743
  Evidence checking is only as useful as the evidence and comparison function supplied by the application. If the evidence is wrong, incomplete, outdated, or unrelated, BOOTH does not independently detect that. Likewise, a weak `compare_fn` can produce a misleading result. This includes the case where retrieved evidence is baked into the model's own prompt as RAG context — a wrong document can make the model's answer both more *confident* and more evidence-*consistent*, without becoming more correct.
@@ -742,6 +794,7 @@ Future BOOTH development may explore:
742
794
  * richer composition of multiple checkpoint results
743
795
  * better evaluation and calibration tooling
744
796
  * additional integrations with retrieval and tool systems
797
+ * an optional, lightweight structured-schema gate on top of `result.parsed` — deferred deliberately until there's a clear, minimal shape for it, rather than reaching for a general schema/validation dependency
745
798
 
746
799
  These are future directions, not capabilities currently guaranteed by the library.
747
800
 
@@ -775,9 +828,9 @@ Run the test suite:
775
828
  pytest
776
829
  ```
777
830
 
778
- The test suite covers the core checkpoint behavior, asynchronous API, parsing behavior, ambiguity handling, reconsideration, custom validation, and evidence checking. CI runs the full suite on push/PR across Python 3.9–3.12.
831
+ The test suite covers the core checkpoint behavior, asynchronous API, parsing behavior, ambiguity handling, reconsideration, custom validation, evidence checking, raw-response exposure via `result.parsed`, and a dedicated regression suite for the `v0.4.4` bugfix batch. CI runs the full suite on push/PR across Python 3.9–3.12.
779
832
 
780
- The validator tests include cases for: `validator=None` full-regression parity, boolean and `(bool, str)` returns, exceptions raised inside a validator, invalid return types, validator never running on ambiguous or unparseable attempts, fail-then-pass producing `REPAIRED`, the distinct validation-failure retry prompt, mixed multi-attempt histories and how `result.method` resolves them, and `check()`/`acheck()` parity under `validator`.
833
+ The `v0.4.4` regression tests specifically verify: a JSON string `"false"` for `ambiguous` is not misread as `True`; a JSON boolean for `confidence` is rejected rather than silently accepted as `1.0`/`0.0`; `ValidatorFn` is importable from the top-level package; a falsy-but-present `chosen_interpretation` (`0`, `""`) is preserved rather than discarded to `None`; `numpy.bool_`-shaped validator returns are accepted; `(bool, None)` and list-shaped validator returns are accepted while genuinely malformed shapes are still rejected; confidence exactly equal to `threshold` passes on both `check()` and `acheck()`; list-wrapped JSON (`[{...}]`) still recovers via the parsing fallback; and the previously-unverified nested-brace edge case's actual behavior is now locked in by a test rather than assumed.
781
834
 
782
835
  ---
783
836
 
@@ -787,9 +840,11 @@ The validator tests include cases for: `validator=None` full-regression parity,
787
840
  2. **Make uncertainty explicit.** When an output does not meet the configured acceptance condition, return a structured status instead of silently passing it through.
788
841
  3. **Treat ambiguity, validation, and confidence as separate, ordered checks.** A confident, validator-passing answer can still be ambiguous; a confident answer can still fail a caller's own validation rule before confidence is ever consulted.
789
842
  4. **Reconsider instead of blindly resampling.** Retries give the model an opportunity to examine its previous response — and the reason it failed (parse failure, validation failure, or low confidence) determines what the model is actually shown.
790
- 5. **Keep evidence retrieval outside BOOTH.** Applications remain free to use their own RAG, search, database, or tool infrastructure.
791
- 6. **Do not pretend agreement is truth.** Agreement with an answer, confidence value, custom validator, or retrieved evidence is not the same as independently proving the claim.
792
- 7. **Stay provider-agnostic.** BOOTH works with different LLM providers because the application supplies the model-calling function.
843
+ 5. **Expose, don't reinterpret.** `result.parsed` is a transparency layer over data BOOTH already has, not a second parser or validator — it shows the model's raw response rather than deciding what it should mean.
844
+ 6. **Reject invalid data, don't silently coerce it.** An out-of-range confidence, a boolean masquerading as a confidence score, or an unrecognized value for `ambiguous` are all treated as reasons to reject the attempt — never guessed at or silently reinterpreted into something that happens not to crash. Applied consistently across every coerced field as of `v0.4.4`.
845
+ 7. **Keep evidence retrieval outside BOOTH.** Applications remain free to use their own RAG, search, database, or tool infrastructure.
846
+ 8. **Do not pretend agreement is truth.** Agreement with an answer, confidence value, custom validator, or retrieved evidence is not the same as independently proving the claim.
847
+ 9. **Stay provider-agnostic.** BOOTH works with different LLM providers because the application supplies the model-calling function.
793
848
 
794
849
  ---
795
850
 
@@ -12,7 +12,7 @@ The name comes from the idea of a **ticket booth, toll booth, or parking/payment
12
12
 
13
13
  ## Current Status
14
14
 
15
- **v0.4.2**
15
+ **v0.4.4**
16
16
 
17
17
  BOOTH currently provides:
18
18
 
@@ -24,7 +24,7 @@ BOOTH currently provides:
24
24
  * synchronous and asynchronous LLM checkpoint functions
25
25
  * evidence-agreement checking against evidence supplied by the caller
26
26
  * configurable confidence and evidence thresholds
27
- * structured result objects, including `result.method` to identify which mechanism produced a result
27
+ * structured result objects, including `result.method` to identify which mechanism produced a result, and `result.parsed` to expose the model's raw JSON response
28
28
  * attempt history
29
29
  * explicit `VERIFIED`, `REPAIRED`, `AMBIGUOUS`, `UNCERTAIN`, and `BLOCKED` statuses
30
30
 
@@ -80,6 +80,8 @@ If BOOTH cannot obtain an acceptable result, it returns `UNCERTAIN`.
80
80
 
81
81
  BOOTH also provides `check_with_evidence()` for applications that already have evidence from their own RAG, search, database, or tool pipeline.
82
82
 
83
+ **On robustness to malformed model output:** every field BOOTH extracts from the model's JSON response is validated against its expected type before being trusted, not just parsed and used as-is. A model returning the JSON string `"false"` instead of the boolean `false` for `ambiguous`, or the boolean `true` instead of a number for `confidence`, is rejected and treated as an unparseable attempt rather than silently coerced into the wrong value — the same discipline BOOTH has always applied to an out-of-range confidence score, now applied consistently across every coerced field. See the [Changelog](CHANGELOG.md)'s `v0.4.4` entry for the specific cases this covers.
84
+
83
85
  ---
84
86
 
85
87
  ## Features
@@ -208,7 +210,7 @@ result = booth.check(
208
210
  )
209
211
  ```
210
212
 
211
- `validator` receives the attempt's answer and returns either:
213
+ `validator` receives the attempt's answer and returns one of:
212
214
 
213
215
  * `True` / `False` — a plain pass/fail. `False` produces a generic failure message.
214
216
  * `(bool, str)` — pass/fail plus a specific reason, shown to the model verbatim on the retry prompt:
@@ -222,14 +224,19 @@ def validate_amount(answer: str):
222
224
  result = booth.check(call_llm, prompt, validator=validate_amount)
223
225
  ```
224
226
 
227
+ * `(bool, None)`, or the equivalent as a **list** rather than a tuple (`[bool, str]` / `[bool, None]`) — accepted with the identical contract as the tuple form above (0.4.4+). `return True, None` is a natural way to write "passed, no message needed," and `[passed, message]` is an easy habit to fall into; both previously triggered an "invalid return type" rejection even though the intent was clear. A `False` with no message gets a generic fallback message, same as bare `False`.
228
+ * `numpy.bool_` (and other duck-typed booleans) are accepted anywhere a plain `bool` is (0.4.4+), recognized by type identity rather than by importing `numpy` — BOOTH stays zero-dependency.
229
+
225
230
  **Ordering:** an attempt is only run through `validator` if it parsed successfully **and** was not flagged ambiguous — a question that's ambiguous as asked isn't something a validator should be judging, and there is nothing to validate if the response never parsed. A validation failure is checked *before* the confidence gate: an answer that fails your validator does not get a chance to pass purely on high self-reported confidence.
226
231
 
227
- An exception raised inside `validator`, or a return value that isn't `bool` or `(bool, str)`, is treated as a failed validation — it never propagates out of `check()`/`acheck()`.
232
+ An exception raised inside `validator`, or a return value that isn't one of the accepted shapes above, is treated as a failed validation — it never propagates out of `check()`/`acheck()`.
228
233
 
229
234
  `validator=None` (the default) is a true no-op: every code path this parameter introduces is unreachable if you never pass it, so existing calls are unaffected.
230
235
 
231
236
  `validator` must be **synchronous**, for both `check()` and `acheck()`. If your validation logic needs to await something (an API call, a DB lookup), resolve it yourself first and pass a plain sync closure in.
232
237
 
238
+ The type alias for a validator function, `ValidatorFn`, is importable from the top-level package (`from booth import ValidatorFn`) for type-hinting your own validator functions.
239
+
233
240
  ---
234
241
 
235
242
  ### `result.method`
@@ -248,7 +255,7 @@ result.method
248
255
  # attempt that did parse and did pass validation
249
256
  ```
250
257
 
251
- This is most useful for `UNCERTAIN` results, where it distinguishes three genuinely different problems that call for different fixes:
258
+ This is most useful for `UNCERTAIN` results, where it distinguishes genuinely different problems that call for different fixes:
252
259
 
253
260
  ```python
254
261
  if result.status == booth.UNCERTAIN:
@@ -264,6 +271,40 @@ if result.status == booth.UNCERTAIN:
264
271
 
265
272
  ---
266
273
 
274
+ ### `result.parsed`
275
+
276
+ Exposes the model's **raw JSON response object**, exactly as returned — before any of BOOTH's own coercion (`str(answer)`, `float(confidence)`, forcing `interpretations` into a list of strings, and so on).
277
+
278
+ ```python
279
+ result = booth.check(call_llm, "What is the order ID for this request?")
280
+
281
+ result.answer # BOOTH's normalized answer, e.g. "ORD-4471"
282
+ result.parsed # the raw dict the model returned, unmodified
283
+ ```
284
+
285
+ This is a **transparency layer, not a second validation layer** — BOOTH already decided `answer`/`confidence`/`ambiguous` from this same object; `parsed` exists so you can see exactly what the model said, including fields BOOTH itself doesn't use:
286
+
287
+ ```python
288
+ def call_llm(prompt: str) -> str:
289
+ # imagine the model, prompted to also include a source field,
290
+ # returns:
291
+ # {"answer": "30 days", "confidence": "0.95", "ambiguous": false,
292
+ # "source_document": "policy.pdf"}
293
+ ...
294
+
295
+ result = booth.check(call_llm, prompt)
296
+
297
+ result.confidence # 0.95 (float — BOOTH coerced it)
298
+ result.parsed["confidence"] # "0.95" (str — the raw value, untouched)
299
+ result.parsed["source_document"] # "policy.pdf" — a field BOOTH never asked for or uses
300
+ ```
301
+
302
+ That type divergence between `result.confidence` and `result.parsed["confidence"]` is deliberate, not a bug — `parsed` is meant to show you precisely what came back, even where BOOTH normalized it for its own use.
303
+
304
+ `parsed` reflects the attempt that determined the result: the winning attempt for `VERIFIED`/`REPAIRED`/`AMBIGUOUS`, the last *successfully parsed* attempt for `UNCERTAIN`, or `None` if no attempt ever parsed. `check_with_evidence()` results always have `parsed=None` — there is no LLM JSON parse involved on that path at all.
305
+
306
+ ---
307
+
267
308
  ### Synchronous and asynchronous APIs
268
309
 
269
310
  BOOTH provides both:
@@ -315,7 +356,7 @@ async def main():
315
356
  asyncio.run(main())
316
357
  ```
317
358
 
318
- Both APIs use the same decision logic, including `validator`. The difference is how the supplied LLM function is called.
359
+ Both APIs use the same decision logic, including `validator` and `parsed`. The difference is how the supplied LLM function is called.
319
360
 
320
361
  ---
321
362
 
@@ -371,7 +412,7 @@ A score of `0.87` passes. A score of `0.62` does not.
371
412
 
372
413
  Boolean comparison results are treated as strict pass/fail values. `evidence_threshold` is not applied to boolean results.
373
414
 
374
- `check_with_evidence()` has no `validator` concept — it is a standalone, single-purpose comparison gate, untouched by the `validator` addition in this release.
415
+ `check_with_evidence()` has no `validator` concept, and its results always have `parsed=None` — it is a standalone, single-purpose comparison gate, untouched by either the `validator` or `parsed` additions.
375
416
 
376
417
  ---
377
418
 
@@ -439,7 +480,7 @@ Optional callback invoked after each attempt.
439
480
 
440
481
  #### `validator` (keyword-only, 0.4.2+)
441
482
 
442
- Optional `Callable[[str], bool | tuple[bool, str]]`. Runs on an attempt's answer only if that attempt parsed successfully and was not ambiguous. See [Custom validation with `validator`](#custom-validation-with-validator) above for the full contract. Must be synchronous. Default `None` — a true no-op.
483
+ Optional `ValidatorFn` — `Callable[[str], bool | tuple[bool, str]]`. Runs on an attempt's answer only if that attempt parsed successfully and was not ambiguous. See [Custom validation with `validator`](#custom-validation-with-validator) above for the full accepted-shape contract (widened in 0.4.4 to also accept `(bool, None)`, list forms, and `numpy.bool_`). Must be synchronous. Default `None` — a true no-op.
443
484
 
444
485
  ---
445
486
 
@@ -485,6 +526,7 @@ Checks an answer against caller-supplied evidence. It:
485
526
  * performs no retries
486
527
  * does not modify a previous `BoothResult`
487
528
  * has no `validator` parameter — it is a standalone comparison gate
529
+ * always returns `parsed=None` — there is no LLM JSON response involved
488
530
  * uses the caller's `compare_fn`
489
531
 
490
532
  #### `answer`
@@ -523,6 +565,7 @@ result.ambiguous
523
565
  result.interpretations
524
566
  result.all_parse_failed
525
567
  result.method
568
+ result.parsed
526
569
  ```
527
570
 
528
571
  ### `answer`
@@ -543,7 +586,7 @@ The comparison score produced by `check_with_evidence()`. `None` for normal `che
543
586
 
544
587
  ### `attempts`
545
588
 
546
- The full history of LLM attempts made by `check()` or `acheck()`, each including per-attempt `passed_validation` / `validation_error` (always `True` / `None` if no `validator` was supplied). Evidence checks do not make attempts, so their attempt list is empty.
589
+ The full history of LLM attempts made by `check()` or `acheck()`, each including per-attempt `passed_validation` / `validation_error` (always `True` / `None` if no `validator` was supplied) and `parsed` (the raw JSON object for that specific attempt, `None` if it failed to parse). Evidence checks do not make attempts, so their attempt list is empty.
547
590
 
548
591
  ### `n_attempts`
549
592
 
@@ -569,6 +612,10 @@ The interpretations reported when the model marks a question as ambiguous.
569
612
 
570
613
  Which mechanism produced the result — `"ambiguity"`, `"evidence"`, `"parse_failure"`, `"validation"`, or `"confidence"`. See [`result.method`](#resultmethod) above.
571
614
 
615
+ ### `parsed` (0.4.3+)
616
+
617
+ The model's raw, uncoerced JSON response object. See [`result.parsed`](#resultparsed) above.
618
+
572
619
  ---
573
620
 
574
621
  ## Result Statuses
@@ -621,6 +668,7 @@ BOOTH currently does **not**:
621
668
  * provide calibrated confidence probabilities
622
669
  * guarantee that retrieved evidence is correct, complete, relevant, or current
623
670
  * guarantee that a custom `validator` is itself correct — a validator can pass a wrong answer or reject a correct one, same as any other application-supplied rule
671
+ * validate or enforce a schema on `result.parsed` — it is exposed as-is, entirely unvalidated
624
672
  * replace application-specific validation or safety systems (though `validator` gives you a documented hook to plug your own logic into BOOTH's retry loop rather than reimplementing that loop yourself)
625
673
 
626
674
  BOOTH is a **checkpoint library**, not an LLM framework, search engine, RAG framework, or autonomous verification system.
@@ -641,6 +689,10 @@ Ambiguity detection depends on the model recognizing the ambiguity. BOOTH can de
641
689
 
642
690
  `validator` is exactly as reliable as the logic you give it. BOOTH enforces that a validator's decision is respected consistently in the retry loop — it does not, and cannot, check whether the validator's own logic is actually correct for your use case.
643
691
 
692
+ ### `result.parsed` is unvalidated
693
+
694
+ `result.parsed` is the model's raw JSON object, exposed as-is. BOOTH does not validate its shape, enforce a schema on it, or guarantee any field beyond the five it extracts for itself (`answer`, `confidence`, `ambiguous`, `interpretations`, `chosen_interpretation`) is present or well-typed. Reading extra fields from `parsed` is entirely the caller's responsibility, including handling missing keys or unexpected types.
695
+
644
696
  ### Evidence quality
645
697
 
646
698
  Evidence checking is only as useful as the evidence and comparison function supplied by the application. If the evidence is wrong, incomplete, outdated, or unrelated, BOOTH does not independently detect that. Likewise, a weak `compare_fn` can produce a misleading result. This includes the case where retrieved evidence is baked into the model's own prompt as RAG context — a wrong document can make the model's answer both more *confident* and more evidence-*consistent*, without becoming more correct.
@@ -697,6 +749,7 @@ Future BOOTH development may explore:
697
749
  * richer composition of multiple checkpoint results
698
750
  * better evaluation and calibration tooling
699
751
  * additional integrations with retrieval and tool systems
752
+ * an optional, lightweight structured-schema gate on top of `result.parsed` — deferred deliberately until there's a clear, minimal shape for it, rather than reaching for a general schema/validation dependency
700
753
 
701
754
  These are future directions, not capabilities currently guaranteed by the library.
702
755
 
@@ -730,9 +783,9 @@ Run the test suite:
730
783
  pytest
731
784
  ```
732
785
 
733
- The test suite covers the core checkpoint behavior, asynchronous API, parsing behavior, ambiguity handling, reconsideration, custom validation, and evidence checking. CI runs the full suite on push/PR across Python 3.9–3.12.
786
+ The test suite covers the core checkpoint behavior, asynchronous API, parsing behavior, ambiguity handling, reconsideration, custom validation, evidence checking, raw-response exposure via `result.parsed`, and a dedicated regression suite for the `v0.4.4` bugfix batch. CI runs the full suite on push/PR across Python 3.9–3.12.
734
787
 
735
- The validator tests include cases for: `validator=None` full-regression parity, boolean and `(bool, str)` returns, exceptions raised inside a validator, invalid return types, validator never running on ambiguous or unparseable attempts, fail-then-pass producing `REPAIRED`, the distinct validation-failure retry prompt, mixed multi-attempt histories and how `result.method` resolves them, and `check()`/`acheck()` parity under `validator`.
788
+ The `v0.4.4` regression tests specifically verify: a JSON string `"false"` for `ambiguous` is not misread as `True`; a JSON boolean for `confidence` is rejected rather than silently accepted as `1.0`/`0.0`; `ValidatorFn` is importable from the top-level package; a falsy-but-present `chosen_interpretation` (`0`, `""`) is preserved rather than discarded to `None`; `numpy.bool_`-shaped validator returns are accepted; `(bool, None)` and list-shaped validator returns are accepted while genuinely malformed shapes are still rejected; confidence exactly equal to `threshold` passes on both `check()` and `acheck()`; list-wrapped JSON (`[{...}]`) still recovers via the parsing fallback; and the previously-unverified nested-brace edge case's actual behavior is now locked in by a test rather than assumed.
736
789
 
737
790
  ---
738
791
 
@@ -742,9 +795,11 @@ The validator tests include cases for: `validator=None` full-regression parity,
742
795
  2. **Make uncertainty explicit.** When an output does not meet the configured acceptance condition, return a structured status instead of silently passing it through.
743
796
  3. **Treat ambiguity, validation, and confidence as separate, ordered checks.** A confident, validator-passing answer can still be ambiguous; a confident answer can still fail a caller's own validation rule before confidence is ever consulted.
744
797
  4. **Reconsider instead of blindly resampling.** Retries give the model an opportunity to examine its previous response — and the reason it failed (parse failure, validation failure, or low confidence) determines what the model is actually shown.
745
- 5. **Keep evidence retrieval outside BOOTH.** Applications remain free to use their own RAG, search, database, or tool infrastructure.
746
- 6. **Do not pretend agreement is truth.** Agreement with an answer, confidence value, custom validator, or retrieved evidence is not the same as independently proving the claim.
747
- 7. **Stay provider-agnostic.** BOOTH works with different LLM providers because the application supplies the model-calling function.
798
+ 5. **Expose, don't reinterpret.** `result.parsed` is a transparency layer over data BOOTH already has, not a second parser or validator — it shows the model's raw response rather than deciding what it should mean.
799
+ 6. **Reject invalid data, don't silently coerce it.** An out-of-range confidence, a boolean masquerading as a confidence score, or an unrecognized value for `ambiguous` are all treated as reasons to reject the attempt — never guessed at or silently reinterpreted into something that happens not to crash. Applied consistently across every coerced field as of `v0.4.4`.
800
+ 7. **Keep evidence retrieval outside BOOTH.** Applications remain free to use their own RAG, search, database, or tool infrastructure.
801
+ 8. **Do not pretend agreement is truth.** Agreement with an answer, confidence value, custom validator, or retrieved evidence is not the same as independently proving the claim.
802
+ 9. **Stay provider-agnostic.** BOOTH works with different LLM providers because the application supplies the model-calling function.
748
803
 
749
804
  ---
750
805
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "boothpy"
7
- version = "0.4.2"
8
- description = "A lightweight checkpoint layer for LLM outputs — Path B (self-reported confidence with reconsideration retries, sync and async) plus Path A's evidence-agreement gate."
7
+ version = "0.4.4"
8
+ description = "A lightweight checkpoint library for LLM outputs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = { file = "LICENSE" }
@@ -5,6 +5,7 @@ from booth.core import (
5
5
  acheck,
6
6
  check_with_evidence,
7
7
  CompareFn,
8
+ ValidatorFn,
8
9
  VERIFIED,
9
10
  REPAIRED,
10
11
  AMBIGUOUS,
@@ -14,7 +15,7 @@ from booth.core import (
14
15
  DEFAULT_MAX_RETRIES,
15
16
  )
16
17
 
17
- __version__ = "0.4.2"
18
+ __version__ = "0.4.4"
18
19
 
19
20
  __all__ = [
20
21
  "Attempt",
@@ -23,6 +24,7 @@ __all__ = [
23
24
  "acheck",
24
25
  "check_with_evidence",
25
26
  "CompareFn",
27
+ "ValidatorFn",
26
28
  "VERIFIED",
27
29
  "REPAIRED",
28
30
  "AMBIGUOUS",