Skip to content

Commit 2378598

Browse files
authored
feat(toolbox-langchain): Enable sync and async context management for ToolboxClient (#308)
* feat(toolbox-langchain): Add context manager support for sync and async clients * docs(toolbox-langchain): Update README to guide using context manager
1 parent 14e4284 commit 2378598

File tree

3 files changed

+110
-39
lines changed

3 files changed

+110
-39
lines changed

packages/toolbox-langchain/README.md

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,20 @@ from toolbox_langchain import ToolboxClient
5757
from langchain_google_vertexai import ChatVertexAI
5858
from langgraph.prebuilt import create_react_agent
5959

60-
toolbox = ToolboxClient("http://127.0.0.1:5000")
61-
tools = toolbox.load_toolset()
60+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
61+
tools = toolbox.load_toolset()
6262

63-
model = ChatVertexAI(model="gemini-2.0-flash-001")
64-
agent = create_react_agent(model, tools)
63+
model = ChatVertexAI(model="gemini-2.0-flash-001")
64+
agent = create_react_agent(model, tools)
6565

66-
prompt = "How's the weather today?"
66+
prompt = "How's the weather today?"
6767

68-
for s in agent.stream({"messages": [("user", prompt)]}, stream_mode="values"):
69-
message = s["messages"][-1]
70-
if isinstance(message, tuple):
71-
print(message)
72-
else:
73-
message.pretty_print()
68+
for s in agent.stream({"messages": [("user", prompt)]}, stream_mode="values"):
69+
message = s["messages"][-1]
70+
if isinstance(message, tuple):
71+
print(message)
72+
else:
73+
message.pretty_print()
7474
```
7575

7676
> [!TIP]
@@ -86,7 +86,7 @@ Import and initialize the toolbox client.
8686
from toolbox_langchain import ToolboxClient
8787

8888
# Replace with your Toolbox service's URL
89-
toolbox = ToolboxClient("http://127.0.0.1:5000")
89+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
9090
```
9191

9292
## Loading Tools
@@ -241,10 +241,10 @@ You can configure these dynamic headers as follows:
241241
```python
242242
from toolbox_langchain import ToolboxClient
243243

244-
client = ToolboxClient(
244+
async with ToolboxClient(
245245
"toolbox-url",
246246
client_headers={"header1": header1_getter, "header2": header2_getter, ...}
247-
)
247+
) as client:
248248
```
249249

250250
### Authenticating with Google Cloud Servers
@@ -273,13 +273,13 @@ For Toolbox servers hosted on Google Cloud (e.g., Cloud Run) and requiring
273273
from toolbox_core import auth_methods
274274

275275
auth_token_provider = auth_methods.aget_google_id_token # can also use sync method
276-
client = ToolboxClient(
276+
async with ToolboxClient(
277277
URL,
278278
client_headers={"Authorization": auth_token_provider},
279-
)
280-
tools = client.load_toolset()
279+
) as client:
280+
tools = client.load_toolset()
281281

282-
# Now, you can use the client as usual.
282+
# Now, you can use the client as usual.
283283
```
284284

285285

@@ -319,16 +319,16 @@ async def get_auth_token():
319319
#### Add Authentication to a Tool
320320

321321
```py
322-
toolbox = ToolboxClient("http://127.0.0.1:5000")
323-
tools = toolbox.load_toolset()
322+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
323+
tools = toolbox.load_toolset()
324324

325-
auth_tool = tools[0].add_auth_token_getter("my_auth", get_auth_token) # Single token
325+
auth_tool = tools[0].add_auth_token_getter("my_auth", get_auth_token) # Single token
326326

327-
multi_auth_tool = tools[0].add_auth_token_getters({"auth_1": get_auth_1}, {"auth_2": get_auth_2}) # Multiple tokens
327+
multi_auth_tool = tools[0].add_auth_token_getters({"auth_1": get_auth_1}, {"auth_2": get_auth_2}) # Multiple tokens
328328

329-
# OR
329+
# OR
330330

331-
auth_tools = [tool.add_auth_token_getter("my_auth", get_auth_token) for tool in tools]
331+
auth_tools = [tool.add_auth_token_getter("my_auth", get_auth_token) for tool in tools]
332332
```
333333

334334
#### Add Authentication While Loading
@@ -354,12 +354,12 @@ async def get_auth_token():
354354
# This example just returns a placeholder. Replace with your actual token retrieval.
355355
return "YOUR_ID_TOKEN" # Placeholder
356356

357-
toolbox = ToolboxClient("http://127.0.0.1:5000")
358-
tool = toolbox.load_tool("my-tool")
357+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
358+
tool = toolbox.load_tool("my-tool")
359359

360-
auth_tool = tool.add_auth_token_getter("my_auth", get_auth_token)
361-
result = auth_tool.invoke({"input": "some input"})
362-
print(result)
360+
auth_tool = tool.add_auth_token_getter("my_auth", get_auth_token)
361+
result = auth_tool.invoke({"input": "some input"})
362+
print(result)
363363
```
364364

365365
## Binding Parameter Values
@@ -374,16 +374,16 @@ modified by the LLM. This is useful for:
374374
### Binding Parameters to a Tool
375375

376376
```py
377-
toolbox = ToolboxClient("http://127.0.0.1:5000")
378-
tools = toolbox.load_toolset()
377+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
378+
tools = toolbox.load_toolset()
379379

380-
bound_tool = tool[0].bind_param("param", "value") # Single param
380+
bound_tool = tool[0].bind_param("param", "value") # Single param
381381

382-
multi_bound_tool = tools[0].bind_params({"param1": "value1", "param2": "value2"}) # Multiple params
382+
multi_bound_tool = tools[0].bind_params({"param1": "value1", "param2": "value2"}) # Multiple params
383383

384-
# OR
384+
# OR
385385

386-
bound_tools = [tool.bind_param("param", "value") for tool in tools]
386+
bound_tools = [tool.bind_param("param", "value") for tool in tools]
387387
```
388388

389389
### Binding Parameters While Loading
@@ -429,10 +429,10 @@ import asyncio
429429
from toolbox_langchain import ToolboxClient
430430

431431
async def main():
432-
toolbox = ToolboxClient("http://127.0.0.1:5000")
433-
tool = await client.aload_tool("my-tool")
434-
tools = await client.aload_toolset()
435-
response = await tool.ainvoke()
432+
async with ToolboxClient("http://127.0.0.1:5000") as toolbox:
433+
tool = await client.aload_tool("my-tool")
434+
tools = await client.aload_toolset()
435+
response = await tool.ainvoke()
436436

437437
if __name__ == "__main__":
438438
asyncio.run(main())

packages/toolbox-langchain/src/toolbox_langchain/async_client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,28 @@ def load_toolset(
190190
strict: bool = False,
191191
) -> list[AsyncToolboxTool]:
192192
raise NotImplementedError("Synchronous methods not supported by async client.")
193+
194+
async def close(self):
195+
"""Close the underlying synchronous client."""
196+
await self.__core_client.close()
197+
198+
async def __aenter__(self):
199+
"""
200+
Enter the runtime context related to this client instance.
201+
202+
Allows the client to be used as an asynchronous context manager
203+
(e.g., `async with AsyncToolboxClient(...) as client:`).
204+
205+
Returns:
206+
self: The client instance itself.
207+
"""
208+
return self
209+
210+
async def __aexit__(self, exc_type, exc_val, exc_tb):
211+
"""
212+
Exit the runtime context and close the internally managed session.
213+
214+
Allows the client to be used as an asynchronous context manager
215+
(e.g., `async with AsyncToolboxClient(...) as client:`).
216+
"""
217+
await self.close()

packages/toolbox-langchain/src/toolbox_langchain/client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,49 @@ def load_toolset(
291291
for core_sync_tool in core_sync_tools:
292292
tools.append(ToolboxTool(core_tool=core_sync_tool))
293293
return tools
294+
295+
def close(self):
296+
"""Close the underlying synchronous client."""
297+
self.__core_client.close()
298+
299+
async def __aenter__(self):
300+
"""
301+
Enter the runtime context related to this client instance.
302+
303+
Allows the client to be used as an asynchronous context manager
304+
(e.g., `async with ToolboxClient(...) as client:`).
305+
306+
Returns:
307+
self: The client instance itself.
308+
"""
309+
return self
310+
311+
async def __aexit__(self, exc_type, exc_val, exc_tb):
312+
"""
313+
Exit the runtime context and close the internally managed session.
314+
315+
Allows the client to be used as an asynchronous context manager
316+
(e.g., `async with ToolboxClient(...) as client:`).
317+
"""
318+
self.close()
319+
320+
def __enter__(self):
321+
"""
322+
Enter the runtime context related to this client instance.
323+
324+
Allows the client to be used as a context manager
325+
(e.g., `with ToolboxClient(...) as client:`).
326+
327+
Returns:
328+
self: The client instance itself.
329+
"""
330+
return self
331+
332+
def __exit__(self, exc_type, exc_val, exc_tb):
333+
"""
334+
Exit the runtime context and close the internally managed session.
335+
336+
Allows the client to be used as a context manager
337+
(e.g., `with ToolboxClient(...) as client:`).
338+
"""
339+
self.close()

0 commit comments

Comments
 (0)