Skip to main content

DocWorm Python SDK

Python SDK for the DocWorm service — document indexing, knowledge graphs, reasoning RAG, and chat.

Installation

pip install docworm

Requires httpx and websockets (installed automatically).

Quick Start

import asyncio
from docworm import DocWorm

client = DocWorm(api_key="dw_live_...")  # or export DOCWORM_API_KEY

# Upload and index a document
upload = client.upload("report.pdf")
client.index(upload.file_id)
status = client.wait_for_indexing(upload.file_id)
print(f"Indexed {status.processed_pages}/{status.total_pages} pages")

# Create a chat and attach documents
client.create_chat("my-chat", display_name="My Chat")
client.attach_documents("my-chat", [upload.file_id])

# Stream a chat turn over SSE
async def main():
    async for event in client.chat_stream_sse("my-chat", "What does the report say?"):
        if event.type == "answer":
            print(event.text, end="")
        elif event.type == "citation":
            c = event.citation
            print(f"\n  [{c.filename}] p.{c.start_page}-{c.end_page}")
        elif event.type == "done":
            print(f"\nTokens: {event.usage.total_tokens}")

asyncio.run(main())

Configuration

client = DocWorm()  # reads DOCWORM_API_KEY, DOCWORM_URL, and config.yaml
client = DocWorm(api_key="dw_live_...")

Authenticate with a DocWorm API key (dw_live_..., sent via X-API-KEY). Copy config.yaml.example to config.yaml for local file configuration.

Models resolve server-side from model_configs/active by default; use client.list_models() to inspect the active defaults. Optional model overrides are supported on chat creation and chat_stream_sse.

Documents

Upload

# From file path
upload = client.upload("report.pdf")
print(upload.file_id)

# From bytes
upload = client.upload(b"<raw pdf bytes>", filename="report.pdf")

# From file object
with open("report.pdf", "rb") as f:
    upload = client.upload(f, filename="report.pdf")

# With custom file_id and scoping
upload = client.upload("report.pdf", file_id="my-custom-id", project_id="proj_1")

Index

# Index an uploaded document (usually status="queued")
resp = client.index(upload.file_id)

# Upload + index in one call (pass file to index)
resp = client.index("doc-123", file="report.pdf")

# Upload + index convenience wrapper (returns the queued IndexResponse)
resp = client.upload_and_index("report.pdf")

# Index a pre-built tree directly
resp = client.index_json("doc-123", tree=[...])

# Batch reindex (up to 100 files, plan-gated concurrency)
resp = client.index_batch(["doc-1", "doc-2"])

# Retry only failed pages of a partial document
resp = client.retry_failed_pages(upload.file_id)

# Cancel in-flight indexing
resp = client.cancel_indexing([upload.file_id])

# Wipe derived state without new bytes (replace flow)
resp = client.reset_index(upload.file_id)

Wait for Indexing

# Poll until ready (default: 600s timeout)
status = client.wait_for_indexing(upload.file_id, timeout=300)
print(f"Ready: {status.ready}, Progress: {status.progress_pct}%")
# Raises RuntimeError on error/cancelled, TimeoutError on expiry

Get Document Status

status = client.get_document_status(upload.file_id)
print(f"Pages: {status.processed_pages}/{status.total_pages}")
print(f"Tree version: {status.tree_version}")

List Documents

docs = client.list_documents()
for doc in docs:
    print(f"{doc['fileId']}: {doc.get('filename')} ({doc.get('status')})")

Tree / Node Map / Excel Preview

tree = client.get_document_tree(upload.file_id)
for node in tree:
    print(f"  {node.node_id}: {node.title}")

node_map = client.get_node_map(upload.file_id)
preview = client.get_excel_preview(upload.file_id)  # Excel files
for sheet in preview["sheets"]:
    print(f"  {sheet['name']}: {sheet['headers']}")

Pages / Images / Original

# List all pages
pages = client.get_document_pages(upload.file_id)
for page in pages:
    print(f"  Page {page.page_num}: {page.status}")

