Commit graph

5 commits

Author SHA1 Message Date
mountain
e18ccdeaf8 fix: prompt guidance referenced fields get_document doesn't return
OPEN_SYSTEM_PROMPT and SCOPED_SYSTEM_PROMPT told the agent to call
get_document(doc_id) "to confirm status and page/line count", but neither
backend returns a page/line count and the local backend has no status field
(get_document returns doc_name/doc_type/doc_description). The agent would
hunt for fields that don't exist, degrading QA. Align both prompts with the
demo's wording ("confirm the document's name and type"). Regression test
asserts the prompts no longer reference the non-existent page/line count.
2026-07-10 10:53:43 +08:00
mountain
b9d021916f fix: prompt-injection delimiter escape, legacy config coercion, gather resilience, true cross-thread concurrency bound
Addresses items 4-8 from the max-effort review of PR #272 (VectifyAI/PageIndex#272).

- agent.py: wrap_with_doc_context() strips '<'/'>' from doc_name/doc_description
  (untrusted: unsanitized filename / LLM-generated from document content) before
  inserting them into the <docs>...</docs> block, so embedded content can never
  form a literal </docs> that closes the delimiter early and escapes the
  untrusted-data boundary SCOPED_SYSTEM_PROMPT relies on. Deterministic
  per-field transform, doesn't touch the (cacheable) static system prompt.

- ConfigLoader.load() (legacy 0.2.x compat) now routes merged overrides through
  IndexConfig before returning, so a legacy 'no' string gets pydantic's bool
  coercion instead of surviving as a truthy non-empty string — page_index_main's
  bare `if opt.if_add_node_summary:` checks (changed from `== 'yes'` elsewhere
  in this PR) were silently inverting caller intent and firing unwanted billed
  LLM calls.

- verify_toc, process_large_node_recursively, tree_parser,
  generate_summaries_for_structure, generate_summaries_for_structure_md: added
  return_exceptions=True to their asyncio.gather calls (llm_completion/
  llm_acompletion raise RuntimeError on retry exhaustion, added earlier in this
  PR), each with a degrade path matching the pattern already used by sibling
  hardened gathers in the same files. One transient LLM failure no longer
  aborts the whole document's indexing.

- _llm_semaphore is now a true process-wide ceiling (threading.Semaphore,
  shared across every thread/event loop) instead of one asyncio.Semaphore per
  event loop -- concurrently indexing N documents on N threads no longer
  multiplies the effective cap by N. A max_concurrency_scope() override is
  layered as a second, nested, per-loop restriction that can only tighten the
  effective cap within the ceiling, never widen past it.

- set_llm_params() mutated a bare process-wide dict with no per-call isolation,
  unlike max_concurrency which already had ContextVar scoping. Added
  llm_params_scope() (mirrors max_concurrency_scope) + IndexConfig.llm_params,
  wired into build_index() the same way max_concurrency already was, so
  concurrent indexing jobs with different llm kwargs don't leak into each
  other.

Adds regression tests for all five. Full suite: 221 passed, 2 skipped (one
pre-existing, unrelated flaky cloud-streaming test intermittently fails on
rerun; confirmed independent of this change).

Claude-Session: https://claude.ai/code/session_01Kx5DgKbhK1N8autqXH8SmS
2026-07-09 11:15:58 +08:00
mountain
956147d864 fix: resolve five P1 defects from the SDK review
- 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
2026-07-07 10:12:51 +08:00
mountain
a47c36a3f5 feat(collection): doc_ids accepts str|list, design cleanups
- 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.
2026-05-15 17:03:17 +08:00
Kylin
c7fe93bb56 feat: add PageIndex SDK with local/cloud dual-mode support (#207) 2026-04-08 20:21:58 +08:00