How to use Claude AI in Laravel?

Now that Anthropic has already opened access to their Claude model, it's time to try the Claude API in our Laravel application. This time, we will try to create a simple chatbot application using Laravel.

laravel-claude-chatbot

The code for this tutorial is available on GitHub. You can get it after reading this tutorial.

Anthropic

To use the Claude API, first you need to register with Anthropic to get a developer account. You can get your developer account from Anthropic here.

After registering a new account, you can go to your console page to get the Anthropic API token from this page.

api-token-page

Anthropic also has documentation for the API that we can use. You can read the documentation here.

Create Laravel Project

Today we will create a super simple chatbot with Laravel. The project will be very simple; we will not use Livewire, Inertia, or anything fancy. We will just use a simple Laravel Blade view for the frontend since the focus of this tutorial will be on how to use the Anthropic API.

Let's create a new Laravel project using this command.

laravel new laravel-claude-chatbot

To interact with the Claude model, you can use the library I created: ahmadrosid/anthropic-php. This library follows the same pattern as laravel-openai, so if you already use laravel-openai in your project, you can use this library as a replacement or backup for an alternative LLM.

Let's add that library to our project using this command.

composer require ahmadrosid/anthropic-php

Setup Frontend Stuff

For the frontend, we will use Bootstrap, and to make it look better, we will use the custom Bootstrap template called Tabler. You can get it from tabler.io.

Let's create a new file in our Laravel project in resources/views/layouts/base.blade.php:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title')</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
    <style>
        :root {
            --tblr-font-sans-serif: 'Inter';
        }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css">
</head>

<body>
    <div class="page">
        <header class="navbar navbar-expand-sm navbar-light d-print-none">
            <div class="container-xl">
                <h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
                    <a href="#">
                        <img src="/logo.png" width="110" height="32" alt="Tabler" class="navbar-brand-image" />
                        <span class="px-2">
                            Laravel Claude Chatbot
                        </span>
                    </a>
                </h1>

                <div class="navbar-nav flex-row order-md-last">
                    <div class="nav-item">
                        <a href="#" class="nav-link d-flex lh-1 text-reset p-0">
                            <span class="avatar avatar-circle avatar-sm" style="background-image: url(https://ahmadrosid.com/profile.png)"></span>
                            <div class="d-none d-xl-block ps-2">
                                <div>Ahmad Rosid</div>
                                <div class="mt-1 small text-secondary">Software Engineer</div>
                            </div>
                        </a>
                    </div>
                </div>
            </div>
        </header>
        <div class="page-wrapper">
            <div class="page-body">
                @yield("content")
            </div>
        </div>
    </div>

    @stack('scripts')
</body>

</html>

Next, let's create a chatbot page in resources/views/welcome.blade.php. In this view, we will extend the base template and add the block for our chatbot page.

@extends('layouts.base')

@section('title', 'Laravel Claude Chatbot')

@section('content')
<div class="row">
    <div class="col-sm-6 col-md-10 col-lg-7 container px-4">
        <div class="card">
            <div id="chat-list" class="card-body" style="height: 600px; overflow-y: auto;">
            </div>
        </div>
    </div>
</div>

<div class="relative">
    <div style="position: absolute; bottom: 10px; left:0; right: 0;">
        <div class="row">
            <div class="col-sm-6 col-md-10 col-lg-7 container py-2 px-4">
                <div class="card">
                    <div class="card-body">
                        <div class="input-group input-group-flat">
                            <input id="input-mesage" type="text" class="form-control" autocomplete="off" placeholder="Type message">
                            <button id="btn-send" class="btn">Send</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Create Streaming Controller

Since calling the Claude API will take time to complete, let's stream the text generation to the user. To do that, we will use server-sent events.

Let's create a controller to handle that:

php artisan make:controller StreamingChatController

Now register that controller in routes/web.php file:

<?php

+use App\Http\Controllers\StreamingChatController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

+Route::get("/chat/streaming", [StreamingChatController::class, 'index']);

And let's implement the server sent event in StreamingChatController, first let setup anthropic library dependency in our controller.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Anthropic\Anthropic;

class StreamingChatController extends Controller
{
    protected $anthropic;

    public function __construct()
    {
        $headers = [
            'anthropic-version' => '2023-06-01',
            'anthropic-beta' => 'messages-2023-12-15',
            'content-type' => 'application/json',
            'x-api-key' => env('ANTHROPIC_API_KEY')
        ];

        $this->anthropic = Anthropic::factory()
            ->withHeaders($headers)
            ->make();
    }

    /**
     * For sending server event
     */
    private function send($event, $data)
    {
        echo "event: {$event}\n";
        echo 'data: ' . $data;
        echo "\n\n";
        ob_flush();
        flush();
    }
}

Make sure to have ANTHROPIC_API_KEY in your .env variable.

Now let's implement the streaming.

public function index(Request $request)
{
    $question = $request->query('question');
    return response()->stream(
        function () use (
            $question
        ) {
            $result_text = "";
            $last_stream_response = null;

            $model = 'claude-3-opus-20240229';
            $max_tokens = 4096;
            $systemMessage = 'You are a helpfull assistant. Answer as concisely as possible.';
            $temperature = 1;
            $messages = [
                [
                    'role' => 'user',
                    'content' => $question
                ]
            ];

            $stream = $this->anthropic->chat()->createStreamed([
                'model' => $model,
                'temperature' => $temperature,
                'max_tokens' => $max_tokens,
                'system' => $systemMessage,
                'messages' => $messages,
            ]);


            foreach ($stream as $response) {
                $text = $response->choices[0]->delta->content;
                if (connection_aborted()) {
                    break;
                }
                $data = [
                    'text' => $text,
                ];
                $this->send("update", json_encode($data));
                $result_text .= $text;
                $last_stream_response = $response;
            }

            $this->send("update", "<END_STREAMING_SSE>");

            logger($last_stream_response->usage->toArray());
        },
        200,
        [
            'Cache-Control' => 'no-cache',
            'Connection' => 'keep-alive',
            'X-Accel-Buffering' => 'no',
            'Content-Type' => 'text/event-stream',
        ]
    );
}

