omarseyam / laravel-chatbot
Conversation-persisted AI chat scaffolding for Laravel, built on the Laravel AI SDK.
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- laravel/ai: ^0.7|^0.8|^0.9|^0.10
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Conversation-persisted AI chat scaffolding for Laravel, built on top of the
Laravel AI SDK. It gives you a
ChatService you can call from a controller or job, stores every
conversation and message in your database, and ships a ready-to-use chat
drawer UI you can drop into any page.
Requirements
- PHP 8.3+
- Laravel 13.x
laravel/ai— install it yourself withcomposer require laravel/ai, publish and run its migrations, and configure at least one provider. It isn't bundled automatically, and this package builds directly on the tables it creates (see "Database setup" below).
Installation
composer require omarseyam/laravel-chatbot composer require laravel/ai php artisan chatbot:install
chatbot:install publishes config/agents.php, publishes this package's
migrations to database/migrations, and copies:
- the chat source files into
app/Ai/... - the two models into
app/Models/... - the chat UI (a Blade component + its JS/CSS) into
app/View/Components/Chatandresources/...
These become part of your application once copied/published, so edit them
freely — running the command again won't overwrite anything that already
exists unless you pass --force.
Database setup
This package does not create its own tables. laravel/ai already creates
agent_conversations and agent_conversation_messages (that's what backs
its RemembersConversations trait), and this package's models use those
same tables — so laravel/ai's migrations have to run first:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
This package's own migrations are published by chatbot:install into
database/migrations (so they're visible and editable, not hidden in
vendor/), and only add the extra columns
AgentConversation/AgentConversationMessage need on top of those two
tables — agent, structured, tool_calls, tool_results, usage,
meta, and a user_id foreign key on both tables. They're dated far in the
future so they always sort — and run — after laravel/ai's migration,
whatever timestamp that one happens to get published with. Each column is
guarded with hasColumn(): added if it's missing, or adjusted with
->change() to the shape this package expects if a column by that name
already exists — so running php artisan migrate again is safe.
Configuration
Register your agents in config/agents.php by mapping an alias to an Agent
class:
return [ 'assistant' => \App\Ai\Agents\ExampleAgent::class, ];
The alias is what you pass as agent on a ChatRequest.
Usage
use App\Ai\Data\ChatRequest; use App\Ai\Services\ChatService; $response = app(ChatService::class)->ask(new ChatRequest( prompt: 'What can you help me with?', conversationId: null, // pass an existing conversation id to continue it agent: 'assistant', )); // $response = ['conversation_id' => '...', 'content' => '...' | [...]]
content is a plain string for a normal text reply, or an array if the
agent implements HasStructuredOutput. MessageService records which case
each stored message is in a structured column, so anything reading
history back — the chat UI, an agent's messages() — knows whether to
json_decode() it.
Conversations and messages are scoped to the authenticated user
(Auth::id()), so a conversationId can only be resumed by the user who
started it.
Preparing an agent to receive conversation history
For an agent to actually see prior turns of a conversation (rather than only
the latest prompt), it needs to accept a $messages array and implement
Conversational. AgentManager::resolve() already passes the mapped
history in for you when one is available — the agent just needs to be
shaped to accept it:
<?php namespace App\Ai\Agents; use Laravel\Ai\Contracts\Agent; use Laravel\Ai\Contracts\Conversational; use Laravel\Ai\Contracts\HasStructuredOutput; use Laravel\Ai\Contracts\HasTools; use Laravel\Ai\Messages\Message; use Laravel\Ai\Promptable; use Stringable; class ExampleAgent implements Agent, Conversational, HasTools, HasStructuredOutput { use Promptable; public function __construct(public ?array $messages = null) {} /** * Get the instructions that the agent should follow. */ public function instructions(): Stringable|string { return "Your role is ..."; } /** * Get the list of messages comprising the conversation so far. * * @return Message[] */ public function messages(): iterable { return $this->messages ?? []; } }
A copy of this lives at app/Ai/Agents/ExampleAgent.php after running
chatbot:install — rename it, register it in config/agents.php, and fill
in instructions() (plus tools()/schema() if you keep the HasTools/
HasStructuredOutput interfaces).
Fetching a conversation's history directly
MessageService::history() returns messages already mapped into
Laravel\Ai\Messages\Message objects, ready to hand to an agent. If you
just need the raw rows — e.g. to render a chat thread in a view — query the
model directly:
AgentConversationMessage::query() ->where('conversation_id', $conversationId) ->oldest() ->select('role', 'content', 'structured') ->get();
The included AIChat Blade component (see "UI" below) already does exactly
this.
Models and linking your own models to a conversation
App\Models\AgentConversation— one row per conversation, with amessages()relation and atitlegenerated from the first prompt.App\Models\AgentConversationMessage— one row per message, storingrole,content,structured,agent,usage,meta,attachments,tool_calls, andtool_results.
AgentConversation ships with a post() relation to a Post model as an
example only — it isn't something the package requires. To link your
own model(s) back to the conversation that produced them (and pull the full
message history behind them), add a conversation_id foreign key column to
that model's own migration, then define the matching relations:
// On your model: public function conversation(): BelongsTo { return $this->belongsTo(AgentConversation::class, 'conversation_id'); } // On AgentConversation, replacing the example post() relation: public function yourModel(): HasOne // or HasMany { return $this->hasOne(YourModel::class, 'conversation_id'); }
Pruning old conversations
AgentConversation uses Laravel's Prunable trait to clean up conversations
older than an hour, skipping any tied to the example post() relation above
(adjust whereDoesntHave('post') in prunable() to match whatever relation
you replace it with, or drop it entirely if nothing needs protecting).
Deleting a conversation — by hand or via pruning — also deletes its
messages, so nothing is left orphaned. Schedule pruning in
routes/console.php:
use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune')->daily();
UI
chatbot:install copies a ready-made chat drawer: a floating button that
opens a panel with message history, a text input, and typing indicator.
app/View/Components/Chat/AIChat.php— loads a conversation's messages and passes them to the view.resources/views/components/chat/a-i-chat.blade.php— the drawer markup.resources/js/ai/*.js— the frontend logic (sending messages, rendering markdown withmarked, syntax highlighting withhighlight.js, sanitizing withdompurify).
Drop it into any page (a class-based component under App\View\Components\Chat
resolves to the <x-chat.a-i-chat> tag):
<x-chat.a-i-chat :conversation-id="$conversationId ?? null" />
A few things need wiring up by hand — none of these are copied automatically, since they touch files this package can't safely overwrite:
-
CSRF token. The chat JS sends a
X-CSRF-TOKENheader read from a meta tag. Add this to the<head>of the layout that renders the chat component:<meta name="csrf-token" content="{{ csrf_token() }}">
-
The API endpoint.
resources/js/ai/api.jshas a placeholder fetch URL — set it to whichever route in your app accepts{ prompt, conversation_id }and callsChatService::ask():const response = await fetch('add the fetch URL here', {
For example:
// routes/web.php Route::post('/ai/chat', function (\Illuminate\Http\Request $request) { return app(\App\Ai\Services\ChatService::class)->ask(new \App\Ai\Data\ChatRequest( prompt: $request->string('prompt')->toString(), conversationId: $request->input('conversation_id'), agent: 'assistant', )); })->middleware('auth');
Then point
api.jsat/ai/chat. -
Import the chat JS. Add this to your existing
resources/js/app.js:import './ai';
-
Styling. The drawer uses Tailwind utility classes plus a handful of custom ones (
.ai-assistant-bubble,.ai-assistant-header,.ai-assistant-copy, and dark-mode variants) that aren't defined by default Tailwind — add them to your own CSS. It also assumes Material Design 3–style color tokens (bg-primary,text-on-surface,border-outline-variant, etc.) are already part of your Tailwind theme; if they aren't, either define them or adjust the classes ina-i-chat.blade.phpto match your own design system. -
npm packages. The chat JS depends on
marked,highlight.js, anddompurify, and the message bubbles use Tailwind's typography plugin:npm install marked highlight.js dompurify @tailwindcss/typography
Contributing
Issues and pull requests are welcome at github.com/omarseyam/laravel-chatbot.
Author
Omar Seyam (@omarseyam) — oseyam02@gmail.com
License
The MIT License (MIT). See LICENSE for details.