How to Define Custom Tools for Qwen-Agent: A Complete Developer Guide
Define custom tools for Qwen-Agent by subclassing BaseTool, implementing the call method, and either registering the class with the @register_tool decorator or passing an instance directly to the Assistant's function_list parameter.
Qwen-Agent extends large language model capabilities through a structured tool-calling framework that converts natural language requests into executable function invocations. To integrate proprietary APIs or custom business logic, developers must implement tools that conform to the framework's abstract base class and schema validation requirements. This guide explores the exact implementation patterns found in the Qwen-Agent repository, referencing specific source files and method signatures.
Understanding the Tool Architecture
The Qwen-Agent tool system relies on three core components that handle discovery, validation, and execution. Understanding these building blocks ensures your custom tools integrate seamlessly with the agent's function-calling loop.
The BaseTool Abstract Class
The BaseTool class in qwen_agent/tools/base.py (lines 9-27) serves as the foundation for all tools. It defines the required interface that every custom tool must implement, specifically the call(self, params, **kwargs) method that executes the tool's logic. The class also manages metadata through the function property, which generates an OpenAI-compatible function schema from the tool's name, description, and parameters attributes.
The register_tool Decorator
The @register_tool decorator, located in qwen_agent/tools/base.py (lines 44-60), inserts tool classes into a global TOOL_REGISTRY dictionary. This registration enables the Assistant class to instantiate tools dynamically by string name rather than requiring direct object references. The decorator accepts a unique identifier string that becomes the tool's canonical name throughout the system.
Assistant Integration and Validation
When initializing an Assistant (defined in qwen_agent/agent.py), the function_list parameter accepts a mixed list containing either registered tool names (strings) or pre-instantiated BaseTool objects. During execution, the framework validates incoming JSON arguments using BaseTool._verify_json_format_args (lines 40-63 in base.py) before invoking the tool's call method, ensuring type safety and required parameter presence.
Step-by-Step Implementation Guide
Follow these concrete steps to create functional custom tools that Qwen-Agent can discover and execute automatically.
1. Subclass BaseTool and Define Metadata
Create a Python class inheriting from BaseTool and specify the required class attributes. The parameters field must follow JSON Schema conventions to enable the LLM to generate valid arguments.
from qwen_agent.tools.base import BaseTool, register_tool
import json5
@register_tool('image_generator')
class ImageGenerator(BaseTool):
description = 'Generate images from text prompts using an external API.'
parameters = [{
'name': 'prompt',
'type': 'string',
'description': 'Detailed description of the image to generate.',
'required': True,
}]
2. Implement the call Method
The call method receives a JSON string (params) containing the function arguments and must return a string representing the tool's output. Parse the JSON using json5.loads() for robust handling of unquoted keys or trailing commas.
def call(self, params: str, **kwargs) -> str:
# Extract arguments from JSON string
args = json5.loads(params)
prompt = args['prompt']
# Execute tool logic (example: URL construction)
import urllib.parse
encoded = urllib.parse.quote(prompt)
image_url = f'https://image.pollinations.ai/prompt/{encoded}'
return json.dumps({'image_url': image_url}, ensure_ascii=False)
3. Register or Instantiate the Tool
You have two integration options depending on your architectural needs:
Option A: Registration by Name (Best for reusable tools)
Use the @register_tool('identifier') decorator as shown above, then reference the tool by string name in the Assistant configuration.
Option B: Direct Instance Pass (Best for configured instances)
Skip the decorator and pass an instantiated object directly to function_list:
from qwen_agent.agents import Assistant
# Instance approach allows per-agent configuration
custom_tool = ImageGenerator()
bot = Assistant(
llm={'model': 'qwen-max'},
function_list=[custom_tool, 'code_interpreter'] # Mix instances and registered names
)
4. Configure the Assistant
Add your tool to the Assistant initialization via the function_list parameter. This example from examples/assistant_add_custom_tool.py (lines 56-66) demonstrates combining custom and built-in tools:
def init_agent_service():
llm_cfg = {'model': 'qwen-max'}
system = ("According to the user's request, you first draw a picture "
"and then automatically run code to download the picture.")
bot = Assistant(
llm=llm_cfg,
name='AI painting',
description='AI painting service',
system_message=system,
function_list=[
'image_generator', # Custom registered tool
'code_interpreter', # Built-in tool
],
files=['doc.pdf'],
)
return bot
Practical Code Examples
These complete implementations demonstrate both registration patterns validated by the Qwen-Agent test suite.
Registered Tool Pattern
This implementation from examples/assistant_add_custom_tool.py shows the decorator-based approach with full parameter schema:
import json, json5, urllib.parse
from qwen_agent.tools.base import BaseTool, register_tool
@register_tool('my_image_gen')
class MyImageGen(BaseTool):
"""Generate an image URL from a textual prompt."""
description = 'AI painting service – input a description, get an image URL.'
parameters = [{
'name': 'prompt',
'type': 'string',
'description': 'English description of the desired image.',
'required': True,
}]
def call(self, params: str, **kwargs) -> str:
prompt = json5.loads(params)['prompt']
prompt = urllib.parse.quote(prompt)
return json.dumps(
{'image_url': f'https://image.pollinations.ai/prompt/{prompt}'},
ensure_ascii=False,
)
Instance-Based Pattern
For scenarios requiring runtime configuration, instantiate the tool directly as shown in tests/agents/test_custom_tool_object.py:
class MyImageGen(BaseTool):
name = 'my_image_gen'
description = 'Generate images from prompts.'
parameters = [{'name': 'prompt', 'type': 'string', 'required': True}]
def call(self, params: str, **kwargs) -> str:
# Tool implementation here
return json.dumps({'status': 'generated'})
def init_agent_service():
llm_cfg = {'model': 'qwen-max'}
system = "You must draw a picture with my_image_gen."
# Pass instance directly instead of string name
tools = [MyImageGen(), 'code_interpreter']
bot = Assistant(
llm=llm_cfg,
system_message=system,
function_list=tools
)
return bot
Minimal Working Example
A self-contained script demonstrating a text transformation tool:
import json, json5
from qwen_agent.agents import Assistant
from qwen_agent.tools.base import BaseTool, register_tool
@register_tool('uppercase')
class UpperCaseTool(BaseTool):
"""Convert text to uppercase."""
description = 'Transforms provided text to all caps.'
parameters = [{
'name': 'text',
'type': 'string',
'description': 'Input text to transform.',
'required': True,
}]
def call(self, params: str, **kwargs) -> str:
txt = json5.loads(params)['text']
return json.dumps({'result': txt.upper()}, ensure_ascii=False)
def main():
bot = Assistant(
llm={'model': 'qwen-max'},
system_message="Use the uppercase tool for text transformation requests.",
function_list=['uppercase'],
)
messages = [{'role': 'user', 'content': 'Convert "hello world" to uppercase'}]
for response in bot.run(messages=messages):
print(response)
if __name__ == '__main__':
main()
Summary
-
Subclass
BaseToolfromqwen_agent/tools/base.pyand implement thecall(self, params, **kwargs)method to define executable logic. -
Define JSON Schema in the
parametersclass attribute to instruct the LLM on valid argument structures; thefunctionproperty automatically converts this to OpenAI-compatible format. -
Register tools using
@register_tool('name')for global discoverability by string name, or pass tool instances directly tofunction_listfor agent-specific configurations. -
Validation occurs automatically via
BaseTool._verify_json_format_argsbefore each invocation, ensuring arguments match the declared schema. -
Integration happens at
Assistantinitialization through thefunction_listparameter, which accepts mixed lists of registered names and live instances.
Frequently Asked Questions
What is the difference between registering a tool and passing an instance?
Registering a tool with @register_tool adds the class to the global TOOL_REGISTRY, allowing you to reference it by string name across multiple agents. Passing an instance provides immediate object integration without global registration, useful for dependency injection or agent-specific tool configurations. Both approaches support the same execution flow and validation logic.
How does Qwen-Agent validate tool arguments?
The framework validates JSON arguments using BaseTool._verify_json_format_args before executing the call method. This internal method checks that required parameters exist and conform to the types specified in the tool's parameters schema, preventing malformed data from reaching your business logic.
Can I use custom tools without the register_tool decorator?
Yes, instantiation works without registration. Create an instance of your BaseTool subclass and pass it directly to the Assistant constructor's function_list parameter. This pattern appears in tests/agents/test_custom_tool_object.py and is ideal when you need to configure tool dependencies or state at initialization time.
Where are tool schemas defined for the LLM?
Tool schemas are defined in the parameters class attribute of your BaseTool subclass. This attribute accepts a list of dictionaries or a JSON Schema object describing each argument's name, type, description, and required status. The Assistant class consumes these schemas through each tool's function property to build the OpenAI-compatible function definitions sent to the LLM.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →