mirror of
https://github.com/VectifyAI/PageIndex.git
synced 2026-06-12 19:55:17 +02:00
fix(filesystem): add pifs ask and chat commands
This commit is contained in:
parent
74d0600261
commit
574125d7dd
3 changed files with 251 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
113
tests/test_pifs_cli.py
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue