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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
2 changes: 1 addition & 1 deletion pageindex/agent_tools.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Expand Down
29 changes: 12 additions & 17 deletions pageindex/client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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}
Expand DownExpand Up@@ -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}
Expand DownExpand Up@@ -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``,
Expand DownExpand Up@@ -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)
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -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).

Expand DownExpand Up@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pageindex/local_chat.py
Original file line numberDiff line numberDiff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/test_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"},
Expand Down
Loading