Create tools by adding the #[Tool] attribute to methods in your agent class — the simplest and most flexible way to define agent capabilities.
The #[Tool] attribute is the recommended way to create tools in LarAgent. It transforms regular PHP methods into tools that your agent can invoke, with automatic schema generation from type hints and descriptions.
Add the #[Tool] attribute to any method in your agent class:
Copy
use LarAgent\Agent;use LarAgent\Attributes\Tool;class WeatherAgent extends Agent{ protected $model = 'gpt-4o-mini'; public function instructions() { return 'You are a helpful weather assistant.'; } #[Tool('Get the current weather for a location')] public function getWeather(string $location, string $unit = 'celsius'): string { $weather = WeatherService::get($location, $unit); return "The weather in {$location} is {$weather['temp']} degrees {$unit}"; }}
LarAgent automatically:
Registers the method as a tool with the given description
Extracts parameter names and types from the method signature
Provide descriptions for each parameter to help the LLM understand what values to pass:
Copy
#[Tool( description: 'Get the current weather for a location', parameterDescriptions: [ 'location' => 'The city and state, e.g. San Francisco, CA', 'unit' => 'Temperature unit: celsius or fahrenheit' ])]public function getWeather(string $location, string $unit = 'celsius'): string{ return WeatherService::get($location, $unit);}
Clear parameter descriptions significantly improve the LLM’s ability to call tools correctly. Describing the expected format and providing examples is generally considered as best practice.
PHP Enums constrain parameter values to a specific set of options:
Copy
// app/Enums/TemperatureUnit.phpnamespace App\Enums;enum TemperatureUnit: string{ case Celsius = 'celsius'; case Fahrenheit = 'fahrenheit';}
Copy
use App\Enums\TemperatureUnit;#[Tool( description: 'Get the current weather for a location')]public function getWeather(string $location, TemperatureUnit $unit = TemperatureUnit::Celsius): string{ return WeatherService::get($location, $unit->value);}
LarAgent automatically generates an enum constraint in the OpenAPI schema before sending to the LLM:
Use DataModel classes as parameters for complex, structured input:
Copy
// app/DataModels/Address.phpnamespace App\DataModels;use LarAgent\Core\Abstractions\DataModel;use LarAgent\Attributes\Desc;class Address extends DataModel{ #[Desc('Street address')] public string $street; #[Desc('City name')] public string $city; #[Desc('State or province')] public string $state; #[Desc('Postal/ZIP code')] public string $postalCode; #[Desc('Country code (ISO 3166-1 alpha-2)')] public string $country = 'US';}
Copy
use App\DataModels\Address;#[Tool('Calculate shipping cost to an address')]public function calculateShipping(Address $address, float $weight): string{ // $address is automatically hydrated as an Address instance $zone = $this->getShippingZone($address->country, $address->state); $cost = $this->calculateCost($zone, $weight); return "Shipping to {$address->city}, {$address->state} costs \${$cost}";}
LarAgent automatically:
Detects the DataModel type hint
Generates a nested JSON schema from the DataModel properties
Converts the LLM’s array response back to a DataModel instance
DataModels can contain other DataModels for deeply structured data:
Copy
class ContactInfo extends DataModel{ #[Desc('Email address')] public string $email; #[Desc('Phone number')] public ?string $phone = null;}class Customer extends DataModel{ #[Desc('Customer full name')] public string $name; #[Desc('Contact information')] public ContactInfo $contact; #[Desc('Shipping address')] public Address $shippingAddress;}
Copy
#[Tool('Create a new customer order')]public function createOrder(Customer $customer, array $items): string{ // Access nested DataModels directly $email = $customer->contact->email; $city = $customer->shippingAddress->city; return "Order created for {$customer->name} in {$city}";}
Combine DataModels and Enums for type-safe structured input:
Copy
enum Priority: string{ case Low = 'low'; case Medium = 'medium'; case High = 'high'; case Urgent = 'urgent';}class SupportTicket extends DataModel{ #[Desc('Brief description of the issue')] public string $title; #[Desc('Detailed description')] public string $description; #[Desc('Priority level')] public Priority $priority = Priority::Medium;}
Copy
#[Tool('Create a support ticket')]public function createTicket(SupportTicket $ticket): string{ if ($ticket->priority === Priority::Urgent) { $this->notifyOnCall($ticket); } return "Ticket created: {$ticket->title} [{$ticket->priority->value}]";}
Use DataModelArray for parameters that accept multiple items:
DataModelArray is like a collection for DataModels, but strictly typed. See DataModel documentation for details.
Copy
use LarAgent\Core\Abstractions\DataModelArray;class LineItem extends DataModel{ #[Desc('Product SKU or ID')] public string $productId; #[Desc('Quantity to order')] public int $quantity; #[Desc('Unit price')] public float $price;}class LineItemArray extends DataModelArray{ public static function allowedModels(): array { return [LineItem::class]; } public function getTotal(): float { return array_reduce($this->items, fn($sum, $item) => $sum + ($item->quantity * $item->price), 0); }}
Copy
#[Tool('Process an order with multiple items')]public function processOrder(Customer $customer, LineItemArray $items): string{ $total = $items->getTotal(); return "Processing order for {$customer->name}: {$items->count()} items, total: \${$total}";}
DataModelArray also supports polymorphic arrays with multiple DataModel types using discriminator fields. See DataModel documentation for details.
PHP 8 union types allow parameters to accept multiple types:
Copy
#[Tool('Process payment')]public function processPayment( string $orderId, CreditCard|BankAccount|PayPalAccount $paymentMethod): string { // LarAgent generates a schema with oneOf for union types if ($paymentMethod instanceof CreditCard) { return $this->chargeCreditCard($orderId, $paymentMethod); } // Handle other payment types...}
Use instance methods when you need access to the agent instance ($this):
Copy
#[Tool('Get user preferences')]public function getUserPreferences(): array{ // Access agent properties or methods return $this->context->get('user_preferences', []);}
Use static methods for self-contained tools that don’t need agent context:
When your tools need data from outside the agent (e.g., from a controller), use custom setter methods:
Copy
namespace App\AiAgents;use LarAgent\Agent;use LarAgent\Attributes\Tool;use App\Models\User;class OrderAgent extends Agent{ protected $model = 'gpt-4o-mini'; protected ?User $currentUser = null; public function setUser(User $user): self { $this->currentUser = $user; return $this; } public function instructions() { return 'You help users manage their orders.'; } #[Tool('Get my recent orders')] public function getMyOrders(int $limit = 5): array { if (!$this->currentUser) { return ['error' => 'No user context available']; } return $this->currentUser->orders() ->latest() ->take($limit) ->get() ->toArray(); } #[Tool('Get my account balance')] public function getMyBalance(): string { if (!$this->currentUser) { return 'Unable to retrieve balance: no user context'; } return "Your current balance is \${$this->currentUser->balance}"; }}
Call the setter before interacting with the agent:
Copy
// In your controllerpublic function chat(Request $request){ $response = OrderAgent::for($request->user()->id) ->setUser($request->user()) ->respond($request->message); return response()->json(['message' => $response]);}
Return $this from setter methods to enable fluent chaining with other agent methods.
#[Tool('Get the current server time')]public function getServerTime(): string{ return now()->toIso8601String();}#[Tool('Get available inventory count')]public function getInventoryCount(): int{ return Product::where('in_stock', true)->count();}
It’s good practice to extract tool groups into traits for reusability and better organization:
Copy
// app/AiAgents/Traits/WeatherTools.phpnamespace App\AiAgents\Traits;use LarAgent\Attributes\Tool;use App\Enums\TemperatureUnit;trait WeatherTools{ #[Tool( description: 'Get the current weather for a location', parameterDescriptions: [ 'location' => 'The city and state, e.g. San Francisco, CA', 'unit' => 'Temperature unit' ] )] public function getWeather(string $location, TemperatureUnit $unit = TemperatureUnit::Celsius): string { return WeatherService::get($location, $unit->value); } #[Tool('Get the weather forecast for the next 5 days')] public function getForecast(string $location): array { return WeatherService::forecast($location, days: 5); }}
Use traits in your agents:
Copy
namespace App\AiAgents;use LarAgent\Agent;use App\AiAgents\Traits\WeatherTools;use App\AiAgents\Traits\CalendarTools;class PersonalAssistant extends Agent{ use WeatherTools, CalendarTools; protected $model = 'gpt-4o'; public function instructions() { return 'You are a personal assistant that can check weather and manage calendar events.'; }}
Copy
class TravelAgent extends Agent{ use WeatherTools; // Reuse weather tools protected $model = 'gpt-4o'; public function instructions() { return 'You help users plan trips and check destination weather.'; } #[Tool('Search for flights')] public function searchFlights(string $origin, string $destination, string $date): array { return FlightService::search($origin, $destination, $date); }}
Organizing tools into traits by domain (e.g., WeatherTools, CalendarTools, PaymentTools) creates a library of reusable capabilities you can mix and match across agents.
Tools can return various types — LarAgent converts them to strings for the LLM:
Copy
// String - returned as-is#[Tool('Get greeting')]public function getGreeting(): string{ return 'Hello, world!';}// Array - JSON encoded#[Tool('Get user data')]public function getUserData(int $userId): array{ return User::find($userId)->toArray();}// Object with __toString - converted to string#[Tool('Get order status')]public function getOrderStatus(string $orderId): Order{ return Order::find($orderId);}
Keep tool responses concise. Large responses consume tokens and may confuse the LLM. Return only the information needed to continue the conversation.
Tool and parameter descriptions are the LLM’s only guide for when and how to use tools. Be specific about:
What the tool does
Expected input formats
What the tool returns
Copy
// ❌ Vague#[Tool('Get data')]public function getData($id) { ... }// ✅ Clear#[Tool('Retrieve customer order history by customer ID')]public function getOrderHistory( string $customerId): array { ... }
Return informative error messages to LLM instead of throwing exceptions:
Copy
#[Tool('Get user by email')]public function getUser(string $email): string{ $user = User::where('email', $email)->first(); if (!$user) { return "No user found with email: {$email}"; } return json_encode($user->only(['id', 'name', 'email']));}
Keep tools focused
Each tool should do one thing well. Split complex operations into multiple tools:
Copy
// ❌ Too broad#[Tool('Manage orders')]public function manageOrder($action, $orderId, $data) { ... }// ✅ Focused#[Tool('Create a new order')]public function createOrder(Customer $customer, LineItemArray $items) { ... }#[Tool('Cancel an existing order')]public function cancelOrder(string $orderId, string $reason) { ... }#[Tool('Get order status')]public function getOrderStatus(string $orderId) { ... }
Limit the number of tools
Too many tools can overwhelm the LLM and lead to poor tool selection. As a general guideline:
Small models (e.g., GPT-4o-mini, Claude Haiku): Up to 10 tools
Large models (e.g., GPT-4o, Claude Sonnet/Opus): Up to 30 tools
If you need more tools, consider:
Grouping related functionality into fewer, more versatile tools
Using different agents for different domains
Dynamically registering only relevant tools based on context
Orchestrating multiple agents with specialized toolsets