Listening to Server Event

Now that we have our streaming endpoint, the next step will be implementing the client to call our SSE endpoint. For that, let's go back to resources/views/welcome.blade.php.

To send a request to the server-sent event, we will need JavaScript.

const queryQuestion = encodeURIComponent(question);
let url = `/chat/streaming?question=${queryQuestion}`;
const source = new EventSource(url);
let sseText = "";

source.addEventListener("update", (event) => {
    if (event.data === "<END_STREAMING_SSE>") {
        source.close();
        return;
    }

    const data = JSON.parse(event.data);
    if (data.text) {
        sseText += data.text;
        messageContent.innerHTML = marked.parse(sseText);
    }
});

source.addEventListener("error", (event) => {
    const errorEl = createErrorElement("An error occurred. Try again later.")
    chatListEl.appendChild(errorEl);
})

And here's the complete code to handle the SSE in our laravel-claude-chatbot.

@push("scripts")
<script src="https://unpkg.com/marked@12.0.1/marked.min.js"></script>
<script>
    let stillWriting = false;

    const btnSend = document.getElementById("btn-send");
    const chatListEl = document.getElementById("chat-list");
    const inputMesage = document.getElementById("input-mesage");

    const createElementFromStr = (str) => {
        const div = document.createElement('div');
        div.innerHTML = str;
        return div;
    }

    const createElementChatItem = (type) => {
        const chatItemElementTemplateBot = `
            <div class="d-flex p-2 rounded mb-2" style="background-color: #f3f4f6">
                <div class="mb-1">
                    <span class="avatar " style="background-image: url(/logo.png)"></span>
                </div>
                <div class="px-3">
                    <div class="text-muted">Chatbot</div>
                    <div id="message-content"></div>
                    <div id="scroll-item"></div>
                </div>
            </div>`;
        const chatItemElementTemplateUser = `
            <div class="d-flex p-2 rounded mb-2" style="background-color: #f3f4f6; ">
                <div class="mb-1">
                    <span class="avatar rounded-circle" style="background-image: url(https://ahmadrosid.com/profile.png)"></span>
                </div>
                <div class="px-3">
                    <div class="text-muted">You</div>
                    <div id="message-content"></div>
                </div>
            </div>`;

        let chatItemElementTemplate = chatItemElementTemplateBot;
        if (type === "user") {
            chatItemElementTemplate = chatItemElementTemplateUser;
        }

        const newElement = createElementFromStr(chatItemElementTemplate)
        newElement.className = "chat-item";
        return newElement;
    }

    const createErrorElement = (message) => {
        const templete = `
            <div class="d-flex p-2 rounded mb-2" style="background-color: #f3f4f6">
                <span class="avatar " style="background-image: url(/logo.png)"></span>
                <div class="px-3">
                    <div class="text-muted mb-2">System</div>
                    <div class="alert alert-danger bg-white" role="alert">
                        <div class="d-flex">
                            <svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg>
                            <div>${message}</div>
                        </div>
                    </div>
                </div>
            </div>`;
        const newElement = createElementFromStr(templete);
        newElement.innerHTML = templete;
        return newElement;
    }

    const triggerStreaming = (question) => {
        stillWriting = true;
        const newBotElement = createElementChatItem();
        const newUserElement = createElementChatItem("user");
        chatListEl.appendChild(newUserElement);
        const messageItemUser = newUserElement.querySelector("#message-content");
        messageItemUser.innerText = question;

        const queryQuestion = encodeURIComponent(question);
        let url = `/chat/streaming?question=${queryQuestion}`;
        const source = new EventSource(url);
        let sseText = "";

        chatListEl.appendChild(newBotElement);
        const messageContent = newBotElement.querySelector("#message-content");
        const scrollItem = newBotElement.querySelector("#scroll-item");
        scrollItem.scrollIntoView();

        source.addEventListener("update", (event) => {
            if (event.data === "<END_STREAMING_SSE>") {
                source.close();
                stillWriting = false;
                return;
            }

            const data = JSON.parse(event.data);
            if (data.text) {
                sseText += data.text;
                messageContent.innerHTML = marked.parse(sseText);
            }
            scrollItem.scrollIntoView();
        });

        source.addEventListener("error", (event) => {
            stillWriting = false;
            console.error('EventSource failed:', event);
            newBotElement.remove();
            newUserElement.remove();
            const errorEl = createErrorElement("An error occurred. Try again later.")
            chatListEl.appendChild(errorEl);
        })
    };

    function submitSendMessage() {
        if (stillWriting) {
            return;
        }

        const inputText = inputMesage.value;
        const btnRetry = document.getElementById("btn-retry");
        if (inputText != "") {
            if (btnRetry) {
                btnRetry.remove();
            }
            inputMesage.value = "";
            triggerStreaming(inputText);
        } else {
            inputText.focus();
        }
    }

    btnSend.addEventListener("click", () => {
        submitSendMessage()
    })

    inputMesage.addEventListener("keyup", function(event) {
        if (event.keyCode === 13) {
            event.preventDefault();
            submitSendMessage();
        }
    });
</script>
@endpush

With all of that done, here's the demo.

This is post 024 of #100DaysToOffload.

Subscribe to download source code for this article here.

I respect your privacy. Unsubscribe at any time.