Skip to content

Wip #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 69 commits into from
Closed

Wip #241

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
9e614b3
chore: Simplify sync_client fixture
anubhav756 May 16, 2025
95a752a
chore: Add unit test cases
anubhav756 May 6, 2025
e96f209
chore: Delint
anubhav756 May 6, 2025
7f67ef8
feat: Warn on insecure tool invocation with authentication
anubhav756 May 6, 2025
014828d
fix!: Warn about https only during tool initialization
anubhav756 May 13, 2025
d1e99a1
fix(toolbox-core): Add strict flag validation to sync client.
anubhav756 May 8, 2025
19acdd3
chore: delint
anubhav756 May 8, 2025
135fd43
chore: Simplify add_headers to make it sync in async client
anubhav756 May 8, 2025
2d2a421
chore: Fix unit tests
anubhav756 May 8, 2025
1e5e9cc
chore: Remove unused typevar variables
anubhav756 May 8, 2025
d8073bd
fix!: Make unauth call error message as PermissionError
anubhav756 May 12, 2025
aec3329
chore: Fix tests
anubhav756 May 13, 2025
1ec0775
fix: Make protected members private
anubhav756 May 9, 2025
661ab6d
chore: Expose members of sync client as well
anubhav756 May 9, 2025
7b8873e
chore: Fix types
anubhav756 May 9, 2025
a24016a
chore: Fix types
anubhav756 May 9, 2025
a5c0f2e
chore: Expose pydantic model from async tool
anubhav756 May 9, 2025
7d342b8
chore: Add test for pydantic_model property
anubhav756 May 12, 2025
83dc31c
chore: Add test coverage for additional @property methods
anubhav756 May 12, 2025
bd815fa
chore: Add unit test coverage for internal properties
anubhav756 May 14, 2025
ce32c56
feat: Make name optional while loading toolset through sync client
anubhav756 May 9, 2025
8a5f738
chore: Define precedence for deprecated 'auth_tokens' vs. 'auth_headers'
anubhav756 May 10, 2025
23cbf59
fix(toolbox-langchain)!: Base toolbox-langchain over toolbox-core
anubhav756 May 8, 2025
05d6bd4
fix: add toolbox-core as package dependency
anubhav756 May 8, 2025
3d09086
fix: Base sync client
anubhav756 May 8, 2025
62823d5
fix: Fix running background asyncio in current loop issue
anubhav756 May 8, 2025
d302420
fix: Base toolbox sync & async tools to toolbox core counterparts
anubhav756 May 8, 2025
08cfefa
fix: Fix getting pydantic model from ToolboxSyncTool
anubhav756 May 8, 2025
37e37bd
fix: Fix issue causing async core tools for creating sync tools
anubhav756 May 8, 2025
02e9297
fix: Fix reading name from correct param
anubhav756 May 8, 2025
e1af8ea
fix: Fix issue of unknown parameter due to pydantic initialization
anubhav756 May 8, 2025
3f042d2
fix: Fix nit error + add comment
anubhav756 May 8, 2025
efead1f
fix: Fix sync tool name assertion
anubhav756 May 8, 2025
62ebf0b
chore: Temporarily remove unittests
anubhav756 May 8, 2025
5a43b51
chore: Remove unused strict flag + fix default values + fix docstring
anubhav756 May 8, 2025
d20a086
fix: Update package to be from git repo
anubhav756 May 9, 2025
f2b944a
fix: Fix toolbox-core package local path
anubhav756 May 9, 2025
de772f3
fix: Fix local package path
anubhav756 May 9, 2025
5ae177a
fix: Update git path
anubhav756 May 9, 2025
4efe737
fix: Fix tests
anubhav756 May 9, 2025
617b2cb
fix: Fix using correct object for fetching loop
anubhav756 May 9, 2025
67e5ab6
fix: Return invoke result
anubhav756 May 9, 2025
b4aa63e
fix: Integration test errors
anubhav756 May 9, 2025
b872a49
chore: Delint
anubhav756 May 9, 2025
5bbfcaf
fix: Fix integration test
anubhav756 May 9, 2025
e0b7e68
fix: Fix integration tests
anubhav756 May 9, 2025
a734c4c
chore: Add unit tests previously deleted
anubhav756 May 9, 2025
ca4f47b
chore: Delint
anubhav756 May 9, 2025
a56540b
fix: Fix using correct protected member variables
anubhav756 May 9, 2025
faf2c06
chore: Delint
anubhav756 May 9, 2025
7978bd1
chore: Fix types
anubhav756 May 9, 2025
4a5429d
fix: Ensure bg loop/thread not null
anubhav756 May 9, 2025
31f5cd4
fix: Check bg loop/thread value
anubhav756 May 9, 2025
d897b43
fix: Revert warnings to prefer auth_tokens over auth_headers
anubhav756 May 9, 2025
12850d3
chore: Update unittests
anubhav756 May 9, 2025
144c899
docs: Improve docstrings
anubhav756 May 10, 2025
4b51d66
chore: Add TODO note
anubhav756 May 10, 2025
7e9308c
chore: Improve TODO note
anubhav756 May 10, 2025
973234c
fix: Fix integration test
anubhav756 May 14, 2025
3f9242d
chore: Delint
anubhav756 May 14, 2025
4ba761a
chore: Rename internal member variable names to be more concise
anubhav756 May 14, 2025
1f00b5f
chore: Delint
anubhav756 May 14, 2025
cbc39a0
chore: Add toolbox actual package version in toml and local path in r…
anubhav756 May 15, 2025
e23b278
fix: Fix editable toolbox-core package path in requirements.txt
anubhav756 May 15, 2025
a03f9bd
fix: Fix lowest supported version until released
anubhav756 May 15, 2025
3fba0f4
fix: Fix issue causing relative path in requirements.txt to cause issues
anubhav756 May 15, 2025
d4d33e6
docs: Fix issue README
anubhav756 May 15, 2025
e859a54
chore: Restore add_auth_token(s) as deprecated for backward compatibi…
anubhav756 May 10, 2025
58433ef
test: WIP for E2E tests
anubhav756 May 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/toolbox-core/src/toolbox_core/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,11 @@ async def load_toolset(

return tools

async def add_headers(
def add_headers(
self, headers: Mapping[str, Union[Callable, Coroutine, str]]
) -> None:
"""
Asynchronously Add headers to be included in each request sent through this client.
Add headers to be included in each request sent through this client.

Args:
headers: Headers to include in each request sent through this client.
Expand Down
100 changes: 71 additions & 29 deletions packages/toolbox-core/src/toolbox_core/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@


import asyncio
from asyncio import AbstractEventLoop
from threading import Thread
from typing import Any, Callable, Coroutine, Mapping, Optional, TypeVar, Union
from typing import Any, Callable, Coroutine, Mapping, Optional, Union

from .client import ToolboxClient
from .sync_tool import ToolboxSyncTool

T = TypeVar("T")
from concurrent.futures import Future


class ToolboxSyncClient:
Expand All @@ -31,7 +31,7 @@ class ToolboxSyncClient:
service endpoint.
"""

__loop: Optional[asyncio.AbstractEventLoop] = None
__loop: Optional[AbstractEventLoop] = None
__thread: Optional[Thread] = None

def __init__(
Expand All @@ -58,11 +58,22 @@ def __init__(
async def create_client():
return ToolboxClient(url, client_headers=client_headers)

# Ignoring type since we're already checking the existence of a loop above.
self.__async_client = asyncio.run_coroutine_threadsafe(
create_client(), self.__class__.__loop
).result()

@property
def _async_client(self) -> ToolboxClient:
return self.__async_client

@property
def _loop(self) -> Optional[AbstractEventLoop]:
return self.__class__.__loop

@property
def _thread(self) -> Optional[Thread]:
return self.__class__.__thread

def close(self):
"""
Synchronously closes the underlying client session. Doing so will cause
Expand All @@ -75,6 +86,46 @@ def close(self):
coro = self.__async_client.close()
asyncio.run_coroutine_threadsafe(coro, self.__loop).result()

def load_tool_future(
self,
name: str,
auth_token_getters: dict[str, Callable[[], str]] = {},
bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {},
) -> Future[ToolboxSyncTool]:
"""
Returns a future that loads a tool from the server.
"""
if not self.__loop or not self.__thread:
raise ValueError("Background loop or thread cannot be None.")

async def async_worker() -> ToolboxSyncTool:
async_tool = await self.__async_client.load_tool(name, auth_token_getters, bound_params)
return ToolboxSyncTool(async_tool, self.__loop, self.__thread)
return asyncio.run_coroutine_threadsafe(async_worker(), self.__loop)

def load_toolset_future(
self,
name: Optional[str] = None,
auth_token_getters: dict[str, Callable[[], str]] = {},
bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {},
strict: bool = False,
) -> Future[list[ToolboxSyncTool]]:
"""
Returns a future that fetches a toolset and loads all tools defined within it.
"""
if not self.__loop or not self.__thread:
raise ValueError("Background loop or thread cannot be None.")

async def async_worker() -> list[ToolboxSyncTool]:
async_tools = await self.__async_client.load_toolset(
name, auth_token_getters, bound_params, strict
)
return [
ToolboxSyncTool(async_tool, self.__loop, self.__thread)
for async_tool in async_tools
]
return asyncio.run_coroutine_threadsafe(async_worker(), self.__loop)

def load_tool(
self,
name: str,
Expand All @@ -100,61 +151,52 @@ def load_tool(
for execution. The specific arguments and behavior of the callable
depend on the tool itself.
"""
coro = self.__async_client.load_tool(name, auth_token_getters, bound_params)

if not self.__loop or not self.__thread:
raise ValueError("Background loop or thread cannot be None.")

async_tool = asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
return ToolboxSyncTool(async_tool, self.__loop, self.__thread)
return self.load_tool_future(name, auth_token_getters, bound_params).result()

def load_toolset(
self,
name: str,
name: Optional[str] = None,
auth_token_getters: dict[str, Callable[[], str]] = {},
bound_params: Mapping[str, Union[Callable[[], Any], Any]] = {},
strict: bool = False,
) -> list[ToolboxSyncTool]:
"""
Synchronously fetches a toolset and loads all tools defined within it.

Args:
name: Name of the toolset to load tools.
name: Name of the toolset to load. If None, loads the default toolset.
auth_token_getters: A mapping of authentication service names to
callables that return the corresponding authentication token.
bound_params: A mapping of parameter names to bind to specific values or
callables that are called to produce values as needed.
strict: If True, raises an error if *any* loaded tool instance fails
to utilize at least one provided parameter or auth token (if any
provided). If False (default), raises an error only if a
user-provided parameter or auth token cannot be applied to *any*
loaded tool across the set.

Returns:
list[ToolboxSyncTool]: A list of callables, one for each tool defined
in the toolset.
"""
coro = self.__async_client.load_toolset(name, auth_token_getters, bound_params)

if not self.__loop or not self.__thread:
raise ValueError("Background loop or thread cannot be None.")

async_tools = asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore
return [
ToolboxSyncTool(async_tool, self.__loop, self.__thread)
for async_tool in async_tools
]
Raises:
ValueError: If validation fails based on the `strict` flag.
"""
return self.load_toolset_future(name, auth_token_getters, bound_params, strict).result()

def add_headers(
self, headers: Mapping[str, Union[Callable, Coroutine, str]]
) -> None:
"""
Synchronously Add headers to be included in each request sent through this client.
Add headers to be included in each request sent through this client.

Args:
headers: Headers to include in each request sent through this client.

Raises:
ValueError: If any of the headers are already registered in the client.
"""
coro = self.__async_client.add_headers(headers)

# We have already created a new loop in the init method in case it does not already exist
asyncio.run_coroutine_threadsafe(coro, self.__loop).result() # type: ignore
self.__async_client.add_headers(headers)

def __enter__(self):
"""Enter the runtime context related to this client instance."""
Expand Down
27 changes: 22 additions & 5 deletions packages/toolbox-core/src/toolbox_core/sync_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
from asyncio import AbstractEventLoop
from inspect import Signature
from threading import Thread
from typing import Any, Callable, Coroutine, Mapping, Sequence, TypeVar, Union
from typing import Any, Callable, Coroutine, Mapping, Sequence, Union

from .protocol import ParameterSchema
from .tool import ToolboxTool

T = TypeVar("T")
from concurrent.futures import Future


class ToolboxSyncTool:
Expand Down Expand Up @@ -69,6 +68,18 @@ def __init__(
f"{self.__class__.__qualname__}.{self.__async_tool.__name__}"
)

@property
def _async_tool(self) -> ToolboxTool:
return self.__async_tool

@property
def _loop(self) -> AbstractEventLoop:
return self.__loop

@property
def _thread(self) -> Thread:
return self.__thread

@property
def __name__(self) -> str:
return self.__async_tool.__name__
Expand Down Expand Up @@ -119,6 +130,13 @@ def _auth_service_token_getters(self) -> Mapping[str, Callable[[], str]]:
def _client_headers(self) -> Mapping[str, Union[Callable, Coroutine, str]]:
return self.__async_tool._client_headers

def call_future(self, *args: Any, **kwargs: Any) -> Future[str]:
"""
Returns future that calls the remote tool with the provided arguments.
"""
coro = self.__async_tool(*args, **kwargs)
return asyncio.run_coroutine_threadsafe(coro, self.__loop)

def __call__(self, *args: Any, **kwargs: Any) -> str:
"""
Synchronously calls the remote tool with the provided arguments.
Expand All @@ -133,8 +151,7 @@ def __call__(self, *args: Any, **kwargs: Any) -> str:
Returns:
The string result returned by the remote tool execution.
"""
coro = self.__async_tool(*args, **kwargs)
return asyncio.run_coroutine_threadsafe(coro, self.__loop).result()
return self.call_future(*args, **kwargs).result()

def add_auth_token_getters(
self,
Expand Down
7 changes: 6 additions & 1 deletion packages/toolbox-core/src/toolbox_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from warnings import warn

from aiohttp import ClientSession
from pydantic import BaseModel

from .protocol import ParameterSchema
from .utils import (
Expand Down Expand Up @@ -158,6 +159,10 @@ def _auth_service_token_getters(self) -> Mapping[str, Callable[[], str]]:
def _client_headers(self) -> Mapping[str, Union[Callable, Coroutine, str]]:
return MappingProxyType(self.__client_headers)

@property
def _pydantic_model(self) -> type[BaseModel]:
return self.__pydantic_model

def __copy(
self,
session: Optional[ClientSession] = None,
Expand Down Expand Up @@ -239,7 +244,7 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str:
for s in self.__required_authn_params.values():
req_auth_services.update(s)
req_auth_services.update(self.__required_authz_tokens)
raise ValueError(
raise PermissionError(
f"One or more of the following authn services are required to invoke this tool"
f": {','.join(req_auth_services)}"
)
Expand Down
2 changes: 1 addition & 1 deletion packages/toolbox-core/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,7 @@ async def test_add_headers_success(
)

async with ToolboxClient(TEST_BASE_URL) as client:
await client.add_headers(static_header)
client.add_headers(static_header)
assert client._ToolboxClient__client_headers == static_header

tool = await client.load_tool(tool_name)
Expand Down
4 changes: 2 additions & 2 deletions packages/toolbox-core/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async def test_run_tool_no_auth(self, toolbox: ToolboxClient):
"""Tests running a tool requiring auth without providing auth."""
tool = await toolbox.load_tool("get-row-by-id-auth")
with pytest.raises(
Exception,
PermissionError,
match="One or more of the following authn services are required to invoke this tool: my-test-auth",
):
await tool(id="2")
Expand Down Expand Up @@ -188,7 +188,7 @@ async def test_run_tool_param_auth_no_auth(self, toolbox: ToolboxClient):
"""Tests running a tool with a param requiring auth, without auth."""
tool = await toolbox.load_tool("get-row-by-email-auth")
with pytest.raises(
ValueError,
PermissionError,
match="One or more of the following authn services are required to invoke this tool: my-test-auth",
):
await tool()
Expand Down
Loading
Loading