Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 0 additions & 39 deletions pageindex/agent_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,6 @@ def _failure(error: str, details: Optional[dict[str, Any]],


def _dumps(payload: dict[str, Any]) -> str:
# Compact, matching _serialized_size — so the size budget measures
# what is actually emitted.
return json.dumps(payload, ensure_ascii=False)


Expand Down Expand Up @@ -706,9 +704,6 @@ def _browse_documents(client, folder_id: str = "root", recursive: bool = False,
else:
scoped = _scope_documents(_all_documents(client), _allowed_ids)
window, total = scoped[offset:offset + limit], len(scoped)
# Advance by what actually arrived — a server may cap its page size —
# and treat an absent/None total like _all_documents does: a full
# window means there may be more.
window_end = offset + len(window)
has_more = bool(window) and (window_end < total if isinstance(total, int)
else len(window) == limit)
Expand Down Expand Up @@ -865,9 +860,6 @@ def _get_document_structure(client, doc_name: str,
waited and entry.get("status") != "failed")

try:
# Prefer the raw stored tree: its nodes carry start_index/end_index
# like the cloud structure tool, where client.get_tree() drops
# end_index and renames fields.
raw_tree = getattr(getattr(client, "_api", None), "raw_tree", None)
tree = raw_tree(entry["id"]) if raw_tree is not None else None
if tree is None:
Expand Down Expand Up @@ -1044,8 +1036,6 @@ def _get_page_content(client, doc_name: str, pages: str,
if out_of_range:
options.insert(0, f"Document has {max_page} pages total - request "
f"pages 1-{max_page}")
# Additive, not either/or: a call can both truncate for size and have
# out-of-range pages — hiding either would misreport what was returned.
if remaining or out_of_range:
parts = [f"Retrieved {len(included)} of {len(requested)} "
"requested pages."]
Expand Down Expand Up @@ -1091,7 +1081,6 @@ def _remove_document(client, doc_names: list[str],
"options": ["Copy each name verbatim from a browse_documents() "
"response"]},
"INVALID_INPUT")
# A repeated name is one deletion, not a second "failed" row.
doc_names = list(dict.fromkeys(doc_names))
if len(doc_names) > 10:
return _failure("Maximum 10 documents can be deleted at once", None,
Expand Down Expand Up @@ -1216,23 +1205,6 @@ def _tool_docstring(description: str, properties: dict[str, Any]) -> str:
return "\n".join(lines)


# Local guidance layer: schema STRUCTURE stays byte-identical to the cloud
# contract minus the hidden cloud-only parameters, and description strings
# adapt to the local surface the same way AGENT_INSTRUCTIONS does — guidance
# must not teach capabilities (folders, semantic ranking) or tools
# (search_documents, get_document_image) that do not exist here. Guard
# tests pin structure (contract-minus-hidden equality), tool references
# (the dead-reference test), and capability phrases (the per-docstring
# phrase test) — a contract refresh that reintroduces a cloud-only
# reference fails loudly.

#: Cloud-only parameters hidden from the local surface — strict-schema
#: frameworks make the dead-end calls inexpressible, and lenient framework
#: argument models drop them before the call (degrading to the bare call).
#: The call_tool path still answers folder_id/sort/query with the guided
#: error envelope; recursive is simply accepted (flattening a folderless
#: library is the identity). Plain functions reject unknown parameters at
#: the Python call boundary.
_LOCAL_HIDDEN_PARAMS: dict[str, tuple[str, ...]] = {
"browse_documents": ("folder_id", "recursive", "sort", "query"),
"get_document": ("folder_id",),
Expand All @@ -1257,7 +1229,6 @@ def _tool_docstring(description: str, properties: dict[str, Any]) -> str:
'Folder browsing and semantic ranking (sort="relevance") are not '
"supported in local mode yet — they work on PageIndex cloud."
),
# The image sentence points at a tool that is not registered locally.
"get_page_content": TOOL_CONTRACT["get_page_content"]["description"]
.replace(" Embedded image paths in the response feed into "
"`get_document_image()`.", ""),
Expand Down Expand Up @@ -1536,13 +1507,6 @@ def remove_document(doc_names: list[str]) -> str:


# ── agent instructions ──
# Local subset of the cloud MCP server's initialize instructions (its
# no-folders variant), trimmed to what exists here: the search_documents
# escalation steps, get_document_image, and the shared read-only-folders
# block are removed, and the sort="relevance" guidance is replaced with
# name/description matching (semantic ranking is cloud-side). Cloud
# clients receive the server's live instructions instead — see
# _base_instructions().

_INSTRUCTIONS_HEADER = (
"PageIndex by Vectify AI is a document platform for uploading and "
Expand Down Expand Up @@ -1638,9 +1602,6 @@ def doc_targeting_block(client, doc_id, scoped: bool = False) -> Optional[str]:
"and would read the newer one. Rename or remove the "
"duplicate, or pass the newer doc_id."
)
# get_document keeps the cloud detail wire shape, which local mode
# serves without the user's metadata tags; the listing carries them
# in both modes.
by_id = {doc.get("id"): doc for doc in listing}
for one_id, detail in zip(doc_ids, details):
if detail.get("metadata") is None:
Expand Down
30 changes: 18 additions & 12 deletions pageindex/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,11 +442,13 @@ def responses(
Document QA over the OpenAI Responses protocol — the agentic surface.

Local only for now. Drives your backend's /responses end to end (no
translation layer), so the ``output`` carries the whole process as
standard items — messages, function calls, and function outputs
(the SDK executes the tools). Append the returned ``output`` to your
next call's ``input`` verbatim to keep provider prompt-cache prefix
continuity and the agent's memory of what it already read.
translation layer). The envelope is official Responses shape —
``output`` carries the model-produced items and parses with the
openai SDK types — and the whole process transcript (including the
tool outputs the SDK executed) rides in the extra ``items`` field.
Append the returned ``items`` to your next call's ``input`` verbatim
to keep provider prompt-cache prefix continuity and the agent's
memory of what it already read.

Requires ``pageindex[openai]`` and a backend that supports the
Responses API; backends that only speak chat.completions should use
Expand All @@ -457,15 +459,15 @@ def responses(

Args:
input: A user message string, or a list of Responses input items
(round-trip prior ``output`` items here).
(round-trip prior ``items`` here).
model: Backend model name (defaults to ``retrieve_model``).
stream: Yield Responses stream events as dicts — one logical
response per call: per-turn backend lifecycle events are
collapsed, sequence numbers are reassigned monotonically,
and ``output_index`` is re-based onto the single logical
``output``; tool outputs are emitted as
``response.output_item.done`` events and the single final
event is the terminal ``response.*`` for the run's status.
``output``. The single final event is the terminal
``response.*`` for the run's status; its ``response``
carries the tool outputs in ``items``.
doc_id: Document ID or list of IDs to scope the conversation.
Keep it identical across a conversation's calls — the
targeting block it adds is re-set each call and is part
Expand Down Expand Up @@ -635,7 +637,9 @@ def as_openai_tools(self, include_management: bool = False,
Cloud (default): the full live read tool set (search, folders,
images — as enabled for your key) as plain function tools,
discovered from the PageIndex MCP server and executed from your
process — works with any model backend. Pass ``hosted=True`` to
process — works with any model backend. Binary tool results
(e.g. ``get_document_image``) arrive as text placeholder stubs
on this in-process path. Pass ``hosted=True`` to
hand the connection to OpenAI instead: one hosted MCP tool, tool
calls executed server-side (lowest latency; requires an
OpenAI-hosted model on the Responses API). The framework's own
Expand Down Expand Up @@ -740,15 +744,17 @@ def as_anthropic_tools(self, include_management: bool = False,
enabled for your key), discovered from the PageIndex MCP server
and executed from your process; the server's input schemas pass
through verbatim (MCP and the Messages API share the schema
shape). The server-side alternative is the Messages API's beta
shape), and binary tool results (e.g. ``get_document_image``)
arrive as text placeholder stubs on this in-process path. The
server-side alternative is the Messages API's beta
MCP connector — ``mcp_servers=[{"type": "url", "name":
"pageindex", "url": f"{BASE_URL}/mcp?tools=read",
"authorization_token": <your PageIndex API key>}]`` (drop
``?tools=read`` for the full tool set) — with no client-side
tools involved. Local: the in-process tools — the same set
``messages()`` runs internally.

Requires ``anthropic>=0.84.0``
Requires ``anthropic>=0.108.0``
(``pip install 'pageindex[anthropic]'``), imported only when this
method is called.

Expand Down
2 changes: 1 addition & 1 deletion pageindex/integrations/anthropic_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def build_anthropic_tools(client, include_management: bool = False,
except ImportError as exc:
raise PageIndexAPIError(
"as_anthropic_tools requires the Anthropic SDK tool runner "
"(anthropic>=0.84.0) — pip install -U anthropic (or pip install "
"(anthropic>=0.108.0) — pip install -U anthropic (or pip install "
"'pageindex[anthropic]')."
) from exc
from ..agent_tools import _tool_specs
Expand Down
2 changes: 0 additions & 2 deletions pageindex/integrations/claude_agent_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

def build_claude_mcp(client, include_management: bool = False, doc_ids=None):
from ..agent_tools import _require_local_scope
# The cloud branch returns a URL config — reject cloud doc_ids so they
# are never silently dropped.
_require_local_scope(client, doc_ids)
if getattr(client, "api_key", None):
# include_management picks the endpoint — the URL itself is the
Expand Down
2 changes: 0 additions & 2 deletions pageindex/integrations/openai_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ def build_openai_tools(client, include_management: bool = False,
) from exc
from ..agent_tools import (_dumps, _failure, _require_local_scope,
_tool_specs)
# The hosted branch returns before _tool_specs — reject cloud doc_ids
# here so they are never silently dropped.
_require_local_scope(client, doc_ids)
if getattr(client, "api_key", None) and hosted:
# include_management picks the endpoint — the URL itself is the
Expand Down
2 changes: 0 additions & 2 deletions pageindex/local_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ def submit_document(
raise PageIndexAPIError(
"Failed to submit document: PDF has no content. All pages are blank."
)
# Fail before paying for indexing when _1.._99 are all taken; the
# binding name resolution happens again at save.
self._unique_doc_name(os.path.basename(file_path))

try:
Expand Down
Loading
Loading