Skip to content

ExtractJob.poll() non-blocking, no .wait() #6

Description

@Cyrille-Lighton

Bug Report

Reported By: User

Affected Environment: Prod

Affected Release: Console 1.1.X

Description

As a developer integrating the LightOn Python SDK on my platform, when I submit an extract() or parse() call in async mode (mode=ExecMode.ASYNC) and call job.poll() once expecting it to block until the job finishes, the call returns immediately without blocking and job.result can still be None.

This results in code that silently proceeds with an incomplete or missing result instead of raising an explicit error, because poll() only refreshes the job's status a single time rather than waiting for completion.

Unlike file ingestion (File/Workspace.ingest()), which exposes a wait=True argument and a File.wait() method to block until a terminal status, ExtractJob and ParseJob have no equivalent. Every caller has to hand-roll their own polling loop, as shown in the SDK's own README and official tutorials.

Note: a team member flagged that wait=True might already exist for this case per the README, checked directly against the GitHub README (lighton-python-sdk) and the official docs: wait=True only exists on ingest()/ingest_many() (file ingestion), not on extract()/parse(). Likely a mix-up with the ingestion API, flagging here for confirmation before closing.

Steps to Reproduce

  1. Launch an async job: job = client.extract(schema=..., path=..., mode=ExecMode.ASYNC)
  2. Call job.poll() once, then read job.result assuming the extraction is complete
  3. Observe that result can still be None if the job hasn't reached a terminal status, with no error raised
  4. Compare with File.get(client, id).wait(), which exists and blocks until a terminal status

Expected Behavior

ExtractJob/ParseJob should expose a .wait(timeout=...) method (or a wait=True argument), symmetric to File.wait(), that blocks until the job reaches a terminal status (completed/failed) and raises on timeout or failure.

Actual Behavior

poll() refreshes the job status exactly once and returns immediately, regardless of whether the job is still in progress. Callers must implement their own while not job.poll().succeeded: ... time.sleep(...) loop, as documented in the SDK's own examples.

Workaround

Write a manual polling loop with the recommended cadence (1s for the first 10s, then 5s, capped at 30s), as shown in the official tutorials:

while not job.poll().succeeded:
    if job.done:  # terminal but not completed → failure
        raise RuntimeError(f"job {job.id} ended as {job.status!r}")
    time.sleep(2)

Additional Information

Error Code: N/A, silent behavior, no error raised (the gap is the absence of an error/blocking mechanism, not a specific error code)

Workaround Suggested By: NA

Base message from User:

Hello,

We used LightOn to extract notation and tables from symbol-heavy scientific
PDFs. parse did exactly what we needed and beat the local tooling we had been
using - details in section D. Along the way we hit several undocumented
constraints on extract that took a lot of blind bisection to identify, so I am
writing them up.

Environment: lighton-sdk==1.0.0, Python 3.11, Windows, API v3.
Test documents: three open-access PDFs (CC BY) - 10 pp / 1.8 MB,
22 pp / 1.3 MB, 86 pp / 40 MB. Happy to send them and our
probe script.

Items 1-3 reproduce identically on two different documents. On the 22-page one,
VALID schemas return the 400 page-limit error while INVALID ones return 422 - so
schema validation runs before the document is examined, and these limits are
document-independent.


A. EXTRACT SCHEMA CONSTRAINTS

  1. description keys are rejected.

    {"type":"object","properties":{"t":{"type":"string"}},
    "required":["t"]}
    -> OK

    {"type":"object","properties":{"t":{"type":"string",
    "description":"title"}},"required":["t"]}
    -> 422 Unprocessable Entity: One or more fields failed validation.

    Highest-impact item. Descriptions are the natural way to steer an
    extraction, and Field(description=...) is idiomatic pydantic.

  2. $ref / $defs are rejected.

    Any nested pydantic model produces them via model_json_schema(), and they
    appear not to be resolved server-side. We had to write a de-referencer that
    inlines definitions before sending.

  3. At most 4 top-level fields.

    Bisected: 4 properties succeed, 5 fail. Each of our 12 fields passed
    individually, so it is a count limit, not a specific field. We now batch into
    groups of four and merge client-side. If intentional, it is worth
    documenting; it is restrictive for document extraction, which is exactly the
    case where you want many fields in one pass.

  4. Errors do not identify the offending field.

    "One or more fields failed validation" and "One or more schema fields failed
    validation" are distinct messages for distinct causes, which is genuinely
    useful - but neither names the field or the rejected keyword. Diagnosing
    items 1-3 took roughly 20 probe calls. Returning the failing path
    (properties.t.description: unsupported keyword) would make that one call.

    COMBINED EFFECT OF 1 AND 2

    The SDK's advertised path -

    client.extract(schema=MyPydanticModel, path=...)
    
    • works only for a flat model with no field descriptions. Since the SDK
      already owns the pydantic-to-JSON-Schema conversion, sanitising there would
      fix this without any server change.

B. SYNC / ASYNC INCONSISTENCIES

  1. Invalid schemas fail silently in async mode.

    The description schema from item 1 is rejected synchronously with a clear
    422. The SAME schema with mode=async is ACCEPTED, and the job then completes
    with status='failed' and no error detail. Async callers get a silent failure
    where sync callers get a diagnosis.

    Schema validation should happen at submission regardless of mode, or the
    failed job should carry the reason.

  2. Both endpoints cap sync at 15 pages, but give contradictory advice.

    parse "For larger documents, split your request across multiple calls."
    extract "For larger documents, use async mode (options.async=true)."

    Extract's is the correct advice - async handled our 22-page and 86-page
    documents without complaint, and splitting is unnecessary. Suggest parse
    adopt extract's wording.

    Separately: page count is not knowable before submission, so callers cannot
    choose a mode reliably. We default everything to async.


C. SDK ERGONOMICS

  1. poll() does not block, and there is no wait().

    The reference says to call .poll() until .succeeded, and poll() correctly
    refreshes once without blocking. But the natural misreading - poll once, read
    .result - yields None rather than raising, which is a quiet failure.
    wait_all() exists but is typed for list[File], not jobs. A
    job.wait(timeout=...) would remove a hand-rolled poll loop from every async
    caller.


D. QUALITY

parse was excellent, and the reason the work got unblocked. It recovered
image-based tables as clean HTML, and correctly resolved glyphs set in the
legacy Adobe Symbol font - a "⊃" that ordinary PDF text extraction had been
mangling into "É". On slide decks it rendered LaTeX properly too.

extract was unreliable on the same content. Asked for an operator legend
(glyph -> meaning), it returned pairs with their definitions swapped -
+ : "dispersed in" and / : "coexistence of phases" are each other's
meanings - mangled several glyphs, classified the document's acronyms as data
rows, and when asked for the rows of a specific table returned prose examples
from the body text instead. We fell back to parsing to Markdown and reading it.

Symbol-to-meaning tables may simply be a hard case, but if you collect eval
material for structured extraction, they look like a productive adversarial one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions