Building Autonomous Agents in Laravel

· 4 min read

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:

  1. Check a user's order status.
  2. Refund an order if eligible.
  3. 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).

  1. LLM thinks: "I need to check order status for ORD-999".
  2. LLM returns: tool_call: check_order_status(ORD-999).
  3. App executes: PHP code runs options DB query.
  4. App returns: {"status": "shipped"}.
  5. LLM receives output and thinks: "Okay, tell the user it is shipped".
  6. 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.

  1. Read-Only vs Write: Mark tools as "Safe" (Query) or "Unsafe" (Refund).
  2. 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?".
  3. 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.

Comments