- SQLite dedup race: add UNIQUE(collection_name, file_hash) and switch
save_document to a plain INSERT. add_document now catches the
IntegrityError from a concurrent add of the same content, cleans up its
managed files, and returns the winning doc_id — instead of two doc_ids
for one file (each having paid for its own LLM indexing).
- PDF image paths: store the absolute path to each extracted image
instead of a path relative to the indexing process's cwd. The
 references broke as soon as a query ran from a different
directory.
- LegacyCloudAPI: URL-encode doc_id / retrieval_id path segments (added
_enc()), matching CloudBackend. An id containing '/', '?', '#' or a
space previously hit the wrong endpoint or produced a malformed URL.
Adds regression tests: UNIQUE enforcement, the add-race resolving to the
winner, absolute image paths, and encoded legacy URLs.
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
- legacy call_llm: open AsyncOpenAI via `async with` so the client (and
its HTTP connection pool) is closed instead of leaked.
- LocalBackend.add_document: fail fast with CollectionNotFoundError when
the collection doesn't exist, before the expensive parse + LLM index
(previously the missing FK only tripped at save time, after paying for
the LLM work). Also raise builtin FileNotFoundError for a missing path
instead of FileTypeError (which now means only "unsupported extension").
- Collection.query(doc_ids=None): the empty-collection guard now always
runs — previously it was skipped once PAGEINDEX_EXPERIMENTAL_MULTIDOC
was set. A single list_documents call serves both the guard and the
multi-doc warning (no separate call just to decide whether to warn).
- CloudBackend.query_stream: on early consumer break / GeneratorExit,
signal the background SSE thread to stop and force-close the response,
so it no longer drains the whole stream in the background.
Adds regression tests for each (client closed, fail-fast on unknown
collection, FileNotFoundError, empty-check under the multidoc env flag,
single list call, early-break thread stop).
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
The new SDK copied the legacy indexing pipeline into pageindex/index/
instead of moving it, leaving two divergent copies of page_index.py /
page_index_md.py / utils.py. They had already drifted (the legacy copy
still compared IndexConfig booleans against 'yes' — a separate fix),
and every pipeline change had to be applied twice.
Make pageindex/index/ the single source of truth (same pattern as the
LegacyCloudAPI shim for the 0.2.x cloud SDK):
- pageindex/index/utils.py absorbs the 27 legacy-only helpers/classes
(get_page_tokens, convert_page_to_int, ConfigLoader, PDF text helpers,
...) so it's the sole utils module. Reconciled the diverged funcs:
kept the modern versions, backported the #331 get_leaf_nodes .get()
fix, and restored remove_fields' max_len parameter (superset).
- index/page_index*.py now import `from .utils import *`;
index/legacy_utils.py (a re-export of the old top-level utils) deleted.
- Top-level page_index.py / page_index_md.py / utils.py become thin
re-export shims that emit PendingDeprecationWarning. The md_to_tree
shim coerces legacy 'yes'/'no' string flags to bool (the canonical
version is boolean-typed).
- ConfigLoader no longer reads the deleted config.yaml; it builds
defaults from IndexConfig (was an unconditional FileNotFoundError).
- __init__.py and retrieve.py import from pageindex.index.* directly so
`import pageindex` does not trip the shims.
Adds tests/test_legacy_shims.py pinning the contract: clean top-level
import doesn't warn, legacy submodule imports warn, symbols still
resolve, shim and canonical share one implementation, the #331 fix and
ConfigLoader-without-yaml both hold, and the md_to_tree coercion works.
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
- AgentRunner.run: offload to a worker-thread event loop when called
from inside a running loop (Jupyter, FastAPI handlers) — mirrors
pipeline._run_async; Runner.run_sync raised RuntimeError there.
- SQLiteStorage: create connections with check_same_thread=False so
close() can actually close connections created by worker threads.
Each thread still gets its own connection via threading.local; with
the default True those closes raised ProgrammingError (silently
swallowed) and leaked every worker connection.
- CloudBackend.query: non-streaming chat completions now use a 300s
timeout and a single attempt. The default 30s ReadTimeout fired
before generation finished and the retry loop re-billed the full
server-side retrieval + generation up to three times. _request gains
retries/timeout overrides; the exhausted-retry path also no longer
sleeps before raising.
- MarkdownParser: content before the first heading (abstract/preamble)
becomes a node instead of being silently dropped and unretrievable;
a file with no headings at all yields a single document node instead
of zero nodes (which pushed an empty page list into the pipeline).
- LegacyCloudAPI.is_retrieval_ready: API failures (revoked key, network
down) now propagate as PageIndexAPIError instead of reading as
"not ready", which turned polling loops into infinite loops.
Adds regression tests for each fix.
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
- local delete_collection: validate the collection name before rmtree.
An unvalidated name like "../.." escaped files_dir and deleted
arbitrary directories (path traversal).
- legacy page_index(): restore the node_id/summary/text/description
enhancements. IndexConfig now carries booleans (pydantic coerces the
legacy 'yes'/'no' strings at the boundary), but page_index_main still
compared `opt.if_add_node_id == 'yes'` — always False — so every
enhancement was silently skipped for legacy-API callers. Conditions
now branch on the booleans, matching pageindex/index/page_index.py.
- LegacyCloudAPI._request: bound every request with a timeout
(30s, 120s read timeout for streamed responses) so a dead connection
can't hang legacy submit/poll/chat callers forever. The legacy
contract tests pinned the missing timeout; updated to pin its
presence instead.
Adds regression tests: path-traversal rejection, 'yes'/'no' -> bool
coercion, and timeout assertions.
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
Fixes the cloud/local contract mismatches from the PR #272 review
(verified against the official API docs — the cloud API has no
folder/collection endpoints publicly, GET /docs supports limit<=100
with offset):
- query_stream: emit a terminal answer_done event with the full answer
(same contract as the local backend); raise CloudAPIError instead of
disguising HTTP errors as answer events; move the initial connect
inside try so a connection failure can no longer strand the consumer
awaiting a sentinel that never arrives
- _request: rewind file objects before retrying so a transient 5xx/429
during upload no longer re-sends an empty multipart body; carry the
HTTP status on CloudAPIError (status_code) and keep the last status
in the max-retries error
- list_documents: paginate with limit/offset instead of a hard-coded
limit=100, so >100-doc collections are no longer silently truncated
(whole-collection queries rely on this list)
- folders: treat only 403/404 as "folders unavailable" (warned via
warnings.warn instead of an invisible logger.warning, matched on
status_code instead of a "403" substring); transient errors now
propagate instead of being permanently cached as folder_id=None
- error taxonomy parity: cloud doc endpoints map HTTP 404 to
DocumentNotFoundError; local get_document raises DocumentNotFoundError
instead of returning {}; local delete_document raises on missing
doc_id instead of silently deleting nothing
- cloud get_document warns that include_text is not supported instead
of silently ignoring it
Adds regression tests for each fix (11 new tests).
Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
list_to_tree() deletes the 'nodes' key from leaf nodes entirely via
clean_node(). Direct access via structure['nodes'] raises KeyError on
these nodes. Using structure.get('nodes') returns None (falsy) safely,
consistent with how 'nodes' is accessed elsewhere in the codebase.
Fixes#330
llm_completion / llm_acompletion (pageindex/utils.py and
pageindex/index/utils.py) set `litellm.drop_params = True` on the litellm
module. litellm is a process-wide singleton, so this leaked into every other
library sharing it (e.g. a host app like OpenKB that exposes its own litellm
config) and could not be turned off.
Replace the hardcoded `temperature=0` + global `drop_params` with a single
PageIndex-owned per-call kwargs mechanism (config._LLM_PARAMS):
- defaults preserve behavior: {"temperature": 0, "drop_params": True};
- passed per call via **get_llm_params(), never writing litellm's globals, so
nothing leaks into other litellm users in the same process;
- externally configurable: pageindex.set_llm_params(drop_params=False,
temperature=1, num_retries=5, ...) or the PAGEINDEX_DROP_PARAMS env shortcut;
- model/messages are reserved (PageIndex supplies them) and rejected.
scripts/e2e_legacy_sdk.py becomes examples/demo_legacy_sdk.py to sit
alongside the other runnable demos (local/cloud/query-modes), and the
README's Runnable examples list now points at it. Docstring command
updated to the new path; the legacy script docstring also calls out
that it exercises the 0.2.x compatibility methods.
The scripts/ directory had no other entries and is removed.
- Collection.query and Backend.query/query_stream accept doc_ids as
str, list[str] or None. Single str is normalized to [str] inside each
backend; bare [] is rejected with ValueError at both layers.
- wrap_with_doc_context wraps the scoped doc list in <docs>...</docs>
and SCOPED_SYSTEM_PROMPT instructs the agent to treat that block as
data, not instructions (defense against prompt injection via
auto-generated doc_description).
- _require_cloud_api now distinguishes api_key="" from api_key=None;
the former gives a targeted error pointing at the empty-string vs
fall-back-to-local situation when legacy SDK methods are called.
- Legacy PageIndexClient.list_documents docstring spells out the
return-shape difference vs collection.list_documents() to flag a
silent migration footgun (paginated dict with id/name keys vs plain
list[dict] with doc_id/doc_name keys).
- Remove dead CloudBackend.get_agent_tools stub (not on the Backend
protocol; only ever returned an empty AgentTools()) and the
SYSTEM_PROMPT alias (OPEN_/SCOPED_SYSTEM_PROMPT are the explicit
names now).
- README quick start and streaming example now pass doc_ids; new
multi-document section shows both str and list forms.
- examples/demo_query_modes.py exercises all five query-mode cases
(single-doc, multi-doc with/without env var, scoped single, scoped
multi) for manual verification.
- get_agent_tools branches on doc_ids:
- scoped (doc_ids=[...]): drops list_documents and hard-enforces a
whitelist on the remaining tools; system prompt switches to
SCOPED_SYSTEM_PROMPT (no list_documents instruction); doc list +
summaries are prepended to the user message via wrap_with_doc_context.
- open (doc_ids=None): unchanged 4-tool agent loop.
- list_documents now exposes doc_description (sqlite + cloud).
- Collection.query emits UserWarning when doc_ids is None and the
collection holds >1 documents; PAGEINDEX_EXPERIMENTAL_MULTIDOC=1
silences it. Single-doc collections skip the warning; empty
collections raise ValueError.
- Agents SDK tracing upload disabled by default (avoids SSL timeouts);
PAGEINDEX_AGENTS_TRACING=1 re-enables it.
- README: new SDK Usage section covering local/cloud quick start,
streaming, multi-doc as experimental, and runnable examples.
pageindex/config.py imports `from pydantic import BaseModel` in production
code, but pyproject.toml only pulled pydantic in transitively via litellm.
A future litellm release could drop or re-pin pydantic and break installs.
Pin to `>=2.5.0,<3.0.0` to match the v2-style BaseModel usage already in the
codebase, and to stay compatible with litellm's own pydantic constraint.
* feat:compatible with Pageindex SDK
* corner cases fixed
* fix: mock behavior of old SDK
* fix: close streaming response and warn on empty api_key
- LegacyCloudAPI: close response in `finally` for both _stream_chat_response
variants so abandoned iterators no longer leak the TCP connection.
- PageIndexClient: emit a warning instead of silently falling back to local
when api_key is the empty string, surfacing typical env-var-unset misconfig.
- FakeResponse: add close()/closed to match the real requests.Response API.
- Add unit coverage for stream close (both paths) and the empty-api_key warning.
- Add scripts/e2e_legacy_sdk.py to smoke-test the legacy SDK contract end-to-end
against api.pageindex.ai.
* chore: mark legacy SDK methods with @deprecated and docstring pointers
- Decorate the 12 PageIndexClient cloud-SDK compat methods with
@typing_extensions.deprecated(..., category=PendingDeprecationWarning):
- IDE/type-checkers render them with a strikethrough hint
- runtime warnings stay silent by default (no spam for existing callers),
surfaceable via `python -W default::PendingDeprecationWarning`
- Add a one-line docstring on each pointing to the Collection-based equivalent.
- Promote typing-extensions to a direct dependency (was transitive via litellm).
---------
Co-authored-by: XinyanZhou <xinyanzhou@XinyanZhoudeMacBook-Pro.local>
Co-authored-by: saccharin98 <xinyanzhou938@gmail.com>
Co-authored-by: mountain <kose2livs@gmail.com>
The cloud backend previously polled tree_resp["retrieval_ready"]
as the ready signal. Empirically this flag is not a reliable
indicator — docs can reach status=="completed" without
retrieval_ready flipping, causing col.add() to wait until the 10
min timeout before giving up on otherwise-successful uploads.
The cloud API's canonical ready signal is status=="completed";
switch the poll to check that instead.
* Consolidate tests/ into examples/documents/
* Add line_count and reorder structure keys
* Lazy-load documents with _meta.json index
* Update demo script and add pre-shipped workspace
* Extract shared helpers for JSON reading and meta entry building
* Add PageIndexClient with retrieve, streaming support and litellm integration
* Add OpenAI agents demo example
* Update README with example agent demo section
* Support separate retrieve_model configuration for index and retrieve
* Integrate litellm for multi-provider LLM support
* recover the default config yaml
* Use litellm.acompletion for native async support
* fix tob
* Rename llm_complete/allm_complete to llm_completion/llm_acompletion, remove unused llm_complete_stream
* Pin litellm to version 1.82.0
* resolve comments
* args from cli is used to overrides config.yaml
* Fix get_page_tokens hardcoded model default
Pass opt.model to get_page_tokens so tokenization respects the
configured model instead of always using gpt-4o-2024-11-20.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove explicit openai dependency from requirements.txt
openai is no longer directly imported; it comes in as a transitive
dependency of litellm. Pinning it explicitly risks version conflicts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Restore openai==1.101.0 pin in requirements.txt
litellm==1.82.0 and openai-agents have conflicting openai version
requirements, but openai==1.101.0 works at runtime for both.
The pin is necessary to prevent litellm from pulling in openai>=2.x
which would break openai-agents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove explicit openai dependency from requirements.txt
openai is not directly used; it comes in as a transitive dependency
of litellm. No openai-agents in this branch so no pin needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix an litellm error log
* resolve comments
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The loop variable `list_index = page_index - start_index` was
overwriting the outer `list_index = incorrect_item['list_index']`,
causing results to be written back to wrong index positions.
Rename the loop variable to `page_list_idx` to avoid shadowing.
Closes#66
Issues are opened by external users who don't have write permissions.
Add allowed_non_write_users: "*" so claude-code-action runs for all
issue authors, not just repo collaborators.
Backfill workflow triggers issue-dedupe via gh workflow run, which
makes the actor github-actions. Add it to allowed_bots so
claude-code-action accepts the trigger.