# Page markdown
content = client.get_page_content(upload.file_id, page_num=1)
print(content.markdown)

# Page JPEG (+ total page count header)
image = client.get_page_image(upload.file_id, page_num=1)
print(image["total_pages"], len(image["content"]))

# Original uploaded file
original = client.get_original(upload.file_id)

Delete / Reprocess

client.delete_document(upload.file_id)
client.reprocess_document(upload.file_id)

Chats

Create / List / Get / Update / Delete

# Create (idempotent)
chat = client.create_chat("research-chat", display_name="Research Chat")
print(chat.id)

# List all
chats = client.list_chats()

# Get one
chat = client.get_chat("research-chat")

# Update metadata
client.update_chat("research-chat", title="New title")

# Delete
client.delete_chat("research-chat")

Attach / Detach Documents

# Attach multiple documents (rebuilds the knowledge graph)
client.attach_documents("research-chat", ["file_abc", "file_def"])

# Detach a document
client.detach_document("research-chat", "file_abc")

Message History

result = client.get_messages("research-chat", limit=20)
for msg in result["messages"]:
    print(f"  [{msg.role}] {msg.content[:100]}")

# Paginate
if result["has_more"]:
    older = client.get_messages("research-chat", limit=20, before=result["messages"][-1].msg_id)

# Save / edit / delete
client.save_message("research-chat", role="user", content="Hello")
client.edit_message("research-chat", msg_id="m1", message="Edited question")
client.update_message("research-chat", msg_id="m2", fields={"content": "Fixed answer"})
client.delete_message("research-chat", msg_id="m3")

Cancel / Retry / Diagnose

# Cancel a streaming turn
client.cancel_chat_message("research-chat", msg_id="streaming-id")

# Prepare a retry, then stream again with the returned ids
client.retry_chat_message("research-chat", msg_id="user-msg-id")

# Inspect a message (steps, citations, buffer state)
client.diagnose_message("research-chat", msg_id="m1")

Artifacts / Outputs / Titles

# Download a run artifact (parsed JSON dict)
artifact = client.get_run_artifact("research-chat", msg_id="m1", artifact_id="artifact_1")

# Download an archived structured output
raw = client.download_output("research-chat", session_id="s1", filename="result.json")

# Consolidate session memories
client.consolidate_memories("research-chat")

# Generate a chat title
title = client.generate_title("quarterly revenue analysis")

Chat Streaming

SSE is the standard streaming path for the hosted service (https://docworm.web.app). chat_stream_sse opens POST /v1/chats/{chat_id}/stream; consume it with async for:

async for event in client.chat_stream_sse("research-chat", "What are the key findings?"):
    if event.type == "thinking":
        print(f"  {event.text}", end="")
    elif event.type == "answer":
        print(f"  {event.text}", end="")
    elif event.type == "citation":
        print(f"\n  Source: {event.citation.filename} p.{event.citation.start_page}")
    elif event.type == "done":
        print(f"\n  Tokens: {event.usage.total_tokens}")
    elif event.type == "error":
        print(f"\n  Error: {event.error}")

# Optional model override for the turn
async for event in client.chat_stream_sse(
    "research-chat", "Compare these", model="...",
):
    ...

# Retry an existing turn
client.retry_chat_message("research-chat", msg_id=user_msg_id)
async for event in client.chat_stream_sse(
    "research-chat", "",
    parent_msg_id=user_msg_id, retry_msg_id=assistant_msg_id,
):
    ...

# Resume a dropped SSE stream
async for event in client.chat_reconnect_sse("research-chat", streaming_msg_id, last_seq=12):
    ...

On the done event, event.usage is a UsageInfo with prompt_tokens, completion_tokens, total_tokens, calls, and thinking_tokens.

For direct-to-service deployments where WebSocket upgrades are available, the WebSocket variants behave the same way:

async for event in client.chat_stream("research-chat", "What are the key findings?"):
    ...

# With per-turn attachments
async for event in client.chat_stream(
    "research-chat", "Compare these",
    attached_docs=[{"id": "file_abc", "name": "report.pdf"}],
):
    ...

# Resume a dropped stream
async for event in client.chat_reconnect("research-chat", streaming_msg_id, last_seq=12):
    ...

# Cancel the current turn and start a new one
async for event in client.chat_interrupt("research-chat", "Actually, do X instead"):
    ...

Reasoning RAG Query

Direct query over documents without a chat:

# Query a single document
resp = client.query(
    "What is the revenue growth?",
    file_id="file_abc123",
)
print(resp.answer)
print(f"Reasoning: {resp.reasoning_traces}")
print(f"Sub-questions: {resp.sub_questions}")
print(f"Nodes retrieved: {resp.nodes_retrieved}")

# Query multiple documents
resp = client.query(
    "Compare revenue across these reports",
    file_ids=["file_abc", "file_def"],
)

# With image context
resp = client.query(
    "Describe the charts",
    file_id="file_abc",
    include_images=True,
)

# With conversation continuity
resp = client.query(
    "Now tell me about expenses",
    file_id="file_abc",
    session_id="session-123",
    message_id="msg-456",
)

Cancel a Query

client.cancel_query(session_id="session-123", message_id="msg-456")

Codegen / Tables / Models

# Generate (+ execute for excel_index) code for a document
result = client.generate_code("research-chat", "file_abc", task="excel_index")
print(result.code_id, result.output)

codes = client.list_codes("research-chat")
code = client.get_code("research-chat", result.code_id)
client.put_code("research-chat", result.code_id, code="...", fork=True)
client.predict({...})  # tabular prediction passthrough
client.get_artifact("research-chat", "my-artifact")

# Tables
client.synthesize_table("file_abc", pages=[1, 2])
client.get_toc("file_abc")

# Models (server-side defaults; optional per-chat/stream overrides)
client.list_models()

Low-Level Escape Hatch

# For endpoints the SDK doesn't wrap yet
data = client._raw_request("GET", "/v1/some-route", params={"file_id": "x"})

Error Handling

from httpx import HTTPStatusError

try:
    client.get_chat("my-chat")
except HTTPStatusError as e:
    if e.response.status_code == 404:
        print("Chat not found")
    elif e.response.status_code == 401:
        print("Invalid credentials")
    else:
        print(f"HTTP {e.response.status_code}: {e.response.text}")

Environment Variables

Variable Default Description
DOCWORM_API_KEY - DocWorm API key (dw_live_...)
DOCWORM_URL https://docworm.web.app Service base URL

License

Apache 2.0 - see LICENSE.

Metadata

Release files for docworm 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docworm 0.6.1
File Size Uploaded Provenance Actions
docworm-0.6.1.tar.gz 26.2 kB With provenance Details for docworm-0.6.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for docworm 0.6.1
File Interpreter ABI Platform Actions
docworm-0.6.1-py3-none-any.whl Python 3 none any Details for docworm-0.6.1-py3-none-any.whl

Total release size: 49.9 kB

Release files / docworm-0.6.1.tar.gz

Download URL docworm-0.6.1.tar.gz
Size 26.2 kB
Tags Source
SHA-256 checksum
How to use checksums
10c592396a9a7f4cb10bb287e12c9bb150e97b5cf36ed5b3df49a882840847b9
BLAKE2b-256 checksum
How to use checksums
50f69c8cdcdf6fbde439ee8ecb9475494b12fd2466444cc0ea74df1929055ae3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / docworm-0.6.1-py3-none-any.whl

Download URL docworm-0.6.1-py3-none-any.whl
Size 23.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a85fe0c90e7d3174f6cb90e16025064ed9d50156edafec61cb092c311646999d
BLAKE2b-256 checksum
How to use checksums
e74b21a6a51885e41b85b1ae485fbdb22b0f0522c5668872cc11b16296b4d60a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for docworm 0.6.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for docworm-0.6.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page