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 = []
|
lines = []
|
||||||
for d in docs:
|
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 ""
|
desc = d.get("doc_description") or ""
|
||||||
if desc:
|
if desc:
|
||||||
line += f" — {_defang_delimiters(desc)}"
|
line += f" — {_defang_delimiters(desc)}"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import requests
|
||||||
from typing import AsyncIterator
|
from typing import AsyncIterator
|
||||||
|
|
||||||
from ..cloud_api import API_BASE # single source of truth for the cloud base URL
|
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
|
from ..events import QueryEvent
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -22,6 +22,13 @@ logger = logging.getLogger(__name__)
|
||||||
_INTERNAL_TOOLS = frozenset({"ToolSearch", "Read", "Grep", "Glob", "Bash", "Edit", "Write"})
|
_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:
|
class CloudBackend:
|
||||||
def __init__(self, api_key: str):
|
def __init__(self, api_key: str):
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
|
|
@ -75,8 +82,10 @@ class CloudBackend:
|
||||||
continue
|
continue
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
body = resp.text[:500] if resp.text else ""
|
body = resp.text[:500] if resp.text else ""
|
||||||
raise CloudAPIError(f"Cloud API error {resp.status_code}: {body}",
|
msg = f"Cloud API error {resp.status_code}: {body}"
|
||||||
status_code=resp.status_code)
|
if resp.status_code == 401:
|
||||||
|
msg += f" — {AUTH_HINT}"
|
||||||
|
raise CloudAPIError(msg, status_code=resp.status_code)
|
||||||
return resp.json() if resp.content else {}
|
return resp.json() if resp.content else {}
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
if attempt == retries - 1:
|
if attempt == retries - 1:
|
||||||
|
|
@ -102,11 +111,21 @@ class CloudBackend:
|
||||||
|
|
||||||
# ── Collection management (mapped to folders) ─────────────────────────
|
# ── 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:
|
def create_collection(self, name: str) -> None:
|
||||||
self._validate_collection_name(name)
|
self._validate_collection_name(name)
|
||||||
try:
|
try:
|
||||||
resp = self._request("POST", "/folder/", json={"name": name})
|
self._folder_id_cache[name] = self._create_folder(name)
|
||||||
self._folder_id_cache[name] = resp.get("folder", {}).get("id")
|
|
||||||
except CloudAPIError as e:
|
except CloudAPIError as e:
|
||||||
if e.status_code in self._FOLDER_UNAVAILABLE:
|
if e.status_code in self._FOLDER_UNAVAILABLE:
|
||||||
self._warn_folder_upgrade()
|
self._warn_folder_upgrade()
|
||||||
|
|
@ -118,12 +137,11 @@ class CloudBackend:
|
||||||
self._validate_collection_name(name)
|
self._validate_collection_name(name)
|
||||||
try:
|
try:
|
||||||
data = self._request("GET", "/folders/")
|
data = self._request("GET", "/folders/")
|
||||||
for folder in data.get("folders", []):
|
for folder in data.get("folders", []) or []:
|
||||||
if folder.get("name") == name:
|
if folder.get("name") == name:
|
||||||
self._folder_id_cache[name] = folder["id"]
|
self._folder_id_cache[name] = folder["id"]
|
||||||
return
|
return
|
||||||
resp = self._request("POST", "/folder/", json={"name": name})
|
self._folder_id_cache[name] = self._create_folder(name)
|
||||||
self._folder_id_cache[name] = resp.get("folder", {}).get("id")
|
|
||||||
except CloudAPIError as e:
|
except CloudAPIError as e:
|
||||||
if e.status_code in self._FOLDER_UNAVAILABLE:
|
if e.status_code in self._FOLDER_UNAVAILABLE:
|
||||||
self._warn_folder_upgrade()
|
self._warn_folder_upgrade()
|
||||||
|
|
@ -148,16 +166,15 @@ class CloudBackend:
|
||||||
self._folder_id_cache[name] = None
|
self._folder_id_cache[name] = None
|
||||||
return None
|
return None
|
||||||
raise
|
raise
|
||||||
for folder in data.get("folders", []):
|
for folder in data.get("folders", []) or []:
|
||||||
if folder.get("name") == name:
|
if folder.get("name") == name:
|
||||||
self._folder_id_cache[name] = folder["id"]
|
self._folder_id_cache[name] = folder["id"]
|
||||||
return folder["id"]
|
return folder["id"]
|
||||||
self._folder_id_cache[name] = None
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def list_collections(self) -> list[str]:
|
def list_collections(self) -> list[str]:
|
||||||
data = self._request("GET", "/folders/")
|
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:
|
def delete_collection(self, name: str) -> None:
|
||||||
folder_id = self._get_folder_id(name)
|
folder_id = self._get_folder_id(name)
|
||||||
|
|
@ -243,23 +260,29 @@ class CloudBackend:
|
||||||
from ..index.utils import parse_pages
|
from ..index.utils import parse_pages
|
||||||
page_nums = set(parse_pages(pages))
|
page_nums = set(parse_pages(pages))
|
||||||
all_pages = resp.get("pages", resp.get("ocr", resp.get("result", [])))
|
all_pages = resp.get("pages", resp.get("ocr", resp.get("result", [])))
|
||||||
if isinstance(all_pages, list):
|
if not isinstance(all_pages, list):
|
||||||
return [
|
return []
|
||||||
{"page": p.get("page", p.get("page_index")),
|
result = []
|
||||||
"content": p.get("content", p.get("markdown", "")),
|
for p in all_pages:
|
||||||
# Cloud OCR pages carry an `images` list (empty on text-only
|
page = _as_int(p.get("page", p.get("page_index")))
|
||||||
# pages). Preserve it — omitting when empty, mirroring the local
|
if page not in page_nums:
|
||||||
# backend — so cloud callers get the same PageContent shape and
|
continue
|
||||||
# the SDK-prompted UI can render figures.
|
entry = {"page": page,
|
||||||
**({"images": p["images"]} if p.get("images") else {})}
|
"content": p.get("content", p.get("markdown", ""))}
|
||||||
for p in all_pages
|
# Cloud OCR pages carry an `images` list (empty on text-only
|
||||||
if p.get("page", p.get("page_index")) in page_nums
|
# pages). Preserve it — omitting when empty, mirroring the local
|
||||||
]
|
# backend — so cloud callers get the same PageContent shape and
|
||||||
return []
|
# the SDK-prompted UI can render figures.
|
||||||
|
if p.get("images"):
|
||||||
|
entry["images"] = p["images"]
|
||||||
|
result.append(entry)
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_tree(nodes: list) -> list:
|
def _normalize_tree(nodes: list | None) -> list:
|
||||||
"""Normalize cloud tree nodes to match local schema."""
|
"""Normalize cloud tree nodes to match local schema."""
|
||||||
|
if not nodes:
|
||||||
|
return []
|
||||||
result = []
|
result = []
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
normalized = {
|
normalized = {
|
||||||
|
|
@ -290,7 +313,7 @@ class CloudBackend:
|
||||||
if folder_id:
|
if folder_id:
|
||||||
params["folder_id"] = folder_id
|
params["folder_id"] = folder_id
|
||||||
data = self._request("GET", "/docs/", params=params)
|
data = self._request("GET", "/docs/", params=params)
|
||||||
batch = data.get("documents", [])
|
batch = data.get("documents", []) or []
|
||||||
docs.extend(
|
docs.extend(
|
||||||
{
|
{
|
||||||
"doc_id": d.get("id", ""),
|
"doc_id": d.get("id", ""),
|
||||||
|
|
@ -405,10 +428,10 @@ class CloudBackend:
|
||||||
return
|
return
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
body = resp.text[:500] if resp.text else ""
|
body = resp.text[:500] if resp.text else ""
|
||||||
raise CloudAPIError(
|
msg = f"Cloud streaming error {resp.status_code}: {body}"
|
||||||
f"Cloud streaming error {resp.status_code}: {body}",
|
if resp.status_code == 401:
|
||||||
status_code=resp.status_code,
|
msg += f" — {AUTH_HINT}"
|
||||||
)
|
raise CloudAPIError(msg, status_code=resp.status_code)
|
||||||
|
|
||||||
current_tool_name = None
|
current_tool_name = None
|
||||||
current_tool_args: list[str] = []
|
current_tool_args: list[str] = []
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from typing import Any, Iterator
|
||||||
|
|
||||||
import requests
|
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
|
# 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
|
# 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
|
raise PageIndexAPIError(f"{error_prefix}: {e}") from e
|
||||||
|
|
||||||
if response.status_code != 200:
|
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
|
return response
|
||||||
|
|
||||||
def submit_document(
|
def submit_document(
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,10 @@ class FileTypeError(PageIndexError, ValueError):
|
||||||
instead of) a bare ValueError one.
|
instead of) a bare ValueError one.
|
||||||
"""
|
"""
|
||||||
pass
|
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):
|
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 = ""
|
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]
|
text += page_list[page_num][0]
|
||||||
return text
|
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):
|
def get_text_of_pdf_pages(pdf_pages, start_page, end_page):
|
||||||
text = ""
|
return _get_text_of_pages(pdf_pages, start_page, end_page)
|
||||||
for page_num in range(start_page-1, end_page):
|
|
||||||
text += pdf_pages[page_num][0]
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def get_text_of_pdf_pages_with_labels(pdf_pages, start_page, end_page):
|
def get_text_of_pdf_pages_with_labels(pdf_pages, start_page, end_page):
|
||||||
text = ""
|
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"
|
text += f"<physical_index_{page_num+1}>\n{pdf_pages[page_num][0]}\n<physical_index_{page_num+1}>\n"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue