Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 3 min read

Adding an AI Chatbot to Your Laravel App with the OpenAI API

Every product I've worked on in the last two years has eventually gotten the same request: "Can we add an AI assistant to this?" As a Laravel developer, the good news is that shipping a production-ready chatbot takes an

Every product I've worked on in the last two years has eventually gotten the same request: "Can we add an AI assistant to this?" As a Laravel developer, the good news is that shipping a production-ready chatbot takes an afternoon β€” no Python microservice, no LangChain, just Laravel and an HTTP API.

In this tutorial we'll build a chatbot that:

  • answers questions in the context of your product (via a system prompt),
  • remembers the conversation within a session,
  • streams nothing fancy β€” just clean JSON in/out you can drop into any Blade view or SPA.

1. Install the OpenAI PHP client

The community-maintained openai-php/laravel package is the de-facto standard:

composer require openai-php/laravel
php artisan openai:install

Add your key to .env:

OPENAI_API_KEY=sk-your-key-here

πŸ’‘ Never commit the key. Use your host's secret manager in production.

2. A dedicated chat service

Keep the LLM logic out of your controller. Create app/Services/ChatService.php:

<?php

namespace App\Services;

use OpenAI\Laravel\Facades\OpenAI;

class ChatService
{
    private const SYSTEM_PROMPT = <<<'PROMPT'
        You are the helpful assistant for Acme Inc's customer portal.
        Answer only questions about the product. Be concise.
        If you don't know the answer, say so and suggest contacting support.
        PROMPT;

    /**
     * @param array<int, array{role: string, content: string}> $history
     */
    public function reply(array $history, string $userMessage): string
    {
        $messages = [
            ['role' => 'system', 'content' => self::SYSTEM_PROMPT],
            ...$history,
            ['role' => 'user', 'content' => $userMessage],
        ];

        $response = OpenAI::chat()->create([
            'model' => 'gpt-4o-mini',
            'messages' => $messages,
            'max_tokens' => 500,
        ]);

        return $response->choices[0]->message->content;
    }
}

3. The controller

Session-based history keeps things simple β€” no migrations needed for v1:

<?php

namespace App\Http\Controllers;

use App\Services\ChatService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class ChatController extends Controller
{
    public function __invoke(Request $request, ChatService $chat): JsonResponse
    {
        $validated = $request->validate([
            'message' => ['required', 'string', 'max:2000'],
        ]);

        $history = $request->session()->get('chat_history', []);

        $answer = $chat->reply($history, $validated['message']);

        // keep the last 10 exchanges to control token costs
        $history[] = ['role' => 'user', 'content' => $validated['message']];
        $history[] = ['role' => 'assistant', 'content' => $answer];
        $request->session()->put('chat_history', array_slice($history, -20));

        return response()->json(['answer' => $answer]);
    }
}

Route it β€” with rate limiting, because every request costs you money:

// routes/web.php
use App\Http\Controllers\ChatController;

Route::post('/chat', ChatController::class)
    ->middleware(['auth', 'throttle:20,1']); // 20 req/min per user

4. A minimal frontend

Drop this into any Blade view β€” no framework required:

<div id="chat">
    <div id="messages"></div>
    <form id="chat-form">
        <input id="chat-input" type="text" placeholder="Ask me anything…" required />
        <button type="submit">Send</button>
    </form>
</div>

<script>
document.getElementById('chat-form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const input = document.getElementById('chat-input');
    const messages = document.getElementById('messages');

    messages.insertAdjacentHTML('beforeend', `<p><b>You:</b> ${input.value}</p>`);

    const res = await fetch('/chat', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        },
        body: JSON.stringify({ message: input.value }),
    });

    const { answer } = await res.json();
    messages.insertAdjacentHTML('beforeend', `<p><b>Bot:</b> ${answer}</p>`);
    input.value = '';
});
</script>

5. Production checklist

Before you ship this to real users:

  • Rate limit (done above) β€” an unthrottled chat endpoint is an open wallet.
  • Cap history (done above) β€” token usage grows with every message otherwise.
  • Validate input length (done above) β€” 2000 chars is plenty for a question.
  • Log conversations β€” a simple chat_logs table pays for itself the first time you debug a weird answer.
  • Set max_tokens β€” bounds your worst-case cost per request.
  • Add a fallback β€” wrap the API call in a try/catch and return "I'm having trouble right now" instead of a 500.

Where to go next

  • Streaming responses β€” OpenAI::chat()->createStreamed() + Server-Sent Events makes the bot feel 10x faster.
  • RAG (Retrieval-Augmented Generation) β€” embed your docs and inject relevant chunks into the system prompt so the bot answers from your knowledge base. That's my next post.
  • Swap providers β€” the same pattern works with Anthropic's Claude API; only the HTTP client changes.

I'm Aditya Kumar (adityakdevin) β€” Tech Lead & full-stack developer building AI-powered web products with Laravel, Vue, and LLM APIs. Find me at adityadev.in.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.