Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
97 changes: 97 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,103 @@ All notable changes to this SDK are documented here. Format follows

## [Unreleased]

Minor release (2.21.0). Existing code keeps working unchanged; nothing to do
on upgrade.

### Added

- **Reads both response shapes.** The API is adding a newer, dated response
shape that gives each field one name across every endpoint. This release
still asks for the original shape (it sends the same `X-Lenz-API-Version`
as before), and every model now reads either shape. A body is read as the
newer shape only when it carries something only that shape has; every other
body is parsed exactly as before (same fields, values, dump, `repr`,
`exclude_unset`, schema and pickle). The newer names are read-only
properties, computed from whichever shape arrived, not model fields:
- `ExtractedClaims.claims`: every claim found, always a list, each an
`ExtractedClaim` with `claim` and (with `locate=True`) `positions`.
- `AssessClaim.status` (`"completed"` / `"failed"`), `AssessClaim.failure`
(`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) and
`AssessClaim.more_claims`; `AssessResponse.status` (`ok` /
`no_checkable_claim` / `error`, the `AssessStatus` constant) and
`AssessResponse.failure`.
- `TaskStatus.failure`, the same block, on a failed verification.
- `TaskAccepted.claim`, `BatchItemResult.claim`, `CandidateClaim.claim`.
- `completed_at` on `Verification`, `VerificationListItem` and
`ReviewVerification`.
- `FailureBlock.code` and `FailureBlock.detail`;
`ReviewAssessment.more_claims`; `ReviewSummary.claims_found`,
`claim_limit_exceeded` (``None`` from the original shape, which says only
that the limit was reached: read `more_claims` there) and
`citation_limit_exceeded`;
`CitecheckSummary.citation_limit_exceeded`.
- Webhooks: an `event_id` property on every event,
`VerificationFailed.failure`, `VerificationNeedsInput.reason` and
`.claims` (properties: `dataclasses.asdict` and `repr` are unchanged). A review or citation-check
event in the newer shape carries no `task_id`; it then reads the
`review_id` / `citecheck_id`, so code keyed on `task_id` keeps one key per
review (deduplicate deliveries on `event_id`, as before). `parse_webhook` and
`LenzWebhooks.parse` read the newer envelope too (`event`, `event_id`,
the work's id, `status`, and the polled body under `verification` /
`review` / `citecheck`).
- Errors: a 409 for a failed run, a 429 and a 422 are read in either shape
(`failure` block, `retry_after`, `errors` list with a sentence `detail`).

### Deprecated

- The older names, kept with the meaning they always had, whichever shape
arrives: `ExtractedClaims.claim` / `identified_claims` / `locations`;
`AssessClaim.error_code` / `hint` / `identified_claims` (a failed row still
reads `verdict == "Error"` and `confidence == "low"`); `AssessResponse.error`
/ `error_code`; `TaskStatus.error` / `failure_reason` / `failure_detail`;
`claim_text` and `CandidateClaim.text`; `modified_at` (computed from
`completed_at` with its original rule: set only when the verification
completed on a later UTC calendar day than it was created);
`FailureBlock.failure_reason`; `ReviewAssessment.error_code` / `hint` /
`identified_claims`; `claim_limit_reached` / `citation_limit_reached`;
`Usage.quota_resets_at` and the `verify` / `ask` / `assess` blocks (computed
from `credits` and `costs` when a response leaves them out). Reading them
does not warn, and the JSON schema is unchanged. "Nothing
checkable" keeps its old spelling in the old fields (`not_a_claim`,
`no_claim`) and reads `no_checkable_claim` in the new ones;
`ExtractedClaims.status` keeps `not_a_claim`.

### Changed

- **`verify`, `verify_and_wait`, `verify_batch` and `verify_batch_and_wait`
no longer send `webhook_url: ""`** when no webhook URL was given (nor an
empty per-item `webhook_url`). The API has always read an empty value there
as "use the key's default webhook", the same as leaving it out, so nothing
changes now; leaving it out keeps that meaning on later API versions, where
`""` means "no webhook". `review` and `citecheck` send `webhook_url` exactly as
before (there `""` means "no webhook").
- A newer-shape body's `model_dump()` holds what the server sent plus the
original fields filled in from it.

### Before the SDK asks for the newer shape

This release only reads the newer shape; it keeps asking for the original
one. Two things to settle before a release sends the newer date:

- A 422's `code` there is the server's new one (`blank_input` where the
original said `blank_item`, `validation_error` where it said nothing), and
its `message` is the server's sentence, not the field list.
- `lenz verify --json` on a `needs_input` run prints each option as the model
dumps it: from the newer shape that is `{claim, domain, text}`, not
`{text, domain}`.

### Correction

- **The /me/usage fields are not removed on 2026-11-29.** Earlier entries
(2.9.0 and 2.14.0) said `UsageCredits.bonus`, `UsageCapacity.credits` and the
per-capability `verify` / `ask` / `assess` blocks would go on that date.
They are deprecated and kept for existing callers; the API keeps sending
them to integrations built against its original shape. Their deprecation
warnings no longer name a date.
- **`TaskStatus.candidates` and `similar_claims` are not removed on
2026-11-29** either (2.18.0 said so). They are deprecated, always empty, and
kept.

## [2.20.0] - 2026-10-05

### Added
Expand Down
80 changes: 51 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ edited = "".join(chars)
`poll_after_seconds`. Pass `on_update=` to see the quick verdicts as soon as
they are in and each deep check as it lands; without it the helper is silent.
It raises `ReviewFailed` when the review fails (`error_code` and `hint` say
why) and `ReviewTimeout` after `timeout` seconds (600 by default); the review
why; `review.failure.code` is the cause in the newer spelling) and `ReviewTimeout` after `timeout` seconds (600 by default); the review
keeps running, and the error carries its `review_id` and the last body read.
To submit without waiting, `client.review(draft)` returns a `review_id`;
read it with `client.get_review(review_id)`, or only the issues with
Expand Down Expand Up @@ -297,7 +297,7 @@ client = Lenz(api_key="lenz_...")
# 1. extract — pull verifiable claims out of any text (free)
# add focus="..." to narrow it to the claims you care about
out = client.extract(text=llm_output)
claims = out.identified_claims or [out.claim]
claims = [c.claim for c in out.claims]

# 2. assess — one call per 20 claims (extract finds up to 100), one row per claim, same order
quick = [row for i in range(0, len(claims), 20) for row in client.assess(claims=claims[i : i + 20]).claims]
Expand All @@ -308,7 +308,7 @@ for c in quick:

# 3. verify — escalate the low-confidence rows to the full panel + citations
# verify_batch_and_wait takes up to 20 claims a call: the first 20 here
doubtful = [{"claim": c.claim} for c in quick if c.verdict != "Error" and c.confidence == "low"][:20]
doubtful = [{"claim": c.claim} for c in quick if c.status == "completed" and c.confidence == "low"][:20]
results = client.verify_batch_and_wait(claims=doubtful) if doubtful else []
for r in results:
if r.verification:
Expand All @@ -322,13 +322,13 @@ print(reply.content)

`assess(claims=[...])` takes up to 20 claims per call and always answers
with exactly one row per claim, in the order sent. A row that could not be
given a verdict comes back in position with `verdict == "Error"`, an
`error_code` (`no_claim` / `framing_failed` / `upstream_unavailable` /
`timeout` — an open set; the last two are the ones worth resending as-is)
and a one-sentence `hint` on what to send next; it is not charged. A
compound item is assessed on its main claim and lists the other claims it
found in `identified_claims` (also with a `hint`) — send those as their own
items to check the rest. `assess(claim="...")` takes one text and answers
given a verdict comes back in position with `status == "failed"` and a
`failure` block: `failure.code` (`no_checkable_claim` / `framing_failed` /
`upstream_unavailable` / `timeout` — an open set; the last two are the ones
worth resending as-is) and `failure.hint`, one sentence on what to send next;
it is not charged. A compound item is assessed on its main claim and lists the
other claims it found in `more_claims` — send those as their own items to
check the rest. `assess(claim="...")` takes one text and answers
with a row per claim found in it, up to 20, at 1 credit each; a text that
makes more claims gets its 20 most check-worthy checked and the rest in
`more_claims`, unchecked and free — send them back as `claims`, 20 a call.
Expand Down Expand Up @@ -387,8 +387,8 @@ your own claims. Use webhooks for production async flows.

## What you get on the client

- **`client.extract(text=...)`** → `ExtractedClaims`. Free, capped at 1000/account/day. Add `focus=` to narrow the list — see [Steering extract](#steering-extract) — and `locate=True` to keep only the claims traced back to your text, with their positions (`out.locations`). Each attempt waits up to 150s by default (a timeout is retried like any transport error, and the call's idempotency key makes the retry replay the first answer); `timeout=` overrides it for that call.
- **`client.assess(claim=...)`** / **`client.assess(claims=[...])`** → `AssessResponse`. Sync. One statement (~15s; `text=` is accepted as an alias: a document is `text`, a claim is `claim`) or a list of up to 20 claims in one call (~15s) — exactly one row per claim, in order; rows that got no verdict are `"Error"` rows with an `error_code` and a `hint`, in position and free. A single text past 20 claims lists the rest in `more_claims` (`[]` otherwise). The two forms are mutually exclusive. `timeout=` overrides the client timeout for that call (both forms default to 100s: a long text can take up to 90s).
- **`client.extract(text=...)`** → `ExtractedClaims`. Free, capped at 1000/account/day. Add `focus=` to narrow the list — see [Steering extract](#steering-extract) — and `locate=True` to keep only the claims traced back to your text, with their positions (each `out.claims[i].positions`). Each attempt waits up to 150s by default (a timeout is retried like any transport error, and the call's idempotency key makes the retry replay the first answer); `timeout=` overrides it for that call.
- **`client.assess(claim=...)`** / **`client.assess(claims=[...])`** → `AssessResponse`. Sync. One statement (~15s; `text=` is accepted as an alias: a document is `text`, a claim is `claim`) or a list of up to 20 claims in one call (~15s) — exactly one row per claim, in order; rows that got no verdict have `status == "failed"` and a `failure` block (`failure.code`, `failure.hint`), in position and free. A single text past 20 claims lists the rest in `more_claims` (`[]` otherwise). The two forms are mutually exclusive. `timeout=` overrides the client timeout for that call (both forms default to 100s: a long text can take up to 90s).
- **`client.verify(...)`** → `TaskAccepted`. Async submit; returns a `task_id`. Get the result by polling (`client.wait(...)` / `client.get_status(...)`) or via a webhook.
- **`client.verify_and_wait(...)`** → `Verification`. Submit + poll until the pipeline lands (sync ergonomic). Equivalent to `wait(verify(...))`.
- **`client.wait(task)`** → `Verification`. Block on a `task_id` (or a `TaskAccepted`) until it terminates. The polling counterpart to a webhook.
Expand Down Expand Up @@ -426,9 +426,9 @@ results = client.verify_batch_and_wait(
)
for r in results:
if r.status == "completed":
print(r.claim_text, "→", r.verification.verdict)
print(r.claim, "→", r.verification.verdict)
else:
print(r.claim_text, "→", r.status) # needs_input | failed | timeout
print(r.claim, "→", r.status) # needs_input | failed | timeout
```

A `failed` item with `status_detail is None` is a verification its account's
Expand Down Expand Up @@ -471,6 +471,31 @@ Every claim-shaped response shares these fields at top level:
| `confidence` | `str` | Categorical: `"high"` \| `"medium"` \| `"low"`. |
| `lenz_score` | `int \| None` | Integer 1–10 (deep verdicts and list endpoints; `assess` omits it). |

### Field names: both response shapes

The API is adding a newer, dated response shape that gives each field one
name across every endpoint. This SDK still asks for the original shape, and
its models read either one. The newer names are attributes already; the
older ones keep working, with the meaning they always had:

| Read this | Instead of (deprecated, still works) |
|---|---|
| `ExtractedClaims.claims` (each `.claim`, `.positions`) | `claim`, `identified_claims`, `locations` |
| `AssessClaim.status` (`"completed"` / `"failed"`) and `.failure` | `verdict == "Error"`, `error_code`, `hint` |
| `AssessClaim.more_claims`, `ReviewAssessment.more_claims` | `identified_claims` |
| `AssessResponse.status` and `.failure` | `error`, `error_code` |
| `TaskStatus.failure` (`code`, `detail`, `hint`, `failure_class`, `retryable`, `docs_url`) | `error`, `failure_reason` and the flat fields |
| `TaskAccepted.claim`, `BatchItemResult.claim`, `CandidateClaim.claim` | `claim_text`, `text` |
| `Verification.completed_at` | `modified_at` |
| `ReviewSummary.claim_limit_exceeded`, `citation_limit_exceeded` | `claim_limit_reached`, `citation_limit_reached` |
| `FailureBlock.code`, `.detail` | `failure_reason` |
| `Usage.credits` and `Usage.costs` | the `verify` / `ask` / `assess` blocks, `quota_resets_at` |

"Nothing checkable" is `no_checkable_claim` in the newer names; the older
fields keep their own spelling (`not_a_claim`, `no_claim`). The newer names
are read-only properties, so an original-shape response parses, dumps and
compares exactly as before.

### A suggested rewrite (`suggested_rewrite`)

A verification can carry `suggested_rewrite`: a suggested rewrite of its
Expand Down Expand Up @@ -644,13 +669,13 @@ them.

`credits.extra` is the non-expiring part of the balance. Its old name,
`credits.bonus`, is deprecated: the same number, it emits a
`DeprecationWarning` when read and goes away on 2026-11-29.
`DeprecationWarning` when read and is kept for existing code.

Per-capability `bonus` is that capability's share of `credits.extra`, so 200
extra credits read as `assess.bonus == 200` and `verify.bonus == 20`. The old
`capability.credits` field is a deprecated alias of `bonus` (it never meant
the pool); reading it emits a `DeprecationWarning` and it goes away on
2026-11-29.
the pool); reading it emits a `DeprecationWarning` and it is kept for
existing code.

