Skip to content

Commit

Permalink
Make it a pip installable package
Browse files Browse the repository at this point in the history
  • Loading branch information
epatey committed Mar 8, 2025
1 parent bc6b8c6 commit c6e8c33
Show file tree
Hide file tree
Showing 70 changed files with 453 additions and 642 deletions.
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python: Debug multi_tool_v1.py",
"name": "Python: Debug multi_tool cli",
"type": "python",
"request": "launch",
"module": "multi_tool_v1",
"module": "inspect_multi_tool",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/src/multi_tool",
"cwd": "${workspaceFolder}/src/eric_testing/src",
"args": ["${input:requestArg}"]
},
{
Expand Down
6 changes: 6 additions & 0 deletions src/eric_testing/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include README.md
include LICENSE
recursive-include src/inspect_multi_tool *.py *.svg *.md
recursive-exclude * __pycache__
recursive-exclude * *.py[cod]
recursive-exclude * *$py.class
57 changes: 57 additions & 0 deletions src/eric_testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Multi-tool Package

A simplified package that provides JSON-RPC tools for inspect_ai.

## Installation

The package can be installed with pip:

```bash
# Install the package directly from the source directory
pip install .

# Or from the project root directory
pip install -e src/multi_tool/
```

## Usage

Once installed, you can use the command-line tool to invoke JSON-RPC methods:

```bash
# Use the command-line tool
multi-tool '{"jsonrpc": "2.0", "method": "editor", "id": 1, "params": {"command": "view", "path": "/tmp"}}'

# Or using Python module syntax
python -m multi_tool '{"jsonrpc": "2.0", "method": "editor", "id": 1, "params": {"command": "view", "path": "/tmp"}}'
```

## Features

1. Simple CLI tool (`multi-tool`) for executing specified tools with given parameters
2. Support for in-process JSON-RPC tools
3. Validation of parameters using Pydantic models

## Package Structure

The package has a minimal structure:

- `multi_tool/`: Main package
- `_in_process_tools/`: Directory containing tool implementations
- `_editor/`: Editor tool implementation
- `_util/`: Utility functions for the tools
- `__main__.py`: Entry point for module execution
- `multi_tool.py`: Main CLI implementation

## Adding New Tools

To add a new tool:

1. Create a new directory under `_in_process_tools/` with an underscore prefix (e.g., `_new_tool/`)
2. Create files in the tool directory:
- `__init__.py`: Empty file for the package
- `tool_types.py`: Pydantic models for the tool parameters
- `<tool>.py`: Implementation of the tool functionality
- `json_rpc_methods.py`: JSON-RPC method definitions

The tool will be automatically detected and loaded when the package is installed.
File renamed without changes.
106 changes: 106 additions & 0 deletions src/eric_testing/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
[build-system]
requires = ["setuptools>=64", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]
include = ["inspect_multi_tool*"]

[tool.setuptools.package-data]
"inspect_multi_tool" = ["**/*.svg", "**/*.md"]


[tool.ruff]
extend-exclude = ["docs"]
src = ["."]

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # flake8
"D", # pydocstyle
"I", # isort
"SIM101", # duplicate isinstance
"UP038", # non-pep604-isinstance
# "RET", # flake8-return
# "RUF", # ruff rules
]
ignore = ["E203", "E501", "D10", "D212", "D415"]

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.mypy]
exclude = ["tests/test_package", "build", "(?:^|/)_resources/", "examples/bridge"]
warn_unused_ignores = true
no_implicit_reexport = true
strict_equality = true
warn_redundant_casts = true
warn_unused_configs = true
# This mypy_path config is a bit odd, it's included to get mypy to resolve
# imports correctly in test files. For example, imports such as
# `from test_helpers.utils import ...` fail mypy without this configuration,
# despite actually working when running tests.
#
# Revisit this if it interferes with mypy running on `src` due to name
# conflicts, but that will hopefully be unlikely.
mypy_path = "tests"

[[tool.mypy.overrides]]
module = ["inspect_ai.*"]
warn_return_any = true
disallow_untyped_defs = true
disallow_any_generics = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
extra_checks = true
disable_error_code = "unused-ignore"

[tool.check-wheel-contents]
ignore = ["W002", "W009"]

[project]
name = "inspect_multi_tool"
version = "0.1.0"
description = "Multi-tool sandbox container code for inspect_ai"
authors = [{ name = "UK AI Security Institute" }]
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT License" }
# dynamic = ["version", "dependencies"]
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
"Operating System :: OS Independent",
]
dependencies = [
"aiohttp",
"httpx",
"jsonrpcserver",
"pydantic",
"returns",
"tenacity",
"playwright",
]

[project.urls]

[project.scripts]
inspect-tool-exec = "inspect_multi_tool._cli.main:main"
inspect-tool-install = "inspect_multi_tool._cli.post_install:main"
inspect-tool-server = "inspect_multi_tool._cli.server:main"

[project.optional-dependencies]

