Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
c7fe93b
feat: add PageIndex SDK with local/cloud dual-mode support (#207)
KylinMountain Apr 6, 2026
27e671e
Update pyproject.toml: switch to poetry and bump to 0.3.0.dev0
rejojer Apr 8, 2026
f5de9c9
Add dist/ to .gitignore
rejojer Apr 8, 2026
edb2031
fix: poll status=="completed" in cloud add_document (#226)
rejojer Apr 10, 2026
6d29886
chore: bump version to 0.3.0.dev1
rejojer Apr 10, 2026
595895c
feat:compatible with Pageindex SDK (#238)
saccharin98 May 11, 2026
3595956
chore(deps): declare pydantic as a direct dependency
KylinMountain May 12, 2026
cbea31d
chore: remove unused `ext` local in PDF image block handling
KylinMountain May 12, 2026
d7b36aa
feat(collection): scoped query mode and experimental multi-doc warning
KylinMountain May 15, 2026
a47c36a
feat(collection): doc_ids accepts str|list, design cleanups
KylinMountain May 15, 2026
9ad8304
chore: move legacy SDK e2e script into examples/
KylinMountain May 15, 2026
f354eb1
docs: drop Environment variables and Runnable examples subsections fr…
KylinMountain May 15, 2026
2cab6b5
fix(llm): configurable per-call litellm params instead of mutating th…
KylinMountain Jun 25, 2026
cc7e43c
fix: use .get() in get_leaf_nodes to avoid KeyError on leaf nodes (#331)
ChiragB254 Jul 3, 2026
284ce00
fix(cloud): align cloud/local backend contracts and harden the HTTP l…
KylinMountain Jul 7, 2026
6a73279
fix: patch three critical defects from the SDK review
KylinMountain Jul 7, 2026
956147d
fix: resolve five P1 defects from the SDK review
KylinMountain Jul 7, 2026
6c948a3
refactor(index): dedupe the copied indexing pipeline behind deprecati…
KylinMountain Jul 7, 2026
fe36e25
fix: six P2 correctness/robustness cleanups from the SDK review
KylinMountain Jul 7, 2026
b3616f7
fix: dedup race, cwd-relative image paths, unencoded legacy URLs
KylinMountain Jul 7, 2026
b2756c7
refactor(sdk): typed returns, protocol contract, parser layering
KylinMountain Jul 7, 2026
154c483
fix(cloud_api): restore legacy is_retrieval_ready swallow-on-error co…
KylinMountain Jul 7, 2026
4a6a948
style(cloud): annotate best-effort response-close in query_stream cle…
KylinMountain Jul 7, 2026
a139bd5
merge upstream/main into dev; port #188 into the index pipeline
KylinMountain Jul 7, 2026
b64d717
fix: load .env explicitly in pageindex __init__
KylinMountain Jul 7, 2026
890b520
fix(sqlite): make concurrent indexing writes robust (no "database is …
KylinMountain Jul 7, 2026
e539283
fix(index): bound LLM concurrency safely, per-index and leak-free
KylinMountain Jul 8, 2026
2d46d68
fix(index): bound LLM concurrency at the leaf, not per gather call
KylinMountain Jul 8, 2026
2a69c76
fix: exact md page selection + restore CHATGPT_API_KEY alias
KylinMountain Jul 8, 2026
703017e
style: give Protocol stubs docstring bodies
KylinMountain Jul 8, 2026
cf7f5ce
fix: address PR #272 review findings (directly-fixable items)
KylinMountain Jul 8, 2026
89132cb
feat(cli): accept both bare flags and legacy yes/no for --if-add-* args
KylinMountain Jul 8, 2026
8f536cb
fix(index): strip node text in the level_based path too (no default l…
KylinMountain Jul 8, 2026
04cb9cb
fix: address xhigh code-review findings on 2d46d68..8f536cb
KylinMountain Jul 8, 2026
623ce92
ci: publish to PyPI on version tags (#347)
KylinMountain Jul 9, 2026
b9d0219
fix: prompt-injection delimiter escape, legacy config coercion, gathe…
KylinMountain Jul 9, 2026
4e6a135
fix: CMYK image drop, empty-doc crash, page_index shadowing, sqlite h…
KylinMountain Jul 9, 2026
e12495d
build: declare pytest as a dev dependency
KylinMountain Jul 9, 2026
214098f
fix: ceiling semaphore permit leak under cancellation
KylinMountain Jul 9, 2026
e00d360
fix: bound every LLM call with a per-request network timeout
KylinMountain Jul 9, 2026
91e016d
fix: reject trailing newline in collection-name validation
KylinMountain Jul 9, 2026
56fc3bf
fix: return actionable error for malformed page ranges in local tool
KylinMountain Jul 9, 2026
4456b92
fix: preserve image metadata in cloud get_page_content
KylinMountain Jul 9, 2026
4c7d108
fix: forward stream_metadata to the chat API in legacy client
KylinMountain Jul 9, 2026
72f623a
test: de-flake query_stream early-break test
KylinMountain Jul 9, 2026
1dffa76
fix: sync requirements.txt with the SDK's runtime dependencies
KylinMountain Jul 9, 2026
fc401c9
fix: tolerate empty body on legacy delete_document
KylinMountain Jul 9, 2026
56590c6
Fix sync LLM concurrency limit
KylinMountain Jul 9, 2026
d3ea9b9
fix: scoped concurrency semaphore over-release on cancellation
KylinMountain Jul 9, 2026
9ad7c68
fix: degrade to empty result on LLM retry exhaustion instead of aborting
KylinMountain Jul 10, 2026
e18ccde
fix: prompt guidance referenced fields get_document doesn't return
KylinMountain Jul 10, 2026
d97231b
fix: empty-list scope, get_tree bool casing, md fence tracking; dedup…
KylinMountain Jul 10, 2026
9ad5412
fix: resolve concurrency and DoS pitfalls from PR #272 review
KylinMountain Jul 10, 2026
3ff04d5
fix: address max-effort review findings
rejojer Jul 13, 2026
f995e64
fix: restore config.yaml as the CLI config base
rejojer Jul 13, 2026
2997104
fix: degrade list_collections when folders are unavailable (403/404)
rejojer Jul 14, 2026
fec44e1
docs: correct stale config.yaml claims and CLI help default
rejojer Jul 14, 2026
c8aa83a
fix: address xhigh review findings for PR #272
rejojer Jul 15, 2026
3a780ab
refactor: move _coerce_bool to index/utils as the shared home
rejojer Jul 15, 2026
929b3df
fix: enforce collection membership on cloud doc-scoped operations
rejojer Jul 15, 2026
6d42559
fix: harden indexing and query paths against malformed inputs
rejojer Jul 15, 2026
6f20092
fix: enforce collection membership on cloud query doc_ids
rejojer Jul 16, 2026
1054132
fix: default SDK retrieve_model to gpt-5.4 instead of following model
rejojer Jul 16, 2026
9f01ba4
Merge upstream/main into dev; port hardening into index/
KylinMountain Jul 17, 2026
ccc8b43
fix: align merged hardening with dev's robustness and content fidelity
KylinMountain Jul 17, 2026
3a1727b
fix: harden edge cases in cloud upload, local doc_type, and PDF utili…
rejojer Jul 18, 2026
dea211b
fix: resolve BASE_URL at request time to restore 0.2.x override seman…
rejojer Jul 18, 2026
0f593c6
fix: restore 0.2.x config resolution and legacy submodule attribute a…
rejojer Jul 18, 2026
a0fb863
fix: restore intended demo paper, retrieve_model exposure, and md sum…
rejojer Jul 18, 2026
7594338
test: keep only the pre-existing test files
rejojer Jul 18, 2026
b6ce958
chore: drop redundant model= in demos (IndexConfig default already ap…
rejojer Jul 18, 2026
43153fe
refactor: spell out collection instead of col in examples, README, an…
rejojer Jul 19, 2026
9980fc9
feat: restore status and page/line counts in local get_document
rejojer Jul 19, 2026
b2b0901
docs: note the CLI (PyPDF2) vs SDK local mode (PyMuPDF) parsing diffe…
rejojer Jul 19, 2026
f2b407f
fix: restore PyPDF2 as the PDF text extractor in the SDK path
rejojer Jul 19, 2026
4f1f01a
fix: make openai-agents a hard dependency in requirements.txt, matchi…
rejojer Jul 19, 2026
2688d15
chore: trim fix-rationale comments, restore --model help, drop stale …
rejojer Jul 19, 2026
472d871
feat: make pymupdf an optional [images] extra
rejojer Jul 19, 2026
74c8f7b
chore: name the extra after its dependency; keep README install secti…
rejojer Jul 19, 2026
36202a6
chore: drop pymupdf from requirements.txt (no longer imported by the …
rejojer Jul 19, 2026
5fc1b81
chore: drop the pymupdf extra — image extraction detects the library …
rejojer Jul 19, 2026
dcf58f1
chore: fix stale 'optional extra' wording in comment
rejojer Jul 19, 2026
6b94738
chore: drop unused httpx[socks] dependency
rejojer Jul 21, 2026
d9c8f77
chore: raise openai and openai-agents floors to the tested API genera…
rejojer Jul 21, 2026
69abfe9
fix: send if_retrieval as a bool, matching the proven 0.2.x wire format
rejojer Jul 21, 2026
ebfda6d
fix: parse cloud responses by their real keys, not guessed fallbacks
rejojer Jul 21, 2026
1790e0c
fix: reconstruct real end_index for cloud trees instead of copying th…
rejojer Jul 21, 2026
4ffc6b5
fix: drop remaining guessed-key fallback in cloud chat response parsing
rejojer Jul 21, 2026
a84d88b
fix: exit with a clear message when PAGEINDEX_API_KEY is unset in clo…
rejojer Jul 21, 2026
80bdfc1
fix: drop construction-time LLM provider validation
rejojer Jul 21, 2026
6567e7a
chore: clarify the cloud-only method error — state the cause and the fix
rejojer Jul 21, 2026
6a451ea
chore: drop PAGEINDEX_API_KEY from the .env comment — the library nev…
rejojer Jul 21, 2026
f555254
fix: fail fast when CloudClient gets an empty api_key
rejojer Jul 21, 2026
3c8214a
chore: drop the lowercase-summary comment — the server parses bools c…
rejojer Jul 21, 2026
64c468c
chore: check PAGEINDEX_API_KEY before downloading the demo PDF
rejojer Jul 21, 2026
7805169
chore: drop unused os import
rejojer Jul 21, 2026
9fc7b68
fix: cancel the agent run when a streaming query is abandoned
rejojer Jul 21, 2026
a114935
fix: delete the DB row before files so an interrupted delete can't le…
rejojer Jul 21, 2026
ad7f152
fix: whitelist parser metadata to its documented fields before storing
rejojer Jul 21, 2026
7eae8ca
fix: cancel the agent run when a local streaming query is abandoned
rejojer Jul 21, 2026
8c7f622
fix: delete the DB row before the files in delete_document
rejojer Jul 21, 2026
457bf18
fix: restore format_structure output normalization in the SDK index path
rejojer Jul 21, 2026
d1824e4
fix: keep the file extension in SDK doc_name, matching legacy and cloud
rejojer Jul 21, 2026
fdd075b
fix: let transport errors propagate raw from legacy cloud methods, ma…
rejojer Jul 22, 2026
93535fc
fix: gate doc_description on node summaries in the SDK pipeline, matc…
rejojer Jul 22, 2026
80493cc
fix: order local list_documents newest-first, matching the cloud API
rejojer Jul 22, 2026
a9bb019
fix: resolve api_key at request time to restore 0.2.x override semantics
rejojer Jul 22, 2026
b8c63eb
fix: ignore unknown config.yaml keys instead of crashing
rejojer Jul 22, 2026
225dcc3
fix: fail delete_collection clearly in cloud mode — the API has no fo…
rejojer Jul 22, 2026
00e9323
fix: map duplicate-folder 400 to CollectionAlreadyExistsError in clou…
rejojer Jul 22, 2026
47c9263
chore: trim fix-rationale comments
rejojer Jul 22, 2026
464d565
feat: use the new DELETE /folder/{id} endpoint in cloud delete_collec…
rejojer Jul 22, 2026
5769d15
chore: drop dead guessed 'tree' key fallback in legacy SDK demo — the…
rejojer Jul 22, 2026
1ac7e13
fix: resolve cloud collections against root-level folders only
rejojer Jul 22, 2026
360ab66
fix: three cloud backend correctness issues
rejojer Jul 23, 2026
0a78f1d
fix: store status in SQLite, derive cloud doc_type from filename
rejojer Jul 23, 2026
0584ac8
fix: track pageindex/_validation.py
rejojer Jul 23, 2026
e94f6d4
fix: use doc_name in agent tools, matching the cloud agent contract
rejojer Jul 23, 2026
11ba63d
fix: drop unrecognized stream_metadata, align sort and page_count
rejojer Jul 23, 2026
42d57b6
fix: drop the deprecation marker from the 0.2.x SDK methods
rejojer Jul 23, 2026
7b23cd9
fix: drop deprecation warnings from the top-level module shims
rejojer Jul 23, 2026
1170b10
perf: lazy-load the legacy indexing exports from pageindex/__init__
rejojer Jul 23, 2026
46fd90d
chore: trim __init__ comments to essentials
rejojer Jul 23, 2026
41f4a0a
perf: import litellm/PyPDF2 inside the functions that use them
rejojer Jul 23, 2026
15b0b18
refactor: drop the __init__ lazy-export machinery, superseded by 41f4a0a
rejojer Jul 23, 2026
d6b22e8
fix: list_documents tie-breaker rowid ASC, matching the cloud's effect
rejojer Jul 23, 2026
3bde8fe
chore: compress the tie-break comment to one line
rejojer Jul 23, 2026
b43b56e
chore: trim non-essential comments
rejojer Jul 23, 2026
40693ac
fix: agent tool get_document_structure now checks document existence
rejojer Jul 24, 2026
c1804ed
fix: use _as_int for max_page to preserve pageNum=0
rejojer Jul 24, 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
63 changes: 63 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Publish to PyPI

# Release flow (the git tag IS the version — nothing to bump in the repo):
# 1. git tag -a v0.3.0.dev2 -m "Release 0.3.0.dev2"
# 2. git push origin v0.3.0.dev2
# 3. This workflow derives the version from the tag, injects it into
# pyproject.toml, builds, publishes to PyPI via OIDC trusted publishing
# (no stored secret), and creates a GitHub Release with generated notes.
#
# The tag must be a PEP 440 version with a leading `v`:
# v0.3.0 v0.3.0rc1 v0.3.0.dev2
# PyPI rejects duplicate version uploads, so each tag must be a new version.
# `pip install pageindex` skips dev/pre releases — install one with
# `pip install pageindex==0.3.0.dev2` or `pip install --pre pageindex`.
#
# One-time setup this workflow depends on:
# - PyPI: add a Trusted Publisher on the `pageindex` project pointing at
# repo VectifyAI/PageIndex, workflow `publish.yml`, environment `pypi`.
# - GitHub: create an Environment named `pypi` (Settings -> Environments).

on:
push:
tags:
- "v*"

jobs:
publish:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # OIDC trusted publishing to PyPI
contents: write # create the GitHub Release
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"

- name: Set version from tag and build
run: |
set -euo pipefail
python -m pip install --upgrade build packaging
VERSION="${GITHUB_REF_NAME#v}"
echo "Publishing version: $VERSION"
# Fail early on a malformed tag instead of publishing a junk version.
python -c "from packaging.version import Version; Version('$VERSION')"
# The git tag is the single source of truth; overwrite the static
# placeholder in [tool.poetry] so the built artifacts carry $VERSION.
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
grep '^version = ' pyproject.toml
python -m build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1.14.0

- name: Create GitHub Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
generate_release_notes: true
files: dist/*
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@ __pycache__
.env*
.venv/
logs/
pageindex.egg-info/
dist/
*.db
venv/
uv.lock

# local SDK test-run artifacts (generated by demos)
examples/workspace/files/
examples/workspace/*.db
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,66 @@ You can generate PageIndex tree structures with this open-source repo. Or use ou

---

# 🚀 SDK Usage

A unified `PageIndexClient` powers both local self-hosted and cloud-managed modes. Mode is auto-detected by whether you pass an `api_key`.

### Install

```bash
pip install pageindex
```

### Quick start

```python
from pageindex import PageIndexClient

# Local mode — uses your LLM key (e.g. OPENAI_API_KEY in env).
# `model` drives indexing; agent QA uses `retrieve_model` (default: gpt-5.4).
client = PageIndexClient(model="gpt-4o-2024-11-20")

collection = client.collection()
doc_id = collection.add("path/to/your.pdf")

print(collection.query("What is the main contribution?", doc_ids=doc_id))

# Cloud mode — fully managed, no LLM key needed:
# client = PageIndexClient(api_key="your-pageindex-api-key")
```

`collection.query(...)` returns the answer string by default. Always pass `doc_ids` for reliable single-document QA — omitting it queries the entire collection, which is experimental (see below).

### Streaming queries

```python
import asyncio

async def main():
async for ev in collection.query("Explain multi-head attention", doc_ids=doc_id, stream=True):
if ev.type == "text_delta":
print(ev.data, end="", flush=True)
elif ev.type == "tool_call":
print(f"\n[tool] {ev.data['name']}")

asyncio.run(main())
```

`ev.type` is one of: `tool_call`, `tool_result`, `text_delta`, `text_done`. A `text_done` fires each time a text message completes — a local agentic query may emit several as the agent narrates between tool calls; the last `text_done` before the stream ends carries the final answer.

### Multi-document collections (experimental)

Passing `doc_ids` scopes the query to a specific subset of documents — this is the recommended path. `doc_ids` accepts a single id (`str`) or a list:

```python
collection.query("What does this paper say?", doc_ids=doc1) # single
collection.query("Compare these two papers", doc_ids=[doc1, doc2]) # multi
```

Omitting `doc_ids` queries the **entire collection** and lets the agent pick which docs to read. This is an **experimental** feature with a naive first implementation — we're actively working on better cross-document retrieval. A `UserWarning` is emitted; set `PAGEINDEX_EXPERIMENTAL_MULTIDOC=1` to silence it.

---

# ⚙️ Package Usage

> **Note:** This package uses standard PDF parsing. For use cases with complex PDFs, our [cloud service](https://pageindex.ai/developer) (via MCP and API) offers enhanced OCR, tree building, and retrieval.
Expand Down Expand Up @@ -181,8 +241,10 @@ You can customize the processing with additional optional arguments:
--max-tokens-per-node Max tokens per node (default: 20000)
--if-add-node-id Add node ID (yes/no, default: yes)
--if-add-node-summary Add node summary (yes/no, default: yes)
--if-add-doc-description Add doc description (yes/no, default: yes)
--if-add-doc-description Add doc description (yes/no, default: no)
--if-add-node-text Add raw text to nodes (yes/no, default: no)
```
A bare flag is shorthand for `yes` (e.g. `--if-add-node-id` turns the option on).
</details>

<details>
Expand Down
57 changes: 32 additions & 25 deletions examples/agentic_vectorless_rag_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
from agents.stream_events import RawResponsesStreamEvent, RunItemStreamEvent
from openai.types.responses import ResponseTextDeltaEvent, ResponseReasoningSummaryTextDeltaEvent

from pageindex import PageIndexClient
import pageindex.utils as utils
from pageindex import LocalClient

PDF_URL = "https://arxiv.org/pdf/2603.15031"

Expand All @@ -52,7 +51,17 @@
"""


def query_agent(client: PageIndexClient, doc_id: str, prompt: str, verbose: bool = False) -> str:
def _normalize_model_for_agents_sdk(model: str) -> str:
"""The OpenAI Agents SDK only recognizes 'openai/' and 'litellm/' model
prefixes; route any other LiteLLM-style provider path (e.g. 'anthropic/...')
through litellm explicitly, mirroring what PageIndex itself does internally
for its built-in agent."""
if model and "/" in model and not model.startswith(("litellm/", "openai/")):
return f"litellm/{model}"
return model


def query_agent(collection, doc_id: str, prompt: str, model: str, verbose: bool = False) -> str:
"""Run a document QA agent using the OpenAI Agents SDK.

Streams text output token-by-token and returns the full answer string.
Expand All @@ -62,12 +71,14 @@ def query_agent(client: PageIndexClient, doc_id: str, prompt: str, verbose: bool
@function_tool
def get_document() -> str:
"""Get document metadata: status, page count, name, and description."""
return client.get_document(doc_id)
doc = collection.get_document(doc_id)
doc.pop("structure", None) # keep tool output small for the LLM context
return json.dumps(doc, ensure_ascii=False)

@function_tool
def get_document_structure() -> str:
"""Get the document's full tree structure (without text) to find relevant sections."""
return client.get_document_structure(doc_id)
return json.dumps(collection.get_document_structure(doc_id), ensure_ascii=False)

@function_tool
def get_page_content(pages: str) -> str:
Expand All @@ -76,13 +87,13 @@ def get_page_content(pages: str) -> str:
Use tight ranges: e.g. '5-7' for pages 5 to 7, '3,8' for pages 3 and 8, '12' for page 12.
For Markdown documents, use line numbers from the structure's line_num field.
"""
return client.get_page_content(doc_id, pages)
return json.dumps(collection.get_page_content(doc_id, pages), ensure_ascii=False)

agent = Agent(
name="PageIndex",
instructions=AGENT_SYSTEM_PROMPT,
tools=[get_document, get_document_structure, get_page_content],
model=client.retrieve_model,
model=_normalize_model_for_agents_sdk(model),
# model_settings=ModelSettings(reasoning={"effort": "low", "summary": "auto"}), # Uncomment to enable reasoning
)

Expand Down Expand Up @@ -130,10 +141,10 @@ async def _run():

try:
asyncio.get_running_loop()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, _run()).result()
except RuntimeError:
return asyncio.run(_run())
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(asyncio.run, _run()).result()


if __name__ == "__main__":
Expand All @@ -152,37 +163,33 @@ async def _run():
f.write(chunk)
print("Download complete.\n")

# Setup
client = PageIndexClient(workspace=WORKSPACE)
# Setup: self-hosted local client + a collection
client = LocalClient(storage_path=str(WORKSPACE))
collection = client.collection("agentic-demo")

# Step 1: Index PDF and view tree structure
print("=" * 60)
print("Step 1: Index PDF and view tree structure")
print("=" * 60)
doc_id = next(
(did for did, doc in client.documents.items() if doc.get('doc_name') == PDF_PATH.name),
None,
)
if doc_id:
print(f"\nLoaded cached doc_id: {doc_id}")
else:
doc_id = client.index(PDF_PATH)
print(f"\nIndexed. doc_id: {doc_id}")
# Content-hash dedup: re-running reuses the existing doc_id, no re-index.
doc_id = collection.add(str(PDF_PATH))
print(f"\ndoc_id: {doc_id}")
print("\nTree Structure (top-level sections):")
structure = json.loads(client.get_document_structure(doc_id))
utils.print_tree(structure)
for node in collection.get_document_structure(doc_id):
print(f" - {node.get('title', '(untitled)')}")

# Step 2: View document metadata
print("\n" + "=" * 60)
print("Step 2: View document metadata")
print("=" * 60)
doc_metadata = client.get_document(doc_id)
print(f"\n{doc_metadata}")
meta = collection.get_document(doc_id)
meta.pop("structure", None)
print("\n" + json.dumps(meta, ensure_ascii=False, indent=2))

# Step 3: Agent Query
print("\n" + "=" * 60)
print("Step 3: Agent Query (auto tool-use)")
print("=" * 60)
question = "Explain Attention Residuals in simple language."
print(f"\nQuestion: '{question}'")
query_agent(client, doc_id, question, verbose=True)
query_agent(collection, doc_id, question, client.retrieve_model, verbose=True)
67 changes: 67 additions & 0 deletions examples/cloud_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Agentic Vectorless RAG with PageIndex SDK - Cloud Demo

Uses CloudClient for fully-managed document indexing and QA.
No LLM API key needed — the cloud service handles everything.

Steps:
1 — Upload and index a PDF via PageIndex cloud
2 — Stream a question with tool call visibility

Requirements:
pip install pageindex
export PAGEINDEX_API_KEY=your-api-key
"""
import asyncio
import os
import sys
from pathlib import Path
import requests
from pageindex import CloudClient

_EXAMPLES_DIR = Path(__file__).parent
PDF_URL = "https://arxiv.org/pdf/2603.15031"
PDF_PATH = _EXAMPLES_DIR / "documents" / "attention-residuals.pdf"

api_key = os.environ.get("PAGEINDEX_API_KEY")
if not api_key:
sys.exit("PAGEINDEX_API_KEY not set — get a key at https://dash.pageindex.ai")

# Download PDF if needed
if not PDF_PATH.exists():
print(f"Downloading {PDF_URL} ...")
PDF_PATH.parent.mkdir(parents=True, exist_ok=True)
with requests.get(PDF_URL, stream=True, timeout=30) as r:
r.raise_for_status()
with open(PDF_PATH, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print("Download complete.\n")

client = CloudClient(api_key=api_key)
collection = client.collection()

doc_id = collection.add(str(PDF_PATH))
print(f"Indexed: {doc_id}\n")

# Streaming query
stream = collection.query("What is the main contribution of this paper?", stream=True)

async def main():
streamed_text = False
async for event in stream:
if event.type == "text_delta":
print(event.data, end="", flush=True)
streamed_text = True
elif event.type == "tool_call":
if streamed_text:
print()
streamed_text = False
args = event.data.get("args", "")
print(f"[tool call] {event.data['name']}({args})")
elif event.type == "text_done":
print()
streamed_text = False

asyncio.run(main())
Loading
Loading