From 2e606b8870abd16268b431333f2db5c31de12501 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 26 Aug 2026 16:17:48 +0800 Subject: [PATCH] fix: the slots accept any Mapping at runtime, as their annotation admits; eight docstrings stop calling cloud tool scoping server-side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4e9c56c widened index=/chat= to Mapping[str, Any] so the exported TypedDicts pass a checker, but _resolve_index_slot/_resolve_chat_slot still dispatched on isinstance(..., dict): a MappingProxyType or ChainMap was pyright-clean and raised "must be a string or a dict" at construction. The resolvers now narrow on Mapping — the comprehension already copies, so a read-only proxy proves the caller's mapping is never mutated. 4e9c56c corrected three of eleven "scoping is server-side" sites; the remaining eight said the same untrue thing about doc_id on cloud (its tools carry no allowlist — targeting is prompt-level, as the runtime error already explains). Deleted rather than reworded. local_chat.py's module docstring predates own-model chat over the cloud bridge; storage_path's prose now names the PathLike 5e2dc9b typed. Claude-Session: https://claude.ai/code/session_01VQ6mruXZBgw9Hjii8KPbQP --- pageindex/agent_tools.py | 2 +- pageindex/client.py | 29 ++++++++++++----------------- pageindex/local_chat.py | 2 +- tests/test_client.py | 9 +++++++++ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/pageindex/agent_tools.py b/pageindex/agent_tools.py index 1c419bff9..6e5bd4c5a 100644 --- a/pageindex/agent_tools.py +++ b/pageindex/agent_tools.py @@ -1503,7 +1503,7 @@ def _tool_specs(client, include_management: bool = False, doc_ids=None, """(name, description, schema, invoke) per tool, for adapters that take the wire schema verbatim. ``invoke`` returns (envelope_text, is_error). Schemas are copies (frameworks keep the dict by reference). ``doc_ids`` - is the local chat scope; cloud scoping is server-side.""" + is the local chat scope.""" _require_local_scope(client, doc_ids) if getattr(client, "api_key", None): bridge = _cloud_bridge(client, gated=not include_management) diff --git a/pageindex/client.py b/pageindex/client.py index f6b9c77f9..254a2aa94 100644 --- a/pageindex/client.py +++ b/pageindex/client.py @@ -126,7 +126,7 @@ def _resolve_index_slot(index) -> "tuple[_CloudKey, dict[str, Any]]": raise PageIndexAPIError( "index is an empty string — pass a local index model name, " 'or "cloud".') - if isinstance(index, dict): + if isinstance(index, Mapping): # None-valued keys mean "absent", exactly like the flat arguments. conf = {name: value for name, value in index.items() if value is not None} @@ -195,7 +195,7 @@ def _resolve_chat_slot(chat) -> "tuple[Optional[str], dict[str, Any]]": raise PageIndexAPIError( "chat is an empty string — pass a model name, or " '"cloud" for the managed chat.') - if isinstance(chat, dict): + if isinstance(chat, Mapping): # None-valued keys mean "absent", exactly like the flat arguments. conf = {name: value for name, value in chat.items() if value is not None} @@ -308,8 +308,9 @@ class PageIndexClient: ``index_model`` covers this. retrieve_model (str, optional): Legacy name for ``chat_model`` — same meaning everywhere, cloud clients included. - storage_path (str, optional): Local mode only — directory where - indexed documents are stored. Defaults to ``./.pageindex``. + storage_path (str or os.PathLike, optional): Local mode only — + directory where indexed documents are stored. Defaults to + ``./.pageindex``. index_backend (dict, optional): Local mode only — connection overrides for the indexing lane's LLM calls. Keys are LiteLLM's own connection params — ``api_key``, ``api_base``, @@ -1201,8 +1202,7 @@ def agent_tools( the full ``/mcp`` list (upload, delete, ...). doc_id: Local only — restrict the tools to this document ID (or list of IDs), enforced at the tool layer: out-of-scope - lookups return NOT_FOUND. Raises on cloud, where scoping - is server-side. + lookups return NOT_FOUND. Raises on cloud. """ from .agent_tools import build_agent_tools return build_agent_tools(self, include_management, doc_ids=doc_id) @@ -1245,8 +1245,7 @@ def as_openai_tools(self, include_management: bool = False, for server-side tool execution (OpenAI models only). doc_id: Local only — restrict the tools to this document ID (or list of IDs), enforced at the tool layer: out-of-scope - lookups return NOT_FOUND. Raises on cloud, where scoping - is server-side. + lookups return NOT_FOUND. Raises on cloud. """ from .integrations.openai_agents import build_openai_tools return build_openai_tools(self, include_management, hosted, @@ -1297,8 +1296,7 @@ def openai_agent_config( Args: doc_id: Document ID or list of IDs to target, as in ``agent_instructions``. Local: also enforced at the tool - layer, not just prompted. Cloud: prompt-level targeting - (tool scoping is server-side). + layer, not just prompted. Cloud: prompt-level targeting. include_management (bool): Also expose tools that modify the library. model: Backend model name; overrides the local default. Same @@ -1386,8 +1384,7 @@ def as_anthropic_tools(self, include_management: bool = False, sync and async runners each accept only their own flavor. doc_id: Local only — restrict the tools to this document ID (or list of IDs), enforced at the tool layer: out-of-scope - lookups return NOT_FOUND. Raises on cloud, where scoping - is server-side. + lookups return NOT_FOUND. Raises on cloud. """ from .integrations.anthropic_sdk import build_anthropic_tools return build_anthropic_tools(self, include_management, asynchronous, @@ -1428,8 +1425,7 @@ def anthropic_runner_config( default). doc_id: Document ID or list of IDs to target, as in ``agent_instructions``. Local: also enforced at the tool - layer, not just prompted. Cloud: prompt-level targeting - (tool scoping is server-side). + layer, not just prompted. Cloud: prompt-level targeting. include_management (bool): Also expose tools that modify the library. asynchronous (bool): Build async runnables for @@ -1475,7 +1471,7 @@ def as_claude_mcp(self, include_management: bool = False, same way at registration (requires ``claude-agent-sdk``; ``pip install 'pageindex[claude]'``). ``doc_id`` (local only) restricts those tools to that document ID (or list), enforced at - the tool layer; it raises on cloud, where scoping is server-side. + the tool layer; it raises on cloud. ``server_name`` names the in-process server — match it to the key you register the entry under (cloud entries carry no name). @@ -1520,8 +1516,7 @@ def claude_agent_config( Args: doc_id: Document ID or list of IDs to target, as in ``agent_instructions``. Local: also enforced at the tool - layer, not just prompted. Cloud: prompt-level targeting - (tool scoping is server-side). + layer, not just prompted. Cloud: prompt-level targeting. include_management (bool): Also allow tools that modify the library. server_name (str): Key the server is registered under; diff --git a/pageindex/local_chat.py b/pageindex/local_chat.py index b4432a1ed..7f482209e 100644 --- a/pageindex/local_chat.py +++ b/pageindex/local_chat.py @@ -1,4 +1,4 @@ -"""Managed local chat: document-QA agents over the local tools.""" +"""Own-model chat: document-QA agents over the local or cloud agent tools.""" from __future__ import annotations import asyncio diff --git a/tests/test_client.py b/tests/test_client.py index 06860267c..ec7ed845e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -289,6 +289,15 @@ def test_slot_flat_equivalence(tmp_path): assert getattr(flat, attr) == getattr(slot, attr), attr +def test_slots_take_any_mapping(): + """The slots are typed Mapping so the exported TypedDicts pass a + checker; the resolvers must accept what the annotation admits — and + read-only proxies prove they never mutate the caller's mapping.""" + client = PageIndexClient(index=types.MappingProxyType({"api_key": "pi-k"}), + chat=types.MappingProxyType({"model": "m"})) + assert (client.api_key, client.chat_model) == ("pi-k", "m") + + def test_same_side_double_spelling_rejected(): for kwargs in ({"api_key": "k", "index": {"api_key": "k"}}, {"index": "m", "index_model": "m"},