Custom Toolkits
Bundle related tool functions into reusable toolkit classes.
Many advanced use-cases will require writing custom Toolkits. A Toolkit is a collection of functions that can be added to an Agent. The functions in a Toolkit are designed to work together, share internal state and provide a better development experience.
Here's the general flow:
- Create a class inheriting the
kern.tools.Toolkitclass. - Add your functions to the class.
- Include all the functions in the
toolsargument to theToolkitconstructor.
For example:
1from typing import List23from kern.agent import Agent4from kern.tools import Toolkit5from kern.utils.log import logger67class ShellTools(Toolkit):8 def __init__(self, working_directory: str = "/", **kwargs):9 self.working_directory = working_directory1011 tools = [12 self.run_shell_command,13 ]1415 super().__init__(name="shell_tools", tools=tools, **kwargs)16 17 def list_files(self, directory: str):18 """19 List the files in the given directory.2021 Args:22 directory (str): The directory to list the files from.23 Returns:24 str: The list of files in the directory.25 """26 import os2728 # List files relative to the toolkit's working_directory29 path = os.path.join(self.working_directory, directory)30 try:31 files = os.listdir(path)32 return "\n".join(files)33 except Exception as e:34 logger.warning(f"Failed to list files in {path}: {e}")35 return f"Error: {e}"36 return os.listdir(directory)3738 def run_shell_command(self, args: List[str], tail: int = 100) -> str:39 """40 Runs a shell command and returns the output or error.4142 Args:43 args (List[str]): The command to run as a list of strings.44 tail (int): The number of lines to return from the output.45 Returns:46 str: The output of the command.47 """48 import subprocess4950 logger.info(f"Running shell command: {args}")51 try:52 logger.info(f"Running shell command: {args}")53 result = subprocess.run(args, capture_output=True, text=True, cwd=self.working_directory)54 logger.debug(f"Result: {result}")55 logger.debug(f"Return code: {result.returncode}")56 if result.returncode != 0:57 return f"Error: {result.stderr}"58 # return only the last n lines of the output59 return "\n".join(result.stdout.split("\n")[-tail:])60 except Exception as e:61 logger.warning(f"Failed to run shell command: {e}")62 return f"Error: {e}"6364agent = Agent(tools=[ShellTools()], markdown=True)65agent.print_response("List all the files in my home directory.")Adding Async Methods
Any toolkit can include async methods alongside sync methods. For operations that benefit from async execution (like HTTP requests, database queries, or browser automation), you can provide both sync and async variants of your tools. The framework automatically uses the appropriate version based on the execution context:
agent.run()/agent.print_response()→ uses sync toolsagent.arun()/agent.aprint_response()→ uses async tools if available, otherwise falls back to sync tools
To add async tools to your Toolkits, use the async_tools parameter:
1from typing import Any, Dict23from kern.agent import Agent4from kern.tools import Toolkit56try:7 import httpx8except ImportError:9 raise ImportError("`httpx` not installed. Run `uv pip install httpx`")101112class APITools(Toolkit):13 def __init__(self, base_url: str, timeout: float = 30.0, **kwargs):14 self.base_url = base_url15 self.timeout = timeout1617 # Sync tools for agent.run() and agent.print_response()18 tools = [19 self.fetch_data,20 self.post_data,21 ]2223 # Async tools for agent.arun() and agent.aprint_response()24 # Format: (async_method, "tool_name")25 async_tools = [26 (self.afetch_data, "fetch_data"),27 (self.apost_data, "post_data"),28 ]2930 super().__init__(name="api_tools", tools=tools, async_tools=async_tools, **kwargs)3132 # Sync methods33 def fetch_data(self, endpoint: str) -> Dict[str, Any]:34 """35 Fetch data from an API endpoint.3637 Args:38 endpoint: The API endpoint to fetch data from (e.g., "/users/123")39 Returns:40 The JSON response from the API41 """42 url = f"{self.base_url}{endpoint}"43 with httpx.Client(timeout=self.timeout) as client:44 response = client.get(url)45 response.raise_for_status()46 return response.json()4748 def post_data(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:49 """50 Post data to an API endpoint.5152 Args:53 endpoint: The API endpoint to post data to54 data: The data to post as JSON55 Returns:56 The JSON response from the API57 """58 url = f"{self.base_url}{endpoint}"59 with httpx.Client(timeout=self.timeout) as client:60 response = client.post(url, json=data)61 response.raise_for_status()62 return response.json()6364 # Async methods (used automatically in async contexts)65 async def afetch_data(self, endpoint: str) -> Dict[str, Any]:66 """67 Fetch data from an API endpoint asynchronously.6869 Args:70 endpoint: The API endpoint to fetch data from (e.g., "/users/123")71 Returns:72 The JSON response from the API73 """74 url = f"{self.base_url}{endpoint}"75 async with httpx.AsyncClient(timeout=self.timeout) as client:76 response = await client.get(url)77 response.raise_for_status()78 return response.json()7980 async def apost_data(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:81 """82 Post data to an API endpoint asynchronously.8384 Args:85 endpoint: The API endpoint to post data to86 data: The data to post as JSON87 Returns:88 The JSON response from the API89 """90 url = f"{self.base_url}{endpoint}"91 async with httpx.AsyncClient(timeout=self.timeout) as client:92 response = await client.post(url, json=data)93 response.raise_for_status()94 return response.json()9596# Create the agent with the toolkit (using JSONPlaceholder - a free fake API for testing)97agent = Agent(tools=[APITools(base_url="https://jsonplaceholder.typicode.com")], markdown=True)9899# Sync usage - uses fetch_data100agent.print_response("Fetch the user with ID 1")101102# Async usage - uses afetch_data automatically103import asyncio104asyncio.run(agent.aprint_response("Fetch the post with ID 1"))The async_tools parameter takes a list of tuples where each tuple contains:
- The async method reference
- The tool name (should match the sync tool name for automatic switching)
The function name of the async tool is different but we register it with same name as the sync function that the LLM sees.
Example: In the above code block, the async tool is afetch_data but the LLM sees it as fetch_data.
Important Tips:
- Fill in the docstrings for each function with detailed descriptions of the function and its arguments.
- Remember that this function is provided to the LLM and is not used elsewhere in code, so the docstring should make sense to an LLM and the name of the functions need to be descriptive.
See the Toolkit Reference for more details.