Available for Q3 2026 projects — Laravel, AI agents & automation
Build With Abdallah logo Build With Abdallah Software · AI · Automation
AI Agents 2 min read May 30, 2026

Build AI Agents with Laravel 13: A Complete Tutorial with Code Examples

Step-by-step tutorial: build a support ticket agent with conversation memory, tool calling, structured output, streaming, failover, queues, and testing. All PHP, no Python.

A
Abdallah Mohamed
Senior Full-Stack Engineer

Build AI Agents with Laravel 13: A Complete Tutorial with Code Examples

Laravel 13 ships with a first-party AI SDK (laravel/ai) — and it changes everything for PHP developers who want to build AI features without touching Python.

No more vendor SDKs scattered across your services. No more home-rolled Http::post() wrappers. No more LangChain envy.

In this tutorial, we'll build a support ticket agent from scratch — with conversation memory, tool calling, structured output, and streaming. Real code, real project structure, everything you can copy and run.


Step 1: Install Laravel 13 and the AI SDK

composer create-project laravel/laravel:^13 my-ai-app
cd my-ai-app

# Install the AI SDK
composer require laravel/ai

# Publish config and migrations
php artisan vendor:publish --provider="Laravel\\Ai\\AiServiceProvider"

# Run migrations (creates agent_conversations + agent_conversation_messages tables)
php artisan migrate

Add your AI provider keys to .env:

OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=...
# Ollama needs no key for local use
OLLAMA_API_KEY=

The SDK supports 14 providers: OpenAI, Anthropic, Gemini, Azure OpenAI, AWS Bedrock, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, Cohere, Jina, and VoyageAI.

Quick sanity check — add this to routes/web.php:

use function Laravel\Ai\agent;

Route::get('/ping-ai', function () {
    return (string) agent(
        instructions: 'You are terse. Reply in five words or fewer.',
    )->prompt('Is Laravel 13 out?');
});

Hit /ping-ai. If you get a five-word response, your credentials work.


Step 2: Create Your First Agent

Agents are PHP classes — not loose prompts. Each one encapsulates instructions, tools, memory, and output schema.

php artisan make:agent SupportAgent

This generates app/Ai/Agents/SupportAgent.php. Let's build it out:

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupOrder;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\MaxTokens;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::Anthropic)]
#[Model('claude-haiku-4-5-20251001')]
#[MaxTokens(1024)]
#[Temperature(0.3)]
#[MaxSteps(5)]
class SupportAgent implements Agent, Conversational, HasTools
{
    use Promptable, RemembersConversations;

    public function instructions(): string
    {
        return <<<PROMPT
        You are a customer support agent. Use tools to look up real data.
        Never guess order details — always use the LookupOrder tool.
        Be concise, helpful, and friendly.
        PROMPT;
    }

    public function tools(): iterable
    {
        return [
            new LookupOrder,
        ];
    }
}

Key concepts:

  • PHP Attributes (#[Provider], #[Model], etc.) configure the agent declaratively
  • RemembersConversations persists chat history to the database automatically
  • MaxSteps(5) prevents runaway tool loops — the most important production guard rail
  • HasTools lets the agent call PHP functions when it needs real data

Step 3: Build a Tool — Let the Agent Query Your Database

Tools are PHP classes the AI model can call on its own. The SDK handles the entire back-and-forth loop.

php artisan make:tool LookupOrder

Implement app/Ai/Tools/LookupOrder.php:

<?php

namespace App\Ai\Tools;

use App\Models\Order;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;

class LookupOrder implements Tool
{
    public function description(): string
    {
        return 'Look up an order by its public order number and return status, total, and line items.';
    }

    public function handle(Request $request): string
    {
        $order = Order::where('number', $request['number'])->first();

        if (! $order) {
            return "No order found with number {$request['number']}.";
        }

        return json_encode([
            'number'  => $order->number,
            'status'  => $order->status,
            'total'   => $order->total->format(),
            'items'   => $order->items->map->only(['name', 'quantity'])->all(),
        ]);
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'number' => $schema->string()
                ->description('The public order number, e.g. ORD-12345')
                ->required(),
        ];
    }
}

The model decides when to call this tool. If a user asks "Where's my order ORD-12345?", the agent calls LookupOrder, gets real data, and responds with actual order details.


Step 4: Use the Agent in a Controller

<?php

namespace App\Http\Controllers;

use App\Ai\Agents\SupportAgent;
use Illuminate\Http\Request;

class SupportChatController extends Controller
{
    public function chat(Request $request)
    {
        $request->validate(['message' => 'required|string']);

        $user = $request->user();

        $response = (new SupportAgent)
            ->forUser($user)
            ->prompt($request->string('message'));

        return [
            'reply' => (string) $response,
            'conversation_id' => $response->conversationId,
        ];
    }

    public function continue(Request $request)
    {
        $request->validate([
            'message' => 'required|string',
            'conversation_id' => 'required|string',
        ]);

        $user = $request->user();

        $response = (new SupportAgent)
            ->forUser($user)
            ->continue($request->conversation_id, as: $user)
            ->prompt($request->string('message'));

        return [
            'reply' => (string) $response,
            'conversation_id' => $response->conversationId,
        ];
    }
}

First call creates a conversation. Pass the conversation_id back on subsequent requests and the SDK reloads prior messages automatically.


Step 5: Add Structured Output

Sometimes you don't want free-form text — you want typed, validated JSON. Let's build a ticket classifier:

php artisan make:agent TicketClassifier --structured
<?php

namespace App\Ai\Agents;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasStructuredOutput;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::OpenAI)]
#[Model('gpt-4.1-mini')]
class TicketClassifier implements Agent, HasStructuredOutput
{
    use Promptable;

    public function instructions(): string
    {
        return 'Classify the support ticket into a category, urgency, and suggested reply.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'category' => $schema->string()
                ->enum(['billing', 'bug', 'feature-request', 'other'])
                ->required(),
            'urgency' => $schema->string()
                ->enum(['low', 'medium', 'high'])
                ->required(),
            'suggested_reply' => $schema->string()->required(),
            'tags' => $schema->array()
                ->items($schema->string())
                ->required(),
        ];
    }
}

Use it:

$classification = (new TicketClassifier)->prompt($ticket->body);

$ticket->update([
    'category' => $classification['category'],   // "billing"
    'urgency'  => $classification['urgency'],    // "high"
    'tags'     => $classification['tags'],       // ["invoice", "overdue"]
]);

No regex. No parsing. The model returns exactly the schema you defined.


Step 6: Stream Responses to a Livewire Component

For anything longer than a sentence, streaming is mandatory — users won't wait 10 seconds staring at a blank screen.

// routes/web.php
Route::post('/chat/stream', function (Request $request) {
    return (new SupportAgent)
        ->forUser($request->user())
        ->stream($request->string('message'));
});

That's the entire backend — it returns a Server-Sent Events stream.

Livewire component:

<?php

namespace App\Livewire;

use App\Ai\Agents\SupportAgent;
use Livewire\Component;

class SupportChat extends Component
{
    public string $message = '';
    public string $reply = '';
    public ?string $conversationId = null;

    public function send()
    {
        $this->reply = '';

        $agent = (new SupportAgent)->forUser(auth()->user());

        $stream = $this->conversationId
            ? $agent->continue($this->conversationId, as: auth()->user())->stream($this->message)
            : $agent->stream($this->message);

        foreach ($stream as $event) {
            if ($event->isText()) {
                $this->reply .= $event->text;
                $this->stream('reply'); // push token to the browser
            }
        }

        $this->conversationId = $stream->response()->conversationId;
    }

    public function render()
    {
        return view('livewire.support-chat');
    }
}

Blade view:

<div>
    <div wire:stream="reply">{{ $reply }}</div>

    <form wire:submit="send">
        <input wire:model="message" placeholder="Ask about your order..." />
        <button type="submit">Send</button>
    </form>
</div>

Tokens appear as they arrive. No waiting.


Step 7: Provider Failover — Built In

In production, you don't want your app to crash because one provider is down:

$response = $agent->prompt(
    'Check order ORD-12345',
    provider: [Lab::Anthropic, Lab::OpenAI, Lab::Gemini],
);

The SDK tries Anthropic first. If it fails — rate limit, outage, whatever — it falls back to OpenAI, then Gemini. No custom retry logic needed.


Step 8: Queue Bulk Operations

Processing thousands of items? Don't block the request:

$agent->queue("Classify ticket: {$ticket->body}")
    ->then(function ($response) use ($ticket) {
        $ticket->update([
            'category' => $response['category'],
            'urgency'  => $response['urgency'],
            'tags'     => $response['tags'],
        ]);
    })
    ->catch(function (Throwable $e) use ($ticket) {
        Log::error("Classification failed for ticket {$ticket->id}", [
            'error' => $e->getMessage(),
        ]);
    });

Works with any Laravel queue driver — database, Redis, SQS.


Step 9: Test Your AI Features

This is the part that was missing from every AI integration before this SDK. Agent::fake():

public function test_classifies_support_ticket(): void
{
    TicketClassifier::fake([
        json_encode([
            'category' => 'billing',
            'urgency' => 'high',
            'suggested_reply' => 'Please check your invoice.',
            'tags' => ['invoice', 'overdue'],
        ]),
    ]);

    $response = $this->post('/api/classify', [
        'body' => 'I was charged twice for my order',
    ]);

    $response->assertOk();

    TicketClassifier::assertPrompted(
        fn ($prompt) => str_contains($prompt, 'charged twice')
    );
}

public function test_prevents_stray_api_calls(): void
{
    TicketClassifier::fake()->preventStrayPrompts();

    // If any code tries to make a real API call, the test fails.
    // No accidental API charges during CI.
}

No HTTP mocks. No fixture files. Just fake() and assertions.


What We Built

In 9 steps, you now have:

  • ✅ A support agent with conversation memory and tool calling
  • ✅ A ticket classifier with structured, typed output
  • Streaming responses to a Livewire chat UI
  • Provider failover for resilience
  • Queue integration for bulk operations
  • Testable AI code with Agent::fake()

All in PHP. All Laravel. No Python microservice. No vendor lock-in.


What Can You Build Next?

  • Customer support bots that read your docs and answer questions
  • Document analysis that extracts structured data from text
  • E-commerce product writers with SEO optimization and deduplication
  • Multi-step research agents that chain reasoning and tool calls
  • Content moderation pipelines with classification and routing

Need help building AI features for your business? Get in touch →


Sources