Adding Tools to Modules
Any module can expose tools to the AI agent by declaring them in module.json and implementing a bus handler. The agent discovers tools automatically — no changes to the AI module or Python service required.
How It Works
module.json (api.provides) → ModuleApiRegistry → AgentGatewayService → Python Agent → Tool Call → ModuleBus → Handler
- Module declares tools in
module.jsonunderapi.provides ModuleBusServiceProviderregisters them inModuleApiRegistryon boot- When a user chats with the agent,
AgentGatewayServicereads all registered tools - Tools are filtered by role patterns and user RBAC permissions
- Matching tools are sent to the Python agent as
ToolPermissionobjects - When the agent decides to call a tool, it POSTs back to Laravel
AgentBridgeControllerroutes the call throughModuleApiBusto the handler
Step 1: Define Tools in module.json
Add an api.provides section to your module's module.json:
{
"name": "Customers",
"alias": "customers",
"api": {
"provides": {
"customers.get": {
"description": "Get customer details by ID including contact info and order history summary",
"handler": "Modules\\Customers\\App\\Services\\CustomerBusHandler",
"method": "getCustomer",
"mode": "sync"
},
"customers.search": {
"description": "Search customers by name, email, phone, or segment",
"handler": "Modules\\Customers\\App\\Services\\CustomerBusHandler",
"method": "searchCustomers",
"mode": "sync"
},
"customers.getOrderHistory": {
"description": "Get a customer's recent order history with status and totals",
"handler": "Modules\\Customers\\App\\Services\\CustomerBusHandler",
"method": "getOrderHistory",
"mode": "sync"
}
}
}
}
Tool naming convention
Use {module_alias}.{action} format:
- Read tools:
customers.get,customers.search,customers.getStats - Write tools:
customers.create,customers.update - Action tools:
orders.processReturn,orders.reserveInventory
The name determines what RBAC permission is inferred (see Permission Inference).
Writing good descriptions
The description is the primary way the LLM decides whether to call a tool. Write descriptions that:
- Explain what data the tool returns or what action it performs
- Mention the parameters it accepts (the LLM uses this to construct arguments)
- Are specific enough to differentiate from similar tools
// Bad — too vague
"description": "Get customer"
// Good — LLM knows what it gets and what to pass
"description": "Get customer details by ID including contact info, address, segment, VIP status, and recent order count"
Step 2: Implement the Bus Handler
Create a service class that handles the tool calls:
<?php
namespace Modules\Customers\App\Services;
use App\Models\Tenant;
class CustomerBusHandler
{
public function getCustomer(array $params): array
{
$customer = \Modules\Customers\App\Models\Customer::findOrFail($params['id']);
return [
'id' => $customer->id,
'name' => $customer->name,
'email' => $customer->email,
'phone' => $customer->phone,
'segment' => $customer->segment,
'vip' => $customer->is_vip,
'total_orders' => $customer->orders()->count(),
'lifetime_value' => $customer->lifetime_value,
'created_at' => $customer->created_at->toIso8601String(),
];
}
public function searchCustomers(array $params): array
{
$query = \Modules\Customers\App\Models\Customer::query();
if (isset($params['q'])) {
$q = $params['q'];
$query->where(function ($qb) use ($q) {
$qb->where('name', 'ilike', "%{$q}%")
->orWhere('email', 'ilike', "%{$q}%")
->orWhere('phone', 'like', "%{$q}%");
});
}
if (isset($params['segment'])) {
$query->where('segment', $params['segment']);
}
return $query->limit(10)->get()->map(fn ($c) => [
'id' => $c->id,
'name' => $c->name,
'email' => $c->email,
'segment' => $c->segment,
])->toArray();
}
public function getOrderHistory(array $params): array
{
$customer = \Modules\Customers\App\Models\Customer::findOrFail($params['customer_id']);
return $customer->orders()
->latest()
->limit($params['limit'] ?? 10)
->get()
->map(fn ($o) => [
'id' => $o->id,
'order_number' => $o->order_number,
'status' => $o->status,
'total' => $o->total,
'created_at' => $o->created_at->toIso8601String(),
])->toArray();
}
}
Handler rules
- Method receives a single
array $paramsargument (the tool arguments from the LLM) - Return an array — it's JSON-encoded and sent back to the Python agent
- Keep responses concise — large payloads waste LLM context tokens
- Handle missing/invalid parameters gracefully with clear error messages
- The handler runs in the tenant's database context (tenancy is already initialized)
Step 3: Register in Composer Autoload
Ensure your module has a PSR-4 autoload entry in backend/composer.json:
{
"autoload": {
"psr-4": {
"Modules\\Customers\\": "modules/Customers/backend/"
}
}
}
Run composer dump-autoload after adding.
Step 4: Assign to Agent Roles
Update agent roles to include the new tools. Either:
Via API:
PUT /api/v1/ai/agent/roles/support
{
"allowed_tools": ["orders.get", "orders.getStats", "customers.*"]
}
Or via default role config in AgentRoleController.php.
Wildcard customers.* matches all tools with the customers. prefix.
Parameter Flow
When the LLM calls a tool, parameters flow through this chain:
LLM generates JSON arguments
→ Python Tool.execute(**kwargs)
→ ToolBridge.execute_tool(name, kwargs)
→ POST /api/v1/internal/agent/tools/{name} { arguments: kwargs }
→ AgentBridgeController::executeTool()
→ ModuleApiBus::call(module, method, arguments)
→ Handler::method(array $params)
The LLM constructs the kwargs based on the tool description. There is no schema validation between the LLM output and the handler — the handler should validate/default parameters defensively.
Adding parameter hints
To help the LLM pass correct parameters, describe them in the tool description:
{
"customers.search": {
"description": "Search customers. Parameters: q (string, search query), segment (string, optional: vip/regular/new), limit (int, optional, default 10)"
}
}
RBAC Integration
Tools automatically integrate with the existing tenant permission system:
| Tool Name | Inferred Permission | Who Can Use |
|---|---|---|
customers.get |
customers.view |
Users with customers.view |
customers.search |
customers.view |
Users with customers.view |
customers.create |
customers.create |
Users with customers.create |
customers.update |
customers.manage |
Users with customers.manage |
No additional permission configuration needed — the Python agent infers requirements from tool names automatically.
Testing a Tool
- Add the tool to
module.jsonand create the handler - Run
php artisan module:syncto update the database - Assign the tool pattern to a role (e.g.,
customers.*for support) - Open the AI chat widget and ask a question that would trigger the tool
- Check Laravel logs for
ModuleBus: Non-module caller accessingto confirm execution - The agent's response should include data from your handler
Existing Module Tools Reference
See the full list of registered tools in Roles & Tools — Modules with Tools.