dist = ["twine", "build"]
16 changes: 16 additions & 0 deletions src/eric_testing/src/inspect_multi_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Multi-tool package for inspect_ai.
Contains tools for web browser, bash, and editor functionality.
"""

__version__ = "0.1.0"

from inspect_multi_tool._util._constants import SERVER_PORT
from inspect_multi_tool._util._load_tools import load_tools

__all__ = [
"__version__",
"SERVER_PORT",
"load_tools",
]
4 changes: 4 additions & 0 deletions src/eric_testing/src/inspect_multi_tool/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._cli.main import main

if __name__ == "__main__":
main()
76 changes: 76 additions & 0 deletions src/eric_testing/src/inspect_multi_tool/_cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import argparse
import asyncio
import subprocess
from typing import Literal

from jsonrpcserver import async_dispatch
from pydantic import BaseModel

from inspect_multi_tool import SERVER_PORT
from inspect_multi_tool._util._common_types import JSONRPCResponseJSON
from inspect_multi_tool._util._json_rpc_helpers import json_rpc_http_call
from inspect_multi_tool._util._load_tools import load_tools

_SERVER_URL = f"http://localhost:{SERVER_PORT}/"


class JSONRPCRequest(BaseModel):
jsonrpc: Literal["2.0"]
method: str
id: int | float | str
params: list[object] | dict[str, object] | None = None


def main() -> None:
asyncio.run(async_main())


# Example/testing requests
# {"jsonrpc": "2.0", "method": "editor", "id": 666, "params": {"command": "view", "path": "/tmp"}}
# {"jsonrpc": "2.0", "method": "bash", "id": 666, "params": {"command": "ls ~/Downloads"}}
async def async_main() -> None:
_ensure_daemon_is_running()

in_process_tools = load_tools("inspect_multi_tool._in_process_tools")

args: argparse.Namespace = parser.parse_args()

validated_request = JSONRPCRequest.model_validate_json(args.request)
tool_name = validated_request.method
assert isinstance(tool_name, str)

print(
await (
_dispatch_local_method
if tool_name in in_process_tools
else _dispatch_remote_method
)(args.request)
)


parser = argparse.ArgumentParser(prog="multi_tool_client")
parser.add_argument(
"request", type=str, help="A JSON string representing the JSON RPC 2.0 request"
)


async def _dispatch_local_method(request_json_str: str) -> JSONRPCResponseJSON:
return JSONRPCResponseJSON(await async_dispatch(request_json_str))


async def _dispatch_remote_method(request_json_str: str) -> JSONRPCResponseJSON:
return await json_rpc_http_call(_SERVER_URL, request_json_str)


def _ensure_daemon_is_running() -> None:
# TODO: Pipe stdout and stderr to proc 1
if b"inspect-tool-server" not in subprocess.check_output(["ps", "aux"]):
subprocess.Popen(
["inspect-tool-server"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)


if __name__ == "__main__":
main()
39 changes: 39 additions & 0 deletions src/eric_testing/src/inspect_multi_tool/_cli/post_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import subprocess
import sys


def install_playwright_dependencies(cmd=None):
"""
Install Playwright browsers and system dependencies.
This function is called as a post-install hook and also available
as a standalone command.
Args:
cmd: Used by setuptools for the entry point. Not used in the function.
"""
try:
print("\n=== Installing Playwright dependencies ===")
print("Installing Playwright browsers...")
subprocess.run([sys.executable, "-m", "playwright", "install"], check=True)

print("\nInstalling Playwright system dependencies...")
subprocess.run([sys.executable, "-m", "playwright", "install-deps"], check=True)
print("=== Playwright setup completed successfully ===\n")
return True
except Exception as e:
print(f"\nError during Playwright setup: {e}", file=sys.stderr)
print(
"You may need to run 'playwright install' and 'playwright install-deps' manually after installation"
)
return False


def main():
"""Main entry point for the script when run as a command."""
success = install_playwright_dependencies()
return 0 if success else 1


if __name__ == "__main__":
sys.exit(main())
24 changes: 24 additions & 0 deletions src/eric_testing/src/inspect_multi_tool/_cli/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from aiohttp.web import Application, Request, Response, run_app
from jsonrpcserver import async_dispatch

from inspect_multi_tool import SERVER_PORT, load_tools


def main() -> None:
load_tools("inspect_multi_tool._remote_tools")

async def handle_request(request: Request) -> Response:
return Response(
text=await async_dispatch(await request.text()),
content_type="application/json",
)

app = Application()
app.router.add_post("/", handle_request)

print(f"Starting server on port {SERVER_PORT}")
run_app(app, port=SERVER_PORT)


if __name__ == "__main__":
main()
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from pathlib import Path
from typing import Literal

from multi_tool._in_process_tools._editor._run import maybe_truncate, run
from multi_tool._util._common_types import ToolException
from inspect_multi_tool._in_process_tools._editor._run import maybe_truncate, run
from inspect_multi_tool._util._common_types import ToolException

DEFAULT_HISTORY_PATH = "/tmp/inspect_editor_history.pkl"
SNIPPET_LINES: int = 4
Expand Down
Loading

0 comments on commit c6e8c33

Please sign in to comment.