Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6e12d7f
feat(v2): resolve dual V1/V2 base URLs from environment
yzld2002 Jul 8, 2026
d28a50f
feat(v2): add V2 response and unified Job types
yzld2002 Jul 8, 2026
c746dee
test: assert V2ParseResponse/V2ExtractResult retain unknown fields
yzld2002 Jul 8, 2026
b37cf9b
feat(v2): normalize parse/extract job envelopes into one Job shape
yzld2002 Jul 8, 2026
e93656c
feat(v2): accept pydantic model / dict / json string as extract schema
yzld2002 Jul 8, 2026
65c82cc
feat(v2): add V2 error types and 504 sync-timeout wrapper
yzld2002 Jul 8, 2026
ebb0695
feat(v2): add v2 sub-client container, absolute-URL mixin, and job wa…
yzld2002 Jul 8, 2026
123dc7a
feat(v2): add client.v2.files.upload staging on the ADE host
yzld2002 Jul 8, 2026
7ad9a12
feat(v2): add client.v2.parse sync parse with 206 + save_to + 504 han…
yzld2002 Jul 8, 2026
2a60b91
test(v2): add regression test for parse multipart sentinel filtering
yzld2002 Jul 8, 2026
10eae76
feat(v2): add client.v2.parse_jobs create/list/get/wait with normaliz…
yzld2002 Jul 8, 2026
30aba43
feat(v2): add client.v2.extract sync extract with schema coercion + i…
yzld2002 Jul 8, 2026
5390b55
feat(v2): add client.v2.extract_jobs create/list/get/wait
yzld2002 Jul 8, 2026
18cdc41
test(v2): async parity smoke tests; close extract_jobs coverage gaps
yzld2002 Jul 8, 2026
083e406
fix(v2): support pydantic v1 in schema coercion
yzld2002 Jul 8, 2026
9a5a0c9
fix(v2): deep-copy pydantic v1 schema before mutating (cache corruption)
yzld2002 Jul 8, 2026
0b5e195
docs(v2): document client.v2 sub-client, environments, and add examples
yzld2002 Jul 8, 2026
c57b6ca
docs(v2): add live smoke-test harness for the V2 endpoints
yzld2002 Jul 8, 2026
457335f
fix(v2): strict has_more bool + preserve created_at=0 (review)
yzld2002 Jul 8, 2026
d372e55
ci: re-trigger PR gates so surface-lock sees the override label
yzld2002 Jul 8, 2026
6e4baa5
fix(v2): treat explicit None as unset in body/query builders + doc fi…
yzld2002 Jul 8, 2026
29386a2
Potential fix for pull request finding
yzld2002 Jul 8, 2026
407bdc7
fix(v2): V2 API host is api.ade.[env] (aide.[env] is spec-only/SSO-ga…
yzld2002 Jul 8, 2026
df12d33
fix(v2): default Job status to pending when create envelope omits it …
yzld2002 Jul 8, 2026
6950460
fix(v2): correct 504 message + Job.raw default_factory (review)
yzld2002 Jul 8, 2026
3890816
fix(v2): tolerate unknown job status; lock Job.raw construct-path ind…
yzld2002 Jul 8, 2026
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
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,105 @@ for job in response.jobs:
print(f"Job {job.job_id}: {job.status}")
```

## V2 API

`client.v2` is a new, **additive** sub-client for LandingAI's next-generation ADE gateway. It does not replace or change anything about the V1 usage above -- `client.parse`, `client.extract`, `client.parse_jobs`, `client.extract_jobs`, etc. all keep working exactly as documented. Use `client.v2.*` when you want the newer parse/extract surface.

The V2 gateway lives on its own host (`api.ade.[env].landing.ai`), separate from the V1 host (`api.va.[env].landing.ai`). Select the environment the same way as V1, via the `environment` argument or the `LANDINGAI_ADE_ENVIRONMENT` env var:

```python
import os
from landingai_ade import LandingAIADE

client = LandingAIADE(
apikey=os.environ.get("VISION_AGENT_API_KEY"),
# one of "production" (default), "eu", "staging", "dev"
# can also be set via the LANDINGAI_ADE_ENVIRONMENT env var instead of passing it here
environment="staging",
)
```

### Sync parse and extract

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()

parse_result = client.v2.parse(
document=Path("path/to/file.pdf"),
save_to="./output_folder", # optional: saves as {input_file}_parse_output.json
)
print(parse_result.markdown)

extract_result = client.v2.extract(
schema='{"type": "object", "properties": {"total": {"type": "string"}}}',
markdown=parse_result.markdown,
strict=False, # prune unsupported schema fields instead of raising a 422
)
print(extract_result.extraction)
```

### Passing a pydantic model as the extract schema

`schema` accepts a pydantic `BaseModel` subclass directly -- no need to convert it yourself:

```python
from pydantic import BaseModel, Field
from landingai_ade import LandingAIADE


class Invoice(BaseModel):
total: str = Field(description="Invoice grand total")


client = LandingAIADE()
result = client.v2.extract(schema=Invoice, markdown_url="https://example.com/doc.md")
print(result.extraction)
```

### Async jobs and `wait()`

For large documents, create a job and poll it, or block until it finishes with `.wait()`:

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()

job = client.v2.parse_jobs.create(document=Path("path/to/large_file.pdf"), priority="priority")
print(job.job_id, job.status)

# Block until the job is terminal (raises JobWaitTimeoutError / JobFailedError on failure paths)
done = client.v2.parse_jobs.wait(job.job_id, timeout=600, raise_on_failure=True)
if done.result is not None:
print(done.result.markdown[:200])

# client.v2.extract_jobs.{create,get,list,wait} mirror the same shape for extract jobs.
```

`parse_jobs.create` and `extract_jobs.create` both return a normalized `Job` -- one shape shared by parse and extract jobs, even though their upstream envelopes differ. Use `job.raw` to reach any field not surfaced on the typed model.

### Uploading a file for `markdown_ref`

`client.v2.files.upload` stages bytes on the ADE data plane and returns a `file_ref` you can pass as `markdown_ref` to extract:

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()

file_ref = client.v2.files.upload(file=Path("path/to/file.md"))
result = client.v2.extract(schema={"type": "object", "properties": {}}, markdown_ref=file_ref)
```

`client.v2.parse`, `client.v2.extract`, and their async counterparts also accept `save_to`, with the same auto-naming behavior as the V1 methods above.

The async client mirrors this entire surface: `AsyncLandingAIADE().v2.parse(...)`, `await client.v2.parse_jobs.wait(...)`, etc.

## Async usage

Simply import `AsyncLandingAIADE` instead of `LandingAIADE` and use `await` with each API call:
Expand Down
60 changes: 60 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,63 @@ Methods:
- <code title="post /v1/ade/extract/jobs">client.extract_jobs.<a href="./src/landingai_ade/resources/extract_jobs.py">create</a>(\*\*<a href="src/landingai_ade/types/extract_job_create_params.py">params</a>) -> <a href="./src/landingai_ade/types/extract_job_create_response.py">ExtractJobCreateResponse</a></code>
- <code title="get /v1/ade/extract/jobs">client.extract_jobs.<a href="./src/landingai_ade/resources/extract_jobs.py">list</a>(\*\*<a href="src/landingai_ade/types/extract_job_list_params.py">params</a>) -> <a href="./src/landingai_ade/types/extract_job_list_response.py">ExtractJobListResponse</a></code>
- <code title="get /v1/ade/extract/jobs/{job_id}">client.extract_jobs.<a href="./src/landingai_ade/resources/extract_jobs.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/extract_job_get_response.py">ExtractJobGetResponse</a></code>

# V2

The `client.v2` sub-client targets LandingAI's next-generation ADE gateway, which lives on its own host (`api.ade.[env].landing.ai`) rather than the V1 host (`api.va.[env].landing.ai`). It is **additive**: `client.v2.*` is a separate surface from the top-level `client.*` (V1) methods documented above, and using it does not change any V1 behavior. See the [README](README.md#v2-api) for environment selection and usage examples.

`client.v2.parse_jobs` and `client.v2.extract_jobs` both return a single, unified <a href="./src/landingai_ade/types/v2/job.py">`Job`</a> shape, even though the underlying parse/extract job envelopes differ upstream -- `Job.raw` retains the full original envelope as an escape hatch for any field not surfaced on the typed model.

Types:

```python
from landingai_ade.types.v2 import (
Job,
JobError,
JobStatus,
V2ExtractMetadata,
V2ExtractResult,
V2FileUploadResponse,
V2ParseBilling,
V2ParseMetadata,
V2ParseResponse,
)
```

- <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/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>).
- <code><a href="./src/landingai_ade/types/v2/file_upload_response.py">V2FileUploadResponse</a></code> -- `file_ref`.

