mirror of
https://github.com/VectifyAI/PageIndex.git
synced 2026-07-15 21:11:05 +02:00
fix: address max-effort review findings
- clamp LLM-derived page indices in _get_text_of_pages and get_text_of_pdf_pages_with_labels; dedupe get_text_of_pdf_pages - guard _normalize_tree and folders/documents iterations against explicit nulls in cloud API responses - coerce cloud OCR page numbers to int before filtering in get_page_content - folder cache: raise on missing folder id instead of caching None; stop caching name-not-found so later lookups can succeed - defang doc_id in agent doc-context prompt - append api-key hint to 401 errors (request, legacy and streaming paths) - remove stale legacy JSON workspace sample data unreadable by the SQLite storage
This commit is contained in:
parent
9ad54122bb
commit
3ff04d501a
7 changed files with 70 additions and 323 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"12345678-abcd-4321-abcd-123456789abc": {
|
||||
"type": "pdf",
|
||||
"doc_name": "attention-residuals.pdf",
|
||||
"doc_description": "This document introduces \"Attention Residuals\" (AttnRes) and its scalable variant \"Block AttnRes,\" novel mechanisms for replacing fixed residual accumulation in neural networks with learned, input-dependent depth-wise attention, addressing limitations of standard residual connections while optimizing memory, computation, and scalability for large-scale training and inference.",
|
||||
"page_count": 21,
|
||||
"path": "../documents/attention-residuals.pdf"
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ def wrap_with_doc_context(docs: list[dict], question: str) -> str:
|
|||
"""
|
||||
lines = []
|
||||
for d in docs:
|
||||
line = f"- {d['doc_id']}: {_defang_delimiters(d.get('doc_name', ''))}"
|
||||
line = f"- {_defang_delimiters(str(d['doc_id']))}: {_defang_delimiters(d.get('doc_name', ''))}"
|
||||
desc = d.get("doc_description") or ""
|
||||
if desc:
|
||||
line += f" — {_defang_delimiters(desc)}"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import requests
|
|||
from typing import AsyncIterator
|
||||
|
||||
from ..cloud_api import API_BASE # single source of truth for the cloud base URL
|
||||
from ..errors import CloudAPIError, DocumentNotFoundError, PageIndexError
|
||||
from ..errors import AUTH_HINT, CloudAPIError, DocumentNotFoundError, PageIndexError
|
||||
from ..events import QueryEvent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -22,6 +22,13 @@ logger = logging.getLogger(__name__)
|
|||
_INTERNAL_TOOLS = frozenset({"ToolSearch", "Read", "Grep", "Glob", "Bash", "Edit", "Write"})
|
||||
|
||||
|
||||
def _as_int(value):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
class CloudBackend:
|
||||
def __init__(self, api_key: str):
|
||||
self._api_key = api_key
|
||||
|
|
@ -75,8 +82,10 @@ class CloudBackend:
|
|||
continue
|
||||
if resp.status_code != 200:
|
||||
body = resp.text[:500] if resp.text else ""
|
||||
raise CloudAPIError(f"Cloud API error {resp.status_code}: {body}",
|
||||
status_code=resp.status_code)
|
||||
msg = f"Cloud API error {resp.status_code}: {body}"
|
||||
if resp.status_code == 401:
|
||||
msg += f" — {AUTH_HINT}"
|
||||
raise CloudAPIError(msg, status_code=resp.status_code)
|
||||
return resp.json() if resp.content else {}
|
||||
except requests.RequestException as e:
|
||||
if attempt == retries - 1:
|
||||
|
|
@ -102,11 +111,21 @@ class CloudBackend:
|
|||
|
||||
# ── Collection management (mapped to folders) ─────────────────────────
|
||||
|
||||
def _create_folder(self, name: str) -> str:
|
||||
"""POST /folder/ and return the new folder id, never a falsy value."""
|
||||
resp = self._request("POST", "/folder/", json={"name": name})
|
||||
folder_id = resp.get("folder", {}).get("id")
|
||||
if not folder_id:
|
||||
raise PageIndexError(
|
||||
f"Cloud API returned no folder id when creating {name!r} "
|
||||
f"(response keys: {list(resp)})"
|
||||
)
|
||||
return folder_id
|
||||
|
||||
def create_collection(self, name: str) -> None:
|
||||
self._validate_collection_name(name)
|
||||
try:
|
||||
resp = self._request("POST", "/folder/", json={"name": name})
|
||||
self._folder_id_cache[name] = resp.get("folder", {}).get("id")
|
||||
self._folder_id_cache[name] = self._create_folder(name)
|
||||
except CloudAPIError as e:
|
||||
if e.status_code in self._FOLDER_UNAVAILABLE:
|
||||
self._warn_folder_upgrade()
|
||||
|
|
@ -118,12 +137,11 @@ class CloudBackend:
|
|||
self._validate_collection_name(name)
|
||||
try:
|
||||
data = self._request("GET", "/folders/")
|
||||
for folder in data.get("folders", []):
|
||||
for folder in data.get("folders", []) or []:
|
||||
if folder.get("name") == name:
|
||||
self._folder_id_cache[name] = folder["id"]
|
||||
return
|
||||
resp = self._request("POST", "/folder/", json={"name": name})
|
||||
self._folder_id_cache[name] = resp.get("folder", {}).get("id")
|
||||
self._folder_id_cache[name] = self._create_folder(name)
|
||||
except CloudAPIError as e:
|
||||
if e.status_code in self._FOLDER_UNAVAILABLE:
|
||||
self._warn_folder_upgrade()
|
||||
|
|
@ -148,16 +166,15 @@ class CloudBackend:
|
|||
self._folder_id_cache[name] = None
|
||||
return None
|
||||
raise
|
||||
for folder in data.get("folders", []):
|
||||
for folder in data.get("folders", []) or []:
|
||||
if folder.get("name") == name:
|
||||
self._folder_id_cache[name] = folder["id"]
|
||||
return folder["id"]
|
||||
self._folder_id_cache[name] = None
|
||||
return None
|
||||
|
||||
def list_collections(self) -> list[str]:
|
||||
data = self._request("GET", "/folders/")
|
||||
return [f["name"] for f in data.get("folders", [])]
|
||||
return [f["name"] for f in data.get("folders", []) or []]
|
||||
|
||||
def delete_collection(self, name: str) -> None:
|
||||
folder_id = self._get_folder_id(name)
|
||||
|
|
@ -243,23 +260,29 @@ class CloudBackend:
|
|||
from ..index.utils import parse_pages
|
||||
page_nums = set(parse_pages(pages))
|
||||
all_pages = resp.get("pages", resp.get("ocr", resp.get("result", [])))
|
||||
if isinstance(all_pages, list):
|
||||
return [
|
||||
{"page": p.get("page", p.get("page_index")),
|
||||
"content": p.get("content", p.get("markdown", "")),
|
||||
# Cloud OCR pages carry an `images` list (empty on text-only
|
||||
# pages). Preserve it — omitting when empty, mirroring the local
|
||||
# backend — so cloud callers get the same PageContent shape and
|
||||
# the SDK-prompted UI can render figures.
|
||||
**({"images": p["images"]} if p.get("images") else {})}
|
||||
for p in all_pages
|
||||
if p.get("page", p.get("page_index")) in page_nums
|
||||
]
|
||||
return []
|
||||
if not isinstance(all_pages, list):
|
||||
return []
|
||||
result = []
|
||||
for p in all_pages:
|
||||
page = _as_int(p.get("page", p.get("page_index")))
|
||||
if page not in page_nums:
|
||||
continue
|
||||
entry = {"page": page,
|
||||
"content": p.get("content", p.get("markdown", ""))}
|
||||
# Cloud OCR pages carry an `images` list (empty on text-only
|
||||
# pages). Preserve it — omitting when empty, mirroring the local
|
||||
# backend — so cloud callers get the same PageContent shape and
|
||||
# the SDK-prompted UI can render figures.
|
||||
if p.get("images"):
|
||||
entry["images"] = p["images"]
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _normalize_tree(nodes: list) -> list:
|
||||
def _normalize_tree(nodes: list | None) -> list:
|
||||
"""Normalize cloud tree nodes to match local schema."""
|
||||
if not nodes:
|
||||
return []
|
||||
result = []
|
||||
for node in nodes:
|
||||
normalized = {
|
||||
|
|
@ -290,7 +313,7 @@ class CloudBackend:
|
|||
if folder_id:
|
||||
params["folder_id"] = folder_id
|
||||
data = self._request("GET", "/docs/", params=params)
|
||||
batch = data.get("documents", [])
|
||||
batch = data.get("documents", []) or []
|
||||
docs.extend(
|
||||
{
|
||||
"doc_id": d.get("id", ""),
|
||||
|
|
@ -405,10 +428,10 @@ class CloudBackend:
|
|||
return
|
||||
if resp.status_code != 200:
|
||||
body = resp.text[:500] if resp.text else ""
|
||||
raise CloudAPIError(
|
||||
f"Cloud streaming error {resp.status_code}: {body}",
|
||||
status_code=resp.status_code,
|
||||
)
|
||||
msg = f"Cloud streaming error {resp.status_code}: {body}"
|
||||
if resp.status_code == 401:
|
||||
msg += f" — {AUTH_HINT}"
|
||||
raise CloudAPIError(msg, status_code=resp.status_code)
|
||||
|
||||
current_tool_name = None
|
||||
current_tool_args: list[str] = []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from typing import Any, Iterator
|
|||
|
||||
import requests
|
||||
|
||||
from .errors import PageIndexAPIError
|
||||
from .errors import AUTH_HINT, PageIndexAPIError
|
||||
|
||||
# Single source of truth for the cloud API base URL — imported by the modern
|
||||
# CloudBackend (as API_BASE) and PageIndexClient so a staging/migration change
|
||||
|
|
@ -47,7 +47,10 @@ class LegacyCloudAPI:
|
|||
raise PageIndexAPIError(f"{error_prefix}: {e}") from e
|
||||
|
||||
if response.status_code != 200:
|
||||
raise PageIndexAPIError(f"{error_prefix}: {response.text}")
|
||||
msg = f"{error_prefix}: {response.text}"
|
||||
if response.status_code == 401:
|
||||
msg += f" — {AUTH_HINT}"
|
||||
raise PageIndexAPIError(msg)
|
||||
return response
|
||||
|
||||
def submit_document(
|
||||
|
|
|
|||
|
|
@ -55,3 +55,10 @@ class FileTypeError(PageIndexError, ValueError):
|
|||
instead of) a bare ValueError one.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
AUTH_HINT = (
|
||||
"api_key must be a PageIndex cloud API key (https://dash.pageindex.ai/api-keys). "
|
||||
"For local mode, omit api_key and set your LLM provider key "
|
||||
"(e.g. OPENAI_API_KEY) in the environment."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -521,9 +521,9 @@ def create_clean_structure_for_description(structure):
|
|||
|
||||
|
||||
def _get_text_of_pages(page_list, start_page, end_page):
|
||||
"""Concatenate text from page_list for pages [start_page, end_page] (1-indexed)."""
|
||||
"""Concatenate text from page_list for pages [start_page, end_page] (1-indexed), clamped to the valid page range."""
|
||||
text = ""
|
||||
for page_num in range(start_page - 1, end_page):
|
||||
for page_num in range(max(start_page, 1) - 1, min(end_page, len(page_list))):
|
||||
text += page_list[page_num][0]
|
||||
return text
|
||||
|
||||
|
|
@ -831,15 +831,12 @@ def get_page_tokens(pdf_path, model=None, pdf_parser="PyPDF2"):
|
|||
|
||||
|
||||
def get_text_of_pdf_pages(pdf_pages, start_page, end_page):
|
||||
text = ""
|
||||
for page_num in range(start_page-1, end_page):
|
||||
text += pdf_pages[page_num][0]
|
||||
return text
|
||||
return _get_text_of_pages(pdf_pages, start_page, end_page)
|
||||
|
||||
|
||||
def get_text_of_pdf_pages_with_labels(pdf_pages, start_page, end_page):
|
||||
text = ""
|
||||
for page_num in range(start_page-1, end_page):
|
||||
for page_num in range(max(start_page, 1) - 1, min(end_page, len(pdf_pages))):
|
||||
text += f"<physical_index_{page_num+1}>\n{pdf_pages[page_num][0]}\n<physical_index_{page_num+1}>\n"
|
||||
return text
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue