fix(pifs): route agent retrieval through browse

This commit is contained in:
BukeLy 2026-05-31 17:40:47 +08:00
parent 27071cb7f5
commit 95e5717ba4
3 changed files with 146 additions and 53 deletions

View file

@ -4,8 +4,8 @@ PageIndex FileSystem (PIFS) agent demo.
This mirrors examples/agentic_vectorless_rag_demo.py, but exposes a corpus This mirrors examples/agentic_vectorless_rag_demo.py, but exposes a corpus
through the PageIndex FileSystem shell instead of direct PageIndex document through the PageIndex FileSystem shell instead of direct PageIndex document
tools. The agent receives one read-only bash-like PIFS tool and must retrieve tools. The agent receives one read-only bash-like PIFS tool and must retrieve
evidence through commands such as ls, tree, find, grep, browse, evidence through commands such as ls, tree, browse, find, grep, cat <path>
cat <path> --structure, cat <path> --page, and cat <path> --node. --structure, cat <path> --page, and cat <path> --node.
The demo registers supported files under examples/documents. When a matching The demo registers supported files under examples/documents. When a matching
examples/documents/results/*_structure.json file exists, it is loaded into the examples/documents/results/*_structure.json file exists, it is loaded into the
@ -72,9 +72,15 @@ Retrieval strategy:
or stable file_ref/document ids. Do not invent temporary ref_N aliases. or stable file_ref/document ids. Do not invent temporary ref_N aliases.
- Folder paths such as /documents are positional command targets; do not put - Folder paths such as /documents are positional command targets; do not put
folder paths inside --where. folder paths inside --where.
- Use browse when available to find likely documents by semantic relevance. - After choosing a folder, use browse with a required quoted query to find
Quote multi-word queries and include a path, for example: likely files, for example:
browse /documents "Federal Reserve supervision regulation" browse /documents "Federal Reserve supervision regulation"
- If the folder is uncertain, use recursive browse from a structural parent,
for example:
browse -R /documents "Federal Reserve supervision regulation"
- browse returns file candidates only; it is not folder semantic recall.
- After browse returns candidates, verify evidence with grep, cat <path>
--structure, cat <path> --node, or cat <path> --page before answering.
- Use find --where only with JSON metadata DSL, for example: - Use find --where only with JSON metadata DSL, for example:
find /documents --where '{"file_format":"pdf"}' find /documents --where '{"file_format":"pdf"}'
- Use grep -R only for lexical evidence; do not treat semantic candidates as - Use grep -R only for lexical evidence; do not treat semantic candidates as
@ -643,14 +649,14 @@ def run_smoke_commands(
) )
command = 'browse /documents "Federal Reserve annual report supervision regulation section page range"' command = 'browse /documents "Federal Reserve annual report supervision regulation section page range"'
summary = execute_json_command(json_executor, command) browse = execute_json_command(json_executor, command)
summary_hits = ((summary.get("data") or {}).get("data") or []) browse_hits = ((browse.get("data") or {}).get("data") or [])
if summary_hits: if browse_hits:
summary_result = f"{len(summary_hits)} browse candidates; top={summary_hits[0].get('external_id')}" summary_result = f"{len(browse_hits)} browse candidates; top={browse_hits[0].get('external_id')}"
else: else:
summary_result = "browse is available, but this tiny two-doc demo returned no candidates" summary_result = "browse is available, but this tiny two-doc demo returned no candidates"
show_capability( show_capability(
label="Semantic browse", label="Relevance browse",
command=command, command=command,
result=summary_result, result=summary_result,
raw=shell_executor.execute(command) if verbose else "", raw=shell_executor.execute(command) if verbose else "",

View file

@ -35,17 +35,19 @@ document contents in the workspace.
If the user asks what tools or capabilities you have, describe only the PIFS If the user asks what tools or capabilities you have, describe only the PIFS
virtual shell capabilities available inside this workspace: ls, tree, find, virtual shell capabilities available inside this workspace: ls, tree, find,
stat, grep, cat, and browse. Do not mention host runtime tools, SDK internals, stat, grep, cat, and browse when they are available. Do not mention host
or orchestration helpers that are not part of the PIFS shell. runtime tools, SDK internals, or orchestration helpers that are not part of the
PIFS shell.
If the user asks a workspace-related topic question without naming a specific If the user asks a workspace-related topic question without naming a specific
file, treat it as a retrieval task. Use available PIFS discovery commands to file, treat it as a retrieval task. Start with ls or tree to understand the
look for relevant files and inspect evidence before answering. Ask the user to folder structure, choose a folder, then use browse with the user's topic as the
clarify only after a reasonable search cannot identify relevant evidence. query to find candidate files. Inspect evidence before answering. Ask the user
to clarify only after a reasonable search cannot identify relevant evidence.
Do not conclude that no relevant document exists from one failed grep. If grep Do not conclude that no relevant document exists from one failed grep. If grep
returns no matches for a workspace topic, verify with available semantic returns no matches for a workspace topic, use browse on a relevant folder or
candidate discovery through browse, or inspect likely document structure, inspect likely document structure before saying that the workspace lacks
before saying that the workspace lacks evidence. evidence.
Follow the task prompt for command policy, retrieval strategy, and answer Follow the task prompt for command policy, retrieval strategy, and answer
format. If the caller needs stricter behavior, pass an explicit system_prompt. format. If the caller needs stricter behavior, pass an explicit system_prompt.
@ -54,25 +56,24 @@ format. If the caller needs stricter behavior, pass an explicit system_prompt.
BASH_TOOL_DESCRIPTION = """ BASH_TOOL_DESCRIPTION = """
Run a command in the PageIndex FileSystem virtual shell. This is not a real Run a command in the PageIndex FileSystem virtual shell. This is not a real
operating-system shell. By default the tool is read-only: use ls, tree, find, operating-system shell. By default the tool is read-only: use ls, tree, find,
grep, cat, stat, head, tail, sed, and browse as described in the workspace grep, cat, stat, head, tail, sed, and browse when listed in the workspace
context. grep -R is lexical evidence search; context. grep -R is lexical evidence search; grep does not support regex
grep does not support regex alternation such as "a|b"; run multiple grep alternation such as "a|b"; run multiple grep commands or use browse for
commands or use browse for semantic candidate discovery instead. browse returns relevance-ranked file discovery instead. Start broad workspace questions with
candidate documents ranked by relevance and does not guarantee literal text ls or tree to understand folders. After choosing a folder, use positional
matches or final answer evidence. After choosing a likely browse candidate, browse syntax with a quoted query, for example:
verify the relevant claim with cat before answering. Use browse when the user browse /documents "Federal Reserve". If the relevant folder is uncertain, use
asks for summary search, semantic search, or vector search and the command is browse -R /documents "Federal Reserve" to retrieve file candidates across that
listed as available. Quote multi-word semantic queries, for example: folder tree. browse returns file candidates only; it does not perform folder
browse /documents "Federal Reserve". Do not write semantic recall and does not guarantee final answer evidence. After choosing a
browse /documents Federal Reserve. Errors are returned as text prefixed with likely browse candidate, verify the relevant claim with cat or grep before
ERROR. Do not call answering. Errors are returned as text prefixed with ERROR. Do not call commands
commands that are not listed as available. When evidence is required, inspect it that are not listed as available. When evidence is required, inspect it with cat
with cat or grep before answering. Prefer shell-like target-first cat syntax or grep before answering. Prefer shell-like target-first cat syntax with stable
with stable targets: cat <path> --structure, cat <path> --page 31-59, and targets: cat <path> --structure, cat <path> --page 31-59, and cat <path> --node
cat <path> --node 0009. You may also use file_ref or document_id when a path is 0009. You may also use file_ref or document_id when a path is ambiguous. Do not reconstruct paths from document titles; use exact targets returned by PIFS
ambiguous. Do not reconstruct paths from document titles; use exact targets commands and quote paths containing spaces. After structure identifies a
returned by PIFS commands and quote paths containing spaces. After structure relevant section node, prefer
identifies a relevant section node, prefer
cat <path> --node <node_id>; use cat <path> --page <range> when the user asks cat <path> --node <node_id>; use cat <path> --page <range> when the user asks
for page-level evidence, no suitable node exists, or exact page text is needed. for page-level evidence, no suitable node exists, or exact page text is needed.
cat <path> --structure is paginated; request more with --offset if needed. Page cat <path> --structure is paginated; request more with --offset if needed. Page
@ -83,8 +84,8 @@ continue with another chunk before answering.
For questions about metadata fields, available summaries, or whether metadata For questions about metadata fields, available summaries, or whether metadata
was provided, inspect stat --schema and stat <target> before making claims. was provided, inspect stat --schema and stat <target> before making claims.
Do not use stat as a general content/topic discovery step. For document Q&A, Do not use stat as a general content/topic discovery step. For document Q&A,
prefer ls/tree to choose a folder, browse/find/grep for candidates, then cat --structure and prefer ls/tree for folder selection, browse for file candidates, then cat
cat --node or cat --page for evidence. --structure and cat --node or cat --page for evidence.
""" """
AGENT_TOOL_POLICY = """ AGENT_TOOL_POLICY = """
@ -94,12 +95,16 @@ Tool policy:
- Use only commands listed in the workspace capabilities. - Use only commands listed in the workspace capabilities.
- Folder paths such as /documents are positional command targets; never put folder paths in --where. - Folder paths such as /documents are positional command targets; never put folder paths in --where.
- Use --where only with metadata fields shown by stat --schema. - Use --where only with metadata fields shown by stat --schema.
- Start with ls or tree to understand workspace and folder structure before semantic file retrieval.
- After choosing a folder, use browse <folder> "<query>" for relevance-ranked file candidates; quote multi-word queries, for example browse /documents "Federal Reserve".
- If the relevant folder is uncertain, use browse -R <folder> "<query>" to search recursively from a structural parent folder.
- browse returns file candidates only; Do not use browse as folder semantic recall.
- browse candidates are not final evidence. After selecting candidates, verify the relevant facts with cat or grep before making source-backed claims.
- grep -R performs lexical evidence search. - grep -R performs lexical evidence search.
- grep does not support regex alternation such as "a|b"; run separate grep commands or use browse for semantic candidate discovery. - grep does not support regex alternation such as "a|b"; run separate grep commands or use browse for relevance-ranked file discovery.
- browse is the semantic candidate-discovery tool and does not guarantee literal text matches or final answer evidence. After selecting a likely browse candidate, verify the relevant facts with cat before answering.
- Do not use find | grep as an exhaustive search or as proof that no document exists; find output can be scoped or limited. Use metadata filters, browse, grep on a narrowed target, or cat on likely candidates instead. - Do not use find | grep as an exhaustive search or as proof that no document exists; find output can be scoped or limited. Use metadata filters, browse, grep on a narrowed target, or cat on likely candidates instead.
- A single failed grep is not enough evidence to say there is no relevant document. If grep returns no matches for a workspace-topic question, verify with browse or inspect likely document structure, before answering no-evidence. - A single failed grep is not enough evidence to say there is no relevant document. If grep returns no matches for a workspace-topic question, verify with browse on a relevant folder or inspect likely document structure before answering no-evidence.
- If the user asks for summary search, semantic search, vector search, or "用 summary 搜", use browse <folder> "<query>"; quote multi-word queries, for example browse /documents "Federal Reserve"; use browse -R <folder> when the folder choice is uncertain; do not translate that request into find --where. - If the user asks for summary search, semantic search, vector search, or "用 summary 搜", use browse <folder> "<query>" with the default summary space; do not translate that request into find --where.
- Tool errors are returned as ERROR text; recover by trying an available command. - Tool errors are returned as ERROR text; recover by trying an available command.
- Use cat or grep to gather evidence before making source-backed claims. - Use cat or grep to gather evidence before making source-backed claims.
- Do not reconstruct a file path from a title. Use exact paths returned by PIFS commands, or use file_ref/document_id when available; quote paths that contain spaces. - Do not reconstruct a file path from a title. Use exact paths returned by PIFS commands, or use file_ref/document_id when available; quote paths that contain spaces.
@ -119,6 +124,15 @@ Tool policy:
- Distinguish default/register metadata from caller-provided custom metadata when the evidence supports it. - Distinguish default/register metadata from caller-provided custom metadata when the evidence supports it.
""" """
LEGACY_SEMANTIC_COMMAND_SURFACE_TERMS = (
"search-summary",
"search-entity",
"search-relation",
"semantic-grep",
"find --name",
"find --relation",
)
STREAM_MODE_ALIASES = { STREAM_MODE_ALIASES = {
"": "off", "": "off",
"none": "off", "none": "off",
@ -259,6 +273,16 @@ def compact_tool_output_preview(
return preview return preview
def agent_visible_command_surface(executor: PIFSCommandExecutor) -> str:
"""Hide legacy semantic command hints from ask/chat default instructions."""
lines = []
for line in executor.describe_available_command_surfaces().splitlines():
if any(term in line for term in LEGACY_SEMANTIC_COMMAND_SURFACE_TERMS):
continue
lines.append(line)
return "\n".join(lines)
def build_agent_initial_context( def build_agent_initial_context(
filesystem: PageIndexFileSystem, filesystem: PageIndexFileSystem,
*, *,
@ -288,7 +312,7 @@ def build_agent_initial_context(
ensure_ascii=False, ensure_ascii=False,
), ),
"Workspace retrieval capabilities:", "Workspace retrieval capabilities:",
executor.describe_available_command_surfaces(), agent_visible_command_surface(executor),
] ]
) )

View file

@ -1,7 +1,10 @@
import ast
import io import io
import os import os
import tempfile
import threading import threading
import unittest import unittest
from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from types import SimpleNamespace from types import SimpleNamespace
@ -15,6 +18,7 @@ from pageindex.filesystem.agent import (
PIFSAgentSession, PIFSAgentSession,
PIFSAgentStreamObserver, PIFSAgentStreamObserver,
build_agent_model_settings, build_agent_model_settings,
build_pifs_agent_instructions,
normalize_agent_stream_mode, normalize_agent_stream_mode,
normalize_reasoning_effort, normalize_reasoning_effort,
normalize_reasoning_summary, normalize_reasoning_summary,
@ -23,6 +27,22 @@ from pageindex.filesystem.agent import (
should_disable_pifs_agent_tracing, should_disable_pifs_agent_tracing,
should_use_openai_compatible_chat_model, should_use_openai_compatible_chat_model,
) )
from pageindex.filesystem import PageIndexFileSystem
def load_demo_agent_prompt() -> str:
demo_path = Path(__file__).resolve().parents[1] / "examples" / "pifs_demo.py"
module = ast.parse(demo_path.read_text(encoding="utf-8"))
for node in module.body:
if isinstance(node, ast.Assign):
names = [
target.id
for target in node.targets
if isinstance(target, ast.Name)
]
if "PIFS_DEMO_AGENT_PROMPT" in names and isinstance(node.value, ast.Constant):
return str(node.value.value)
raise AssertionError("PIFS_DEMO_AGENT_PROMPT not found")
class StructuredAnswer(BaseModel): class StructuredAnswer(BaseModel):
@ -215,22 +235,65 @@ class PIFSAgentStreamTest(unittest.TestCase):
self.assertIn("Do not run stat merely to understand what a document says", AGENT_TOOL_POLICY) self.assertIn("Do not run stat merely to understand what a document says", AGENT_TOOL_POLICY)
self.assertIn("Do not use stat as a general content/topic discovery step", BASH_TOOL_DESCRIPTION) self.assertIn("Do not use stat as a general content/topic discovery step", BASH_TOOL_DESCRIPTION)
def test_prompt_routes_semantic_search_to_browse(self): def test_prompt_routes_topic_retrieval_through_browse_after_folder_exploration(self):
self.assertIn("Start with ls or tree", AGENT_TOOL_POLICY)
self.assertIn('browse <folder> "<query>"', AGENT_TOOL_POLICY)
self.assertIn('browse /documents "Federal Reserve"', BASH_TOOL_DESCRIPTION)
self.assertIn("If the relevant folder is uncertain", AGENT_TOOL_POLICY)
self.assertIn('browse -R <folder> "<query>"', AGENT_TOOL_POLICY)
self.assertIn("browse returns file candidates only", AGENT_TOOL_POLICY)
self.assertIn("verify the relevant facts with cat or grep", AGENT_TOOL_POLICY)
self.assertIn("cat <target> --structure", AGENT_TOOL_POLICY)
self.assertIn("cat <target> --node <node_id>", AGENT_TOOL_POLICY)
self.assertIn("cat <target> --page", AGENT_TOOL_POLICY)
self.assertIn("Do not use browse as folder semantic recall", AGENT_TOOL_POLICY)
def test_default_agent_prompts_do_not_suggest_legacy_semantic_commands(self):
prompt_surface = "\n".join(
[AGENT_SYSTEM_PROMPT, BASH_TOOL_DESCRIPTION, AGENT_TOOL_POLICY]
)
for old_command in ( for old_command in (
"search-summary", "search-summary",
"search-entity", "search-entity",
"search-relation", "search-relation",
"semantic-grep", "semantic-grep",
"find --name",
"find --relation",
): ):
self.assertNotIn(old_command, BASH_TOOL_DESCRIPTION) self.assertNotIn(old_command, prompt_surface)
self.assertNotIn(old_command, AGENT_TOOL_POLICY)
self.assertIn("Use browse when the user", BASH_TOOL_DESCRIPTION) def test_demo_prompt_uses_browse_strategy_and_not_legacy_semantic_search(self):
self.assertIn('use browse <folder> "<query>"', AGENT_TOOL_POLICY) demo_prompt = load_demo_agent_prompt()
self.assertIn('browse /documents "Federal Reserve"', BASH_TOOL_DESCRIPTION)
self.assertIn("browse -R <folder>", AGENT_TOOL_POLICY) self.assertIn("Start with ls or tree", demo_prompt)
self.assertIn("do not translate that request into find --where", AGENT_TOOL_POLICY) self.assertIn('browse /documents "Federal Reserve supervision regulation"', demo_prompt)
self.assertIn("verify the relevant facts with cat", AGENT_TOOL_POLICY) self.assertIn('browse -R /documents "Federal Reserve supervision regulation"', demo_prompt)
self.assertIn("verify the relevant claim with cat", BASH_TOOL_DESCRIPTION) self.assertIn("verify", demo_prompt)
self.assertIn("cat <path> --structure", demo_prompt)
self.assertNotIn("search-summary", demo_prompt)
def test_built_agent_instructions_filter_legacy_semantic_command_surface(self):
class LegacySemanticBackend:
semantic_tool_channels = ("summary", "entity", "relation")
with tempfile.TemporaryDirectory() as workspace:
filesystem = PageIndexFileSystem(
workspace,
semantic_retrieval_backend=LegacySemanticBackend(),
)
instructions = build_pifs_agent_instructions(filesystem)
self.assertIn('browse [-R] <folder> "<query>"', instructions)
for old_command in (
"search-summary",
"search-entity",
"search-relation",
"semantic-grep",
"find --name",
"find --relation",
):
self.assertNotIn(old_command, instructions)
def test_prompt_rejects_find_grep_as_exhaustive_search(self): def test_prompt_rejects_find_grep_as_exhaustive_search(self):
self.assertIn("Do not use find | grep as an exhaustive search", AGENT_TOOL_POLICY) self.assertIn("Do not use find | grep as an exhaustive search", AGENT_TOOL_POLICY)