Methods:

- <code title="post /v2/parse">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">parse</a>(\*, document=..., document_url=..., model=..., options=..., password=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseResponse</a></code>

Synchronous parse. Provide exactly one of `document` (file) or `document_url`. Returns a `V2ParseResponse` on both full success (HTTP 200) and partial success (HTTP 206, where `result.metadata.failed_pages` lists unparsed pages). Raises `V2SyncTimeoutError` (from `landingai_ade.lib.v2_errors`) on a 504; use `parse_jobs` for long-running documents.

- <code title="post /v2/parse/jobs">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">create</a>(\*, document=..., document_url=..., model=..., options=..., password=..., output_save_url=..., priority=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/parse/jobs/{job_id}">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/parse/jobs">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">list</a>(\*, page=..., page_size=..., status=...) -> JobList[<a href="./src/landingai_ade/types/v2/job.py">Job</a>]</code>
- <code>client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">wait</a>(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>

Blocks, polling `.get(job_id)` with exponential backoff, until the job reaches a terminal status. Raises `JobWaitTimeoutError` if `timeout` seconds elapse first, and `JobFailedError` if `raise_on_failure=True` and the terminal job carries an `error` (not simply every `failed`/`cancelled` status).

- <code title="post /v2/extract">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">extract</a>(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code>

Synchronous extract. `schema` accepts a pydantic `BaseModel` subclass, a `dict`, or a JSON-encoded string -- all are coerced to a JSON Schema object. Provide exactly one of `markdown`, `markdown_ref` (e.g. from `client.v2.files.upload`), or `markdown_url`. `strict=True` rejects schemas with unsupported fields (HTTP 422) instead of silently pruning them. Raises `V2SyncTimeoutError` on a 504; use `extract_jobs` for long-running documents.

- <code title="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., priority=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs/{job_id}">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">list</a>(\*, page=..., page_size=..., status=...) -> JobList[<a href="./src/landingai_ade/types/v2/job.py">Job</a>]</code>
- <code>client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">wait</a>(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>

Same polling/timeout semantics as `parse_jobs.wait`. Extract jobs have no `cancelled` status, so `raise_on_failure` only ever triggers on `failed`.

- <code title="post /v1/files">client.v2.files.<a href="./src/landingai_ade/resources/v2/files.py">upload</a>(\*, file) -> str</code>

Stages a file's bytes on the ADE data plane and returns a `file_ref` string, which can be passed as `markdown_ref` to `client.v2.extract`/`client.v2.extract_jobs.create`. Served on the ADE host under `/v1/files` (not `/v2/...`). Raises `LandingAiadeError` if the response has no `file_ref`.

Notes:

