Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7e7c46a
feat: add local mode to the PageIndex SDK client
rejojer Aug 5, 2026
d413de2
chore: package pageindex 0.3.0.dev4 for PyPI
rejojer Aug 5, 2026
3425ae1
docs: add SDK section to README; move the agentic demo onto the SDK
rejojer Aug 5, 2026
26f595e
refactor: rebuild cloud_api on the 0.2.8 client text
rejojer Aug 5, 2026
80fbaf9
refactor: drop the local retrieval endpoints — cloud-only, deprecated
rejojer Aug 5, 2026
fd2ba98
feat: manifest.json — one-file document listings for the local store
rejojer Aug 6, 2026
cd44ef0
fix: align local doc_id prefix and createdAt format with the cloud
rejojer Aug 6, 2026
b9e1955
feat: optional metadata tags on submit_document (both modes)
rejojer Aug 6, 2026
7c1c855
docs: state that createdAt is UTC and show how to localize it
rejojer Aug 6, 2026
cc2b352
fix: createdAt carries milliseconds, matching the cloud's datetime(3)
rejojer Aug 6, 2026
319eea2
fix: createdAt at millisecond precision, matching the timestamp(3) co…
rejojer Aug 6, 2026
a2aad2c
chore: trim non-essential comments
rejojer Aug 6, 2026
d4bf06c
fix: close the local_store crash and corruption holes found in review
rejojer Aug 6, 2026
a612527
docs: correct two docstring claims and the pymupdf note
rejojer Aug 6, 2026
bffb116
chore: target 0.2.9 for the local-mode release
rejojer Aug 6, 2026
ca06874
ci: publish to PyPI on version tags
rejojer Aug 6, 2026
3937da3
chore: tighten the store docstring to essentials
rejojer Aug 6, 2026
e6de437
fix: contain invalid-UTF-8 corruption; fail loud on unreadable data f…
rejojer Aug 6, 2026
608c9cc
feat: LocalClient and CloudClient for explicit mode selection
rejojer Aug 6, 2026
7d07924
feat: explicit-mode clients PageIndexCloudClient and PageIndexLocalCl…
rejojer Aug 6, 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.2.9 -m "Release 0.2.9"
# 2. git push origin v0.2.9
# 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.2.9 v0.2.9rc1 v0.2.9.dev1
# PyPI rejects duplicate version uploads, so each tag must be a new version.
# Plain `pip install pageindex` skips dev/rc pre-releases — install one
# explicitly with `pip install pageindex==0.2.9.dev1`.
#
# 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/*
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ __pycache__
.env*
.venv/
logs/
.pageindex/
dist/
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,33 @@ python3 run_pageindex.py --md_path /path/to/your/document.md
>
> Add `--optimize` to refine the tree structure for more efficient retrieval (with an LLM expansion pass).

## 🐍 Python SDK: Cloud & Local

