Appearance
Custom Tools
Extend Iris's capabilities by creating custom tools that integrate with your services and data. Custom tools have full access to Laravel's ecosystem -databases, APIs, queues, and more.
Tool Anatomy
Every tool is a class that extends Prism's Tool base class. Here's the structure:
php
<?php
declare(strict_types=1);
namespace App\Tools;
use App\Models\User;
use Prism\Prism\Tool;
class ExampleTool extends Tool
{
public function __construct(
protected User $user, // Injected by Iris
protected SomeService $service, // Any other dependencies
) {
$this
->as('tool_name') // Name Iris uses to call it
->for('Description of what the tool does') // Helps Iris decide when to use it
->withStringParameter('param', 'Description') // Define parameters
->using($this); // Tell Prism to use __invoke
}
public function __invoke(string $param): string
{
// Your tool logic here
return 'Result message';
}
}For complete documentation on parameter types, return values, and advanced patterns, see the Prism Tools Documentation.
Registering Tools
Register your tools in config/iris-custom.php. Your tools are appended to the core tools:
php
return [
'tools' => [
App\Extensions\Tools\FetchNotesTool::class,
App\Extensions\Tools\WeatherTool::class,
],
];To disable core tools you don't need:
php
return [
'disabled_tools' => [
App\Tools\GenerateImageTool::class,
App\Tools\Calendar\CreateCalendarEventTool::class,
],
];Injecting the User
Iris resolves tools from Laravel's container with the authenticated user bound. This lets you scope tool behavior to the current user automatically:
php
<?php
declare(strict_types=1);
namespace App\Extensions\Tools;
use App\Models\User;
use Prism\Prism\Tool;
class FetchNotesTool extends Tool
{
public function __construct(
protected User $user,
) {
$this
->as('fetch_notes')
->for('Fetch the user\'s saved notes')
->using($this);
}
public function __invoke(): string
{
$notes = $this->user
->notes()
->latest()
->take(10)
->get();
if ($notes->isEmpty()) {
return 'No notes found.';
}
return $notes
->map(fn ($note) => "- {$note->title}: {$note->content}")
->join("\n");
}
}You can also inject any service registered in Laravel's container:
php
public function __construct(
protected User $user,
protected WeatherService $weather,
protected CacheManager $cache,
) {
// ...
}Return Values
Tools return strings that Iris incorporates into its response. Follow these guidelines:
Success Responses
Be concise and informative. Include relevant details Iris can relay to the user:
php
// Good
return "Note created: '{$note->title}' (ID: {$note->id})";
// Too verbose
return "The note creation operation completed successfully. The note with the title '{$note->title}' has been saved to the database with ID {$note->id}. You can now reference this note in future conversations.";Empty Results
Clearly indicate when no results were found:
php
if ($notes->isEmpty()) {
return 'No notes found.';
}
// Or with more context
if ($notes->isEmpty()) {
return 'No notes found matching that search. Try different keywords.';
}Error Responses
Return error messages as strings -don't throw exceptions unless something is truly broken. Iris can explain the error to the user:
php
public function __invoke(string $location): string
{
$apiKey = $this->user->settings->weather_api_key;
if (! $apiKey) {
return 'Weather API key not configured. Add one in Settings > Integrations.';
}
try {
return $this->weather->current($location, $apiKey);
} catch (RateLimitException $e) {
return 'Weather service rate limit reached. Try again in a few minutes.';
} catch (Throwable $e) {
report($e); // Log the actual error
return 'Unable to fetch weather data. The service may be temporarily unavailable.';
}
}TIP
Return user-friendly messages. Iris will present these to the user, so "Weather API key not configured" is better than "Missing WEATHER_API_KEY environment variable."
Complete Example: Task Management Tool
Here's a complete example showing a tool that manages tasks:
php
<?php
declare(strict_types=1);
namespace App\Extensions\Tools;
use App\Models\Task;
use App\Models\User;
use Prism\Prism\Tool;
class CreateTaskTool extends Tool
{
public function __construct(
protected User $user,
) {
$this
->as('create_task')
->for('Create a new task or todo item for the user')
->withStringParameter('title', 'Task title')
->withStringParameter('description', 'Task description (optional)')
->withStringParameter('dueDate', 'Due date in YYYY-MM-DD format (optional)')
->withStringParameter('priority', 'Priority: low, medium, or high (default: medium)')
->using($this);
}
public function __invoke(
string $title,
string $description = '',
?string $dueDate = null,
string $priority = 'medium',
): string {
// Validate priority
if (! in_array($priority, ['low', 'medium', 'high'])) {
return "Invalid priority '{$priority}'. Use low, medium, or high.";
}
// Parse due date if provided
$parsedDueDate = null;
if ($dueDate) {
try {
$parsedDueDate = Carbon::parse($dueDate);
} catch (Throwable) {
return "Couldn't parse due date '{$dueDate}'. Use YYYY-MM-DD format.";
}
}
// Create the task
$task = $this->user->tasks()->create([
'title' => $title,
'description' => $description,
'due_date' => $parsedDueDate,
'priority' => $priority,
]);
$response = "Task created: '{$task->title}'";
if ($parsedDueDate) {
$response .= " (due {$parsedDueDate->format('M j, Y')})";
}
return $response;
}
}Testing Tools
Test tools by binding a user to the container and resolving the tool:
php
<?php
use App\Extensions\Tools\FetchNotesTool;
use App\Models\Note;
use App\Models\User;
it('fetches notes for the authenticated user', function () {
$user = User::factory()
->has(Note::factory()->count(3))
->create();
app()->instance(User::class, $user);
$tool = resolve(FetchNotesTool::class);
$result = $tool();
expect($result)->toContain($user->notes->first()->title);
});
it('handles users with no notes', function () {
$user = User::factory()->create();
app()->instance(User::class, $user);
$tool = resolve(FetchNotesTool::class);
$result = $tool();
expect($result)->toBe('No notes found.');
});
it('respects the limit parameter', function () {
$user = User::factory()
->has(Note::factory()->count(20))
->create();
app()->instance(User::class, $user);
$tool = resolve(FetchNotesTool::class);
$result = $tool(limit: 5);
// Only 5 notes should appear
expect(substr_count($result, '- '))->toBe(5);
});Tool Description Best Practices
The for() description helps Iris decide when to use your tool. Make it clear and specific:
php
// Good - specific about what it does
->for('Fetch the user\'s saved notes, optionally filtered by tag')
// Good - mentions when to use it
->for('Get current weather conditions for a location. Use when user asks about weather.')
// Bad - too vague
->for('Handle notes')
// Bad - too technical
->for('Executes SELECT query against notes table with pagination')Organizing Tools
For complex applications, organize tools into subdirectories:
app/
├── Extensions/
│ └── Tools/
│ ├── Notes/
│ │ ├── FetchNotesTool.php
│ │ ├── CreateNoteTool.php
│ │ └── DeleteNoteTool.php
│ ├── Tasks/
│ │ ├── ListTasksTool.php
│ │ └── CreateTaskTool.php
│ └── Weather/
│ └── GetWeatherTool.phpRegister them all in config:
php
return [
'tools' => [
App\Extensions\Tools\Notes\FetchNotesTool::class,
App\Extensions\Tools\Notes\CreateNoteTool::class,
App\Extensions\Tools\Notes\DeleteNoteTool::class,
App\Extensions\Tools\Tasks\ListTasksTool::class,
App\Extensions\Tools\Tasks\CreateTaskTool::class,
App\Extensions\Tools\Weather\GetWeatherTool::class,
],
];