Three functions over the ethos-parser CLI. Standard library only, no runtime dependencies, and
not on PyPI yet.
import ethos_parser
representation = ethos_parser.extract("contract.pdf")
grounding = ethos_parser.ground(representation)
node_id = representation["representation"]["nodes"][0]["id"] # minted by the engine
node = ethos_parser.node_get(representation, node_id)| Function | Runs | Returns |
|---|---|---|
extract(path) |
ethos-parser extract <path> |
DocumentRepresentation v0 |
ground(representation) |
ethos-parser ground <path> |
ethos.grounding.v1 |
node_get(representation, node_id) |
nothing — the checks are ported | that artifact's node record |
extract and ground run the same subcommands you would run in a shell and hand back exactly what
those subcommands printed, parsed as JSON. There is no second serializer in this package, so there
is nowhere for the two to drift apart. A test re-canonicalizes what extract returned and compares
it byte for byte against the CLI's stdout.
That is also why there is no PyO3 extension. A native binding would reach the engine by a second path, and a second path is a second thing that can disagree with the first — plus a wheel matrix, for a speed-up nobody has measured a need for.
The engine mints every locator, returns it as an opaque id, and re-validates it on the way back in.
- No function here takes a coordinate. No page, no box, no
x/y, no row/column pair — not optionally, not keyword-only, not behind a flag. A test reads the signatures and fails on those names. node_gettakes an id you copied out of a representation the engine returned.- A forged id fails closed.
node_getraisesNodeNotFound, neverNoneand never{}. An edited artifact raisesFingerprintMismatchbefore any lookup happens, because its payload no longer hashes to its own declared digest.
node_get is the one function with no subcommand behind it, so its checks are ported:
src/ethos_parser/_c14n.py runs the same parity vectors as the Rust implementation.
markdown, html and verify are deliberately not wrapped.
An optional extra, so the plain install still pulls nothing:
pip install 'ethos-parser[langchain]'from ethos_parser.langchain import tools
llm_with_tools = llm.bind_tools(tools()) # or a LangGraph ToolNodeLocators travel in the tool's artifact, never in content. Each tool declares
response_format="content_and_artifact", so a ToolMessage carries the record in .artifact and
only counts in .content:
| Tool | content |
artifact |
|---|---|---|
extract |
{n} page(s), {m} node(s). Locators are in the artifact. |
DocumentRepresentation v0 |
ground |
ethos-parser mcp's own reply text: {n} element(s) with a measured box; {m} omitted for having none., then a clause for each thing the schema's limits took — spans withheld, elements omitted, tables withheld |
ethos.grounding.v1 |
node_get |
1 node, kind `{kind}`. |
the node record |
A box in content is a locator the model can edit and then cite, which is the whole hazard. The
summaries are MCP's words, so the three adapters cannot drift into three different sentences about
one document: ground's is the text ethos-parser mcp itself replied with — it states facts the
artifact does not carry, such as what the schema's limits withheld — and extract's is compared
byte for byte in a test. node_get spells the kind the way the artifact does (text_run). The
ground tool starts ethos-parser mcp and sends it a path, never the object; if the record is
refused it runs ethos-parser ground on the same path, so the error raised is ground()'s own.
There is no verify tool and no LangGraph adapter — a StructuredTool is already what LangGraph
binds.
The ethos-parser binary is a prerequisite; nothing here downloads one.
cargo build --release --locked # from the repository root
pip install -e 'packages/python[dev]'
ETHOS_PARSER=target/release/ethos-parser pytest packages/pythonETHOS_PARSER is checked first and taken literally — a path named there that does not exist is an
error, not a reason to go hunting for some other build. Then ethos-parser on PATH, then the
workspace target/. The suite also refuses a binary whose version does not match Cargo.toml,
because a stale build answers every question plausibly and would make a byte-identity check go green
about the wrong engine.