Single Tool Calling

The simplest pattern is calling a single tool. This is useful for straightforward operations like getting weather data or sending an email.
$weatherTool = new WeatherTool();

$response = Prism::text()
    ->using(config('toolbox.ai.provider'))
    ->withSystemPrompt("You can get weather data for any location.
        Provide a natural, conversational response including:
        - Temperature
        - Conditions
        - Humidity
        - Wind speed")
    ->withPrompt("What's the weather in London?")
    ->withTools([$weatherTool])
    ->withMaxSteps(2)  // One for tool call, one for summary
    ->generate();

Understanding Single Tool Steps

When using a single tool, you typically need 2 steps:
  1. Step 1: Call the tool and get data
  2. Step 2: Process the data and provide a natural language response

Multiple Tool Calling

For more complex operations, you can chain multiple tools together. This is useful for workflows like getting weather data and sending it via email.
$weatherTool = new WeatherTool();
$emailTool = new EmailTool();

$response = Prism::text()
    ->using(config('toolbox.ai.provider'))
    ->withSystemPrompt("You can get weather data and send it via email.
        For weather + email operations:
        1. First use the weather tool to get the data
        2. Then use the email tool to send that data
        3. Format the weather data nicely in the email body
        
        When sending emails:
        - Use 'Weather Update' as subject if not specified
        - Format the weather data in a readable way")
    ->withPrompt("Send the current weather in London to user@example.com")
    ->withTools([$weatherTool, $emailTool])
    ->withMaxSteps(5)  // Important: More steps needed for multiple tools
    ->generate();

Understanding Multiple Tool Steps

For multiple tools, the steps typically flow like this:
  1. Step 1: Call first tool (e.g., weather)
  2. Step 2: Process first tool’s data
  3. Step 3: Call second tool (e.g., email)
  4. Step 4: Process second tool’s result
  5. Step 5: Provide final summary

Complex Example: Weather + Crypto + Email

Here’s an example of chaining three tools to send both weather and crypto data via email:
$weatherTool = new WeatherTool();
$cryptoTool = new CryptoTool();
$emailTool = new EmailTool();

$response = Prism::text()
    ->using(config('toolbox.ai.provider'))
    ->withSystemPrompt("You can get weather data, crypto prices, and send emails.
        For this combined operation:
        1. Get the weather data for the specified location
        2. Get the crypto price for the specified coin
        3. Send an email containing both pieces of information
        
        Format the data nicely in the email, with sections for:
        - Current Weather Conditions
        - Cryptocurrency Prices")
    ->withPrompt("Send London's weather and Bitcoin's price to user@example.com")
    ->withTools([$weatherTool, $cryptoTool, $emailTool])
    ->withMaxSteps(7)  // (2 × number of tools) + 1
    ->generate();

Best Practices

When working with tools, follow these guidelines:

Setting Max Steps

  • Single tool: Use 2 steps (call + summary)
  • Multiple tools: Use (2 × number of tools) + 1
  • Complex workflows: Add extra steps if needed for better processing

System Prompts

  • Be specific about the order of operations
  • Include formatting instructions
  • Specify default values (e.g., email subjects)
  • Define error handling preferences

Monitoring and Debugging

Track tool execution and responses using structured logging:
$debugInfo = [];

$response = Prism::text()
    ->withSystemPrompt("Get weather data and send via email")
    ->withPrompt("Send London weather to user@example.com")
    ->withTools([$weatherTool, $emailTool])
    ->withMaxSteps(5)
    ->generate();

// Log response structure
$debugInfo['response'] = [
    'has_text' => isset($response->text),
    'steps_count' => count($response->steps ?? []),
    'finish_reason' => $response->finishReason?->value,
];

// Log each step's details
foreach ($response->steps as $index => $step) {
    $debugInfo['steps'][] = [
        'index' => $index,
        'text' => $step->text ?? null,
        'finish_reason' => $step->finishReason?->value,
        'tool_calls' => collect($step->toolCalls)->map(fn($call) => [
            'tool' => $call->name,
            'args' => $call->arguments(),
        ])->toArray(),
        'tool_results' => collect($step->toolResults)->map(fn($result) => [
            'tool' => $result->toolName,
            'result' => $result->result,
        ])->toArray(),
    ];
}

// Log for analysis
Log::info('Tool Execution Debug Info:', $debugInfo);
Example debug output:
{
    "response": {
        "has_text": true,
        "steps_count": 5,
        "finish_reason": "stop"
    },
    "steps": [
        {
            "index": 0,
            "text": "I'll get the weather for London",
            "finish_reason": "tool_calls",
            "tool_calls": [
                {
                    "tool": "weather",
                    "args": {"location": "London"}
                }
            ],
            "tool_results": [
                {
                    "tool": "weather",
                    "result": {"temp": 18, "conditions": "cloudy"}
                }
            ]
        }
        // ... more steps
    ]
}

Error Handling

Implement robust error handling with retries and fallbacks:
class WeatherEmailService
{
    private $maxRetries = 3;
    private $debugInfo = [];

    public function sendWeatherUpdate(string $location, string $email)
    {
        try {
            $response = $this->executeWithRetry(function () use ($location, $email) {
                return Prism::text()
                    ->withSystemPrompt("Get weather and send via email. Handle errors gracefully.")
                    ->withPrompt("Send {$location} weather to {$email}")
                    ->withTools([$this->weatherTool, $this->emailTool])
                    ->withMaxSteps(5)
                    ->generate();
            });

            $this->validateResponse($response);
            return $this->processResponse($response);

        } catch (WeatherToolException $e) {
            Log::error('Weather API Error', [
                'location' => $location,
                'error' => $e->getMessage(),
                'debug_info' => $this->debugInfo
            ]);
            
            // Fallback to cached weather data
            return $this->sendCachedWeather($location, $email);

        } catch (EmailToolException $e) {
            Log::error('Email Send Error', [
                'email' => $email,
                'error' => $e->getMessage(),
                'debug_info' => $this->debugInfo
            ]);

            // Queue for retry
            WeatherEmailRetryJob::dispatch($location, $email)
                ->delay(now()->addMinutes(5));

            throw new ServiceException(
                "Unable to send email. Queued for retry.",
                previous: $e
            );
        }
    }

    private function executeWithRetry(callable $operation)
    {
        $attempt = 1;
        $lastException = null;

        while ($attempt <= $this->maxRetries) {
            try {
                return $operation();
            } catch (Exception $e) {
                $lastException = $e;
                Log::warning("Attempt {$attempt} failed", [
                    'error' => $e->getMessage(),
                    'debug_info' => $this->debugInfo
                ]);
                
                if ($attempt < $this->maxRetries) {
                    sleep(pow(2, $attempt)); // Exponential backoff
                }
                $attempt++;
            }
        }

        throw new ServiceException(
            "Operation failed after {$this->maxRetries} attempts",
            previous: $lastException
        );
    }

    private function validateResponse($response)
    {
        if (empty($response->steps)) {
            throw new ServiceException("No steps in response");
        }

        // Check if we got weather data
        $hasWeatherData = collect($response->steps)
            ->flatMap->toolResults
            ->contains(fn($result) => 
                $result->toolName === 'weather' && !empty($result->result)
            );

        if (!$hasWeatherData) {
            throw new ServiceException("No weather data in response");
        }
    }
}
This implementation shows:
  • Structured logging of all steps and tool calls
  • Retry logic with exponential backoff
  • Specific error handling for each tool
  • Fallback behaviors (cached data, retry queue)
  • Response validation
  • Detailed error context for debugging

Common Patterns

Here are some common tool calling patterns with practical examples:

Sequential Processing

When one tool’s output is needed for the next tool’s input:
// Example: Get weather and email it
$response = Prism::text()
    ->withSystemPrompt("First get the weather, then email it to the user")
    ->withPrompt("Send London's weather to user@example.com")
    ->withTools([$weatherTool, $emailTool])
    ->withMaxSteps(5)  // 2 for weather, 2 for email, 1 for final summary
    ->generate();

Conditional Branching

When the choice of second tool depends on the first tool’s result:
// Example: Check weather and either send warning email or update status
$response = Prism::text()
    ->withSystemPrompt("Check the weather. If severe conditions, send warning email.
        Otherwise, just log the status.")
    ->withPrompt("Monitor weather conditions in Miami")
    ->withTools([$weatherTool, $emailTool, $statusTool])
    ->withMaxSteps(5)  // 2 for weather, 2 for chosen action, 1 for summary
    ->generate();

Data Aggregation

When you need to combine data from multiple tools:
// Example: Send both weather and crypto data in one email
$response = Prism::text()
    ->withSystemPrompt("Get both weather and crypto data, then send combined report")
    ->withPrompt("Send London weather and Bitcoin price to user@example.com")
    ->withTools([$weatherTool, $cryptoTool, $emailTool])
    ->withMaxSteps(7)  // 2 for weather, 2 for crypto, 2 for email, 1 for summary
    ->generate();

Interactive Processing

When you need multiple interactions with the same tool:
// Example: Compare weather in multiple cities
$response = Prism::text()
    ->withSystemPrompt("Get weather for multiple cities and compare them")
    ->withPrompt("Compare weather in London, Paris, and New York")
    ->withTools([$weatherTool])
    ->withMaxSteps(7)  // 2 steps per city (3 cities) + 1 for comparison
    ->generate();