Skip to content

Commit a647c58

Browse files
committed
feat: add basic toolbox-core implementation
1 parent 266e148 commit a647c58

17 files changed

+448
-45
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@
33

44
# vscode
55
.vscode/
6+
7+
# python
8+
env
9+
venv
10+
*.pyc
11+
.python-version
12+
**.egg-info/
13+

packages/toolbox-core/pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ authors = [
99
{name = "Google LLC", email = "[email protected]"}
1010
]
1111

12-
# TODO: Add deps
13-
#dependencies = [
14-
#]
12+
dependencies = [
13+
"pydantic>=2.7.0,<3.0.0",
14+
"aiohttp>=3.8.6,<4.0.0",
15+
]
1516

1617
classifiers = [
1718
"Intended Audience :: Developers",
@@ -43,6 +44,7 @@ test = [
4344
"isort==6.0.1",
4445
"mypy==1.15.0",
4546
"pytest==8.3.5",
47+
"aioresponses==0.7.8"
4648
]
4749
[build-system]
4850
requires = ["setuptools"]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aiohttp==3.11.14
2+
pydantic==2.10.6

packages/toolbox-core/src/toolbox_core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from .client import DummyClass
15+
from .client import ToolboxClient
1616

17-
__all__ = ["DummyClass"]
17+
__all__ = ["ToolboxClient"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

packages/toolbox-core/src/toolbox_core/client.py

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,153 @@
44
# you may not use this file except in compliance with the License.
55
# You may obtain a copy of the License at
66
#
7-
# http://www.apache.org/licenses/LICENSE-2.0
7+
# http://www.apache.org/licenses/LICENSE-2.0
88
#
99
# Unless required by applicable law or agreed to in writing, software
1010
# distributed under the License is distributed on an "AS IS" BASIS,
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from typing import Optional
1516

16-
class DummyClass:
17-
def __init__(self):
18-
self.val = "dummy value"
17+
from aiohttp import ClientSession
18+
19+
from .protocol import ManifestSchema, ToolSchema
20+
from .tool import ToolboxTool
21+
22+
23+
class ToolboxClient:
24+
"""
25+
An asynchronous client for interacting with a Toolbox service.
26+
27+
Provides methods to discover and load tools defined by a remote Toolbox
28+
service endpoint. It manages an underlying `aiohttp.ClientSession`.
29+
"""
30+
31+
__base_url: str
32+
__session: ClientSession
33+
34+
def __init__(
35+
self,
36+
url: str,
37+
session: Optional[ClientSession] = None,
38+
):
39+
"""
40+
Initializes the ToolboxClient.
41+
42+
Args:
43+
url: The base URL for the Toolbox service API (e.g., "http://localhost:8000").
44+
session: An optional existing `aiohttp.ClientSession` to use.
45+
If None (default), a new session is created internally. Note that
46+
if a session is provided, its lifecycle (including closing)
47+
should typically be managed externally.
48+
"""
49+
self.__base_url = url
50+
51+
# If no aiohttp.ClientSession is provided, make our own
52+
if session is None:
53+
session = ClientSession()
54+
self.__session = session
55+
56+
def __parse_tool(self, name: str, schema: ToolSchema) -> ToolboxTool:
57+
"""Internal helper to create a callable tool from its schema."""
58+
tool = ToolboxTool(
59+
session=self.__session,
60+
base_url=self.__base_url,
61+
name=name,
62+
desc=schema.description,
63+
params=[p.to_param() for p in schema.parameters],
64+
)
65+
return tool
66+
67+
async def __aenter__(self):
68+
"""
69+
Enter the runtime context related to this client instance.
70+
71+
Allows the client to be used as an asynchronous context manager
72+
(e.g., `async with ToolboxClient(...) as client:`).
73+
74+
Returns:
75+
self: The client instance itself.
76+
"""
77+
return self
78+
79+
async def __aexit__(self, exc_type, exc_val, exc_tb):
80+
"""
81+
Exit the runtime context and close the internally managed session.
82+
83+
Allows the client to be used as an asynchronous context manager
84+
(e.g., `async with ToolboxClient(...) as client:`).
85+
"""
86+
await self.close()
87+
88+
async def close(self):
89+
"""
90+
Asynchronously closes the underlying client session. Doing so will cause
91+
any tools created by this Client to cease to function.
92+
93+
If the session was provided externally during initialization, the caller
94+
is responsible for its lifecycle, but calling close here will still
95+
attempt to close it.
96+
"""
97+
await self.__session.close()
98+
99+
async def load_tool(
100+
self,
101+
name: str,
102+
) -> ToolboxTool:
103+
"""
104+
Asynchronously loads a tool from the server.
105+
106+
Retrieves the schema for the specified tool from the Toolbox server and
107+
returns a callable object (`ToolboxTool`) that can be used to invoke the
108+
tool remotely.
109+
110+
Args:
111+
name: The unique name or identifier of the tool to load.
112+
113+
Returns:
114+
ToolboxTool: A callable object representing the loaded tool, ready
115+
for execution. The specific arguments and behavior of the callable
116+
depend on the tool itself.
117+
118+
"""
119+
120+
# request the definition of the tool from the server
121+
url = f"{self.__base_url}/api/tool/{name}"
122+
async with self.__session.get(url) as response:
123+
json = await response.json()
124+
manifest: ManifestSchema = ManifestSchema(**json)
125+
126+
# parse the provided definition to a tool
127+
if name not in manifest.tools:
128+
# TODO: Better exception
129+
raise Exception(f"Tool '{name}' not found!")
130+
tool = self.__parse_tool(name, manifest.tools[name])
131+
132+
return tool
133+
134+
async def load_toolset(
135+
self,
136+
name: str,
137+
) -> list[ToolboxTool]:
138+
"""
139+
Asynchronously fetches a toolset and loads all tools defined within it.
140+
141+
Args:
142+
name: Name of the toolset to load tools.
143+
144+
Returns:
145+
list[ToolboxTool]: A list of callables, one for each tool defined
146+
in the toolset.
147+
"""
148+
# Request the definition of the tool from the server
149+
url = f"{self.__base_url}/api/toolset/{name}"
150+
async with self.__session.get(url) as response:
151+
json = await response.json()
152+
manifest: ManifestSchema = ManifestSchema(**json)
153+
154+
# parse each tools name and schema into a list of ToolboxTools
155+
tools = [self.__parse_tool(n, s) for n, s in manifest.tools.items()]
156+
return tools

0 commit comments

Comments
 (0)