- `parse_jobs.list` / `extract_jobs.list` both return a `JobList` (a `list[Job]` subclass) carrying pagination metadata: `.has_more`, `.org_id`, `.page`, `.page_size`.
- All `client.v2.*` methods accept the usual `extra_headers`, `extra_query`, `extra_body`, and `timeout` overrides; sync methods additionally accept `save_to` (parse/extract only, not the job-creation methods) to write the response to disk, mirroring V1's `save_to`.
48 changes: 48 additions & 0 deletions examples/v2_extract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env -S rye run python
"""Runnable example: extract structured data with the V2 (client.v2) sub-client,
using a pydantic model directly as the extraction schema.

This targets the ADE gateway host (api.ade.[env].landing.ai), separate from the
V1 host used by client.extract(). It is purely additive -- V1 usage elsewhere
in this SDK is unaffected.

Set VISION_AGENT_API_KEY (and optionally LANDINGAI_ADE_ENVIRONMENT) before running:

export VISION_AGENT_API_KEY="My Apikey"
export LANDINGAI_ADE_ENVIRONMENT="staging" # optional; defaults to "production"
./examples/v2_extract.py
"""

from __future__ import annotations

from pydantic import Field, BaseModel

from landingai_ade import LandingAIADE
from landingai_ade.types.v2 import V2ExtractResult


class Invoice(BaseModel):
total: str = Field(description="Invoice grand total")
vendor: str = Field(description="Name of the vendor issuing the invoice")


# apikey is read from VISION_AGENT_API_KEY; environment from LANDINGAI_ADE_ENVIRONMENT
# (or pass apikey=..., environment=... explicitly).
client = LandingAIADE()

# `schema` accepts a pydantic BaseModel subclass directly -- it's coerced to a
# JSON Schema dict for you. A dict or a JSON-encoded string also work.
result = client.v2.extract(
schema=Invoice,
markdown_url="https://example.com/invoice.md",
strict=False, # prune unsupported schema fields instead of raising a 422
)
print(result.extraction)

# Long-running extractions can instead go through the async jobs route:
job = client.v2.extract_jobs.create(schema=Invoice, markdown_url="https://example.com/invoice.md")
done = client.v2.extract_jobs.wait(job.job_id, timeout=600)
if isinstance(done.result, V2ExtractResult):
print(done.result.extraction)
else:
print(f"No result available (error={done.error})")
43 changes: 43 additions & 0 deletions examples/v2_parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env -S rye run python
"""Runnable example: parse a document with the V2 (client.v2) sub-client.

This targets the ADE gateway host (api.ade.[env].landing.ai), separate from the
V1 host used by client.parse(). It is purely additive -- V1 usage elsewhere in
this SDK is unaffected.

Set VISION_AGENT_API_KEY (and optionally LANDINGAI_ADE_ENVIRONMENT) before running:

export VISION_AGENT_API_KEY="My Apikey"
export LANDINGAI_ADE_ENVIRONMENT="staging" # optional; defaults to "production"
./examples/v2_parse.py
"""

from __future__ import annotations

from pathlib import Path

from landingai_ade import LandingAIADE
from landingai_ade.types.v2 import V2ParseResponse

# apikey is read from VISION_AGENT_API_KEY; environment from LANDINGAI_ADE_ENVIRONMENT
# (or pass apikey=..., environment=... explicitly).
client = LandingAIADE()

document_path = Path(__file__).parent / "sample.pdf"

# Create an async parse job for a (potentially large) document.
job = client.v2.parse_jobs.create(document=document_path, priority="priority")
print(f"Created parse job {job.job_id} (status={job.status})")

# Block until the job reaches a terminal state.
done = client.v2.parse_jobs.wait(job.job_id, timeout=600)
print(f"Job finished with status={done.status}")

if isinstance(done.result, V2ParseResponse):
print(done.result.markdown[:200] if done.result.markdown else None)
else:
print(f"No result available (error={done.error})")

# For smaller documents you can skip the job/wait dance entirely:
sync_result = client.v2.parse(document=document_path)
print(sync_result.markdown[:200] if sync_result.markdown else None)
Loading