Before you manage a Library
Public Preview

Manage Libraries and documents with the SDK

The Duale AI SDK manages a persistent Library and its documents, covering upload, ingestion polling, state inspection, and deletion.

Manage a persistent Library with the SDK: create it, upload documents, poll ingestion state, share access, and delete resources.

  • create() returns an active Library or creates a new one with agent access.
  • wait_for_document() polls every five seconds with a 360-second default timeout.
  • Documents move through queued, processing, ready, or failed states; failed is terminal.
  • A deleted document stays in Recently deleted for 30 days before permanent removal.
  • Grants carry subject, action, and effect fields; a refusal beats every allow.

Summaries were generated by AI. Generative AI is experimental.

Manage a persistent Library, its path, and its documents. You can upload files, wait until they are readable, inspect current state, and delete resources. Task attachments use the same Library resource through a conventional task-scoped path.

Before you manage a Library

Have these values and the test file ready before the first call:

  • Set DUALE_TOKEN to a token for your tenant.
  • Set DUALE_TENANT_ID to the tenant path segment used by Library routes.
  • Existing Libraries require a matching grant for each operation. When the SDK creates a Library, it grants the token’s agent access to that new Library.
  • Put a non-empty contract.pdf file beside the script below.

The calls below can create a Library. They do not grant access to an existing one.

Plan the Library layout

Group a Library by who can read it, and grant each agent only the Libraries it needs. A grant covers a whole Library, so two sets of documents with different readers belong in two Libraries.

Design your Libraries states how many to create and where to draw the line between them. Limits states what bounds a Library. Decide the layout before you run the call below, because a grant is easier to plan than to unpick.

Create a Library and upload a document

create() returns an active Library at the requested path when the token can change it. Otherwise, it creates a new Library there and grants the token’s agent access. Paths are non-unique display metadata. An inaccessible or deleted Library does not reserve its path. This operation never changes access to another Library at the same path.

import asyncio
from pathlib import Path

from duale import (
    LibraryCreateRequest,
    LibraryDocumentGetRequest,
    LibraryPatchRequest,
    LibraryResponseDocumentStatus,
    LibraryUpdateRequest,
    create_sdk,
)


async def main() -> None:
    async with create_sdk() as sdk:
        create_request = LibraryCreateRequest(path="projects/contracts")
        library = await sdk.libraries.create(create_request)
        library = await sdk.libraries.update(
            LibraryUpdateRequest(
                library_id=library.id,
                patch=LibraryPatchRequest(
                    tags={"project": "contracts", "year": 2026},
                ),
            ),
        )

        attachments = sdk.prepare_attachments(
            [(Path("contract.pdf"), "Signed client contract")],
        )
        receipts = await sdk.libraries.upload(library.id, attachments)
        receipt = receipts[attachments[0].key]

        document = await sdk.libraries.wait_for_document(
            LibraryDocumentGetRequest(
                library_id=library.id,
                document_id=receipt.document_id,
            ),
        )
        if document.status is LibraryResponseDocumentStatus.failed:
            print(document.failure)
            return

        print(library.id, document.document_id, document.status.value)


asyncio.run(main())

The request carries only path. The server creates the Library id and returns it on library.id. If the call fails before it returns a response, call sdk.libraries.create(create_request) again. The retry returns the writable Library that the first attempt created. It creates one only when that attempt left none.

upload() returns one receipt per attachment, keyed by the matching PreparedAttachment.key. Each receipt includes the stable library_id, new document_id, initial queued status, and polling location.

Use library_id and document_id with SDK methods. Ignore the receipt’s location field.

wait_for_document() performs a GET every five seconds until the document reaches ready or failed. A 360-second timeout bounds the whole polling operation by default; pass a positive timeout= value to change that bound.

Three rules follow from that default:

  • Raise timeout= for large files. The platform allows 30 minutes to extract a document and 15 to index it, so the 360-second default is shorter than a large file can legitimately take.
  • A TimeoutError bounds the client only. Ingestion continues, and the document still reaches ready or failed.
  • Poll with get_document() rather than uploading again. Each upload() creates a new document instead of replacing the one in flight.

After a client timeout, keep polling the same document to a terminal state. Do not upload a copy to test the first one.

Verify document ingestion

A successful run prints the Library id, document id, and terminal status:

0192f7c1-6e2a-7c31-9a4d-2b6f8c1d4e90 0192f7d9-712b-7c31-8f08-845c30a42a31 ready

The returned PublicIndexedDocument also carries progress_pct, page_count, tags, and problem details in failure. A failed document always carries them; Errors and reliability lists the causes.

What the platform checks before a document is readable

Between upload and ready, the platform verifies each file and can reject it. Two checks fail the document rather than the request, so they surface as a terminal failed status with a code rather than as an error on upload().

  • Integrity. The platform recomputes the file’s SHA-256 and size and compares them with what the upload declared. A mismatch fails the document with HASH_MISMATCH or SIZE_MISMATCH.
  • Malware. Every file is scanned before extraction. A signature match fails the document with MALWARE_DETECTED, and the platform keeps the file with that failed document rather than extracting or indexing it.

Errors and reliability lists every terminal code and what to do about it.

File types the platform extracts

The platform extracts text from documents, spreadsheets, presentations, images, videos, email, subtitles, data, and code. From a video it reads the text visible in the frames, not the speech.

The platform reads text, so supply a recording as a transcript or as a subtitle file. It accepts nine subtitle formats, among them .srt and .vtt.

Documents include .pdf, .docx, .doc, .rtf, .odt, .epub, .html, .md, .txt, .tex, .pages, .xps, .cbz, and .mhtml. Data covers .xml, .json, .jsonl, and .ndjson. Limits counts the extensions in each of the nine categories.

