elliottlawson / converse
A Laravel package for storing and managing AI conversation history in your applications
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.22
- orchestra/testbench: ^9.4
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- spatie/laravel-ray: ^1.40
This package is auto-updated.
Last update: 2025-06-13 16:27:56 UTC
README
A Laravel package for storing and managing AI conversation history with any LLM provider. Built to handle the real-world needs of AI-powered applications, including streaming responses, function calling, and conversation branching.
Features
- Provider Agnostic: Works with any AI provider (OpenAI, Anthropic, Google, etc.)
- Type-Safe Message Helpers: Dedicated methods for each message type
- Streaming Support: Elegant handling of streaming responses with chunk storage
- Event Driven: Real-time broadcasting support for live updates
- Soft Deletes: Full soft delete support with cascading deletes
- Flexible Storage: JSON metadata fields for provider-specific data
Installation
composer require elliottlawson/converse
Publish the config and migrations:
php artisan vendor:publish --provider="ElliottLawson\Converse\ConverseServiceProvider"
php artisan migrate
Basic Usage
Setting Up Your Model
use App\Models\User; use ElliottLawson\Converse\Traits\HasAIConversations; class User extends Model { use HasAIConversations; }
Creating Conversations
$user = User::find(1); $conversation = $user->startConversation([ 'title' => 'Help with Laravel', 'metadata' => [ 'provider' => 'anthropic', 'model' => 'claude-3-5-sonnet', ], ]);
Adding Messages
use ElliottLawson\Converse\Models\Conversation; // User messages $conversation->addUserMessage('How do I deploy Laravel to production?'); // Assistant responses $conversation->addAssistantMessage('There are several ways to deploy Laravel...'); // System prompts $conversation->addSystemMessage('You are a helpful Laravel expert.'); // Tool/Function calls $conversation->addToolCallMessage('get_deployment_options(framework="laravel")'); $conversation->addToolResultMessage('{"options": ["Forge", "Vapor", "Docker"]}');
Bulk Importing Messages
Perfect for importing conversation history or migrating from other systems:
use ElliottLawson\Converse\Messages\UserMessage; use ElliottLawson\Converse\Messages\AssistantMessage; use ElliottLawson\Converse\Messages\SystemMessage; use ElliottLawson\Converse\Enums\MessageRole; // Using message DTOs $messages = $conversation->addMessages([ new SystemMessage('You are a Laravel expert'), new UserMessage('How do I deploy to production?'), new AssistantMessage('Let me help you with deployment...'), ]); // With metadata in DTOs $messages = $conversation->addMessages([ new UserMessage('Analyze this code', ['timestamp' => now(), 'ip' => request()->ip()]), new AssistantMessage('I found several issues...', ['model' => 'gpt-4', 'tokens' => 245]), ]); // Simple strings - pass multiple arguments directly (all treated as user messages) $messages = $conversation->addMessages( 'Hello', 'I need help with Laravel', 'How do I set up queues?' ); // Or pass an array (all treated as user messages) $messages = $conversation->addMessages([ 'Hello', 'I need help with Laravel', 'How do I set up queues?', ]); // With roles using enum directly $messages = $conversation->addMessages([ ['role' => MessageRole::System, 'content' => 'You are a Laravel expert'], ['role' => MessageRole::User, 'content' => 'How do I deploy to production?'], ['role' => MessageRole::Assistant, 'content' => 'Let me help you with deployment...'], ]); // With roles as strings (automatically converted to enum) $messages = $conversation->addMessages([ ['role' => 'system', 'content' => 'You are a Laravel expert'], ['role' => 'user', 'content' => 'How do I deploy to production?'], ['role' => 'assistant', 'content' => 'Let me help you with deployment...'], ]);
Working with Metadata
Metadata allows you to store provider-specific information, request details, or any custom data alongside messages:
// Track user context $conversation->addUserMessage('What about costs?', [ 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent(), 'session_id' => session()->getId(), ]); // Store AI provider details $conversation->addAssistantMessage('Here are the pricing details...', [ 'model' => 'gpt-4-turbo', 'temperature' => 0.7, 'max_tokens' => 500, 'prompt_tokens' => 85, 'completion_tokens' => 150, 'total_cost' => 0.0234, ]); // Function call metadata $conversation->addToolCallMessage('search_products(query="laptops")', [ 'function_name' => 'search_products', 'arguments' => ['query' => 'laptops'], 'call_id' => 'call_abc123', ]);
Streaming Responses
Most AI providers stream responses token by token. This package makes it easy to store these streams while maintaining a great user experience:
use ElliottLawson\Converse\Models\Message; // Start streaming an assistant response $message = $conversation->startStreamingAssistant([ 'model' => 'claude-3-5-sonnet', 'request_id' => Str::uuid(), ]); // In your streaming handler, append chunks as they arrive foreach ($aiProvider->stream($prompt) as $chunk) { $message->appendChunk($chunk); // Broadcast to your frontend broadcast(new StreamUpdate($message, $chunk)); } // When streaming completes successfully $message->completeStreaming([ 'prompt_tokens' => 150, 'completion_tokens' => 423, 'total_tokens' => 573, 'finish_reason' => 'stop', 'duration_ms' => 2341, ]); // Handle streaming failures gracefully if ($error) { $message->failStreaming('Connection lost', [ 'error_code' => 'NETWORK_ERROR', 'attempted_retry' => true, 'partial_response' => true, ]); }
You can also stream user input for voice transcription or real-time collaboration:
$message = $conversation->startStreamingUser([ 'input_method' => 'voice', 'language' => 'en-US', ]);
Managing Conversations
The package provides several methods to organize and retrieve conversations:
use App\Models\User; use ElliottLawson\Converse\Models\Conversation; $user = User::find(1); // Get all conversations for a user $conversations = $user->conversations() ->latest() ->get(); // Find a conversation by its ID $conversation = $user->conversations()->find($conversationId); // Get only active conversations (excluding soft-deleted) $activeChats = $user->activeConversations() ->whereDate('created_at', '>=', now()->subDays(7)) ->get(); // Continue an existing conversation $conversation = $user->continueConversation($conversationId); $conversation->addUserMessage('Actually, I have another question...'); // Clean up old conversations $user->conversations() ->where('updated_at', '<', now()->subMonths(6)) ->each->delete(); // Soft deletes // Restore an accidentally deleted conversation $conversation = $user->conversations() ->withTrashed() ->find($id); $conversation->restore();
Standalone Conversations
You can also create conversations without associating them with a model:
use ElliottLawson\Converse\Models\Conversation; $conversation = Conversation::create([ 'title' => 'Quick Chat', 'metadata' => ['source' => 'api'], ]); $conversation->addUserMessage('Hello!'); $conversation->addAssistantMessage('Hi there!');
Events
The package dispatches events throughout the conversation lifecycle, perfect for real-time updates, analytics, and integrations:
use ElliottLawson\Converse\Events\ConversationCreated; use ElliottLawson\Converse\Events\MessageCreated; use ElliottLawson\Converse\Events\ChunkReceived; use ElliottLawson\Converse\Events\MessageCompleted; // In your EventServiceProvider protected $listen = [ ConversationCreated::class => [ SendWelcomeMessage::class, TrackConversationAnalytics::class, ], MessageCreated::class => [ BroadcastMessageToUser::class, CheckForModeration::class, ], ChunkReceived::class => [ StreamChunkToWebSocket::class, ], MessageCompleted::class => [ CalculateCosts::class, UpdateUserCredits::class, ], ];
Example Listeners
namespace App\Listeners; use ElliottLawson\Converse\Events\MessageCreated; use Illuminate\Contracts\Queue\ShouldQueue; class BroadcastMessageToUser implements ShouldQueue { public function handle(MessageCreated $event): void { $message = $event->message; $conversation = $message->conversation; // Broadcast to user's private channel broadcast(new NewMessage($message)) ->toOthers() ->onChannel("user.{$conversation->conversable_id}"); } }
namespace App\Listeners; use ElliottLawson\Converse\Events\ChunkReceived; class StreamChunkToWebSocket { public function handle(ChunkReceived $event): void { $chunk = $event->chunk; $message = $chunk->message; // Stream to conversation channel broadcast(new StreamUpdate( conversationId: $message->conversation_id, messageId: $message->id, chunk: $chunk->content, sequence: $chunk->sequence ))->toOthers(); } }
namespace App\Listeners; use ElliottLawson\Converse\Events\MessageCompleted; class CalculateCosts { public function handle(MessageCompleted $event): void { $message = $event->message; if ($message->role->value === 'assistant' && isset($message->metadata['tokens'])) { $cost = $this->calculateTokenCost( $message->metadata['prompt_tokens'] ?? 0, $message->metadata['completion_tokens'] ?? 0, $message->metadata['model'] ?? 'gpt-3.5-turbo' ); // Store cost for billing $message->conversation->conversable->billing()->create([ 'tokens_used' => $message->metadata['tokens'], 'cost' => $cost, 'model' => $message->metadata['model'], ]); } } } ## Advanced Usage ### Retrieving Messages ```php // Get all messages in a conversation $messages = $conversation->messages; // Get the last message $lastMessage = $conversation->lastMessage; // Use scopes for cleaner queries $userMessages = $conversation->messages()->user()->get(); $assistantMessages = $conversation->messages()->assistant()->get(); $systemMessages = $conversation->messages()->system()->get(); // Get only completed messages $completed = $conversation->messages()->completed()->get(); // Get messages still streaming $streaming = $conversation->messages()->streaming()->first(); // Get failed messages $failed = $conversation->messages()->failed()->get(); // Combine scopes $failedUserMessages = $conversation->messages() ->user() ->failed() ->get();
Message Chunks for Streaming
// Access chunks for a streamed message $chunks = $message->chunks; foreach ($chunks as $chunk) { echo $chunk->content; echo "Received at: " . $chunk->created_at; }
Working with Metadata
// Query conversations by metadata $openAIChats = $user->conversations() ->where('metadata->provider', 'openai') ->get(); // Find high-token conversations $expensive = $user->conversations() ->whereHas('messages', function ($query) { $query->where('metadata->tokens', '>', 1000); }) ->get();
Finding Conversations by UUID
UUIDs are useful for public URLs, APIs, and external references:
// For public links or API endpoints $conversation = Conversation::where('uuid', $uuid)->firstOrFail(); // Verify ownership if needed if ($conversation->conversable_id !== $user->id) { abort(403); }
License
The MIT License (MIT). Please see License File for more information.