mirror of
https://github.com/VectifyAI/PageIndex.git
synced 2026-10-02 07:44:37 +08:00
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user