The `pageindex` package on PyPI is the Python SDK for the [PageIndex API](https://docs.pageindex.ai) — and the same client now also runs fully **locally**, powered by this repo's indexing pipeline (including Flash).

```bash
pip3 install --upgrade pageindex # local mode ships in pageindex >= 0.2.9; earlier versions are cloud-only
```

```python
from pageindex import PageIndexClient

client = PageIndexClient(api_key="YOUR_PAGEINDEX_API_KEY") # cloud: managed OCR, tree building, retrieval
client = PageIndexClient() # local: same methods on your machine, using your LLM key (e.g. OPENAI_API_KEY)

doc_id = client.submit_document("doc.pdf")["doc_id"] # local mode blocks until indexing finishes
doc_id = client.submit_document("doc.pdf", mode="flash")["doc_id"] # local mode with PageIndex Flash

tree = client.get_tree(doc_id, node_summary=True)["result"]

answer = client.chat_completions(
messages=[{"role": "user", "content": "Summarize the key findings"}],
doc_id=doc_id,
)["choices"][0]["message"]["content"]
```

Local documents are stored as plain JSON under `./.pageindex` (configurable via `storage_path`). Local mode supports PDFs; folders, `beta_headers`, `enable_citations`, and the deprecated retrieval API (`submit_query`/`get_retrieval`) remain cloud-only — each method's docstring spells out the differences. To pin the mode at construction instead of inferring it from `api_key`, use `PageIndexCloudClient` (fails without a real key) or `PageIndexLocalClient` (has no key parameter).

## 🚀 Agentic Vectorless RAG: An Example

For a simple, end-to-end **agentic vectorless RAG** example using **self-hosted PageIndex** (with OpenAI Agents SDK), see [`examples/agentic_vectorless_rag_demo.py`](examples/agentic_vectorless_rag_demo.py).
Expand Down
59 changes: 41 additions & 18 deletions examples/agentic_vectorless_rag_demo.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
"""
Agentic Vectorless RAG with PageIndex - Demo

A simple example of building a document QA agent with self-hosted PageIndex
and the OpenAI Agents SDK. Instead of vector similarity search and chunking,
PageIndex builds a hierarchical tree index and uses agentic LLM reasoning for
human-like, context-aware retrieval.
A simple example of building a document QA agent with the PageIndex SDK in
local mode and the OpenAI Agents SDK. Instead of vector similarity search and
chunking, PageIndex builds a hierarchical tree index and uses agentic LLM
reasoning for human-like, context-aware retrieval.

Agent tools:
- get_document() — document metadata (status, page count, etc.)
- get_document_structure() — tree structure index of a document
- get_page_content() — retrieve text content of specific pages

Steps:
1 — Index a PDF and view its tree structure index
1 — Index a PDF locally and view its tree structure index
2 — View document metadata
3 — Ask a question (agent reasons over the index and auto-calls tools)

Requirements: pip install openai-agents
Requirements: pip install openai-agents; OPENAI_API_KEY in the environment.
"""
import sys
import json
Expand All @@ -39,19 +39,34 @@

_EXAMPLES_DIR = Path(__file__).parent
PDF_PATH = _EXAMPLES_DIR / "documents" / "attention-residuals.pdf"
WORKSPACE = _EXAMPLES_DIR / "workspace"
STORAGE_PATH = _EXAMPLES_DIR / ".pageindex"

AGENT_SYSTEM_PROMPT = """
You are PageIndex, a document QA assistant.
TOOL USE:
- Call get_document() first to confirm status and page/line count.
- Call get_document() first to confirm status and page count.
- Call get_document_structure() to identify relevant page ranges.
- Call get_page_content(pages="5-7") with tight ranges; never fetch the whole document.
- Before each tool call, output one short sentence explaining the reason.
Answer based only on tool output. Be concise.
"""


def _parse_pages(pages: str) -> list[int]:
"""Parse a pages string like '5-7', '3,8', or '12' into a list of ints."""
result = []
for part in pages.split(","):
part = part.strip()
if "-" in part:
start, end = (int(x) for x in part.split("-", 1))
if start > end:
raise ValueError(f"Invalid range '{part}': start must be <= end")
result.extend(range(start, end + 1))
else:
result.append(int(part))
return sorted(set(result))


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

Expand All @@ -62,21 +77,28 @@ 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)
return json.dumps(client.get_document(doc_id))

@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)
tree = client.get_tree(doc_id, node_summary=True)["result"]
return json.dumps(utils.remove_fields(tree, fields=["text"]), ensure_ascii=False)

@function_tool
def get_page_content(pages: str) -> str:
"""
Get the text content of specific pages or line numbers.
Get the text content of specific pages.
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)
try:
wanted = set(_parse_pages(pages))
except ValueError:
return json.dumps({"error": f"Invalid pages format: {pages!r}. Use '5-7', '3,8', or '12'."})
all_pages = client.get_ocr(doc_id, format="page")["result"]
return json.dumps(
[p for p in all_pages if p["page_index"] in wanted], ensure_ascii=False
)

agent = Agent(
name="PageIndex",
Expand Down Expand Up @@ -152,24 +174,25 @@ async def _run():
f.write(chunk)
print("Download complete.\n")

# Setup
client = PageIndexClient(workspace=WORKSPACE)
# Setup: local mode — no PageIndex API key needed, your LLM key does the work
client = PageIndexClient(storage_path=str(STORAGE_PATH))

# 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),
(doc["id"] for doc in client.list_documents(limit=100)["documents"]
if doc["name"] == PDF_PATH.name),
None,
)
if doc_id:
print(f"\nLoaded cached doc_id: {doc_id}")
else:
doc_id = client.index(PDF_PATH)
doc_id = client.submit_document(str(PDF_PATH))["doc_id"]
print(f"\nIndexed. doc_id: {doc_id}")
print("\nTree Structure (top-level sections):")
structure = json.loads(client.get_document_structure(doc_id))
structure = client.get_tree(doc_id, node_summary=True)["result"]
utils.print_tree(structure)

# Step 2: View document metadata
Expand Down
Loading
Loading