fix(filesystem): add pifs ask and chat commands

This commit is contained in:
BukeLy 2026-05-26 14:14:45 +08:00
parent 74d0600261
commit 574125d7dd
3 changed files with 251 additions and 9 deletions

View file

@ -6,10 +6,128 @@ import shlex
import sys
from pathlib import Path
from .agent import REASONING_EFFORT_CHOICES, REASONING_SUMMARY_CHOICES, run_pifs_agent
from .commands import PIFSCommandError, PIFSCommandExecutor
from .core import PageIndexFileSystem
AGENT_STREAM_MODE_CHOICES = ("off", "tools", "model", "all")
DEFAULT_AGENT_MODEL = "gpt-5.4-mini"
EXIT_COMMANDS = {"exit", "quit", ":q"}
def _agent_model_default() -> str:
return (
os.environ.get("PIFS_AGENT_MODEL")
or os.environ.get("PIFS_MODEL")
or DEFAULT_AGENT_MODEL
)
def _add_agent_arguments(
parser: argparse.ArgumentParser,
*,
workspace_default: str | None,
) -> None:
parser.add_argument("--workspace", default=workspace_default)
parser.add_argument("--model", default=_agent_model_default())
parser.add_argument(
"--stream-mode",
default="off",
choices=AGENT_STREAM_MODE_CHOICES,
)
parser.add_argument("--max-turns", type=int, default=20)
parser.add_argument("--max-seconds", type=float, default=60)
parser.add_argument(
"--reasoning-effort",
choices=REASONING_EFFORT_CHOICES,
default=None,
)
parser.add_argument(
"--reasoning-summary",
choices=REASONING_SUMMARY_CHOICES,
default=None,
)
def _parse_agent_command(
command_name: str,
argv: list[str],
*,
workspace_default: str | None,
) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog=f"pifs {command_name}",
description=f"PageIndex FileSystem {command_name}",
)
_add_agent_arguments(parser, workspace_default=workspace_default)
if command_name == "ask":
parser.add_argument("question", nargs=argparse.REMAINDER)
args = parser.parse_args(argv)
if not args.workspace:
parser.error("--workspace is required unless PIFS_WORKSPACE is set")
return args
def _filesystem_from_workspace(workspace: str) -> PageIndexFileSystem:
return PageIndexFileSystem(Path(workspace).expanduser())
def _agent_kwargs(args: argparse.Namespace) -> dict[str, object]:
return {
"model": args.model,
"stream_mode": args.stream_mode,
"max_turns": args.max_turns,
"max_seconds": args.max_seconds,
"reasoning_effort": args.reasoning_effort,
"reasoning_summary": args.reasoning_summary,
}
def _run_ask(argv: list[str], *, workspace_default: str | None) -> int:
args = _parse_agent_command("ask", argv, workspace_default=workspace_default)
question_tokens = [token for token in args.question if token != "--"]
question = " ".join(question_tokens).strip()
if not question:
raise ValueError("ask requires a question")
filesystem = _filesystem_from_workspace(args.workspace)
print(run_pifs_agent(filesystem, question, **_agent_kwargs(args)))
return 0
def _run_chat(argv: list[str], *, workspace_default: str | None) -> int:
args = _parse_agent_command("chat", argv, workspace_default=workspace_default)
filesystem = _filesystem_from_workspace(args.workspace)
while True:
try:
question = input("pifs> ").strip()
except EOFError:
break
except KeyboardInterrupt:
print()
break
if not question:
continue
if question.lower() in EXIT_COMMANDS:
break
print(run_pifs_agent(filesystem, question, **_agent_kwargs(args)))
return 0
def _run_passthrough(
command_tokens: list[str],
*,
workspace: str,
json_output: bool,
) -> int:
filesystem = _filesystem_from_workspace(workspace)
executor = PIFSCommandExecutor(filesystem, json_output=json_output)
command = " ".join(shlex.quote(token) for token in command_tokens)
print(executor.execute(command))
return 0
def main(argv: list[str] | None = None) -> int:
argv = list(sys.argv[1:] if argv is None else argv)
parser = argparse.ArgumentParser(description="PageIndex FileSystem CLI")
@ -20,20 +138,28 @@ def main(argv: list[str] | None = None) -> int:
command_tokens = [token for token in args.command if token != "--"]
json_output = args.json_output
if "--json" in command_tokens:
command_tokens = [token for token in command_tokens if token != "--json"]
json_output = True
if not args.workspace:
parser.error("--workspace is required unless PIFS_WORKSPACE is set")
if not command_tokens:
parser.error("a filesystem command is required")
filesystem = PageIndexFileSystem(Path(args.workspace).expanduser())
executor = PIFSCommandExecutor(filesystem, json_output=json_output)
try:
command = " ".join(shlex.quote(token) for token in command_tokens)
print(executor.execute(command))
command_name = command_tokens[0]
command_args = command_tokens[1:]
if command_name == "ask":
return _run_ask(command_args, workspace_default=args.workspace)
if command_name == "chat":
return _run_chat(command_args, workspace_default=args.workspace)
if "--json" in command_tokens:
command_tokens = [token for token in command_tokens if token != "--json"]
json_output = True
if not args.workspace:
parser.error("--workspace is required unless PIFS_WORKSPACE is set")
return _run_passthrough(
command_tokens,
workspace=args.workspace,
json_output=json_output,
)
except PIFSCommandError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2

