Drop the footnote citation format; alarm on instruction drift

pageindex-chat removed `footnote` from the cited_answer prompt
(2fc3d298), leaving the SDK advertising a format the cloud now rejects:
`citation_prompt("footnote")` raised a raw ProtocolError against the
cloud while local mode happily returned the frozen copy — the same call,
two behaviours.

Also adds the drift alarm the instructions never had. The tool contract
and the citation prompts each have a live parity test; AGENT_INSTRUCTIONS
only had a non-empty check, so a cloud edit to shared guidance could
leave local agents on stale instructions unnoticed. The new test lets a
cloud line through only when it names a tool or parameter local mode does
not have.

Verified against the live server: 8 live tests pass, 167 offline.
This commit is contained in:
Ray
2026-09-18 01:16:40 +08:00
parent d44799f33f
commit bf0645cc28
3 changed files with 50 additions and 24 deletions
-11
View File
@@ -1652,17 +1652,6 @@ CITATIONS
- When page content includes block_id values, citations MUST be block-level: copy the exact block_id of the supporting block. Page-only cites are allowed ONLY when the tool output carries no block_id (legacy documents, structure outlines). NEVER invent or alter block_id values.
- For a claim drawn from multiple blocks on one page, add one tag per supporting block (at most 3); beyond that, cite the single strongest block.
- Each tag must reference a SINGLE page integer. For multi-page citations, use separate tags.""",
"footnote": """\
GROUNDING
- Answer only from the user's PageIndex documents. Call get_page_content() and state only what was actually read there.
- Never fill a gap from general knowledge. When the documents do not answer the question, say so.
CITATIONS
- Cite only statements supported by tool outputs, as a Markdown footnote: put [^n] immediately after the claim and define it at the end of the answer as [^n]: {docName}, p. {pageNumber} or [^n]: {docName}, p. {pageNumber}, block {blockId}.
- When page content includes block_id values, citations MUST be block-level: copy the exact block_id of the supporting block. Page-only cites are allowed ONLY when the tool output carries no block_id (legacy documents, structure outlines). NEVER invent or alter block_id values.
- For a claim drawn from multiple blocks on one page, add one footnote per supporting block (at most 3); beyond that, cite the single strongest block.
- Each footnote must reference a SINGLE page integer. For multi-page citations, use separate footnotes.
- Number footnotes from 1 in order of first use; reuse a number when the same page and block support another claim. Every marker needs a definition and every definition a marker.""",
}
+7 -7
View File
@@ -2168,11 +2168,11 @@ class PageIndexClient:
``format`` picks how a citation is written: ``"cite"`` (the
``<cite doc= page= block=/>`` tags PageIndex chat writes and
renders — the default), ``"markdown"`` (a bracketed
``[doc, p. N]`` reference, for hosts that strip tags) or
``"footnote"`` (Markdown footnotes); the server rejects any
other value. Local documents: the SDK's frozen copy of the
same prompt (page-level — local page content has no blocks).
renders — the default) or ``"markdown"`` (a bracketed
``[doc, p. N]`` reference, for hosts that strip tags); the
server rejects any other value. Local documents: the SDK's
frozen copy of the same prompt (page-level — local page
content has no blocks).
"""
from .agent_tools import fetch_citation_prompt
return fetch_citation_prompt(self, format or "cite")
@@ -2190,8 +2190,8 @@ class PageIndexClient:
and the block's ``text``. Reads both tag formats PageIndex chat
writes: ``<cite doc= page= block=/>`` (own-model
``chat(citations=True)``) and ``<doc=…;page=…;block=…>`` (the
managed chat). The markdown and footnote formats of
``citation_prompt()`` are prose for readers and are not parsed.
managed chat). The markdown format of
``citation_prompt()`` is prose for readers and is not parsed.
Args:
answer (str): The answer text, tags included.
+43 -6
View File
@@ -2267,6 +2267,43 @@ def test_live_cloud_instructions_nonempty():
assert bridge.instructions()
# Cloud-only capabilities: every instruction line the local copy drops
# names one of these. Whatever local mode gains, drop its marker here.
_CLOUD_ONLY_MARKERS = (
"get_folder_structure", "search_documents", "get_document_image",
"image_path", "folder", "recursive", 'sort="relevance"',
)
# The two lines no marker catches — they exist only because the sections
# above do: the discovery heading and the escalation ladder's closer.
_CLOUD_ONLY_LINES = (
"DOCUMENT DISCOVERY (three-step funnel):",
"Only after ALL five steps have been tried may you conclude the "
"document is not in the library. Do NOT fall back to general "
"knowledge \u2014 if the user's question references their own "
"documents, exhaust every discovery path first.",
)
@pytest.mark.skipif(not LIVE_KEY, reason="PAGEINDEX_API_KEY not set")
def test_live_cloud_instructions_local_parity():
"""Drift alarm for the frozen AGENT_INSTRUCTIONS: every line the cloud
serves and the local copy drops must name a tool or parameter local
mode does not have. A cloud edit to shared guidance lands here instead
of leaving local agents on stale instructions."""
from pageindex.mcp_bridge import McpBridge
bridge = McpBridge("https://api.pageindex.ai/mcp",
{"Authorization": f"Bearer {LIVE_KEY}"})
frozen = set(AGENT_INSTRUCTIONS.split("\n"))
unexplained = [
line for line in bridge.instructions().split("\n")
if line.strip() and line not in frozen
and line not in _CLOUD_ONLY_LINES
and not any(marker in line for marker in _CLOUD_ONLY_MARKERS)
]
assert not unexplained, unexplained
# ── agent_instructions ──
def test_agent_instructions_default(client):
@@ -2391,16 +2428,16 @@ def test_citation_prompt_local_frozen_copy(client):
one text per format, PageIndex chat's cite format by default, only
local tools named."""
from pageindex.agent_tools import LOCAL_CITATION_PROMPTS
assert len(set(LOCAL_CITATION_PROMPTS.values())) == 3
assert len(set(LOCAL_CITATION_PROMPTS.values())) == 2
assert client.citation_prompt() == LOCAL_CITATION_PROMPTS["cite"]
assert client.citation_prompt(format="") == LOCAL_CITATION_PROMPTS["cite"]
for fmt in ("markdown", "cite", "footnote"):
for fmt in ("markdown", "cite"):
text = client.citation_prompt(format=fmt)
assert text == LOCAL_CITATION_PROMPTS[fmt]
assert "CITATIONS" in text and "get_document_image" not in text
named = set(re.findall(r"\b(\w+)\(", text))
assert named and named <= set(tool_names(include_management=True))
with pytest.raises(PageIndexAPIError, match="markdown, cite, footnote"):
with pytest.raises(PageIndexAPIError, match="markdown, cite"):
client.citation_prompt(format="bogus")
@@ -2419,13 +2456,13 @@ def test_live_local_citation_prompts_match_cloud():
@pytest.mark.skipif(not LIVE_KEY, reason="PAGEINDEX_API_KEY not set")
def test_live_cloud_citation_prompt_formats():
"""The real server serves cited_answer in all three formats, each a
"""The real server serves cited_answer in both formats, each a
distinct rendering of the same rules."""
cloud = PageIndexCloudClient(api_key=LIVE_KEY)
texts = {fmt: cloud.citation_prompt(format=fmt)
for fmt in ("markdown", "cite", "footnote")}
for fmt in ("markdown", "cite")}
assert all("CITATIONS" in text for text in texts.values())
assert len(set(texts.values())) == 3
assert len(set(texts.values())) == 2
assert cloud.citation_prompt() == texts["cite"]