# Requirements
- PHP 8.2 or higher
- Laravel 10, 11, or 12
# Installation
Install via Composer then run the install command:
composer require paparascaldev/sidekick
php artisan sidekick:install
This publishes the config file and runs migrations. Alternatively, install manually:
composer require paparascaldev/sidekick
php artisan vendor:publish --tag=sidekick-config
php artisan migrate
# Configuration
Add your API keys to .env:
SIDEKICK_OPENAI_TOKEN=your-openai-key
SIDEKICK_CLAUDE_TOKEN=your-anthropic-key
SIDEKICK_MISTRAL_TOKEN=your-mistral-key
SIDEKICK_COHERE_TOKEN=your-cohere-key
The published config file (config/sidekick.php) looks like this:
return [
// Default provider when none specified
'default' => env('SIDEKICK_DEFAULT_PROVIDER', 'openai'),
// Default provider + model per capability
'defaults' => [
'text' => ['provider' => 'openai', 'model' => 'gpt-4o'],
'image' => ['provider' => 'openai', 'model' => 'dall-e-3'],
'audio' => ['provider' => 'openai', 'model' => 'tts-1'],
'transcription' => ['provider' => 'openai', 'model' => 'whisper-1'],
'embedding' => ['provider' => 'openai', 'model' => 'text-embedding-3-small'],
'moderation' => ['provider' => 'openai', 'model' => 'text-moderation-latest'],
],
// Provider API keys and base URLs
'providers' => [
'openai' => ['api_key' => env('SIDEKICK_OPENAI_TOKEN'), 'base_url' => '...'],
'anthropic' => ['api_key' => env('SIDEKICK_CLAUDE_TOKEN'), 'base_url' => '...'],
'mistral' => ['api_key' => env('SIDEKICK_MISTRAL_TOKEN'), 'base_url' => '...'],
'cohere' => ['api_key' => env('SIDEKICK_COHERE_TOKEN'), 'base_url' => '...'],
],
// Register custom providers
'custom_providers' => [],
// Chat widget settings
'widget' => [
'enabled' => env('SIDEKICK_WIDGET_ENABLED', false),
'route_prefix' => 'sidekick',
'middleware' => ['web'],
'provider' => env('SIDEKICK_WIDGET_PROVIDER', 'openai'),
'model' => env('SIDEKICK_WIDGET_MODEL', 'gpt-4o'),
'system_prompt' => env('SIDEKICK_WIDGET_SYSTEM_PROMPT', 'You are a helpful assistant.'),
'max_tokens' => 1024,
],
// HTTP timeout and retry settings
'http' => [
'timeout' => env('SIDEKICK_HTTP_TIMEOUT', 30),
'connect_timeout' => env('SIDEKICK_HTTP_CONNECT_TIMEOUT', 10),
'retry' => ['times' => 0, 'sleep' => 100],
],
];
# Quick Start
Use the Facade or the global helper:
use PapaRascalDev\Sidekick\Facades\Sidekick;
// Using the facade
$response = Sidekick::text()->withPrompt('Hello')->generate();
// Using the helper function
$response = sidekick()->text()->withPrompt('Hello')->generate();
# Text Generation
$response = Sidekick::text()
->using('openai', 'gpt-4o')
->withSystemPrompt('You are a helpful assistant.')
->withPrompt('What is Laravel?')
->generate();
echo $response->text; // "Laravel is a PHP web framework..."
echo $response->usage->totalTokens; // 150
echo $response->meta->latencyMs; // 523.4
echo $response->finishReason; // "stop"
echo (string) $response; // Same as $response->text
TextBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withPrompt(string $prompt) | Add a user message |
withSystemPrompt(string $prompt) | Set the system prompt |
withMessages(array $messages) | Set the full message history |
addMessage(Role $role, string $content) | Append a single message with a specific role |
withMaxTokens(int $maxTokens) | Set max tokens (default: 1024) |
withTemperature(float $temp) | Set temperature (default: 1.0) |
generate(): TextResponse | Execute and return a TextResponse |
stream(): StreamResponse | Execute and return a streamable StreamResponse |
# Streaming
$stream = Sidekick::text()
->using('anthropic', 'claude-sonnet-4-20250514')
->withPrompt('Write a haiku about coding')
->stream();
// Iterate over chunks
foreach ($stream as $chunk) {
echo $chunk;
}
// Get the full buffered text after iteration
$fullText = $stream->text();
// Or return as an SSE response from a controller
return $stream->toResponse();
# Conversations
Database-backed conversations with automatic message persistence:
// Start a conversation
$convo = Sidekick::conversation()
->using('openai', 'gpt-4o')
->withSystemPrompt('You are a travel advisor.')
->withMaxTokens(2048)
->begin();
$response = $convo->send('I want to visit Japan.');
echo $response->text;
// Get the conversation ID to resume later
$conversationId = $convo->getConversation()->id;
// Resume later
$convo = Sidekick::conversation()->resume($conversationId);
$response = $convo->send('What about accommodation?');
// Delete a conversation
$convo->delete();
ConversationBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withSystemPrompt(string $prompt) | Set the system prompt |
withMaxTokens(int $maxTokens) | Set max tokens (default: 1024) |
begin(): self | Start a new conversation (persisted to DB) |
resume(string $id): self | Resume an existing conversation by UUID |
send(string $message): TextResponse | Send a message and get a response |
delete(): bool | Delete the conversation and its messages |
getConversation(): ?Conversation | Get the underlying Eloquent model |
# Image Generation
$response = Sidekick::image()
->using('openai', 'dall-e-3')
->withPrompt('A sunset over mountains')
->withSize('1024x1024')
->withQuality('hd')
->count(2)
->generate();
echo $response->url(); // First image URL
echo $response->urls; // Array of all URLs
echo $response->revisedPrompt; // DALL-E's revised prompt (if any)
ImageBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withPrompt(string $prompt) | Set the image prompt |
withSize(string $size) | Set dimensions (default: 1024x1024) |
withQuality(string $quality) | Set quality: standard or hd |
count(int $count) | Number of images to generate (default: 1) |
generate(): ImageResponse | Execute and return an ImageResponse |
# Audio (Text-to-Speech)
$response = Sidekick::audio()
->using('openai', 'tts-1')
->withText('Hello, welcome to Sidekick!')
->withVoice('nova')
->withFormat('mp3')
->generate();
$response->save('audio/welcome.mp3'); // Save to default disk
$response->save('audio/welcome.mp3', 's3'); // Save to specific disk
echo $response->format; // "mp3"
AudioBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withText(string $text) | Set the text to speak |
withVoice(string $voice) | Set the voice (default: alloy) |
withFormat(string $format) | Set audio format (default: mp3) |
generate(): AudioResponse | Execute and return an AudioResponse |
# Transcription
$response = Sidekick::transcription()
->using('openai', 'whisper-1')
->withFile('/path/to/audio.mp3')
->withLanguage('en')
->generate();
echo $response->text; // Transcribed text
echo $response->language; // "en"
echo $response->duration; // Duration in seconds
echo (string) $response; // Same as $response->text
TranscriptionBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withFile(string $filePath) | Path to the audio file |
withLanguage(string $language) | Hint the language (optional) |
generate(): TranscriptionResponse | Execute and return a TranscriptionResponse |
# Embeddings
$response = Sidekick::embedding()
->using('openai', 'text-embedding-3-small')
->withInput('Laravel is a great framework')
->generate();
$vector = $response->vector(); // First embedding vector (array of floats)
$all = $response->embeddings; // All embedding vectors
echo $response->usage->totalTokens; // Token usage
EmbeddingBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withInput(string|array $input) | Text or array of texts to embed |
generate(): EmbeddingResponse | Execute and return an EmbeddingResponse |
# Moderation
$response = Sidekick::moderation()
->using('openai', 'text-moderation-latest')
->withContent('Some text to moderate')
->generate();
if ($response->isFlagged()) {
// Content was flagged
}
if ($response->isFlaggedFor('violence')) {
// Specifically flagged for violence
}
// Inspect all categories
foreach ($response->categories as $category) {
echo "{$category->category}: flagged={$category->flagged}, score={$category->score}\n";
}
ModerationBuilder Methods
| Method | Description |
|---|---|
using(string $provider, ?string $model) | Set provider and model |
withContent(string $content) | Text to moderate |
generate(): ModerationResponse | Execute and return a ModerationResponse |
# Utility Methods
Convenient shorthand methods on the facade:
// Summarize text (returns string)
$summary = Sidekick::summarize('Long text here...', maxLength: 500);
// Translate text (returns string)
$translated = Sidekick::translate('Hello', 'French');
// Extract keywords (returns string, comma-separated)
$keywords = Sidekick::extractKeywords('Some article text...');
# Knowledge Base / RAG
Sidekick includes a built-in Retrieval-Augmented Generation system with vector search, chunking, and knowledge base management.
Configuration
RAG settings in config/sidekick.php:
'knowledge' => [
'embedding' => ['provider' => 'openai', 'model' => 'text-embedding-3-small'],
'chunking' => ['chunk_size' => 2000, 'overlap' => 200],
'search' => ['default_limit' => 5, 'min_score' => 0.3, 'driver' => VectorSearch::class],
],
Ingesting Content
use PapaRascalDev\Sidekick\Facades\Sidekick;
// Ingest text
Sidekick::knowledge('my-kb')
->ingest('Our return policy is 30 days with receipt.', 'faq');
// Ingest a file
Sidekick::knowledge('my-kb')
->ingestFile('/path/to/faq.md');
// Ingest multiple texts
Sidekick::knowledge('my-kb')
->ingestMany(['Text one...', 'Text two...'], 'bulk-source');
// Use a different embedding provider
Sidekick::knowledge('my-kb')
->using('mistral', 'mistral-embed')
->ingest('Content here...');
Artisan Commands
# Ingest a file
php artisan sidekick:ingest my-kb --file=/path/to/faq.md
# Ingest inline text
php artisan sidekick:ingest my-kb --text="Return policy is 30 days."
# Ingest a directory of .txt/.md/.html/.csv files
php artisan sidekick:ingest my-kb --dir=/path/to/docs
# Purge and re-ingest
php artisan sidekick:ingest my-kb --purge --dir=/path/to/docs
Searching
$results = Sidekick::knowledge('my-kb')->search('What is your return policy?');
foreach ($results as $chunk) {
echo $chunk->content; // The text content
echo $chunk->similarity; // Cosine similarity score
echo $chunk->source; // Source label
}
Ask (Search + Generate)
One-liner: search the knowledge base and generate a grounded answer:
$answer = Sidekick::knowledge('my-kb')->ask('What is your return policy?');
Widget Integration
Connect a knowledge base to the chat widget:
SIDEKICK_WIDGET_ENABLED=true
SIDEKICK_WIDGET_KNOWLEDGE_BASE=my-kb
Or in config/sidekick.php:
'widget' => [
'knowledge_base' => 'my-kb',
'rag_context_chunks' => 5,
'rag_min_score' => 0.3,
],
When configured, the widget automatically searches the knowledge base for each user message and injects relevant context into the system prompt. If RAG fails, it falls back gracefully to the original system prompt.
Custom Search Drivers
use PapaRascalDev\Sidekick\Contracts\SearchesKnowledge;
class PgVectorSearch implements SearchesKnowledge
{
public function search(KnowledgeBase $kb, array $queryEmbedding, int $limit = 5, float $minScore = 0.3): Collection
{
// Your pgvector implementation
}
}
// Register in config/sidekick.php
'knowledge' => [
'search' => ['driver' => \App\Search\PgVectorSearch::class],
],
Managing Knowledge Bases
$kb = Sidekick::knowledge('my-kb');
$kb->chunkCount(); // Number of chunks stored
$kb->purge(); // Delete all chunks
$kb->getKnowledgeBase(); // Get the Eloquent model
# Chat Widget
Sidekick ships with a drop-in Alpine.js chat widget. Enable it in your .env:
SIDEKICK_WIDGET_ENABLED=true
SIDEKICK_WIDGET_PROVIDER=openai
SIDEKICK_WIDGET_MODEL=gpt-4o
SIDEKICK_WIDGET_SYSTEM_PROMPT="You are a helpful assistant."
Blade Component
<x-sidekick::chat-widget
position="bottom-right"
theme="dark"
title="AI Assistant"
placeholder="Ask me anything..."
button-label="Chat"
/>
Widget Props
| Prop | Default | Options |
|---|---|---|
position | bottom-right | bottom-right, bottom-left, top-right, top-left |
theme | light | light, dark |
title | Chat Assistant | Any string |
placeholder | Type a message... | Any string |
button-label | Chat | Any string |
Prerequisites
The widget requires Alpine.js and a CSRF meta tag:
<meta name="csrf-token" content="{{ csrf_token() }}">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
# Custom Providers
Register your own AI providers at runtime or via config:
// Runtime registration
Sidekick::registerProvider('ollama', function ($app) {
return new OllamaProvider(config('sidekick.providers.ollama'));
});
// Or in config/sidekick.php
'custom_providers' => [
'ollama' => \App\Sidekick\OllamaProvider::class,
],
Custom providers should implement ProviderContract and the relevant capability interfaces: ProvidesText, ProvidesImages, ProvidesAudio, ProvidesTranscription, ProvidesEmbeddings, ProvidesModeration.
# Testing
Sidekick ships with first-class testing support via Sidekick::fake():
use PapaRascalDev\Sidekick\Facades\Sidekick;
use PapaRascalDev\Sidekick\Responses\TextResponse;
use PapaRascalDev\Sidekick\ValueObjects\Meta;
use PapaRascalDev\Sidekick\ValueObjects\Usage;
public function test_my_feature(): void
{
$fake = Sidekick::fake([
new TextResponse(
text: 'Mocked response',
usage: new Usage(10, 20, 30),
meta: new Meta('openai', 'gpt-4o'),
),
]);
// ... run your code that uses Sidekick ...
$fake->assertTextGenerated(); // At least one text generation
$fake->assertTextGenerated(2); // Exactly 2 text generations
$fake->assertNothingSent(); // No API calls were made
$fake->assertProviderUsed('openai');
$fake->assertModelUsed('gpt-4o');
$fake->assertPromptContains('expected text');
}
# Events
All events are in the PapaRascalDev\Sidekick\Events namespace:
| Event | Properties | When |
|---|---|---|
RequestSending | provider, model, capability | Before a request is sent |
ResponseReceived | provider, model, capability, response | After a successful response |
StreamChunkReceived | provider, model, chunk | For each streaming chunk |
RequestFailed | provider, model, capability, exception | When a request fails |
# Provider Capabilities
| Capability | OpenAI | Anthropic | Mistral | Cohere |
|---|---|---|---|---|
| Text | Yes | Yes | Yes | Yes |
| Image | Yes | - | - | - |
| Audio (TTS) | Yes | - | - | - |
| Transcription | Yes | - | - | - |
| Embedding | Yes | - | Yes | - |
| Moderation | Yes | - | - | - |
# Upgrading from v1
Sidekick v2.0 is a complete rewrite with breaking changes. Follow these steps to upgrade.
1. Requirements
- PHP 8.2+ (was 8.0+)
- Laravel 10+ (dropped Laravel 9 support)
2. Update Composer
composer require paparascaldev/sidekick:^2.0
3. Publish the new config
php artisan vendor:publish --tag=sidekick-config --force
4. Update .env
SIDEKICK_OPENAI_TOKEN=your-key
SIDEKICK_CLAUDE_TOKEN=your-key
SIDEKICK_MISTRAL_TOKEN=your-key
SIDEKICK_COHERE_TOKEN=your-key
5. Run migrations
php artisan migrate
class column is now provider, and max_tokens changed from bigInteger to unsignedInteger. Existing conversation data is not automatically migrated — back it up before running migrations.
6. Update your code
Text generation
// v1
$driver = new OpenAi();
$sidekick = Sidekick::create($driver);
$response = $sidekick->complete('gpt-4', 'system prompt', 'user message', [], 1024);
// v2
use PapaRascalDev\Sidekick\Facades\Sidekick;
$response = Sidekick::text()
->using('openai', 'gpt-4o')
->withSystemPrompt('system prompt')
->withPrompt('user message')
->generate();
echo $response->text; // Typed DTO instead of raw array
Conversations
// v1
$convo = new SidekickConversation();
$convo->begin(new OpenAi(), 'gpt-4', 'System prompt');
$response = $convo->sendMessage('Hello');
// v2
$convo = Sidekick::conversation()
->using('openai', 'gpt-4o')
->withSystemPrompt('System prompt')
->begin();
$response = $convo->send('Hello');
echo $response->text;
Image generation
// v1
$sidekick = Sidekick::create(new OpenAi());
$response = $sidekick->image()->generate('dall-e-3', 'A sunset');
// v2
$response = Sidekick::image()
->using('openai', 'dall-e-3')
->withPrompt('A sunset')
->generate();
echo $response->url();
Audio generation
// v1
$sidekick = Sidekick::create(new OpenAi());
$response = $sidekick->audio()->fromText('tts-1', 'Hello');
// v2
$response = Sidekick::audio()
->using('openai', 'tts-1')
->withText('Hello')
->generate();
$response->save('audio/hello.mp3');
Utilities
// v1
$utils = sidekickUtilities(new OpenAi());
$summary = $utils->summarize('Long text...');
// v2
$summary = Sidekick::summarize('Long text...');
$translated = Sidekick::translate('Hello', 'French');
$keywords = Sidekick::extractKeywords('Some text');
Helper functions
// v1
$driver = sidekick(new OpenAi());
$convo = sidekickConversation();
$utils = sidekickUtilities(new OpenAi());
// v2
$manager = sidekick(); // Returns SidekickManager
$manager->text()->using('openai', 'gpt-4o')->withPrompt('Hi')->generate();
7. Remove old playground
If you previously installed the v1 playground, remove these files:
resources/views/Pages/Sidekick/resources/views/Components/Sidekick/routes/web.sidekick.php- Remove
require base_path('routes/web.sidekick.php');fromroutes/web.php
The v2 chat widget replaces the playground.
Removed Classes
| v1 Class | v2 Replacement |
|---|---|
Sidekick (static factory) | SidekickManager via facade |
SidekickConversation | ConversationBuilder |
SidekickDriverInterface | ProviderContract + capability interfaces |
Drivers\OpenAi | Providers\OpenAiProvider |
Drivers\Claude | Providers\AnthropicProvider |
Drivers\Mistral | Providers\MistralProvider |
Drivers\Cohere | Providers\CohereProvider |
Features\Completion | Integrated into providers |
Features\Image | ImageBuilder |
Features\Audio | AudioBuilder |
Features\Embedding | EmbeddingBuilder |
Features\Moderate | ModerationBuilder |
Features\Transcribe | TranscriptionBuilder |
Utilities\Utilities | SidekickManager utility methods |
Facades\SidekickConversation | Use Sidekick::conversation() |
Models\SidekickConversation | Models\Conversation |
Models\SidekickConversationMessage | Models\ConversationMessage |
New in v2
- Fluent builder API for all capabilities
- Typed readonly response DTOs
- Streaming support with SSE
- Event system (RequestSending, ResponseReceived, etc.)
Sidekick::fake()for testing- Alpine.js chat widget
- Custom provider registration
- Knowledge Base / RAG with vector search
- Publishable config file