Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
85482c5
fix local mode edge cases
rejojer Aug 8, 2026
16b7a71
fix: keep the litellm/ prefix normalization the demo depends on
rejojer Aug 10, 2026
3562145
fix: restore the published 0.2.8 helper signatures the cookbooks call
rejojer Aug 10, 2026
871e840
perf: resolve the indexing stack lazily from pageindex/__init__
rejojer Aug 10, 2026
0954f81
fix: wrap PDF read failures in PageIndexAPIError on local submit
rejojer Aug 10, 2026
bc71c2f
fix: address code review findings across local mode and publish workflow
rejojer Aug 11, 2026
96a48b0
fix: tighten CI permissions, timestamp format, import style, and docs…
rejojer Aug 11, 2026
75052d4
fix: accept GOOGLE_API_KEY for Gemini; mark pre-releases in GitHub
rejojer Aug 11, 2026
c19ba54
fix: preserve print_tree backward compat with 0.2.8 positional call
rejojer Aug 11, 2026
a97059c
refactor: print_tree param order — exclude_fields second for 0.2.8 co…
rejojer Aug 11, 2026
feb10c0
refactor: remove _require_llm_key pre-check entirely
rejojer Aug 11, 2026
6d1f83d
fix: let LLM provider errors propagate instead of wrapping them
rejojer Aug 11, 2026
75ecbd9
refactor: catch only RuntimeError instead of isinstance check on openai
rejojer Aug 11, 2026
6eb38c1
fix: restore createdAt to 6-digit .177000 format matching cloud DATET…
rejojer Aug 11, 2026
e6c0f4a
fix: resolve 15 review findings from PR #389
rejojer Aug 11, 2026
158ce78
fix: close publish workflow injection and detect flash silent summary…
rejojer Aug 11, 2026
ce37bc1
refactor: make chat_completions cloud-only until agent-based local ch…
rejojer Aug 11, 2026
f532edb
fix: verification follow-ups for the chat removal commit
rejojer Aug 11, 2026
8402016
fix: detect classic-path silent summary failure like flash already does
rejojer Aug 11, 2026
2a3965e
fix: accurate wording for retrieve_model docstring and empty-pages error
rejojer Aug 11, 2026
5917a46
chore: trim non-essential comments, fix three docstring issues
rejojer Aug 11, 2026
9e978ba
feat: add get_page_content and get_tree include_text parameter
rejojer Aug 11, 2026
b847f90
simplify: drop redundant try/except in demo get_page_content tool
rejojer Aug 11, 2026
c6ad93c
feat: add get_document_structure convenience method
rejojer Aug 11, 2026
b978fb9
test: cover get_page_content, get_document_structure, include_text=False
rejojer Aug 11, 2026
9121b3e
fix: guard against four edge-case crashes found in PR #389 review
rejojer Aug 11, 2026
2224618
chore: trim verbose optional-dep comments in requirements.txt
rejojer Aug 11, 2026
8fab4a7
revert: restore README.md to main — SDK section deferred to next version
rejojer Aug 11, 2026
d208989
fix: eliminate double PDF parse in local standard indexing
rejojer Aug 11, 2026
ebe7f3b
test: assert page_list is passed and correctly shaped
rejojer Aug 11, 2026
89ab764
fix: wire include_text to cloud API and guard get_page_content on pro…
rejojer Aug 11, 2026
b25fa5c
Merge remote-tracking branch 'origin/main' into sdk-local
rejojer Aug 11, 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
85 changes: 85 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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:
test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"]
name: test py${{ matrix.python-version }}
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -r requirements.txt pytest
- run: python -m pytest -q

publish:
needs: test
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.
export VERSION
python -c "import os; from packaging.version import Version; Version(os.environ['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 }}
prerelease: ${{ contains(github.ref_name, 'rc') || contains(github.ref_name, 'dev') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
generate_release_notes: true
files: dist/*
10 changes: 2 additions & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,16 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"]
# "without" proves the package works with no agent framework
# installed; "with" covers the framework integration paths.
agent-frameworks: [without, with]
name: py${{ matrix.python-version }} (${{ matrix.agent-frameworks }} frameworks)
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
cache: pip
# requirements.txt works on every branch; the packaging metadata
# (pyproject) does not exist on all of them.
- run: pip install -r requirements.txt pytest
- if: matrix.agent-frameworks == 'with'
run: pip install openai-agents claude-agent-sdk
# python -m pytest puts the repo root on sys.path, so the in-repo
# `pageindex` package is imported without an install step.
- run: python -m pytest -q
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/
38 changes: 19 additions & 19 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,12 +39,12 @@

_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.
Expand All @@ -62,27 +62,26 @@ 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)
return json.dumps(client.get_document_structure(doc_id), 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)
return json.dumps(client.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=getattr(client, "retrieve_model", None),
# model_settings=ModelSettings(reasoning={"effort": "low", "summary": "auto"}), # Uncomment to enable reasoning
)

Expand Down Expand Up @@ -152,24 +151,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