From 95e5717ba4e794de2f636d785ab7e984c4e26a2c Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 31 May 2026 17:40:47 +0800 Subject: [PATCH 1/2] fix(pifs): route agent retrieval through browse --- examples/pifs_demo.py | 24 +++++---- pageindex/filesystem/agent.py | 92 +++++++++++++++++++++------------ tests/test_pifs_agent_stream.py | 83 +++++++++++++++++++++++++---- 3 files changed, 146 insertions(+), 53 deletions(-) diff --git a/examples/pifs_demo.py b/examples/pifs_demo.py index a12f48d..0019942 100644 --- a/examples/pifs_demo.py +++ b/examples/pifs_demo.py @@ -4,8 +4,8 @@ PageIndex FileSystem (PIFS) agent demo. This mirrors examples/agentic_vectorless_rag_demo.py, but exposes a corpus through the PageIndex FileSystem shell instead of direct PageIndex document tools. The agent receives one read-only bash-like PIFS tool and must retrieve -evidence through commands such as ls, tree, find, grep, browse, -cat --structure, cat --page, and cat --node. +evidence through commands such as ls, tree, browse, find, grep, cat +--structure, cat --page, and cat --node. The demo registers supported files under examples/documents. When a matching 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. - Folder paths such as /documents are positional command targets; do not put folder paths inside --where. -- Use browse when available to find likely documents by semantic relevance. - Quote multi-word queries and include a path, for example: +- After choosing a folder, use browse with a required quoted query to find + likely files, for example: 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 + --structure, cat --node, or cat --page before answering. - Use find --where only with JSON metadata DSL, for example: find /documents --where '{"file_format":"pdf"}' - 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"' - summary = execute_json_command(json_executor, command) - summary_hits = ((summary.get("data") or {}).get("data") or []) - if summary_hits: - summary_result = f"{len(summary_hits)} browse candidates; top={summary_hits[0].get('external_id')}" + browse = execute_json_command(json_executor, command) + browse_hits = ((browse.get("data") or {}).get("data") or []) + if browse_hits: + summary_result = f"{len(browse_hits)} browse candidates; top={browse_hits[0].get('external_id')}" else: summary_result = "browse is available, but this tiny two-doc demo returned no candidates" show_capability( - label="Semantic browse", + label="Relevance browse", command=command, result=summary_result, raw=shell_executor.execute(command) if verbose else "", diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index f9b2241..24598dd 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -35,17 +35,19 @@ document contents in the workspace. If the user asks what tools or capabilities you have, describe only the PIFS virtual shell capabilities available inside this workspace: ls, tree, find, -stat, grep, cat, and browse. Do not mention host runtime tools, SDK internals, -or orchestration helpers that are not part of the PIFS shell. +stat, grep, cat, and browse when they are available. Do not mention host +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 -file, treat it as a retrieval task. Use available PIFS discovery commands to -look for relevant files and inspect evidence before answering. Ask the user to -clarify only after a reasonable search cannot identify relevant evidence. +file, treat it as a retrieval task. Start with ls or tree to understand the +folder structure, choose a folder, then use browse with the user's topic as the +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 -returns no matches for a workspace topic, verify with available semantic -candidate discovery through browse, or inspect likely document structure, -before saying that the workspace lacks evidence. +returns no matches for a workspace topic, use browse on a relevant folder or +inspect likely document structure before saying that the workspace lacks +evidence. Follow the task prompt for command policy, retrieval strategy, and answer 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 = """ 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, -grep, cat, stat, head, tail, sed, and browse as described in the workspace -context. grep -R is lexical evidence search; -grep does not support regex alternation such as "a|b"; run multiple grep -commands or use browse for semantic candidate discovery instead. browse returns -candidate documents ranked by relevance and does not guarantee literal text -matches or final answer evidence. After choosing a likely browse candidate, -verify the relevant claim with cat before answering. Use browse when the user -asks for summary search, semantic search, or vector search and the command is -listed as available. Quote multi-word semantic queries, for example: -browse /documents "Federal Reserve". Do not write -browse /documents Federal Reserve. Errors are returned as text prefixed with -ERROR. Do not call -commands that are not listed as available. When evidence is required, inspect it -with cat or grep before answering. Prefer shell-like target-first cat syntax -with stable targets: cat --structure, cat --page 31-59, and -cat --node 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 commands and quote paths containing spaces. After structure -identifies a relevant section node, prefer +grep, cat, stat, head, tail, sed, and browse when listed in the workspace +context. grep -R is lexical evidence search; grep does not support regex +alternation such as "a|b"; run multiple grep commands or use browse for +relevance-ranked file discovery instead. Start broad workspace questions with +ls or tree to understand folders. After choosing a folder, use positional +browse syntax with a quoted query, for example: +browse /documents "Federal Reserve". If the relevant folder is uncertain, use +browse -R /documents "Federal Reserve" to retrieve file candidates across that +folder tree. browse returns file candidates only; it does not perform folder +semantic recall and does not guarantee final answer evidence. After choosing a +likely browse candidate, verify the relevant claim with cat or grep before +answering. Errors are returned as text prefixed with ERROR. Do not call commands +that are not listed as available. When evidence is required, inspect it with cat +or grep before answering. Prefer shell-like target-first cat syntax with stable +targets: cat --structure, cat --page 31-59, and cat --node +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 +commands and quote paths containing spaces. After structure identifies a +relevant section node, prefer cat --node ; use cat --page when the user asks for page-level evidence, no suitable node exists, or exact page text is needed. cat --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 was provided, inspect stat --schema and stat before making claims. 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 -cat --node or cat --page for evidence. +prefer ls/tree for folder selection, browse for file candidates, then cat +--structure and cat --node or cat --page for evidence. """ AGENT_TOOL_POLICY = """ @@ -94,12 +95,16 @@ Tool policy: - Use only commands listed in the workspace capabilities. - 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. +- Start with ls or tree to understand workspace and folder structure before semantic file retrieval. +- After choosing a folder, use browse "" for relevance-ranked file candidates; quote multi-word queries, for example browse /documents "Federal Reserve". +- If the relevant folder is uncertain, use browse -R "" 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 does not support regex alternation such as "a|b"; run separate grep commands or use browse for semantic candidate 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. +- grep does not support regex alternation such as "a|b"; run separate grep commands or use browse for relevance-ranked file discovery. - 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. -- If the user asks for summary search, semantic search, vector search, or "用 summary 搜", use browse ""; quote multi-word queries, for example browse /documents "Federal Reserve"; use browse -R when the folder choice is uncertain; do not translate that request into find --where. +- 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 "" 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. - 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. @@ -119,6 +124,15 @@ Tool policy: - 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 = { "": "off", "none": "off", @@ -259,6 +273,16 @@ def compact_tool_output_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( filesystem: PageIndexFileSystem, *, @@ -288,7 +312,7 @@ def build_agent_initial_context( ensure_ascii=False, ), "Workspace retrieval capabilities:", - executor.describe_available_command_surfaces(), + agent_visible_command_surface(executor), ] ) diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index f4475a7..8908abd 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -1,7 +1,10 @@ +import ast import io import os +import tempfile import threading import unittest +from pathlib import Path from unittest.mock import patch from types import SimpleNamespace @@ -15,6 +18,7 @@ from pageindex.filesystem.agent import ( PIFSAgentSession, PIFSAgentStreamObserver, build_agent_model_settings, + build_pifs_agent_instructions, normalize_agent_stream_mode, normalize_reasoning_effort, normalize_reasoning_summary, @@ -23,6 +27,22 @@ from pageindex.filesystem.agent import ( should_disable_pifs_agent_tracing, 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): @@ -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 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 ""', 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 ""', 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 --structure", AGENT_TOOL_POLICY) + self.assertIn("cat --node ", AGENT_TOOL_POLICY) + self.assertIn("cat --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 ( "search-summary", "search-entity", "search-relation", "semantic-grep", + "find --name", + "find --relation", ): - self.assertNotIn(old_command, BASH_TOOL_DESCRIPTION) - self.assertNotIn(old_command, AGENT_TOOL_POLICY) - self.assertIn("Use browse when the user", BASH_TOOL_DESCRIPTION) - self.assertIn('use browse ""', AGENT_TOOL_POLICY) - self.assertIn('browse /documents "Federal Reserve"', BASH_TOOL_DESCRIPTION) - self.assertIn("browse -R ", AGENT_TOOL_POLICY) - self.assertIn("do not translate that request into find --where", AGENT_TOOL_POLICY) - self.assertIn("verify the relevant facts with cat", AGENT_TOOL_POLICY) - self.assertIn("verify the relevant claim with cat", BASH_TOOL_DESCRIPTION) + self.assertNotIn(old_command, prompt_surface) + + def test_demo_prompt_uses_browse_strategy_and_not_legacy_semantic_search(self): + demo_prompt = load_demo_agent_prompt() + + self.assertIn("Start with ls or tree", demo_prompt) + self.assertIn('browse /documents "Federal Reserve supervision regulation"', demo_prompt) + self.assertIn('browse -R /documents "Federal Reserve supervision regulation"', demo_prompt) + self.assertIn("verify", demo_prompt) + self.assertIn("cat --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] ""', 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): self.assertIn("Do not use find | grep as an exhaustive search", AGENT_TOOL_POLICY) From b5cc404776b6896f85a56f84daf74ecb2c74cd16 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 31 May 2026 21:16:49 +0800 Subject: [PATCH 2/2] refactor(pifs): remove legacy command prompt filter --- pageindex/filesystem/agent.py | 21 +-------------------- tests/test_pifs_agent_stream.py | 22 ---------------------- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/pageindex/filesystem/agent.py b/pageindex/filesystem/agent.py index 24598dd..282f67e 100644 --- a/pageindex/filesystem/agent.py +++ b/pageindex/filesystem/agent.py @@ -124,15 +124,6 @@ Tool policy: - 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 = { "": "off", "none": "off", @@ -273,16 +264,6 @@ def compact_tool_output_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( filesystem: PageIndexFileSystem, *, @@ -312,7 +293,7 @@ def build_agent_initial_context( ensure_ascii=False, ), "Workspace retrieval capabilities:", - agent_visible_command_surface(executor), + executor.describe_available_command_surfaces(), ] ) diff --git a/tests/test_pifs_agent_stream.py b/tests/test_pifs_agent_stream.py index 8908abd..de93856 100644 --- a/tests/test_pifs_agent_stream.py +++ b/tests/test_pifs_agent_stream.py @@ -273,28 +273,6 @@ class PIFSAgentStreamTest(unittest.TestCase): self.assertIn("cat --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] ""', 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): self.assertIn("Do not use find | grep as an exhaustive search", AGENT_TOOL_POLICY) self.assertIn("find output can be scoped or limited", AGENT_TOOL_POLICY)