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_TOKENto a token for your tenant. - Set
DUALE_TENANT_IDto 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.pdffile 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
TimeoutErrorbounds the client only. Ingestion continues, and the document still reachesreadyorfailed. - Poll with
get_document()rather than uploading again. Eachupload()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 readyThe 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_MISMATCHorSIZE_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-formatsUse 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.
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:
- Upload the new version
Call
upload()with the new file. The receipt carries a newdocument_id. - Wait until it is readable
Call
wait_for_document()on that id until the document reachesready. - Delete the old versionCall
delete_document()on the olddocument_id.
The order decides what an agent reads in between:
- 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:
- 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, solibrary:*covers all of them.
- Field
effect- What it holds
allowordeny.
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.