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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,20 @@ from landingai_ade.types.v2 import (
V2ExtractResult,
V2FileUploadResponse,
V2ParseBilling,
V2ParseElement,
V2ParseGrounding,
V2ParseGroundingElement,
V2ParseGroundingEntry,
V2ParseGroundingPage,
V2ParseMetadata,
V2ParsePage,
V2ParseResponse,
V2ParseStructure,
)
```

- <code><a href="./src/landingai_ade/types/v2/job.py">Job</a></code> -- unified job shape: `job_id`, `status` (<code><a href="./src/landingai_ade/types/v2/job.py">JobStatus</a></code>: `pending` / `processing` / `completed` / `failed` / `cancelled`), `created_at`, `completed_at`, `progress`, `result` (a `V2ParseResponse` for parse jobs, a `V2ExtractResult` for extract jobs, or `None` until completion), `error` (<code><a href="./src/landingai_ade/types/v2/job.py">JobError</a></code>), `raw` (the full original envelope as a `dict`), and the `.is_terminal` property.
- <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseResponse</a></code> -- `markdown`, `structure`, `grounding`, `metadata` (<code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseMetadata</a></code>, which nests <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseBilling</a></code>). Loosely typed pending a published gateway schema; unknown fields are retained.
- <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseResponse</a></code> -- `markdown`, `structure`, `grounding`, `metadata` (<code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseMetadata</a></code>, which nests <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseBilling</a></code>). `structure` is a typed <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseStructure</a></code> tree (`document` → <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParsePage</a></code> → <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseElement</a></code>); `grounding` is the parallel <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseGrounding</a></code> tree (<code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseGroundingPage</a></code> → <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseGroundingElement</a></code> → <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseGroundingEntry</a></code>) carrying boxes and parts. Element `type`/page `status` are permissive strings and unknown keys are retained.
- <code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code> -- `extraction`, `extraction_metadata`, `markdown`, `metadata` (<code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractMetadata</a></code>, which nests <code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractBilling</a></code>).
- <code><a href="./src/landingai_ade/types/v2/file_upload_response.py">V2FileUploadResponse</a></code> -- `file_ref`.

Expand Down
7 changes: 7 additions & 0 deletions src/landingai_ade/types/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

from .job import Job as Job, JobError as JobError, JobStatus as JobStatus
from .parse_response import (
V2ParsePage as V2ParsePage,
V2ParseBilling as V2ParseBilling,
V2ParseElement as V2ParseElement,
V2ParseMetadata as V2ParseMetadata,
V2ParseResponse as V2ParseResponse,
V2ParseGrounding as V2ParseGrounding,
V2ParseStructure as V2ParseStructure,
V2ParseGroundingPage as V2ParseGroundingPage,
V2ParseGroundingEntry as V2ParseGroundingEntry,
V2ParseGroundingElement as V2ParseGroundingElement,
)
from .extract_response import (
V2ExtractResult as V2ExtractResult,
Expand Down
103 changes: 97 additions & 6 deletions src/landingai_ade/types/v2/parse_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@

from typing import List, Optional

from pydantic import Field

from ..._models import BaseModel
Comment thread
yzld2002 marked this conversation as resolved.

__all__ = ["V2ParseBilling", "V2ParseMetadata", "V2ParseResponse"]
__all__ = [
Comment thread
yzld2002 marked this conversation as resolved.
"V2ParseBilling",
"V2ParseMetadata",
"V2ParseElement",
"V2ParsePage",
"V2ParseStructure",
"V2ParseGroundingEntry",
"V2ParseGroundingElement",
"V2ParseGroundingPage",
"V2ParseGrounding",
"V2ParseResponse",
]


class V2ParseBilling(BaseModel):
Expand All @@ -23,12 +36,90 @@ class V2ParseMetadata(BaseModel):
billing: Optional[V2ParseBilling] = None


# --- `structure`: the logical document tree (no spatial data) ------------------
#
# `type` and `status` are typed as permissive `str` (not `Literal`) so a novel
# element/status value from the gateway never fails deserialization; unknown keys
# are retained via `BaseModel`'s `extra="allow"`.


class V2ParseElement(BaseModel):
"""A document element (text, table, table_cell, figure, ...). Keys off `type`;
optional fields are only present for the relevant element types."""

type: str
id: str
# Unicode code-point offsets `[start, end)` into the top-level `markdown`.
span: List[int]
# The cells of a `table` element (each a `table_cell`); only set for tables.
children: Optional[List["V2ParseElement"]] = None
# Table-cell geometry; only set on `table_cell` elements.
row: Optional[int] = None
col: Optional[int] = None
colspan: Optional[int] = None
rowspan: Optional[int] = None


class V2ParsePage(BaseModel):
type: str = "page"
# 0-indexed page number in the source document.
page: int
span: List[int]
width: Optional[int] = None
height: Optional[int] = None
dpi: Optional[int] = None
status: str = "ok"
reason: Optional[str] = None
children: List[V2ParseElement] = Field(default_factory=list)


class V2ParseStructure(BaseModel):
"""Root of the `structure` tree (`document -> page -> element`)."""

type: str = "document"
children: List[V2ParsePage] = Field(default_factory=list)

Comment thread
yzld2002 marked this conversation as resolved.

# --- `grounding`: mirrors `structure`, carrying the spatial data ---------------


class V2ParseGroundingEntry(BaseModel):
"""One fine-grained grounding segment (line-level or finer)."""

span: List[int]
# Bounding box `[left, top, right, bottom]` in source-page pixels.
box: List[int]


class V2ParseGroundingElement(BaseModel):
type: str
id: str
span: List[int]
box: List[int]
parts: List[V2ParseGroundingEntry] = Field(default_factory=list)
# The cells of a `table` element; only set for tables.
children: Optional[List["V2ParseGroundingElement"]] = None
Comment thread
Copilot marked this conversation as resolved.
Comment thread
yzld2002 marked this conversation as resolved.
Comment thread
yzld2002 marked this conversation as resolved.


class V2ParseGroundingPage(BaseModel):
type: str = "page"
page: int
span: List[int]
children: List[V2ParseGroundingElement] = Field(default_factory=list)

Comment thread
yzld2002 marked this conversation as resolved.

class V2ParseGrounding(BaseModel):
"""Root of the `grounding` tree, mirroring `structure` with spatial data."""

type: str = "document"
children: List[V2ParseGroundingPage] = Field(default_factory=list)

Comment thread
yzld2002 marked this conversation as resolved.

class V2ParseResponse(BaseModel):
"""V2 parse result. The gateway spec types this loosely; fields are permissive
and extra keys are retained. Re-verify against the typed schema when the gateway
publishes one."""
"""V2 parse result. `structure` and `grounding` are typed trees mirroring the
published gateway schema; unknown element types and extra keys are retained."""

markdown: Optional[str] = None
structure: Optional[object] = None
grounding: Optional[object] = None
structure: Optional[V2ParseStructure] = None
grounding: Optional[V2ParseGrounding] = None
metadata: Optional[V2ParseMetadata] = None
Comment thread
yzld2002 marked this conversation as resolved.
108 changes: 107 additions & 1 deletion tests/api_resources/v2/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,63 @@
APIKEY = "My Apikey"
PARSE_BODY: Dict[str, Any] = {
"markdown": "# Hello",
"structure": [{"type": "text"}],
"structure": {"type": "document", "children": []},
"grounding": {"type": "document", "children": []},
"metadata": {"req_id": "r1", "job_id": "j1", "model_version": "dpt-3", "page_count": 1, "failed_pages": []},
}

# A fully-populated ParseResponse mirroring the typed gateway schema: a document
# with one page holding a text element and a table (with a table_cell child), plus
# the parallel grounding tree carrying boxes/parts.
RICH_PARSE_BODY: Dict[str, Any] = {
"markdown": "# Invoice",
"metadata": {"req_id": "r1", "job_id": "j1", "model_version": "dpt-3", "page_count": 1, "failed_pages": []},
"structure": {
"type": "document",
"children": [
{
"type": "page",
"page": 0,
"span": [0, 100],
"width": 800,
"height": 1000,
"dpi": 150,
"status": "ok",
"children": [
{"type": "text", "id": "e1", "span": [0, 20]},
{
"type": "table",
"id": "e2",
"span": [20, 80],
"children": [
{"type": "table_cell", "id": "e3", "span": [25, 30],
"row": 0, "col": 1, "colspan": 2, "rowspan": 1},
],
},
],
}
],
},
"grounding": {
"type": "document",
"children": [
{
"type": "page",
"page": 0,
"span": [0, 100],
"children": [
{"type": "text", "id": "e1", "span": [0, 20], "box": [10, 10, 200, 30],
"parts": [{"span": [0, 20], "box": [10, 10, 200, 30]}]},
{"type": "table", "id": "e2", "span": [20, 80], "box": [10, 40, 400, 300], "parts": [],
"children": [
{"type": "table_cell", "id": "e3", "span": [25, 30], "box": [12, 42, 100, 80], "parts": []},
]},
],
}
],
},
}


@respx.mock
def test_parse_sync_ok_routes_to_v2_and_sends_options_json() -> None:
Expand Down Expand Up @@ -159,6 +212,59 @@ def test_parse_sync_206_returns_response_with_failed_pages() -> None:
assert result.metadata is not None and result.metadata.failed_pages == [3]


@respx.mock
def test_parse_sync_typed_structure_and_grounding() -> None:
# The gateway now returns a typed ParseResponse; `structure` and `grounding`
# deserialize into typed models with attribute access, not raw dicts.
from landingai_ade.types.v2 import V2ParseGrounding, V2ParseStructure

client = LandingAIADE(apikey=APIKEY)
respx.post("https://api.ade.landing.ai/v2/parse").mock(
return_value=httpx.Response(200, json=RICH_PARSE_BODY)
)
result = client.v2.parse(document=b"pdf")

assert isinstance(result.structure, V2ParseStructure)
page = result.structure.children[0]
assert page.page == 0 and page.width == 800 and page.dpi == 150 and page.status == "ok"
text, table = page.children[0], page.children[1]
assert text.type == "text" and text.id == "e1"
assert table.type == "table" and table.children is not None
cell = table.children[0]
assert cell.type == "table_cell" and cell.row == 0 and cell.col == 1 and cell.colspan == 2

assert isinstance(result.grounding, V2ParseGrounding)
g_text = result.grounding.children[0].children[0]
assert g_text.box == [10, 10, 200, 30]
assert g_text.parts[0].span == [0, 20] and g_text.parts[0].box == [10, 10, 200, 30]
g_table = result.grounding.children[0].children[1]
assert g_table.children is not None and g_table.children[0].box == [12, 42, 100, 80]


@respx.mock
def test_parse_sync_tolerates_unknown_element_type_and_extra_keys() -> None:
# Novel element `type` values and extra keys must not break deserialization
# (`type` is a permissive str; BaseModel retains extra keys).
client = LandingAIADE(apikey=APIKEY)
body: Dict[str, Any] = {
"markdown": "x",
"metadata": {"req_id": "r1", "job_id": "j1", "model_version": "m", "page_count": 1, "failed_pages": []},
"structure": {
"type": "document",
"children": [
{"type": "page", "page": 0, "span": [0, 5], "future_field": 7, "children": [
{"type": "some_future_kind", "id": "e9", "span": [0, 5]},
]},
],
},
"grounding": {"type": "document", "children": []},
}
respx.post("https://api.ade.landing.ai/v2/parse").mock(return_value=httpx.Response(200, json=body))
result = client.v2.parse(document=b"pdf")
assert result.structure is not None
assert result.structure.children[0].children[0].type == "some_future_kind"

Comment thread
yzld2002 marked this conversation as resolved.

@respx.mock
def test_parse_sync_504_raises_v2_sync_timeout() -> None:
client = LandingAIADE(apikey=APIKEY, max_retries=0)
Expand Down
7 changes: 5 additions & 2 deletions tests/test_v2_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ def test_extract_result_parses_nested_metadata() -> None:
assert r.metadata.credit_usage == 0.0 # default


def test_parse_response_tolerates_loose_shape() -> None:
def test_parse_response_builds_from_dicts() -> None:
# `structure`/`grounding` are typed trees; `metadata` (with nested `billing`)
# and the whole response build cleanly from plain dicts.
r = V2ParseResponse(
markdown="# hi",
structure=[{"type": "text"}],
structure={"type": "document", "children": [{"type": "page", "page": 0, "span": [0, 4]}]}, # type: ignore[arg-type]
metadata={ # type: ignore[arg-type]
"req_id": "r1",
"job_id": "j1",
Expand All @@ -90,6 +92,7 @@ def test_parse_response_tolerates_loose_shape() -> None:
},
)
assert r.markdown == "# hi"
assert r.structure is not None and r.structure.children[0].page == 0
assert r.metadata is not None and r.metadata.billing is not None
assert r.metadata.billing.total_credits == 3.0

Expand Down
Loading