### Depth pricing

Expand Down Expand Up @@ -815,7 +840,7 @@ At most 300 characters. A longer focus is rejected with a 422 rather than
truncated, so you never get a subset you did not ask for.

When the document has claims but none fall within your focus, `status` is
`"no_match"` and `identified_claims` is empty. The unfocused list is never
`"no_match"` and `claims` is empty. The unfocused list is never
substituted — widen the focus and call again.

```python
Expand All @@ -838,27 +863,24 @@ to your text, and to learn where the text makes each one:

```python
out = client.extract(text=draft, locate=True)
for loc in out.locations or []:
for pos in loc.positions or []:
print(loc.claim, "->", pos.text)
for c in out.claims:
for pos in c.positions or []:
print(c.claim, "->", pos.text)
if pos.start is not None:
assert draft[pos.start : pos.end] == pos.text
```

A claim found nowhere in the text, or found with a different figure, is left
out; if that leaves no claim, `status` is `"not_a_claim"`. `locations` has one
`ClaimLocation` per returned claim, in the order of `identified_claims` (one
entry for a single `claim`), each with its `positions` (every place the text
makes it, in text order, at least one and at most 10). Each is a `Position`:
out; if that leaves no claim, `status` is `"not_a_claim"`. Each entry of
`claims` has its `positions` (every place the text makes it, in text order,
at least one and at most 10). Each is a `Position`:
`start` and `end` index the text you sent in Unicode code points, so
`text[start:end]` works natively; `end` is exclusive. Both are `None` when the
input was a URL, since the page is not returned; `pos.text` carries the
passage.

`locations` is `[]` when every claim was left out (`status` is then
`"not_a_claim"`), and `None` when `locate` was not set, when the extraction
found no claims, or
when the claims could not be located — the list is then returned unfiltered.
`positions` is `None` when `locate` was not set, or when the claims could not
be located — the list is then returned unfiltered.
Locating adds a few seconds. `locate` defaults to off; leave it `None` to use
the server default, or pass `False` to turn it off explicitly.

Expand Down
Loading
Loading