kosong.tooling.simple
1import asyncio 2import inspect 3import json 4from collections.abc import Iterable 5from typing import TYPE_CHECKING, Any, Self 6 7from kosong.message import ToolCall 8from kosong.tooling import ( 9 CallableTool, 10 CallableTool2, 11 HandleResult, 12 Tool, 13 ToolResult, 14 ToolReturnValue, 15 Toolset, 16) 17from kosong.tooling.error import ( 18 ToolNotFoundError, 19 ToolParseError, 20 ToolRuntimeError, 21) 22from kosong.utils.typing import JsonType 23 24if TYPE_CHECKING: 25 26 def type_check( 27 simple: "SimpleToolset", 28 ): 29 _: Toolset = simple 30 31 32type ToolType = CallableTool | CallableTool2[Any] 33"""The tool type that can be added to the `SimpleToolset`.""" 34 35 36class SimpleToolset: 37 """A simple toolset that can handle tool calls concurrently.""" 38 39 _tool_dict: dict[str, ToolType] 40 41 def __init__(self, tools: Iterable[ToolType] | None = None): 42 """Initialize the simple toolset with an optional iterable of tools.""" 43 self._tool_dict = {} 44 if tools: 45 for tool in tools: 46 self += tool 47 48 def __iadd__(self, tool: ToolType) -> Self: 49 """ 50 @public 51 Add a tool to the toolset. 52 """ 53 return_annotation = inspect.signature(tool.__call__).return_annotation 54 55 # Check if the return annotation is ToolReturnValue 56 # Supports both actual type and string annotation (when using 57 # `from __future__ import annotations`) 58 if return_annotation is ToolReturnValue: 59 pass 60 elif isinstance(return_annotation, str): 61 # String annotation - check if it matches ToolReturnValue 62 # Accept any suffix of the full module path, e.g.: 63 # "ToolReturnValue", "tooling.ToolReturnValue", "kosong.tooling.ToolReturnValue" 64 full_name = f"{ToolReturnValue.__module__}.ToolReturnValue" 65 full_parts = full_name.split(".") 66 if not any( 67 return_annotation == ".".join(full_parts[i:]) for i in range(len(full_parts)) 68 ): 69 raise TypeError( 70 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 71 f"but got `{return_annotation}`" 72 ) 73 else: 74 raise TypeError( 75 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 76 f"but got `{return_annotation}`" 77 ) 78 79 self._tool_dict[tool.name] = tool 80 return self 81 82 def __add__(self, tool: ToolType) -> "SimpleToolset": 83 """ 84 @public 85 Return a new toolset with the given tool added. 86 """ 87 new_toolset = SimpleToolset() 88 new_toolset._tool_dict = self._tool_dict.copy() 89 new_toolset += tool 90 return new_toolset 91 92 def add(self, tool: ToolType) -> None: 93 """ 94 @public 95 Add a tool to the toolset. 96 """ 97 self += tool 98 99 def remove(self, tool_name: str) -> None: 100 """ 101 @public 102 Remove a tool from the toolset. 103 """ 104 if tool_name not in self._tool_dict: 105 raise KeyError(f"Tool `{tool_name}` not found in the toolset.") 106 del self._tool_dict[tool_name] 107 108 @property 109 def tools(self) -> list[Tool]: 110 return [tool.base for tool in self._tool_dict.values()] 111 112 def handle(self, tool_call: ToolCall) -> HandleResult: 113 if tool_call.function.name not in self._tool_dict: 114 return ToolResult( 115 tool_call_id=tool_call.id, 116 return_value=ToolNotFoundError(tool_call.function.name), 117 ) 118 119 tool = self._tool_dict[tool_call.function.name] 120 121 try: 122 arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False) 123 except json.JSONDecodeError as e: 124 return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e))) 125 126 async def _call(): 127 try: 128 ret = await tool.call(arguments) 129 return ToolResult(tool_call_id=tool_call.id, return_value=ret) 130 except Exception as e: 131 return ToolResult(tool_call_id=tool_call.id, return_value=ToolRuntimeError(str(e))) 132 133 return asyncio.create_task(_call())
The tool type that can be added to the SimpleToolset.
class
SimpleToolset:
37class SimpleToolset: 38 """A simple toolset that can handle tool calls concurrently.""" 39 40 _tool_dict: dict[str, ToolType] 41 42 def __init__(self, tools: Iterable[ToolType] | None = None): 43 """Initialize the simple toolset with an optional iterable of tools.""" 44 self._tool_dict = {} 45 if tools: 46 for tool in tools: 47 self += tool 48 49 def __iadd__(self, tool: ToolType) -> Self: 50 """ 51 @public 52 Add a tool to the toolset. 53 """ 54 return_annotation = inspect.signature(tool.__call__).return_annotation 55 56 # Check if the return annotation is ToolReturnValue 57 # Supports both actual type and string annotation (when using 58 # `from __future__ import annotations`) 59 if return_annotation is ToolReturnValue: 60 pass 61 elif isinstance(return_annotation, str): 62 # String annotation - check if it matches ToolReturnValue 63 # Accept any suffix of the full module path, e.g.: 64 # "ToolReturnValue", "tooling.ToolReturnValue", "kosong.tooling.ToolReturnValue" 65 full_name = f"{ToolReturnValue.__module__}.ToolReturnValue" 66 full_parts = full_name.split(".") 67 if not any( 68 return_annotation == ".".join(full_parts[i:]) for i in range(len(full_parts)) 69 ): 70 raise TypeError( 71 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 72 f"but got `{return_annotation}`" 73 ) 74 else: 75 raise TypeError( 76 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 77 f"but got `{return_annotation}`" 78 ) 79 80 self._tool_dict[tool.name] = tool 81 return self 82 83 def __add__(self, tool: ToolType) -> "SimpleToolset": 84 """ 85 @public 86 Return a new toolset with the given tool added. 87 """ 88 new_toolset = SimpleToolset() 89 new_toolset._tool_dict = self._tool_dict.copy() 90 new_toolset += tool 91 return new_toolset 92 93 def add(self, tool: ToolType) -> None: 94 """ 95 @public 96 Add a tool to the toolset. 97 """ 98 self += tool 99 100 def remove(self, tool_name: str) -> None: 101 """ 102 @public 103 Remove a tool from the toolset. 104 """ 105 if tool_name not in self._tool_dict: 106 raise KeyError(f"Tool `{tool_name}` not found in the toolset.") 107 del self._tool_dict[tool_name] 108 109 @property 110 def tools(self) -> list[Tool]: 111 return [tool.base for tool in self._tool_dict.values()] 112 113 def handle(self, tool_call: ToolCall) -> HandleResult: 114 if tool_call.function.name not in self._tool_dict: 115 return ToolResult( 116 tool_call_id=tool_call.id, 117 return_value=ToolNotFoundError(tool_call.function.name), 118 ) 119 120 tool = self._tool_dict[tool_call.function.name] 121 122 try: 123 arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False) 124 except json.JSONDecodeError as e: 125 return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e))) 126 127 async def _call(): 128 try: 129 ret = await tool.call(arguments) 130 return ToolResult(tool_call_id=tool_call.id, return_value=ret) 131 except Exception as e: 132 return ToolResult(tool_call_id=tool_call.id, return_value=ToolRuntimeError(str(e))) 133 134 return asyncio.create_task(_call())
A simple toolset that can handle tool calls concurrently.
SimpleToolset(tools: Iterable[ToolType] | None = None)
42 def __init__(self, tools: Iterable[ToolType] | None = None): 43 """Initialize the simple toolset with an optional iterable of tools.""" 44 self._tool_dict = {} 45 if tools: 46 for tool in tools: 47 self += tool
Initialize the simple toolset with an optional iterable of tools.
def
__iadd__(self, tool: ToolType) -> Self:
49 def __iadd__(self, tool: ToolType) -> Self: 50 """ 51 @public 52 Add a tool to the toolset. 53 """ 54 return_annotation = inspect.signature(tool.__call__).return_annotation 55 56 # Check if the return annotation is ToolReturnValue 57 # Supports both actual type and string annotation (when using 58 # `from __future__ import annotations`) 59 if return_annotation is ToolReturnValue: 60 pass 61 elif isinstance(return_annotation, str): 62 # String annotation - check if it matches ToolReturnValue 63 # Accept any suffix of the full module path, e.g.: 64 # "ToolReturnValue", "tooling.ToolReturnValue", "kosong.tooling.ToolReturnValue" 65 full_name = f"{ToolReturnValue.__module__}.ToolReturnValue" 66 full_parts = full_name.split(".") 67 if not any( 68 return_annotation == ".".join(full_parts[i:]) for i in range(len(full_parts)) 69 ): 70 raise TypeError( 71 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 72 f"but got `{return_annotation}`" 73 ) 74 else: 75 raise TypeError( 76 f"Expected tool `{tool.name}` to return `ToolReturnValue`, " 77 f"but got `{return_annotation}`" 78 ) 79 80 self._tool_dict[tool.name] = tool 81 return self
Add a tool to the toolset.
83 def __add__(self, tool: ToolType) -> "SimpleToolset": 84 """ 85 @public 86 Return a new toolset with the given tool added. 87 """ 88 new_toolset = SimpleToolset() 89 new_toolset._tool_dict = self._tool_dict.copy() 90 new_toolset += tool 91 return new_toolset
Return a new toolset with the given tool added.
def
add(self, tool: ToolType) -> None:
93 def add(self, tool: ToolType) -> None: 94 """ 95 @public 96 Add a tool to the toolset. 97 """ 98 self += tool
Add a tool to the toolset.
def
remove(self, tool_name: str) -> None:
100 def remove(self, tool_name: str) -> None: 101 """ 102 @public 103 Remove a tool from the toolset. 104 """ 105 if tool_name not in self._tool_dict: 106 raise KeyError(f"Tool `{tool_name}` not found in the toolset.") 107 del self._tool_dict[tool_name]
Remove a tool from the toolset.
tools: list[kosong.tooling.Tool]
113 def handle(self, tool_call: ToolCall) -> HandleResult: 114 if tool_call.function.name not in self._tool_dict: 115 return ToolResult( 116 tool_call_id=tool_call.id, 117 return_value=ToolNotFoundError(tool_call.function.name), 118 ) 119 120 tool = self._tool_dict[tool_call.function.name] 121 122 try: 123 arguments: JsonType = json.loads(tool_call.function.arguments or "{}", strict=False) 124 except json.JSONDecodeError as e: 125 return ToolResult(tool_call_id=tool_call.id, return_value=ToolParseError(str(e))) 126 127 async def _call(): 128 try: 129 ret = await tool.call(arguments) 130 return ToolResult(tool_call_id=tool_call.id, return_value=ret) 131 except Exception as e: 132 return ToolResult(tool_call_id=tool_call.id, return_value=ToolRuntimeError(str(e))) 133 134 return asyncio.create_task(_call())