syafiq-unijaya/laravel-ai-chatbox
A drop-in AI chatbox widget for Laravel. One Blade directive — no build tools required in your application.
Connect to any OpenAI-compatible API including Ollama, OpenAI, Groq, LM Studio, and OpenRouter. Includes real-time token streaming, conversation memory, a full RAG (Retrieval-Augmented Generation) system, an admin dashboard, and a PHP facade for calling AI from anywhere in your codebase.
Widget & Frontend
@aichatbox into any Blade layout — nothing else requiredlocalStorage or sessionStorageAI & Streaming
AI facade for calling providers directly from controllers, jobs, or commandsConversation Memory
RAG — Retrieval-Augmented Generation
.md and .txt documents; the chatbox retrieves relevant context automatically/ai-chatbox/rag with upload, reprocess, and delete actionsAdmin & Operations
/ai-chatbox/admin with config diagnostics, live error/warning/notice checks, and provider details/ai-chatbox/admin/conversations (requires database memory driver)ai-chatbox:prune-conversations Artisan command — bulk-delete inactive conversations with --days, --dry-run, and --force options; schedulable via Laravel's task scheduler| Version | |
|---|---|
| PHP | 8.2 or higher |
| Laravel | 10, 11, or 12 |
No Node.js or npm is required in your application. The Vue bundle is pre-compiled and shipped as a static asset. The
bladeandlivewiredrivers need no compiled assets at all.
From Packagist:
composer require syafiq-unijaya/laravel-ai-chatbox
Publish CSS + JS to public/vendor/ai-chatbox/
php artisan vendor:publish --tag=ai-chatbox-assets
Publish the config file to customise defaults
php artisan vendor:publish --tag=ai-chatbox-config
If you plan to use RAG or the database memory driver, run the migrations:
Publish the migration files
php artisan vendor:publish --tag=ai-chatbox-migrations &&
php artisan migrate
The package defaults to the ollama provider on localhost:11434. Set your active provider and its credentials in .env:
AI_CHATBOX_ACTIVE_PROVIDER=ollama
OLLAMA_URL=http://localhost:11434/v1/chat/completions
OLLAMA_TOKEN=your-ollama-token
OLLAMA_MODEL=gpt-oss:120b
AI_CHATBOX_LANGUAGE=English
See AI Providers for examples covering OpenAI, Groq, LM Studio, and more.
Running Ollama in WSL?
localhostfrom a Windows host may not reach WSL. Find your WSL IP and use it:# run inside WSL ip addr show eth0 | grep 'inet 'OLLAMA_URL=http://172.x.x.x:11434/v1/chat/completions AI_CHATBOX_SSRF_PROTECTION=false
Local Ollama on macOS/Linux? SSRF protection is on by default and blocks
localhost. Disable it for local development:AI_CHATBOX_SSRF_PROTECTION=false
{{-- e.g. resources/views/layouts/app.blade.php --}}
@aichatbox
The chatbox appears as a floating button on every page that uses the layout. Done.
Use
@aichatboxConfiginstead if you are building your own frontend (React, Svelte, etc.) — it outputs onlywindow.AiChatboxConfigwithout any widget HTML or scripts.
Publish and edit config/ai-chatbox.php to change any default.
| Key | Env var | Default | Description |
|---|---|---|---|
active_provider |
AI_CHATBOX_ACTIVE_PROVIDER |
ollama |
Provider to use — must match a key under providers. The provider's api_url, api_token, and api_model are always the authoritative values. |
api_url,api_token, andapi_modelare not top-level env vars. They are always sourced from the active named provider. See AI Providers.
| Key | Env var | Default | Description |
|---|---|---|---|
language |
- | English |
Language the AI must reply in — leave empty to let the model decide |
system_prompt |
- | You are a helpful assistant... |
System message sent on every request — use {language} as a placeholder |
The language value is enforced at two points per request:
{language} placeholder in system_prompt is substituted at runtime[Important: Reply in {language} only.] is appended to every user message, which improves compliance on smaller modelsThese are project-level settings best changed by publishing the config file:
php artisan vendor:publish --tag=ai-chatbox-config
Then edit config/ai-chatbox.php directly.
| Key | Env var | Default | Description |
|---|---|---|---|
temperature |
- | 0.7 |
Creativity — 0.0 deterministic, 1.0 creative |
max_tokens |
- | null |
Max reply length — null lets the model decide |
timeout |
AI_CHATBOX_TIMEOUT |
30 |
Request timeout in seconds — increase for slow local models |
| Key | Env var | Default | Description |
|---|---|---|---|
frontend |
- | vue |
UI driver — vue, blade, livewire, or none |
title |
AI_CHATBOX_TITLE |
AI Assistant |
Widget header title |
greeting |
- | Hi! How can I help you today? |
Opening message — leave empty to disable |
placeholder |
- | Type your message... |
Input placeholder text |
theme_color |
- | #0dad35 |
Primary colour (hex) |
color_scheme |
- | auto |
Colour scheme for the widget and admin pages — auto (OS preference), light, or dark |
position |
- | bottom-right |
Widget position — bottom-right, bottom-left, top-right, top-left |
toggle_icon |
- | null |
Custom image for the floating toggle button — URL or asset path; null uses the built-in chat bubble SVG |
markdown |
- | true |
Render AI replies as Markdown |
sound |
- | true |
Play a ping when the AI replies |
sound_volume |
- | 0.3 |
Volume — 0.0 silent, 1.0 full |
stream |
AI_CHATBOX_STREAM |
true |
Stream replies token-by-token via SSE |
| Key | Env var | Default | Description |
|---|---|---|---|
history_enabled |
AI_CHATBOX_HISTORY |
true |
Include previous messages for context |
history_limit |
- | 50 |
Max user+assistant pairs kept per thread |
context_token_limit |
- | 4000 |
Max estimated tokens of history per request — trims oldest pairs first (0 = rely on history_limit only) |
memory_driver |
AI_CHATBOX_MEMORY_DRIVER |
session |
Server-side history driver — session or database |
storage |
- | local |
Browser storage — local (persists across sessions) or session (clears on tab close) |
| Key | Env var | Default | Description |
|---|---|---|---|
route_prefix |
- | ai-chatbox |
URL prefix for all chatbox routes |
middleware |
- | ['web', 'throttle:20,1', 'ai-chatbox.cors'] |
Middleware stack for chatbox API routes |
rate_limit |
AI_CHATBOX_RATE_LIMIT |
20 |
Max requests per window per IP |
rate_window |
AI_CHATBOX_RATE_WINDOW |
1 |
Rate limit window in minutes |
health_check |
AI_CHATBOX_HEALTH_CHECK |
true |
Ping the AI service before opening the widget |
offline_message |
- | AI service is currently unreachable. |
Toast shown when the service is unreachable |
ssrf_protection |
AI_CHATBOX_SSRF_PROTECTION |
true |
Block requests to private/reserved IP ranges |
allowed_origins |
- | [env('APP_URL')] |
Origins allowed to call chatbox endpoints (CORS) |
rag_admin_middleware |
- | ['web', 'auth'] |
Middleware for the Knowledge Base (/ai-chatbox/rag) pages |
admin_middleware |
- | null |
Middleware for the Admin dashboard — inherits rag_admin_middleware when null |
| Key | Env var | Default | Description |
|---|---|---|---|
rag_enabled |
AI_CHATBOX_RAG |
false |
Master switch — enable RAG context injection |
rag_embedding_timeout |
AI_CHATBOX_EMBEDDING_TIMEOUT |
10 |
Timeout in seconds for every embedding HTTP request — applies to all providers |
rag_top_k |
- | 3 |
Number of chunks retrieved per query |
rag_chunk_size |
- | 500 |
Target chunk size in tokens (~4 chars/token) |
rag_chunk_overlap |
- | 50 |
Overlap between consecutive chunks in tokens |
rag_similarity_threshold |
- | 0.2 |
Minimum cosine similarity score (0.0–1.0) |
rag_context_prompt |
- | (see below) | Instruction prepended to retrieved chunks — use {chunks} as placeholder |
rag_processing_timeout |
AI_CHATBOX_RAG_PROCESSING_TIMEOUT |
0 |
Max seconds for a single upload or reprocess — 0 = no limit |
rag_embedding_urlandrag_embedding_modelare per-provider settings defined inside theprovidersblock and resolved through the active named provider. See AI Providers.
Every API connection is configured through a named provider. Set AI_CHATBOX_ACTIVE_PROVIDER to the provider name, then configure that provider's own env vars.
Named providers are defined under the providers key in config/ai-chatbox.php. Each entry can override any global setting; everything else is inherited.
// config/ai-chatbox.php
'providers' => [
'ollama' => [
'api_url' => env('OLLAMA_URL', 'http://localhost:11434/v1/chat/completions'),
'api_token' => env('OLLAMA_TOKEN', 'your-ollama-token'),
'api_model' => env('OLLAMA_MODEL', 'gpt-oss:120b'),
'rag_embedding_url' => env('OLLAMA_EMBEDDING_URL', 'http://localhost:11434/v1/embeddings'),
'rag_embedding_model' => env('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
],
'openai' => [
'api_url' => env('OPENAI_URL', ''),
'api_token' => env('OPENAI_API_KEY', ''),
'api_model' => env('OPENAI_MODEL', ''),
'rag_embedding_url' => env('OPENAI_EMBEDDING_URL', ''),
'rag_embedding_model' => env('OPENAI_EMBEDDING_MODEL', ''),
],
'lmstudio' => [
'api_url' => env('LMSTUDIO_URL', 'http://127.0.0.1:1234/v1/chat/completions'),
'api_token' => env('LMSTUDIO_TOKEN', 'lmstudio'),
'api_model' => env('LMSTUDIO_MODEL', 'phi-3.5-mini-instruct'),
'rag_embedding_url' => env('LMSTUDIO_EMBEDDING_URL', 'http://127.0.0.1:1234/v1/embeddings'),
'rag_embedding_model' => env('LMSTUDIO_EMBEDDING_MODEL', 'text-embedding-nomic-embed-text-v1.5'),
],
],
groqand other providers are not in the default config — add them as custom entries after publishing (see OpenRouter example below for the pattern).
Env var reference per provider:
| Provider | URL | Token | Model | Embedding URL | Embedding model |
|---|---|---|---|---|---|
ollama |
OLLAMA_URL |
OLLAMA_TOKEN |
OLLAMA_MODEL |
OLLAMA_EMBEDDING_URL |
OLLAMA_EMBEDDING_MODEL |
openai |
OPENAI_URL |
OPENAI_API_KEY |
OPENAI_MODEL |
OPENAI_EMBEDDING_URL |
OPENAI_EMBEDDING_MODEL |
lmstudio |
LMSTUDIO_URL |
LMSTUDIO_TOKEN |
LMSTUDIO_MODEL |
LMSTUDIO_EMBEDDING_URL |
LMSTUDIO_EMBEDDING_MODEL |
rag_embedding_timeout(AI_CHATBOX_EMBEDDING_TIMEOUT) is a universal setting — it applies to all providers and is not overridable per provider. Configure it once in the RAG section.
The chatbox widget and the
AIfacade both resolve through the same named provider.AI_CHATBOX_ACTIVE_PROVIDERcontrols which provider is active for both.
AI_CHATBOX_ACTIVE_PROVIDER=ollama
OLLAMA_URL=http://localhost:11434/v1/chat/completions
OLLAMA_TOKEN=your-ollama-token
OLLAMA_MODEL=gpt-oss:120b
AI_CHATBOX_SSRF_PROTECTION=false
AI_CHATBOX_ACTIVE_PROVIDER=ollama
OLLAMA_URL=https://ollama.com/api/chat
OLLAMA_TOKEN=your_ollama_api_key
OLLAMA_MODEL=gpt-oss:120b
AI_CHATBOX_ACTIVE_PROVIDER=openai
OPENAI_URL=https://api.openai.com/v1/chat/completions
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o
Groq is not in the default config — add it after publishing config/ai-chatbox.php:
'providers' => [
'groq' => [
'api_url' => env('GROQ_URL', 'https://api.groq.com/openai/v1/chat/completions'),
'api_token' => env('GROQ_API_KEY', ''),
'api_model' => env('GROQ_MODEL', ''),
],
// ... other providers
],
AI_CHATBOX_ACTIVE_PROVIDER=groq
GROQ_URL=https://api.groq.com/openai/v1/chat/completions
GROQ_API_KEY=gsk_...
GROQ_MODEL=llama-3.3-70b-versatile
AI_CHATBOX_ACTIVE_PROVIDER=lmstudio
LMSTUDIO_URL=http://localhost:1234/v1/chat/completions
LMSTUDIO_TOKEN=lmstudio
LMSTUDIO_MODEL=your-loaded-model-name
AI_CHATBOX_SSRF_PROTECTION=false
Start LM Studio, load a model, and enable the Local Server tab. The model name must match exactly what LM Studio displays.
Add a custom entry to your published config/ai-chatbox.php:
'providers' => [
'openrouter' => [
'api_url' => env('OPENROUTER_URL', 'https://openrouter.ai/api/v1/chat/completions'),
'api_token' => env('OPENROUTER_API_KEY', ''),
'api_model' => env('OPENROUTER_MODEL', 'mistralai/mistral-7b-instruct'),
],
// ... other providers
],
AI_CHATBOX_ACTIVE_PROVIDER=openrouter
OPENROUTER_URL=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_API_KEY=sk-or-...
OPENROUTER_MODEL=mistralai/mistral-7b-instruct
The frontend setting controls how @aichatbox renders. All drivers share the same backend API routes and window.AiChatboxConfig — only the widget layer differs.
| Driver | Widget | Streaming | JS dependency | Assets required |
|---|---|---|---|---|
vue |
Vue 3 SFC | SSE | Bundled chatbox.js |
vendor:publish --tag=ai-chatbox-assets |
blade |
Vanilla JS | SSE | None (marked.js from CDN if Markdown on) | Same |
livewire |
Alpine.js | SSE | Alpine.js (bundled with Livewire 3) | Same |
none |
- | Your choice | None | Not required |
No env var. Publish the config and set
frontenddirectly inconfig/ai-chatbox.php:
// config/ai-chatbox.php
'frontend' => 'vue', // Vue 3 widget (default)
'frontend' => 'blade', // Vanilla JS, no framework
'frontend' => 'livewire', // Alpine.js via Livewire
'frontend' => 'none', // API + config only
vue — Vue 3 (default)No extra setup. The pre-compiled bundle mounts to #ai-chatbox-app and reads window.AiChatboxConfig.
blade — Vanilla JSA self-contained widget with no framework dependency. Uses the same CSS as the Vue driver (identical HTML class names), so all appearance options apply equally.
If AI_CHATBOX_MARKDOWN=true, marked.js and DOMPurify are loaded from jsDelivr at runtime. Set AI_CHATBOX_MARKDOWN=false to remove the CDN dependency entirely.
livewire — Livewire + Alpine.jsRenders an Alpine.js widget. Livewire 3 bundles Alpine.js automatically — no additional scripts needed.
The package registers a Livewire component, so you can also mount the widget independently:
<livewire:ai-chatbox />
If you use
<livewire:ai-chatbox />without@aichatbox, add@aichatboxConfigto your layout so the widget has access to its configuration.
{{-- layout --}}
@aichatboxConfig
...
{{-- anywhere on the page --}}
<livewire:ai-chatbox />
none — API-only / custom frontendOutputs only window.AiChatboxConfig. Use this when building your own React, Svelte, or other framework frontend.
@aichatboxConfig produces the same output regardless of the frontend setting:
@aichatboxConfig
window.AiChatboxConfig reference:
window.AiChatboxConfig = {
url, // POST /ai-chatbox/message — full JSON reply
streamUrl, // POST /ai-chatbox/stream — SSE token stream
clearUrl, // POST /ai-chatbox/clear — clear session history
healthUrl, // GET /ai-chatbox/health — liveness check
token, // CSRF token
stream, // boolean
healthCheck, // boolean
title,
placeholder,
greeting,
markdown, // boolean
sound, // boolean
soundVolume, // 0.0–1.0
position, // 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
storageKey, // localStorage/sessionStorage key (scoped per app + user)
storageType, // 'local' | 'session'
offlineMessage,
themeColor,
};
All API endpoints accept { message, thread_id } as JSON. Responses:
| Endpoint | Response |
|---|---|
POST /ai-chatbox/message |
{ "reply": "..." } |
POST /ai-chatbox/stream |
SSE events: data: {"token":"..."} ending with data: [DONE] |
POST /ai-chatbox/clear |
{ "status": "ok" } |
GET /ai-chatbox/health |
{ "status": "online" } or { "status": "offline", "message": "...", "code": "E##" } |
The AI facade lets you call any configured AI provider directly from controllers, jobs, Artisan commands, or services — without touching the chatbox widget.
use SyafiqUnijaya\AiChatbox\AI;
// Use the active provider (resolves to AI_CHATBOX_ACTIVE_PROVIDER)
$reply = AI::chat('Summarise this document: ...');
// Use a specific named provider
$reply = AI::provider('openai')->chat('Translate to French: ...');
$reply = AI::provider('lmstudio')->chat('Write a test for this function...');
$reply = AI::provider('ollama')->chat('What is the capital of France?');
Every modifier returns a new immutable instance — the original provider is never mutated.
$reply = AI::provider('openai')
->withModel('gpt-4o-mini')
->withTemperature(0.2)
->withSystemPrompt('You are a JSON-only responder. Return only valid JSON.')
->withMaxTokens(512)
->withTimeout(60)
->chat($prompt);
| Method | Description |
|---|---|
->withModel(string $model) |
Override the model for this call |
->withSystemPrompt(string $prompt) |
Override the system prompt |
->withLanguage(string $lang) |
Override the reply language |
->withTemperature(float $temp) |
Override creativity (0.0–1.0) |
->withMaxTokens(?int $tokens) |
Override max reply length (null = model default) |
->withTimeout(int $seconds) |
Override the HTTP timeout |
->withConfig(array $overrides) |
Merge arbitrary config overrides |
// Pass a callback — tokens are emitted synchronously
AI::provider('openai')->stream($prompt, [], function (string $token) {
echo $token;
ob_flush(); flush();
});
// Or receive a Closure to invoke later
$reader = AI::provider('default')->stream($prompt);
$reader(fn(string $token) => print($token));
$history = [
['role' => 'user', 'content' => 'Previous question'],
['role' => 'assistant', 'content' => 'Previous answer'],
];
$reply = AI::provider('default')->chat('Follow-up question', $history);
| Usage | Resolves to |
|---|---|
| Chatbox widget | Provider named by AI_CHATBOX_ACTIVE_PROVIDER |
AI::chat() / AI::provider('default') |
Provider named by AI_CHATBOX_ACTIVE_PROVIDER |
AI::provider('openai') |
openai entry in config/ai-chatbox.php |
AI::provider('ollama') |
ollama entry in config/ai-chatbox.php |
Each time a user first opens the chatbox, a UUID v4 thread ID is generated in the browser and stored in localStorage (or sessionStorage). This ID is sent with every message and scopes the server-side history — so multiple independent conversations never share context.
Thread A (UUID: 550e8400...) → session key: ai_chatbox_history_550e8400...
Thread B (UUID: 6ba7b810...) → session key: ai_chatbox_history_6ba7b810...
The pencil icon in the widget header starts a fresh thread. The trash icon clears the current thread's history. Thread IDs survive page refresh — the same conversation context is restored on return.
History is stored in the PHP session and sent to the AI on every subsequent message.
AI_CHATBOX_MEMORY_DRIVER=session
AI_CHATBOX_HISTORY=true
AI_CHATBOX_HISTORY_LIMIT=50
Set AI_CHATBOX_HISTORY=false to make every message standalone (no context sent):
AI_CHATBOX_HISTORY=false
Switch to the database driver to persist history in Eloquent models. History survives PHP session expiry, is shared across all PHP workers, and can be queried or exported.
AI_CHATBOX_MEMORY_DRIVER=database
Run the migration if you haven't already:
php artisan migrate
This creates:
| Table | Purpose |
|---|---|
ai_chatbox_conversations |
One row per thread, keyed by UUID |
ai_chatbox_messages |
All messages per thread (role + content) |
The Conversations Viewer in the admin dashboard requires this driver.
To revert, set AI_CHATBOX_MEMORY_DRIVER=session. Existing database records are preserved but ignored until you switch back.
Chat bubbles are persisted in the browser, automatically scoped to prevent history leaking between different apps or different authenticated users on the same device.
| Setting | Behaviour |
|---|---|
AI_CHATBOX_STORAGE=local |
Persists across browser sessions (default) |
AI_CHATBOX_STORAGE=session |
Cleared when the tab is closed |
When using the database memory driver, conversation records accumulate over time. The ai-chatbox:prune-conversations command permanently deletes conversations (and their messages via cascade) that have had no activity beyond the configured retention period.
# Use the default from config (AI_CHATBOX_PRUNE_DAYS, default 30 days)
php artisan ai-chatbox:prune-conversations
# Override the retention period at runtime
php artisan ai-chatbox:prune-conversations --days=60
# Preview what would be deleted without making any changes
php artisan ai-chatbox:prune-conversations --dry-run
# Run even when memory_driver is not set to 'database' (e.g. cleanup after switching drivers)
php artisan ai-chatbox:prune-conversations --force
Register the command in your application's routes/console.php (Laravel 11+) or app/Console/Kernel.php (Laravel 10):
Laravel 11+ (routes/console.php):
use Illuminate\Support\Facades\Schedule;
Schedule::command('ai-chatbox:prune-conversations')->daily();
Laravel 10 (app/Console/Kernel.php):
protected function schedule(Schedule $schedule): void
{
$schedule->command('ai-chatbox:prune-conversations')->daily();
}
| Key | Env var | Default | Description |
|---|---|---|---|
conversation_prune_days |
AI_CHATBOX_PRUNE_DAYS |
30 |
Conversations inactive for longer than this many days are deleted |
AI_CHATBOX_PRUNE_DAYS=30 # default — 30 days retention
AI_CHATBOX_PRUNE_DAYS=90 # longer retention
AI_CHATBOX_PRUNE_DAYS=7 # aggressive cleanup
The command performs the following checks before deleting anything:
| Condition | Behaviour |
|---|---|
memory_driver is not database |
Exits with an error — use --force to override |
ai_chatbox_conversations table missing |
Exits with an error and prints migration instructions |
ai_chatbox_messages table missing |
Warns but continues (cascade may not apply) |
--days value is less than 1 |
Exits with an error |
| No matching conversations found | Exits cleanly with an informational message |
How "last activity" is determined:
updated_aton the conversation row is updated every timesaveHistoryis called — i.e., every time the user sends a message. Conversations that have genuinely had no activity for--daysdays are safe to remove.
Two independent limits control how much is sent to the AI per request.
These settings have no env var. Publish the config file and edit
config/ai-chatbox.phpdirectly:php artisan vendor:publish --tag=ai-chatbox-config
Limits the amount of conversation history included per request. History is trimmed oldest-pair-first using a ~4 chars/token heuristic.
// config/ai-chatbox.php
'context_token_limit' => 4000, // phi3:mini, llama3 8B (default)
'context_token_limit' => 8000, // llama3 70B, Mixtral
'context_token_limit' => 32000, // GPT-4o, Claude
'context_token_limit' => 0, // disabled — rely on history_limit only
Limits how long the AI's reply can be. Leave null to let the model decide.
// config/ai-chatbox.php
'max_tokens' => 512, // short replies
'max_tokens' => 2048, // longer replies
'max_tokens' => null, // model default (default)
Both limits work together: history_limit caps by message count, context_token_limit caps by estimated tokens. Whichever is reached first applies.
// config/ai-chatbox.php
'temperature' => 0.2, // focused, deterministic
'temperature' => 0.7, // balanced (default)
'temperature' => 1.0, // creative, unpredictable
When AI_CHATBOX_STREAM=true (default), AI replies are streamed token-by-token via Server-Sent Events. Each word appears as it is generated, with a blinking ▋ cursor while in progress.
AI_CHATBOX_STREAM=true # stream token-by-token (default)
AI_CHATBOX_STREAM=false # wait for the full reply before displaying
How it works:
POST /ai-chatbox/stream using the Fetch API and ReadableStream'stream' => true and reads 1024-byte chunksdata: {"token":"word"}\n\ndata: [DONE]\n\nServer configuration:
Nginx proxy_buffering off; (the package sets X-Accel-Buffering: no automatically)
PHP output_buffering = Off in php.ini
RAG lets the chatbox answer questions about your own data — documents, FAQs, knowledge bases — without fine-tuning any model.
User sends a message
↓
Message is embedded → cosine similarity search across all indexed chunks
↓
Top-K most relevant chunks are injected as a system message
↓
AI answers using your knowledge-base context
1. Enable RAG and set embedding config on your active provider:
AI_CHATBOX_RAG=true
# Set embedding settings for your active provider (example: ollama)
OLLAMA_EMBEDDING_URL=http://localhost:11434/v1/embeddings
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
2. Run the migration:
php artisan migrate
3. Upload documents at /ai-chatbox/rag (requires an authenticated user by default).
Every subsequent chat message will automatically retrieve and inject relevant context.
RAG uses a separate embedding API — distinct from the chat API. Any provider with an /embeddings endpoint works.
Embedding settings are configured per named provider via rag_embedding_url, rag_embedding_model, and rag_embedding_timeout. They are resolved through the active provider — there are no global embedding env vars.
| Provider | Env var | Example value |
|---|---|---|
| Ollama | OLLAMA_EMBEDDING_URL |
http://localhost:11434/v1/embeddings |
| Ollama | OLLAMA_EMBEDDING_MODEL |
nomic-embed-text or mxbai-embed-large |
| LM Studio | LMSTUDIO_EMBEDDING_URL |
http://127.0.0.1:1234/v1/embeddings |
| LM Studio | LMSTUDIO_EMBEDDING_MODEL |
your loaded embedding model name |
| OpenAI | OPENAI_EMBEDDING_URL |
https://api.openai.com/v1/embeddings |
| OpenAI | OPENAI_EMBEDDING_MODEL |
text-embedding-3-small or text-embedding-3-large |
Ollama: Pull the embedding model first:
ollama pull nomic-embed-text
| Format | Extension | Notes |
|---|---|---|
| Plain text | .txt |
Chunked directly |
| Markdown | .md |
Heading structure is preserved across chunks |
Maximum upload size: 10 MB per file.
Documents are split into overlapping text chunks before embedding:
rag_chunk_size = 500 tokens ≈ 2 000 chars (default)
rag_chunk_overlap = 50 tokens ≈ 200 chars (default)
The chunker:
. ! ?) for oversized paragraphschunk_overlap characters into the next chunk so context is not lost at boundariesOn every chat message:
rag_similarity_threshold (default 0.2) are discardedrag_top_k (default 3) chunks are injected as a system message using rag_context_prompt, replacing the {chunks} placeholderThe default prompt instructs the model to treat retrieved chunks as its primary source and say "I don't have that information in my knowledge base" if the answer is not found there. Customise via .env:
AI_CHATBOX_RAG_CONTEXT_PROMPT="Use only the following context to answer:\n\n{chunks}\n\nDo not use any other knowledge."
Scale: Similarity is computed in PHP and works well up to a few thousand chunks. For larger knowledge bases, consider switching to a database with native vector support such as
pgvectorfor PostgreSQL.
Visit /ai-chatbox/rag (authenticated users only).
| Action | Description |
|---|---|
| Upload | Select a .md or .txt file, optionally set a title, click Upload & Index |
| Reprocess | Re-chunk and re-embed an existing document after changing chunk or embedding settings |
| Delete | Remove the document and all its chunks permanently (confirmation required) |
Each document shows its status (Pending → Processing → Ready / Failed), chunk count, and expandable error details on failure.
Visit /ai-chatbox/admin (authenticated users only).
| Section | Content |
|---|---|
| Stat cards | RAG document/chunk counts; conversation and message counts (database driver) |
| Configuration diagnostics | Live validation of every config group at page load — errors (red), warnings (amber), notices (blue). All clear → green banner |
| Config values | All resolved settings grouped by section |
| Named providers | All configured providers and their settings |
| Environment | Laravel version, PHP version, app environment, debug mode |
Diagnostic checks include: missing API URL/token/model, placeholder tokens left in place, APP_DEBUG on in production, SSRF conflicts with local URLs, open CORS origins, missing database tables, invalid RAG chunk settings, failed or unembedded documents, weak admin middleware, and selected frontend driver not installed.
Visit /ai-chatbox/admin/conversations (requires memory_driver=database).
Two separate middleware keys control access:
| Key | Controls | Default |
|---|---|---|
rag_admin_middleware |
Knowledge Base — /ai-chatbox/rag (document upload/delete) |
['web', 'auth'] |
admin_middleware |
Admin dashboard — /ai-chatbox/admin and conversations viewer |
inherits rag_admin_middleware when null |
By default both require an authenticated user. For production, publish the config and tighten each independently:
// config/ai-chatbox.php
// Restrict document management to admins only
'rag_admin_middleware' => ['web', 'auth', 'role:admin'],
// Allow all authenticated users to view the dashboard (read-only diagnostics)
'admin_middleware' => ['web', 'auth'],
Common middleware examples:
'rag_admin_middleware' => ['web', 'auth', 'role:admin'], // Spatie roles
'rag_admin_middleware' => ['web', 'auth', 'can:manage-chatbox'], // Laravel Gates
'admin_middleware' => ['web', 'auth:sanctum'], // Sanctum
All routes are registered under the configured prefix (ai-chatbox by default).
GET /ai-chatbox/health Health check — ping the AI service
POST /ai-chatbox/message Send a message, receive a full JSON reply
POST /ai-chatbox/stream Send a message, stream SSE tokens
POST /ai-chatbox/clear Clear server-side history for a thread
GET /ai-chatbox/rag Knowledge Base — list indexed documents [auth]
POST /ai-chatbox/rag Knowledge Base — upload a document [auth]
DELETE /ai-chatbox/rag/{id} Knowledge Base — delete a document [auth]
POST /ai-chatbox/rag/{id}/reprocess Knowledge Base — re-chunk and re-embed [auth]
GET /ai-chatbox/admin Admin dashboard [auth]
GET /ai-chatbox/admin/conversations Conversations list [auth]
GET /ai-chatbox/admin/conversations/data Conversations JSON (paginated) [auth]
GET /ai-chatbox/admin/conversations/{id}/messages Messages for a conversation (JSON) [auth]
The health check pings the configured api_url to verify the AI service is reachable. To prevent Server-Side Request Forgery (SSRF), requests to private and reserved IP ranges are blocked by default: localhost, 10.x, 172.16.x, 192.168.x, 169.254.x.
AI_CHATBOX_SSRF_PROTECTION=true # production — keep enabled (default)
AI_CHATBOX_SSRF_PROTECTION=false # local Ollama / LM Studio — disable
The package registers an ai-chatbox.cors middleware that restricts chatbox endpoints to requests originating from your application's URL. Cross-origin requests from other domains are rejected with 403.
To permit additional origins, publish the config:
'allowed_origins' => [
env('APP_URL', 'http://localhost'),
'https://other-allowed-origin.example.com',
],
By default the chatbox is accessible to guests. To restrict it to authenticated users:
// config/ai-chatbox.php
'middleware' => ['web', 'throttle:20,1', 'ai-chatbox.cors', 'auth'],
// or with Sanctum:
'middleware' => ['web', 'throttle:20,1', 'ai-chatbox.cors', 'auth:sanctum'],
Conversation history is stored in localStorage by default. For privacy-sensitive applications, switch to sessionStorage:
AI_CHATBOX_STORAGE=session
Do not enter passwords, tokens, or other secrets into the chatbox regardless of the storage driver — any script running on the page can read browser storage.
The color_scheme setting controls both the chat widget and all admin pages (/ai-chatbox/admin, /ai-chatbox/admin/conversations, /ai-chatbox/rag):
| Value | Behaviour |
|---|---|
auto (default) |
Follows the OS/browser prefers-color-scheme preference |
light |
Always light, regardless of OS preference |
dark |
Always dark, regardless of OS preference |
No env var. Publish the config and set
color_schemedirectly inconfig/ai-chatbox.php:
// config/ai-chatbox.php
'color_scheme' => 'auto', // OS preference (default)
'color_scheme' => 'light',
'color_scheme' => 'dark',
Run
php artisan config:clearafter changing config values.
Publish views to override any Blade template:
php artisan vendor:publish --tag=ai-chatbox-views
Published to resources/views/vendor/ai-chatbox/:
| File | Driver | Purpose |
|---|---|---|
chatbox.blade.php |
all | Main dispatcher — routes to the active driver |
chatbox-config.blade.php |
all | Outputs window.AiChatboxConfig |
chatbox-vue.blade.php |
vue |
CSS link + Vue mount point + JS bundle |
chatbox-blade.blade.php |
blade |
Full vanilla JS widget |
livewire/chatbox.blade.php |
livewire |
Alpine.js widget |
The package is organised into four explicit layers. Each layer communicates only with the layer directly above or below it; controllers contain no business logic.
┌──────────────────────────────────────────────────────┐
│ Layer 4 — UI │
│ ChatboxController · RagController · AdminController │
│ Blade views · Vue 3 · Blade · Livewire drivers │
│ HTTP request / response only │
├──────────────────────────────────────────────────────┤
│ Layer 3 — RAG │
│ RagRetriever · EmbeddingService · DocumentChunker │
│ RagDocument + RagChunk models │
│ Document upload, chunking, embedding, retrieval │
├──────────────────────────────────────────────────────┤
│ Layer 2 — Memory │
│ ContextManager │
│ SessionConversationRepository │
│ DatabaseConversationRepository │
│ Conversation + Message models │
│ History persistence and context trimming │
├──────────────────────────────────────────────────────┤
│ Layer 1 — AI Engine │
│ OpenAiCompatibleEngine · AiEngineInterface │
│ PromptBuilder · HealthChecker │
│ AiEngineException (error codes E01–E19) │
│ HTTP calls, prompt assembly, error handling │
└──────────────────────────────────────────────────────┘
Source layout:
src/
├── Config/
│ └── ai-chatbox.php
├── Console/
│ └── Commands/
│ └── PruneConversations.php # ai-chatbox:prune-conversations
├── Database/
│ └── Migrations/
├── Engine/
│ ├── Contracts/AiEngineInterface.php
│ ├── OpenAiCompatibleEngine.php
│ ├── HealthChecker.php
│ └── PromptBuilder.php
├── Http/
│ ├── Controllers/
│ └── Middleware/CorsMiddleware.php
├── Memory/
│ ├── Contracts/ConversationRepositoryInterface.php
│ ├── SessionConversationRepository.php
│ ├── DatabaseConversationRepository.php
│ ├── ContextManager.php
│ └── Models/
│ ├── Conversation.php
│ └── Message.php
├── Models/
│ ├── RagDocument.php
│ └── RagChunk.php
├── Services/
│ ├── RagRetriever.php
│ ├── EmbeddingService.php
│ └── DocumentChunker.php
├── resources/
│ └── views/
│ ├── chatbox.blade.php
│ ├── chatbox-config.blade.php
│ ├── chatbox-vue.blade.php
│ ├── chatbox-blade.blade.php
│ ├── admin.blade.php
│ ├── admin-conversations.blade.php
│ ├── rag.blade.php
│ └── livewire/chatbox.blade.php
├── AI.php # Facade
├── AiManager.php # Provider registry + singleton
└── AiChatboxServiceProvider.php
Implement AiEngineInterface to support a provider that is not OpenAI-compatible (Anthropic, Gemini, Cohere, etc.):
use SyafiqUnijaya\AiChatbox\Engine\Contracts\AiEngineInterface;
use SyafiqUnijaya\AiChatbox\Engine\Exceptions\AiEngineException;
class AnthropicEngine implements AiEngineInterface
{
public function validateConfig(array $options): void
{
if (empty($options['api_token'])) {
throw new AiEngineException('E03', 'API token missing', 500);
}
}
public function complete(array $messages, array $options = []): string
{
// Call Anthropic Messages API, return the reply as a plain string
}
public function stream(array $messages, array $options, callable $onToken): string
{
// Call $onToken('word') per token, return the full assembled reply
}
public function beginStream(array $messages, array $options): \Closure
{
// Open the HTTP connection before response()->stream() starts
// Return a closure: fn(callable $onToken): string
$this->validateConfig($options);
return function (callable $onToken): string {
// read stream, call $onToken per token, return full reply
};
}
}
Bind in a service provider:
use SyafiqUnijaya\AiChatbox\Engine\Contracts\AiEngineInterface;
$this->app->bind(AiEngineInterface::class, AnthropicEngine::class);
Implement ConversationRepositoryInterface to store history in Redis, MongoDB, or any other backend:
use SyafiqUnijaya\AiChatbox\Memory\Contracts\ConversationRepositoryInterface;
class RedisConversationRepository implements ConversationRepositoryInterface
{
public function getHistory(string $threadId): array
{
return json_decode(Redis::get("chat:{$threadId}") ?? '[]', true);
}
public function saveHistory(string $threadId, array $history): void
{
Redis::set("chat:{$threadId}", json_encode($history));
}
public function trimToLimit(string $threadId, int $maxPairs): void
{
$history = $this->getHistory($threadId);
$this->saveHistory($threadId, array_slice($history, -($maxPairs * 2)));
}
public function clear(string $threadId): void
{
Redis::del("chat:{$threadId}");
}
}
Bind in a service provider:
use SyafiqUnijaya\AiChatbox\Memory\Contracts\ConversationRepositoryInterface;
$this->app->bind(ConversationRepositoryInterface::class, RedisConversationRepository::class);
Binding a custom implementation directly overrides the
memory_driverconfig key selection.
If the widget shows an offline toast or requests fail, check storage/logs/laravel.log for an error code (E01–E19). Full reference: TROUBLESHOOTING.md
composer test
The test suite covers: controller responses, error classification, session history, conversation thread isolation, token-based context trimming, SSE streaming, RAG document upload/delete/reprocess, RAG context injection, CORS middleware, SSRF protection, health check logic, AiManager named provider resolution, AiProvider fluent modifiers and immutability, the AI facade, and the ai-chatbox:prune-conversations command (pre-flight checks, deletion, boundary conditions, cascade, --dry-run, --force, config key precedence) — using PHPUnit 11 and Orchestra Testbench.
MIT — see LICENSE for details.
.env ReferenceAll available settings with their default values.
# ── Active Provider ────────────────────────────────────────────────────────────
# api_url, api_token, and api_model are always sourced from the active provider.
AI_CHATBOX_ACTIVE_PROVIDER=ollama
# ── Response Tuning ────────────────────────────────────────────────────────────
AI_CHATBOX_TIMEOUT=30
# ── Conversation History ───────────────────────────────────────────────────────
AI_CHATBOX_HISTORY=true
# ── Streaming ─────────────────────────────────────────────────────────────────
AI_CHATBOX_STREAM=true
# ── Health Check ──────────────────────────────────────────────────────────────
AI_CHATBOX_HEALTH_CHECK=true
# ── Security ──────────────────────────────────────────────────────────────────
AI_CHATBOX_SSRF_PROTECTION=true # disable for local Ollama / LM Studio
AI_CHATBOX_RATE_LIMIT=20
AI_CHATBOX_RATE_WINDOW=1
# ── Widget Appearance ─────────────────────────────────────────────────────────
AI_CHATBOX_TITLE="AI Assistant"
# ── Memory ────────────────────────────────────────────────────────────────────
AI_CHATBOX_MEMORY_DRIVER=session # session | database
# ── RAG ───────────────────────────────────────────────────────────────────────
# Embedding URL and model are per-provider — set them in the provider block below.
# Tuning values (top_k, chunk_size, etc.) are set in the published config file.
AI_CHATBOX_RAG=false
AI_CHATBOX_EMBEDDING_TIMEOUT=10 # universal — applies to all providers
AI_CHATBOX_RAG_PROCESSING_TIMEOUT=0 # 0 = no limit
# ── Named Provider Credentials ────────────────────────────────────────────────
# The chatbox widget and AI facade both resolve through these env vars.
# AI_CHATBOX_ACTIVE_PROVIDER selects which block is used.
# Ollama
OLLAMA_URL=http://localhost:11434/v1/chat/completions
OLLAMA_TOKEN=your-ollama-token
OLLAMA_MODEL=gpt-oss:120b
OLLAMA_EMBEDDING_URL=http://localhost:11434/v1/embeddings
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
# LM Studio
LMSTUDIO_URL=http://127.0.0.1:1234/v1/chat/completions
LMSTUDIO_TOKEN=lmstudio
LMSTUDIO_MODEL=phi-3.5-mini-instruct
LMSTUDIO_EMBEDDING_URL=http://127.0.0.1:1234/v1/embeddings
LMSTUDIO_EMBEDDING_MODEL=text-embedding-nomic-embed-text-v1.5
# OpenAI
OPENAI_URL=https://api.openai.com/v1/chat/completions
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o
OPENAI_EMBEDDING_URL=https://api.openai.com/v1/embeddings
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
# Groq / OpenRouter / custom providers — add a 'providers' entry in config/ai-chatbox.php
# (see Provider examples in the docs above)
How can I help you explore Laravel packages today?