26
The exception hierarchy, and what raises what
RedlineError base: an edit could not be located or applied ClauseError a clause number could not be resolved StalePlanError a plan was computed against a different version RedlineCredentialsError a model-backed reviewer has no API key
examples/26_errors.py โ ran in 0.35s ยท exit 0
Console output
============================================================================
26 ยท Errors
============================================================================
--- the hierarchy ---
RedlineError RuntimeError <- Exception <- BaseException
ClauseError RedlineError <- RuntimeError <- Exception
StalePlanError RedlineError <- RuntimeError <- Exception
RedlineCredentialsError RuntimeError <- Exception <- BaseException
catching RedlineError catches all of them
--- RedlineError โ locating failures ---
find_paragraph matched nothing
RedlineError: no paragraph matched {'contains': 'not in this contract'}
an unknown paragraph style
RedlineError: unknown paragraph style 'Nonesuch'; this document defines 'Heading 1', 'Heading 2', 'Heading 3',
a strict op that matched nothing
RedlineError: [0] op 'replace_text' matched nothing: {'op': 'replace_text', 'old': 'absent', 'new': 'x'}
ParagraphIndex.paragraph out of range
RedlineError: para_id 9999 out of range (0-91)
a malformed model payload
RedlineError: malformed edit payload:
#0: 'edit' does not accept 'replacment'
an action plan that fails its schema check
RedlineError: invalid action items:
X: 'renumber_clause' is derived by the planner and must not be supplied
full_redline with nothing to do
RedlineError: full_redline needs something to do: pass revised=, actions=, reviewer= or comments=
--- ClauseError โ a clause number that does not resolve ---
a hallucinated clause number
ClauseError: no clause numbered '99.9' in the document
an action pointing at one
ClauseError: no clause numbered '99.9' in the document
a hallucinated clause fails loudly at the plan stage, instead of
silently editing the wrong paragraph
--- StalePlanError โ the plan and the document disagree ---
re-verifying after the document moved on
StalePlanError: document moved on: plan was computed against 902892107d00df8a, document is now 95cb5abfbfa477f2.
--- RedlineCredentialsError โ names the variable to set ---
OpenAIReviewer
RuntimeError: the OpenAI reviewer needs the openai SDK: pip install openai
ClaudeReviewer
RuntimeError: the Claude reviewer needs the anthropic SDK: pip install anthropic
the two SDKs fail at different moments -- OpenAI refuses to build a
client without a key, Anthropic builds one and rejects the first
request -- so the translation wraps both
--- recording a failure instead of raising ---
AI-001 applied replaced 'thirty (30) days' -> 'forty-five (45) days'
AI-002 failed text 'not in this clause' not found in scope
strict=False records; strict=True raises on the first failure
--- and Rejection, which is data rather than an exception ---
<Rejection.TARGET_AMBIGUOUS: 'target_ambiguous'>
span occurs more than once and is under 25 characters; quote more context, set occurrence=n, or set occurrence=0 for all of them
the ParagraphIndex layer reports rather than raises, so one bad edit in
a batch of forty does not lose the other thirty-nine
Source
"""26 ยท The exception hierarchy, and what raises what.
RedlineError base: an edit could not be located or applied
ClauseError a clause number could not be resolved
StalePlanError a plan was computed against a different version
RedlineCredentialsError a model-backed reviewer has no API key
"""
import docx
from _shared import OUT, SOURCE, banner, fresh, section
from docx_redline import (
ClauseError,
ClauseTree,
ParagraphIndex,
RedlineCredentialsError,
RedlineEdit,
RedlineError,
StalePlanError,
apply_actions,
full_redline,
load_edits,
verify_plan,
)
from docx_redline.editing.ops import apply_operations
banner("26 ยท Errors")
section("the hierarchy")
for exc in (RedlineError, ClauseError, StalePlanError, RedlineCredentialsError):
bases = " <- ".join(c.__name__ for c in exc.__mro__[1:4])
print(f" {exc.__name__:<24} {bases}")
print(" catching RedlineError catches all of them")
def raises(title, fn):
try:
fn()
except Exception as exc:
print(f"\n {title}")
lines = [line for line in str(exc).splitlines() if line.strip()]
print(f" {type(exc).__name__}: {lines[0][:96]}")
for extra in lines[1:2]:
print(f" {extra.strip()[:94]}")
section("RedlineError โ locating failures")
raises(
"find_paragraph matched nothing",
lambda: fresh().find_paragraph(contains="not in this contract"),
)
raises(
"an unknown paragraph style",
lambda: fresh().apply_style(fresh().find_paragraph(contains="3.2 Invoicing"), "Nonesuch"),
)
raises(
"a strict op that matched nothing",
lambda: apply_operations(
fresh(), [{"op": "replace_text", "old": "absent", "new": "x"}], strict=True
),
)
raises("ParagraphIndex.paragraph out of range", lambda: ParagraphIndex(fresh()).paragraph(9999))
raises(
"a malformed model payload",
lambda: load_edits([{"para_id": 0, "target": "x", "replacment": "y"}]),
)
raises(
"an action plan that fails its schema check",
lambda: apply_actions(fresh(), [{"id": "X", "type": "renumber_clause", "clause": "3.1"}]),
)
# the output path is never written -- the call raises before it gets that far
raises("full_redline with nothing to do", lambda: full_redline(SOURCE, OUT / "never.docx"))
section("ClauseError โ a clause number that does not resolve")
tree = ClauseTree(docx.Document(SOURCE).element.body)
raises("a hallucinated clause number", lambda: tree.get("99.9"))
raises(
"an action pointing at one",
lambda: apply_actions(
fresh(), [{"id": "X", "type": "delete_clause", "clause": "99.9"}], strict=True
),
)
print(" a hallucinated clause fails loudly at the plan stage, instead of")
print(" silently editing the wrong paragraph")
section("StalePlanError โ the plan and the document disagree")
rl = fresh()
index = ParagraphIndex(rl)
stale = index.fingerprint()
index.apply([RedlineEdit(19, "thirty (30) days", "forty-five (45) days")])
raises("re-verifying after the document moved on", lambda: verify_plan(index, stale))
section("RedlineCredentialsError โ names the variable to set")
import os
saved = {k: os.environ.pop(k, None) for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY")}
try:
from docx_redline import ClaudeReviewer, OpenAIReviewer
for cls in (OpenAIReviewer, ClaudeReviewer):
raises(cls.__name__, lambda cls=cls: cls().propose(tree, "Review this."))
finally:
for k, v in saved.items():
if v is not None:
os.environ[k] = v
print("\n the two SDKs fail at different moments -- OpenAI refuses to build a")
print(" client without a key, Anthropic builds one and rejects the first")
print(" request -- so the translation wraps both")
section("recording a failure instead of raising")
report = apply_actions(
fresh(),
[
{
"id": "AI-001",
"type": "replace_text",
"clause": "3.2",
"find": "thirty (30) days",
"replace": "forty-five (45) days",
},
{
"id": "AI-002",
"type": "replace_text",
"clause": "3.2",
"find": "not in this clause",
"replace": "x",
},
],
strict=False,
)
for res in report.results:
print(f" {res.id} {res.status:<8} {res.detail[:64]}")
print(" strict=False records; strict=True raises on the first failure")
section("and Rejection, which is data rather than an exception")
report = ParagraphIndex(fresh()).apply([RedlineEdit(6, "Provider", "Vendor")])
res = report.rejected[0]
print(f" {res.reason!r}")
print(f" {res.detail}")
print(" the ParagraphIndex layer reports rather than raises, so one bad edit in")
print(" a batch of forty does not lose the other thirty-nine")
What it wrote
This example prints its result rather than saving a document โ read the
console output beside it.