Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d375c00
feat: local mode for the PageIndex SDK (v0.2.9) (#389)
rejojer Aug 11, 2026
6b343f5
feat: agent tools — the cloud MCP tool contract on the client
rejojer Aug 9, 2026
8737799
fix: agent tools review — next_steps order, resolve caching, error se…
rejojer Aug 10, 2026
2ba9035
fix: agent tools review 2 — bridge thread safety, browse paging, meta…
rejojer Aug 10, 2026
3f13158
fix: agent tools review 3 — page-span cap, duplicate names, wait resi…
rejojer Aug 10, 2026
40b1706
feat: surface the stored document name from submit_document
rejojer Aug 10, 2026
bf9e6da
fix: add missing page_list kwarg in duplicate-name test mock
rejojer Aug 11, 2026
dece6e6
revert: keep README.md unchanged from main — SDK section deferred
rejojer Aug 11, 2026
3c37cdc
feat: serve cloud agent instructions live from the MCP server
rejojer Aug 11, 2026
50fd618
fix: local relevance sort answers honestly instead of imitating
rejojer Aug 11, 2026
f1301e7
docs: note the cloud+Claude instructions duplication trade-off in as_…
rejojer Aug 11, 2026
20ed81f
fix: unsupported-capability envelopes say local-mode-yet, point to cloud
rejojer Aug 11, 2026
8dc929f
fix: local tool descriptions pre-announce cloud-only capabilities
rejojer Aug 11, 2026
2b929ee
refactor: localized tool guidance replaces the appended LOCAL MODE note
rejojer Aug 11, 2026
e790c37
feat: hide cloud-only parameters from the local tool surface
rejojer Aug 11, 2026
1fa3eb7
fix: incremental-review findings — bridge cache, guards, envelope drift
rejojer Aug 11, 2026
63b767f
chore: keep the demo's doc_id cache file out of the repo
rejojer Aug 11, 2026
6c9fe2e
test: live envelope field-parity guard against cloud response drift
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ __pycache__
.env*
.venv/
logs/
.pageindex/
dist/
*.doc_id
92 changes: 34 additions & 58 deletions examples/agentic_vectorless_rag_demo.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,60 @@
"""
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.

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
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.

The agent tools come straight from the SDK — ``client.as_openai_tools()``
exposes the PageIndex tool contract (browse_documents, get_document,
get_document_structure, get_page_content) and ``client.agent_instructions()``
provides the retrieval playbook, so the whole agent is a few lines. Swap
``PageIndexLocalClient()`` for ``PageIndexCloudClient(api_key=...)`` and the
same code runs against the cloud.

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 "pageindex[openai]"; OPENAI_API_KEY in the environment.
"""
import sys
import json
import asyncio
import concurrent.futures
from pathlib import Path
import requests

sys.path.insert(0, str(Path(__file__).parent.parent))

from agents import Agent, Runner, function_tool, set_tracing_disabled
from agents.model_settings import ModelSettings
from agents import Agent, Runner, set_tracing_disabled
from agents.stream_events import RawResponsesStreamEvent, RunItemStreamEvent
from openai.types.responses import ResponseTextDeltaEvent, ResponseReasoningSummaryTextDeltaEvent

from pageindex import PageIndexClient
from pageindex import PageIndexAPIError, PageIndexLocalClient
import pageindex.utils as utils

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

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

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_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.
"""
DOC_ID_PATH = _EXAMPLES_DIR / "documents" / "attention-residuals.doc_id"
STORAGE_PATH = _EXAMPLES_DIR / ".pageindex"


def query_agent(client: PageIndexClient, doc_id: str, prompt: str, verbose: bool = False) -> str:
def query_agent(client: PageIndexLocalClient, doc_id: str, prompt: 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.
Tool calls are always printed; verbose=True also prints arguments and output previews.
"""

@function_tool
def get_document() -> str:
"""Get document metadata: status, page count, name, and description."""
return 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)

@function_tool
def get_page_content(pages: str) -> str:
"""
Get the text content of specific pages or line numbers.
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)

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

async def _run():
Expand Down Expand Up @@ -152,24 +123,29 @@ 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 = PageIndexLocalClient(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),
None,
)
doc_id = None
if DOC_ID_PATH.exists():
cached = DOC_ID_PATH.read_text().strip()
try:
client.get_document(cached)
doc_id = cached
except PageIndexAPIError:
DOC_ID_PATH.unlink()
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), wait=True)["doc_id"]
DOC_ID_PATH.write_text(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