One route holds the current list. It needs library:upload, and it changes only with a release, so you can cache its response. Call it to populate a file picker:

GET {DUALE_ENDPOINT}/libraries/v1/tenants/{tenant_id}/document-uploads/supported-formats

Use the response, not the examples above, to decide whether the current deployment accepts a file.

Document states

A document moves through four states, and status reports which one it is in: queued, processing, ready, or failed. Branch on those four values and nothing else.

The diagram shows two more positions than status has values. Deleting a document leaves its status unchanged and adds deleted_at; a document deleted permanently returns nothing. Neither position is a value your code can read.

States a document moves throughAn upload enters the queued state, then processing, where the platform verifies the file and scans it for malware before extracting and indexing it. Processing ends either at ready, where the document is searchable, or at failed, which is terminal and carries an error code. Deleting a ready document moves it to Recently deleted, where it stays for 30 days. From there a restore sends it back through queued, or the platform deletes it permanently after the window, which cannot be reversed. Deleting the whole Library also makes the document unrecoverable immediately, from either the ready state or the Recently deleted state. Physical removal follows asynchronously.

failed carries a code and never becomes ready on its own. Nothing recovers a document once the platform has deleted it permanently, so read status after every upload and before every recovery action.

Replace a document

Upload the new version, wait until it is readable, then delete the old one. A document supports three operations: create it, change its tags, and delete it. None of them replaces a file in place, so a new version of a file is a new document with its own id.

Use this order:

  1. Upload the new version

    Call upload() with the new file. The receipt carries a new document_id.

  2. Wait until it is readable

    Call wait_for_document() on that id until the document reaches ready.

  3. Delete the old version
    Call delete_document() on the old document_id.

The order decides what an agent reads in between:

What you doWhat an agent reads until you finish
Keep the order aboveBoth versions answer between steps 1 and 3, and one version after
Delete firstNo version answers until the new document reaches ready
Stop after step 2Both versions keep answering until someone deletes one
  • What you do
    Keep the order above
    What an agent reads until you finish
    Both versions answer between steps 1 and 3, and one version after
  • What you do
    Delete first
    What an agent reads until you finish
    No version answers until the new document reaches ready
  • What you do
    Stop after step 2
    What an agent reads until you finish
    Both versions keep answering until someone deletes one

Step 3 stays reversible for 30 days. Delete a document or Library states that window and the restore path.

Reuse and inspect a Library

await sdk.libraries.list() returns the Libraries that the token can access. Pass LibraryGetRequest to get() for the current path and tags.

Pass LibraryUpdateRequest with a nested LibraryPatchRequest to update(). The update records a revision of the Library’s path and tags. The revision also records who made the change and when. It never versions a document. A Library id stays stable when its path changes.

tags= replaces the complete tag map; it does not merge keys. Read the current tags first when you need to preserve keys, or pass tags={} to clear them.

Pass LibraryDocumentListRequest to list_documents() to list documents that are not deleted. Pass LibraryDocumentGetRequest to get_document() to read one document’s current status without polling. A list request must set limit. It must set cursor to the prior page cursor, or to None for the first page.

Share a Library

Creating a Library grants your agent every action on it. Another agent, person, or group reaches it only through a grant you add.

A grant carries three fields:

FieldWhat it holds
subjectThe agent, user, or group the grant applies to. A trailing * matches a prefix.
actionOne Library action. A trailing * matches a prefix, so library:* covers all of them.
effectallow or deny.
  • Field
    subject
    What it holds
    The agent, user, or group the grant applies to. A trailing * matches a prefix.
  • Field
    action
    What it holds
    One Library action. A trailing * matches a prefix, so library:* covers all of them.
  • Field
    effect
    What it holds
    allow or deny.

A refusal beats every allow. Access and isolation states what a refusal holds against and the order the platform applies to one request.

Adding and revoking grants needs library:admin on that Library, which its creator holds. The core Python client does not wrap the grant routes: add and revoke them in the Dashboard, on the Library’s share control. Library actions lists what each action covers.

A grant does not:

  • Narrow what a built-in role already reaches. The editor and viewer roles carry Library access across the whole tenant. Agents and access lists what each built-in role can do.
  • Scope what one task reads. An agent’s document tools reach every Library it can read, not only the one a task names. Agent harness states that reach.

Delete a document or Library

Pass LibraryDocumentDeleteRequest to delete_document() to move one document into the 30-day Recently deleted window. Repeating the call succeeds. Pass LibraryDeleteRequest to delete() to stop normal reads and writes and remove its documents. That call also succeeds when the Library is already absent.

The core client cannot reverse either operation. A deleted document stays in Recently deleted for 30 days. During that time, a caller with library:write on the Library can restore it in the Dashboard or through the platform restore route.

Restoring re-indexes the document. It returns through queued and processing before it is readable again. After 30 days, the document becomes eligible for permanent removal and no interface can recover it. A deleted Library has no restore route. Its record and grants become eligible for removal after 24 hours.

Deleting a document does not erase it at once. The platform keeps its bytes through the 30-day window, then reclaims eligible records and content asynchronously. Do not use the end of the window as an exact physical-removal deadline.

Deleting the Library ends that window early. It makes every document in the Library unrecoverable, including documents in Recently deleted. Record, grant, and content reclamation then runs asynchronously.

Images inside an uploaded document reach an image-capable model as input when image support is enabled for it—see Images in a document.

Use Attach documents when you want the SDK to choose the conventional path and submit the files with a task. See the API reference for method signatures and Errors and reliability for authorization and transport failures.