Skip to content

Block citations resolve to bounding boxes: get_block() and resolve_citations() - #503

Merged
rejojer merged 5 commits into
mainfrom
feat/resolve-citations
Sep 13, 2026
Merged

rejojer merged 5 commits into
mainfrom
feat/resolve-citations

Conversation

@rejojer

@rejojer rejojer commented Sep 11, 2026

Copy link
Copy Markdown
Member

What

Two client methods for turning block-level citations into page coordinates:

  • client.get_block(doc_id, block_id) — one layout block as the API returns it (doc_id, page, block_id, bbox, block_type, text), via GET /doc/{doc_id}/block/{block_id}/. bbox is [x0, y0, x1, y1] in thousandths of the page's width and height (0-1000), origin top-left. Cloud-only; local mode raises, because local page content has no blocks.
  • client.resolve_citations(answer, doc_id=None) — parses the <cite doc= page= block=/> tags own-model chat(citations=True) writes and the <doc=…;page=…;block=…> tags the managed chat writes (the cloud parser's expressions, tightened: quotes paired, names and tag bodies stopped at the tag's edge, attribute names anchored at a word boundary, so an unterminated or malformed tag scans in linear time; names and block ids trimmed), deduplicates, maps document names to ids, and merges get_block()'s fields into each block-level entry. Output is the managed chat's citations entry shape plus doc_id and text.

Semantics

  • doc_id — what chat(doc_id=...) took — scopes the name → id lookup; without it the client lists your own library (100 per page, ids deduplicated). doc_id=[] fails loud like chat(doc_id=[]).
  • Two documents under one cited name raise PageIndexAPIError naming both ids — no guessing.
  • A block the document does not have (a model's slip) keeps its entry without bbox; so does a block that cannot be looked up (local mode, a document you cannot read). Every other status re-raises.
  • A document not in the library keeps doc_id: None. The markdown / footnote formats of citation_prompt() are prose and are not parsed.

Depends on

VectifyAI/pageindex-compute PR #607 — the block route — squash-merged into dev as 6842852a and deployed to Dev (Sep 13). Live-verified there against a document submitted with beta_headers=["block_reference"]: get_block() returns the bbox; resolve_citations() merges it on both the library and the doc_id= path, keeps a missing block without a bbox, returns doc_id: None for an unknown name, and resolves the <doc=…;page=…;block=…> tag the managed chat itself wrote (enable_citations=True) to a table block's bbox. Tests here run against fakes.

Tests

11 in tests/test_client.py: parser cases for both tag formats (padded names and block ids trimmed), a linear-time bound on unterminated tags and on a tag whose body is one long word, cloud resolution (found / missing block / unknown document / doc_id scope including a repeated id / argument guards including doc_id=[]), an apostrophe inside a quoted name next to a bare <doc=…> tag, full-library paging, a listing without total that re-serves a document after an upload shifted the window, shared-name refusal, a 403 block kept and a 500 re-raised, request wiring, and local mode's page-level result plus the cloud-only refusal on get_block and the bbox-less block entry. Full suite 619 green, pyright clean.

Docs (chat.mdx / documents.mdx) and the release note follow with the release.

…tations()

resolve_citations(answer, doc_ids=None) reads the <cite doc= page= block=/>
and <doc=…;page=…;block=…> tags of a cited answer and returns one entry per
distinct citation with the document id and, for block-level citations on
cloud documents, the block's page, bbox, type and text from the new
GET /doc/{doc_id}/block/{block_id}/ route, wrapped as get_block(). Local
mode stays page-level; a name shared by two documents raises instead of
guessing; a block the document lacks keeps its entry without a bbox.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-11T12:35:43.170858Z 8d3708e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…kups

_CITE_ATTR_RE pairs its quotes, so an apostrophe inside a quoted name
(Moody's Outlook.pdf) stays in the name instead of ending it.

_OLD_CITATION_RE stops a name at < or >, so a <doc=name> tag without its
own ';' no longer swallows the text up to the next tag's ';' and that
citation with it.

The block lookup keeps its bbox-less entry on a status-less raise (local
mode, where pages have no blocks) and on 403, as doc_targeting_block
does; a transport failure still propagates with its status.

The library listing is _all_documents(), which tolerates an absent
'total' and ends on the empty page; ids are deduplicated before the
collision check, so a document re-served after an upload shifted the
window is one document, not a collision with itself.

doc_ids=[] fails loud like chat(doc_id=[]) instead of resolving nothing.

Docstrings: the bbox coordinate space on both get_block(); the
shared-document promise dropped, since the metadata route is owner-only.
_CITE_TAG_RE's second alternative (the paired form) was unreachable: the
first alternative matches the opening tag whenever a '>' follows, and
without one neither alternative can match. Dropping it, stopping the tag
body at '<' and letting the group absorb the whitespace makes an
unterminated '<cite ' scan linear: '<cite ' plus 4000 spaces went from
2 minutes to 0.1 ms, 99 KB of unterminated tags from 27 minutes to
about a millisecond. Output is identical on every shape tried, the
paired form included.
Two holes of the kinds already closed, left open one line and one loop
away from their siblings.

The old format's block group still crossed tag boundaries the way its
name group used to: "<doc=a.pdf;page=3;block=p3_text_5 <cite doc=..." put
the whole following citation inside block_id and, since a block lookup
that 404s is now kept without a bbox, lost it silently. It stops at the
tag's edge like the name beside it; a block id has never contained < or
>, the server writes p{page}_{type}_{seq}.

The library listing reads entries with .get() like the rest of the
codebase does: _all_documents() tolerates a listing whose shape varies,
so reading its result with raw subscripts put the KeyError back one
level down. An entry without a name or an id cannot answer a citation
either way, so it is skipped.
…like chat()

add() trims both values itself: the name, which each caller used to
trim, and the block id, which neither did. "block=p3_text_5 >" and
block=" p3_text_5 " now look up p3_text_5. A padded id 404s, and a 404
is kept without a bbox, so the padding lost the bbox silently and handed
the padded id back to the caller.

_CITE_ATTR_RE anchors the attribute name at a word boundary. Without
\b the greedy (\w+) restarted at every character of a long word inside
a tag body: '<cite ' + 64 KB of x + '>' cost 19 s on the caller's
thread. Anchored, 64 KB is 1.6 ms and 1 MB 27 ms. Output is identical:
the leftmost match of (\w+)= always begins at a word boundary (0 diffs
on 30,000 random inputs). The linear-time test now reaches this
expression; its inputs had no '>' and never did.

The scope parameter is doc_id, the name chat(), chat_completions() and
document_context() give the same str-or-list argument, and the one the
docstring already used to describe it. The integration builders keep
their doc_ids.
@rejojer
rejojer merged commit a30a805 into main Sep 13, 2026
9 checks passed
@rejojer
rejojer deleted the feat/resolve-citations branch September 13, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant