A Laravel package that connects LLMs (OpenRouter, Google Gemini) to database queries, vision models, structured JSON schemas, and safety guardrails.
- 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.
- PHP: 7.3, 8.0, 8.1, 8.2, or 8.3
- Laravel / Illuminate: 8.x, 9.x, 10.x, or 11.x
Install the package with Composer:
composer require tobiebenezer/php-aiIf you are developing locally with a path repository, add this to your root composer.json:
"repositories": [
{
"type": "path",
"url": "../php-ai"
}
]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 migrateAdd 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-flashFor advanced settings, edit config/ai.php to define custom profiles, tools, guardrails, and limits.
The package formats image payloads for both OpenRouter and Google Gemini without requiring separate code paths.
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;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'];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',
]);
}
}The package discovers classes implementing Tobiebenezer\Ai\Contracts\Tool inside your tool directory (default: app/Ai/Tools/).
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.';
}
}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'];
}
}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).
<?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();
}
}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);
}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 AnthropicProviderWhen 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.
Set a monthly token threshold in config/ai.php:
'budget' => [
'monthly_token_limit' => 5000000,
],When exceeded, requests stop and throw a GuardrailException.
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.
See CHANGELOG.md for version release notes.
Proprietary. All rights reserved.