Building Autonomous Agents in Laravel
Building Autonomous Agents in Laravel
If RAG is about "Reading", Agents are about "Doing".
An Autonomous Agent is a system where the LLM acts as the orchestrator. It decides which code to run to solve a user's problem.
Today, we will build a "Support Agent" that can:
- Check a user's order status.
- Refund an order if eligible.
- Search the knowledge base.
The Core Mechanism: Tool Definitions
OpenAI models accept a tools parameter. This is a JSON schema description of your PHP functions.
Step 1: Mapping PHP Classes to Tools
Let's create a structure to manage tools cleanly.
// app/AI/Tools/CheckOrderStatus.php
namespace App\AI\Tools;
use Closure;
class CheckOrderStatus
{
public function definition(): array
{
return [
'type' => 'function',
'function' => [
'name' => 'check_order_status',
'description' => 'Get the current status and tracking info of an order.',
'parameters' => [
'type' => 'object',
'properties' => [
'order_id' => [
'type' => 'string',
'description' => 'The order reference number (e.g., ORD-123)'
]
],
'required' => ['order_id']
]
]
];
}
public function handle(array $arguments): string
{
$order = \App\Models\Order::where('reference', $arguments['order_id'])->first();
if (!$order) {
return json_encode(['error' => 'Order not found']);
}
return json_encode([
'status' => $order->status,
'shipped_at' => $order->shipped_at,
'items' => $order->items->pluck('name')
]);
}
}
Step 2: The Agent Runner
The complexity of agents lies in the "Re-Act" loop (Reason + Action).
- LLM thinks: "I need to check order status for ORD-999".
- LLM returns:
tool_call: check_order_status(ORD-999). - App executes: PHP code runs options DB query.
- App returns:
{"status": "shipped"}. - LLM receives output and thinks: "Okay, tell the user it is shipped".
- LLM final response: "Your order was shipped!"
Here is a simplified Runner implementation:
// app/AI/AgentRunner.php
class AgentRunner
{
protected array $tools = [];
public function registerTool($class)
{
$this->tools[] = new $class;
}
public function chat(array $history)
{
// 1. Prepare definitions
$definitions = array_map(fn($t) => $t->definition(), $this->tools);
// 2. Initial Call
$response = Http::withToken(config('services.openai.key'))->post('...', [
'model' => 'gpt-4o',
'messages' => $history,
'tools' => $definitions,
]);
$message = $response->json('choices.0.message');
// 3. Check for Tool Calls
if (isset($message['tool_calls'])) {
// Append assistant's "thought" to history
$history[] = $message;
foreach ($message['tool_calls'] as $toolCall) {
$functionName = $toolCall['function']['name'];
$arguments = json_decode($toolCall['function']['arguments'], true);
// Find matching tool instance
$tool = collect($this->tools)->first(fn($t) => $t->definition()['function']['name'] === $functionName);
// EXECUTE PHP CODE
$result = $tool->handle($arguments);
// Report back to LLM
$history[] = [
'role' => 'tool',
'tool_call_id' => $toolCall['id'],
'content' => $result
];
}
// 4. Recursive Call (The Loop)
// Call the API again with the tool outputs so it can generate the final answer
return $this->chat($history);
}
return $message['content'];
}
}
Step 3: Safety and permissioning
Allowing AI to execute code is risky.
- Read-Only vs Write: Mark tools as "Safe" (Query) or "Unsafe" (Refund).
- Confirmation: If the agent decides to call
refund_order, don't execute immediately. Return a special UI state to the frontend asking the user "Do you want to proceed with Refund?". - Context isolation: Ideally, the tool should only access data belonging to the authenticated user.
public function handle(array $args): string
{
// Getting current user from context, NOT from args provided by AI
$user = auth()->user();
$order = $user->orders()->find($args['order_id']); // Scoped query
// ...
}
Conclusion
Agents effectively allow you to expose your entire Laravel Service layer to a natural language interface. This pattern is ideal for internal admin panels ("Show me all users who signed up yesterday and export to CSV") where building specific UI buttons for every possible permutation is impossible.