View file

@ -12,3 +12,6 @@ dependencies = [
"pyyaml==6.0.2",
"sqlite-vec>=0.1.9",
]
[project.scripts]
pifs = "pageindex.filesystem.cli:main"

113
tests/test_pifs_cli.py Normal file
View file

@ -0,0 +1,113 @@
from pathlib import Path
class FakeFileSystem:
def __init__(self, workspace):
self.workspace = Path(workspace)
def test_cli_passthrough_invokes_pifs_command_executor(monkeypatch, capsys, tmp_path):
from pageindex.filesystem import cli
workspace = tmp_path / "workspace"
executor_instances = []
class FakeExecutor:
def __init__(self, filesystem, *, json_output=False):
self.filesystem = filesystem
self.json_output = json_output
self.commands = []
executor_instances.append(self)
def execute(self, command):
self.commands.append(command)
return f"executed:{command}"
monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem)
monkeypatch.setattr(cli, "PIFSCommandExecutor", FakeExecutor)
status = cli.main(["--workspace", str(workspace), "ls", "/documents", "--json"])
assert status == 0
assert capsys.readouterr().out == "executed:ls /documents\n"
assert len(executor_instances) == 1
assert executor_instances[0].filesystem.workspace == workspace
assert executor_instances[0].json_output is True
assert executor_instances[0].commands == ["ls /documents"]
def test_cli_ask_invokes_agent_with_question(monkeypatch, capsys, tmp_path):
from pageindex.filesystem import cli
workspace = tmp_path / "workspace"
agent_calls = []
def fake_run_pifs_agent(filesystem, question, **kwargs):
agent_calls.append((filesystem, question, kwargs))
return "agent answer"
monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem)
monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent)
status = cli.main(
[
"ask",
"--workspace",
str(workspace),
"--model",
"test-model",
"--stream-mode",
"tools",
"--max-turns",
"7",
"--max-seconds",
"3.5",
"--reasoning-effort",
"low",
"--reasoning-summary",
"concise",
"What",
"is",
"inside?",
]
)
assert status == 0
assert capsys.readouterr().out == "agent answer\n"
filesystem, question, kwargs = agent_calls[0]
assert filesystem.workspace == workspace
assert question == "What is inside?"
assert kwargs == {
"model": "test-model",
"stream_mode": "tools",
"max_turns": 7,
"max_seconds": 3.5,
"reasoning_effort": "low",
"reasoning_summary": "concise",
}
def test_cli_chat_runs_one_question_and_exits(monkeypatch, capsys, tmp_path):
from pageindex.filesystem import cli
workspace = tmp_path / "workspace"
inputs = iter(["", "Summarize the workspace", "exit"])
agent_calls = []
def fake_run_pifs_agent(filesystem, question, **kwargs):
agent_calls.append((filesystem, question, kwargs))
return f"answer:{question}"
monkeypatch.setattr(cli, "PageIndexFileSystem", FakeFileSystem)
monkeypatch.setattr(cli, "run_pifs_agent", fake_run_pifs_agent)
monkeypatch.setattr("builtins.input", lambda prompt="": next(inputs))
status = cli.main(["chat", "--workspace", str(workspace), "--model", "test-model"])
assert status == 0
assert capsys.readouterr().out == "answer:Summarize the workspace\n"
assert len(agent_calls) == 1
filesystem, question, kwargs = agent_calls[0]
assert filesystem.workspace == workspace
assert question == "Summarize the workspace"
assert kwargs["model"] == "test-model"