feat: agent tools β OpenAI Agents SDK, Claude Agent SDK, and any framework, local & cloud - #393
Open
rejojer wants to merge 6 commits into
Open
feat: agent tools β OpenAI Agents SDK, Claude Agent SDK, and any framework, local & cloud#393rejojer wants to merge 6 commits into
rejojer wants to merge 6 commits into
Conversation
rejojer
force-pushed
the
feat/agent-tools
branch
10 times, most recently
from
August 10, 2026 07:39
3346d30 to
5feb847
Compare
Four new client methods make PageIndex documents available to agent frameworks, in both modes, with the mode decided solely by the client constructor: - agent_tools(): plain functions (browse_documents, get_document, get_document_structure, get_page_content) matching the PageIndex cloud MCP server's tools/list β same names, schemas, descriptions, and JSON response envelopes β so agent prompts port unchanged between the cloud MCP connection and these in-process tools. Tools never raise; errors come back in the same envelope. remove_document ships behind include_management=False. - as_openai_tools(): the same tools wrapped for the OpenAI Agents SDK. - as_claude_mcp(): one mcp_servers entry for the Claude Agent SDK β cloud clients get the remote MCP config (the framework connects to api.pageindex.ai/mcp and discovers the full cloud tool set), local clients get an in-process SDK MCP server. - agent_instructions(doc_id=None): orchestration guidance for the agent's system prompt; doc_id (same shape as chat_completions) appends the target documents. submit_document() gains wait=True: poll get_document status until completed, raise on failed or after 30 minutes β the manual polling loop every cloud caller writes today spins forever on a failed document. Neither framework becomes a dependency: imports happen at call time with actionable errors, and pageindex[openai] / pageindex[claude] extras are floor-only pins. tests/data/cloud_mcp_contract.json freezes the tool contract; a parity test guards against drift. 36 new tests (95 total), plus a live OpenAI Agents SDK run over a seeded local store verifying the structure-first navigation flow end to end.
rejojer
force-pushed
the
feat/agent-tools
branch
from
August 10, 2026 08:09
5feb847 to
4e82306
Compare
β¦mantics - Large-doc next_steps now says structure-first, consistent with tool descriptions and agent instructions - _remove_document fetches document list once instead of per-name - call_tool returns error envelope for unknown names instead of raising - _not_ready_error timed_out flag reflects actual wait outcome - openai_agents.py docstring corrected to match default (FunctionTools) - Removed unused ModelSettings import from demo
β¦data merge - McpBridge reads session/protocol headers under the lock (now RLock: _ensure_initialized posts while holding it). openai-agents runs sync tools on threads and executes parallel tool calls concurrently, so bridge functions genuinely race; a torn read sent a new session id with a stale protocol header. Measured: one session expiry under 8 threads cost 4 initializations before, minimal 2 after. - Session-expiry retry also resets the negotiated protocol version, so the re-handshake carries no stale MCP-Protocol-Version header. - browse_documents time sort pages list_documents natively instead of fetching the whole library to slice one window (relevance still needs the full list for scoring). - _await_completion: a status refetch that nulls out metadata no longer clobbers the listing's copy (setdefault was a no-op on existing None). - Structure tool reads the raw stored tree via a named LocalAPI raw_tree() seam instead of reaching into _api._store internals; drop the redundant deepcopy before _format_structure (store re-reads from disk, formatting builds fresh containers). - Shared pageindex/_version.py replaces _sdk_version duplicated in mcp_bridge and the Claude integration. Left as-is after source verification against the cloud MCP: first-page budget bypass, pageNum falsy-zero, and the page-gap fallback text are letter-for-letter cloud behavior β parity wins over local repair.
Publishing was the only automation touching code: a tag builds and ships to PyPI without ever running a test, and pull requests get no checks at all. This runs pytest on a small matrix β Python 3.10 (the floor) and 3.13, each with and without the agent frameworks installed, so the lazy-import contract (the package must work with neither framework present) is enforced rather than assumed.
β¦lience, contract drift - _parse_page_spec bounds the requested span arithmetically (10k pages) before materializing it; pages="1-1000000000" previously expanded to a billion integers inside the caller's process. - Local submit_document uniquifies document names the way the cloud upload does (taken name -> _1.._99, then reject with the cloud's own message). Same-name duplicates broke name-addressed tools: resolution always picks the newest, so older duplicates were unreachable. - agent_instructions(doc_id=...) now fails loud when the pinned doc's name is shadowed by a newer same-name document (legacy stores predate the rename) β it previews resolution with the same _resolve_document the tools use, so the check cannot drift from actual behavior. - submit_document(wait=True) tolerates transient network errors, not just API errors; a dropped connection at minute 25 of a 30-minute wait no longer kills it. Third strike wraps into PageIndexAPIError per the documented contract. - The live contract-parity test compares full per-param schemas, not just names and descriptions. It immediately caught real drift the shallow check had been passing: the server now emits nullables as anyOf unions and stamps MAX_SAFE_INTEGER maxima on offset/part. Contract and snapshot updated to the served wire form; _annotation_for learned anyOf so bridge signatures stay Optional[str] instead of degrading to Any. Adjudicated, not changed: the allowed_tools wildcard example stays (docstring advice covers scoping; Ray's call), and raw-length response accounting stays (letter-for-letter cloud behavior, parity wins).
Member
Author
Code reviewFound 5 issues:
PageIndex/pageindex/agent_tools.py Lines 580 to 583 in 7d37c3f
PageIndex/pageindex/agent_tools.py Lines 657 to 671 in 7d37c3f
PageIndex/pageindex/agent_tools.py Lines 968 to 976 in 7d37c3f
PageIndex/pageindex/agent_tools.py Lines 594 to 611 in 7d37c3f
PageIndex/pageindex/integrations/claude_agent_sdk.py Lines 15 to 23 in 7d37c3f π€ Generated with Claude Code - If this code review was useful, please react with π. Otherwise, react with π. |
Compute PR #558 makes /doc/ return {"doc_id", "name"} carrying the
post-dedup-rename name. Mirror it end to end: local submit returns the
stored name, the client warns when it differs from the uploaded file
name (read via .get so older cloud servers stay compatible), the local
name-exhaustion check runs before indexing instead of after the LLM
spend, and the demo caches doc_id in a file instead of name-matching β
a renamed document made the name lookup re-index on every run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #389 (the SDK with local mode). Adds an agent integration layer to the client: the PageIndex cloud MCP tool contract, runnable in-process against either mode.
What
One line per framework, identical across local and cloud β the mode is decided by the client constructor alone:
Document QA, end to end
An actual run (local mode, OpenAI Agents SDK, over
examples/documents/q1-fy25-earnings.pdf):This is the reasoning-based retrieval loop working as designed: the agent reads the tree structure first, picks tight page ranges, and answers strictly from tool output with page citations β no vector index, no chunking, and the retrieval "intelligence" is the host agent's own model (the navigation tools themselves make no LLM calls).
Design
browse_documents/get_document/get_document_structure/get_page_content, doc_name-addressed, same input schemas, descriptions, and JSON response envelopes as the hosted MCP server'stools/listβ agent prompts port unchanged between the cloud MCP connection and these in-process tools.tests/data/cloud_mcp_contract.jsonfreezes the contract; a parity test guards drift.search_documents,get_document_image) are not registered, mirroring the server's gating semantics.remove_documentis behindinclude_management=False.as_claude_mcp()hands the framework the remote MCP config,as_openai_tools()serves the full set as plain function tools from your process (any model backend;hosted=Trueopts into a hosted MCP tool with server-side execution on the Responses API, for OpenAI models), andagent_tools()discovers the livetools/listthrough a built-in minimal MCP client and synthesizes one plain function per tool (signatures and docstrings from the server's schemas, calls proxied from your process) β so the plan-gated tool set, including new server-side tools, arrives without an SDK release, and works from any framework or model backend. The default is the compatible path;hosted=Trueis the explicit latency optimization (the framework's ownMCPServerStreamableHttpagainstapi.pageindex.ai/mcpremains the async-native alternative). On a local client, all three serve the in-process contract subset. Because plain functions have no framework permission layer,agent_tools()applies its management gate in both modes: by default only tools the server marks read-only (readOnlyHint) are exposed, andinclude_management=Trueopens the complete list β the same switch that gatesremove_documentlocally.{"error", "errorCode", "next_steps"}envelope the cloud emits, so agent behavior is uniform across frameworks.openai-agents/claude-agent-sdkare imported at call time with actionable errors;pageindex[openai]/pageindex[claude]extras carry floor-only pins.import pageindexand every existing client feature work with neither installed (covered by tests).submit_document(wait=True): pollsget_documentstatus with growing intervals; returns oncompleted, raises onfailedor after 30 minutes. The manual polling loop cloud callers write today spins forever on a failed document; default stayswait=Falseso batch submission is unaffected.agent_instructions(doc_id=None)supplies the retrieval playbook (structure-first over 20 pages, tight page ranges, persistence protocol) for the agent's system prompt;doc_id(str or list, same shape aschat_completions) appends the target documents β in the run above it is what let the agent skip discovery and go straight to the named document.Verification
Optionalso strict schemas don't force values (browse.querywas unusable for time-sort on the cloud+OpenAI path); a server annotation regression can no longer silently zero out the toolset (loud error instead); exec synthesis hardened (fixed internal def name + dict-literal args β tool/param names can no longer recurse or shadow builtins); SSE parsing handles CRLF multi-message bodies; transport failures wrap intoPageIndexAPIErrorper the documented contract;hosted=Truenow routes non-read-only tools through the Responses API approval flow ({"never": {"read_only": true}}) instead of auto-approving everything; localget_document_structureserves the raw stored tree so nodes carrystart_index/end_indexlike the cloud (live-verified shape);openai-agentsfloor raised to>=0.8.0(older versions run sync tools inline on the event loop); tool annotations added to the frozen contract and passed to the Claude in-process server;wait=tolerates transient poll failures; failed documents get a real "processing failed" message instead of "still processing".PAGEINDEX_API_KEYis set) that diffs the frozen contract β descriptions, schemas, required, annotations β against the real server'stools/list: contract parity vs the frozen snapshot, tool behavior against a seeded local store (no LLM calls), framework-missing/-installed behavior both ways,wait=completed/failed/timeout semantics.agent_tools()discovered this key's gated tool set (7 read-only tools;include_management=Trueaddsremove_document; upload tools correctly absent per plan), a realbrowse_documentscall returned the expected envelope, and a live parity check matched the frozen contract letter-for-letter on all shared tools β after catching and fixing one real bug (SSE responses decoded as latin-1 by requests' charset guess; SSE is UTF-8 by spec).examples/agentic_vectorless_rag_demo.pyrewritten to the new API (its inline tool definitions collapse intoclient.as_openai_tools()).Follow-ups (not in this PR): a stdio
pageindex-mcpentry point for non-Python MCP hosts (pageindex[mcp]), and the docs-site agent-integration page.