tobiebenezer / php-ai
Modern Laravel AI assistant framework with multi-provider, multimodal vision, tool execution, and guardrails.
Requires
- php: ^7.3|^8.0
- illuminate/contracts: ^8.0
- illuminate/support: ^8.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel package that connects LLMs (OpenRouter, Google Gemini) to database queries, vision models, structured JSON schemas, and safety guardrails.
Key features
- Multi-provider support: OpenRouter and Google Gemini adapters with profile-based configuration.
- Multimodal vision: analyze images, receipts, and documents directly through vision models.
- Tool discovery and execution: automatically scans and runs custom tools with conversational loops and error recovery.
- Analytical database queries: extend
AnalyticalToolto query Eloquent models with column filtering, grouping, and aggregations. - Guardrails: run pre-prompt instructions and lifecycle checks before and after tool calls.
- Dynamic runtime settings: change providers, API keys, and models from database settings without editing
.env. - Structured JSON outputs: return responses matching your JSON schema.
- Audit logging: record prompts, responses, tool calls, token usage, and latency in database tables.
- Token budgeting: set monthly token limits to prevent runaway API spend.
- Artisan generators: create tools, guardrails, and providers with CLI commands.
- Livewire chat and queues: prebuilt chat component and asynchronous job handling.
Requirements and compatibility
- PHP: 7.3, 8.0, 8.1, 8.2, or 8.3
- Laravel / Illuminate: 8.x, 9.x, 10.x, or 11.x
Installation
Install the package with Composer:
composer require tobiebenezer/php-ai
If you are developing locally with a path repository, add this to your root composer.json:
"repositories": [ { "type": "path", "url": "../php-ai" } ]
Configuration and migrations
Publish the configuration, migrations, and stubs:
# Publish everything php artisan vendor:publish --provider="Tobiebenezer\Ai\AiServiceProvider" # Or publish individually php artisan vendor:publish --tag="ai-config" php artisan vendor:publish --tag="ai-migrations" php artisan vendor:publish --tag="ai-stubs"
Run the migrations to create the request and tool call log tables:
php artisan migrate
Configuration and environment
Add your provider keys to .env:
AI_PROFILE=openrouter # OpenRouter OPENROUTER_API_KEY=your-openrouter-key OPENROUTER_MODEL=google/gemini-2.0-flash # Google Gemini GEMINI_API_KEY=your-gemini-key GEMINI_MODEL=gemini-2.0-flash
For advanced settings, edit config/ai.php to define custom profiles, tools, guardrails, and limits.
Multimodal vision and document OCR
The package formats image payloads for both OpenRouter and Google Gemini without requiring separate code paths.
Basic image analysis
Use AiMessage::userWithImage($prompt, $base64Data, $mimeType):
use Tobiebenezer\Ai\AiAssistant; use Tobiebenezer\Ai\DTO\AiMessage; use Tobiebenezer\Ai\DTO\AiRequest; $assistant = app(AiAssistant::class); $imagePath = storage_path('app/receipts/receipt.jpg'); $base64Image = base64_encode(file_get_contents($imagePath)); $mimeType = mime_content_type($imagePath) ?: 'image/jpeg'; $message = AiMessage::userWithImage( 'Describe this image and extract any visible text.', $base64Image, $mimeType ); $response = $assistant->respond(new AiRequest([ 'messages' => [$message], ])); echo $response->content;
Structured JSON extraction from images
Combine an image message with response_schema to extract structured data (such as bank receipts or invoices) as typed JSON:
$request = new AiRequest([ 'messages' => [ AiMessage::userWithImage( 'Extract the transaction reference, amount, and date from this receipt.', $base64Image, 'image/jpeg' ) ], 'options' => [ 'temperature' => 0.1, ], 'response_schema' => [ 'type' => 'object', 'properties' => [ 'is_valid_receipt' => ['type' => 'boolean'], 'bank_name' => ['type' => ['string', 'null']], 'session_id' => ['type' => ['string', 'null']], 'amount' => ['type' => ['number', 'null']], 'date' => ['type' => ['string', 'null']], ], 'required' => ['is_valid_receipt'], ], ]); $response = $assistant->respond($request); $data = json_decode($response->content, true); $bankRef = $data['session_id']; $amount = $data['amount'];
Dynamic runtime settings service
When users update API keys or switch providers from an admin dashboard, you can apply those changes at runtime without rewriting .env or restarting processes.
Set your service class in config/ai.php:
// config/ai.php 'settings_service' => \App\Services\Ai\AiSettingsService::class,
Implement an apply(): void method. The runner calls it before preparing each request:
<?php namespace App\Services\Ai; use App\Models\Config; use Illuminate\Support\Facades\Cache; class AiSettingsService { public function apply(): void { $configs = Cache::remember('app_ai_configs', 3600, function () { return Config::where('category', 'ai')->pluck('value', 'tag'); }); $activeProvider = $configs['ai_provider'] ?? 'openrouter'; config([ 'ai.default_profile' => $activeProvider, 'ai.providers.openrouter.key' => $configs['ai_openrouter_key'] ?? env('OPENROUTER_API_KEY'), 'ai.profiles.openrouter.model' => $configs['ai_openrouter_model'] ?? 'google/gemini-2.0-flash', 'ai.providers.gemini.key' => $configs['ai_gemini_key'] ?? env('GEMINI_API_KEY'), 'ai.profiles.gemini.model' => $configs['ai_gemini_model'] ?? 'gemini-2.0-flash', ]); } }
Writing custom tools
The package discovers classes implementing Tobiebenezer\Ai\Contracts\Tool inside your tool directory (default: app/Ai/Tools/).
Extending AnalyticalTool
For database queries, extend Tobiebenezer\Ai\Tools\AnalyticalTool. It handles input validation, query building, joins, and aggregates:
<?php namespace App\Ai\Tools; use Tobiebenezer\Ai\Tools\AnalyticalTool; use App\Models\Sale; class QuerySalesTool extends AnalyticalTool { protected function modelClass() { return Sale::class; } protected function filterableColumns() { return ['branch_id', 'staff_id', 'status_id']; } protected function groupableColumns() { return ['branch_id', 'staff_id', 'status_id']; } protected function aggregateableColumns() { return ['total', 'discount']; } protected function defaultSelects() { return [ 'sales.id', 'sales.total', 'sales.discount', 'sales.created_at', 'branches.name as branch_name', ]; } protected function joins() { return [ ['branches', 'sales.branch_id', '=', 'branches.id'], ]; } public function description() { return 'Query sales transactions with totals, discounts, and branch associations.'; } }
Implementing Tool directly
For external API integrations (weather, maps, CRM lookups), implement Tool directly:
<?php namespace App\Ai\Tools; use Tobiebenezer\Ai\Contracts\Tool; use Tobiebenezer\Ai\Guardrails\GuardrailContext; class ExternalWeatherTool implements Tool { public function name() { return 'get_weather'; } public function description() { return 'Retrieve current weather for a city.'; } public function schema() { return [ 'type' => 'object', 'properties' => [ 'city' => ['type' => 'string', 'description' => 'The city name'] ], 'required' => ['city'] ]; } public function profiles() { return ['*']; } public function isReadOnly() { return true; } public function execute(array $arguments, GuardrailContext $context) { // Call external weather API return ['temp' => '27C', 'condition' => 'Sunny']; } }
Guardrail pipeline
Guardrails check and restrict behavior before and during execution:
InstructionGuardrail: appends instructions to the system prompt before execution.RuntimeGuardrail: intercepts execution phases (BEFORE_PROVIDER_REQUEST,BEFORE_TOOL_CALL,AFTER_TOOL_RESULT,BEFORE_FINAL_RESPONSE).
Custom guardrail example
<?php namespace App\Ai\Guardrails; use Tobiebenezer\Ai\Contracts\RuntimeGuardrail; use Tobiebenezer\Ai\Guardrails\GuardrailContext; use Tobiebenezer\Ai\Guardrails\GuardrailDecision; use Tobiebenezer\Ai\Guardrails\GuardrailEvent; class SensitiveDataBlockGuardrail implements RuntimeGuardrail { public function appliesTo(GuardrailContext $context) { return true; } public function check(GuardrailEvent $event, GuardrailContext $context) { if ($event->phase === GuardrailEvent::BEFORE_PROVIDER_REQUEST) { // Inspect input for sensitive keywords } return GuardrailDecision::allow(); } }
Overriding default guardrails
To replace a default guardrail (such as CapabilitiesGuardrail), bind your class in AppServiceProvider:
// app/Providers/AppServiceProvider.php use Tobiebenezer\Ai\Guardrails\CapabilitiesGuardrail as BaseCapabilities; use App\Ai\Guardrails\CustomCapabilitiesGuardrail; public function boot() { $this->app->bind(BaseCapabilities::class, CustomCapabilitiesGuardrail::class); }
Artisan generators
Use Artisan commands to scaffold components:
# Generate a general AI tool php artisan make:ai-tool WeatherTool # Generate an analytical Eloquent tool php artisan make:ai-tool QuerySalesTool --analytical # Generate a runtime guardrail php artisan make:ai-guardrail BlockListGuardrail --runtime # Generate a custom provider adapter php artisan make:ai-provider AnthropicProvider
Audit logs
When logging is enabled in config/ai.php, the package records:
ai_request_logs: prompts, responses, model names, token counts, and latency.ai_tool_call_logs: tool names, arguments, outputs, execution status, and timings.
Monthly token budget
Set a monthly token threshold in config/ai.php:
'budget' => [ 'monthly_token_limit' => 5000000, ],
When exceeded, requests stop and throw a GuardrailException.
Automatic error recovery
If a tool throws an error (such as an invalid column name in a query), the runner catches it and passes the diagnostic message back to the LLM. The model reads the error, adjusts its arguments, and retries the tool call.
Changelog
See CHANGELOG.md for version release notes.
License
Proprietary. All rights reserved.