AI Architecture for Module Developers
How to add AI capabilities to your LaraDashboard module using the Capability/Action/Context pattern — register actions, provide context, and integrate with the AI command processor.
AI Architecture for Module Developers
LaraDashboard provides a modular AI system that any module can extend. Modules register capabilities (groups of AI actions), actions (executable AI operations), and context providers (domain data for the AI). The core AI engine handles provider communication, intent matching, and execution.
Architecture Overview
User Command ("Score this contact")
│
▼
┌──────────────────┐
│ AiCommandProcessor│ ← Matches command to registered action
└──────┬───────────┘
│
▼
┌──────────────────┐ ┌───────────────────┐
│ ActionRegistry │ ←── │ Module registers │
│ (finds action) │ │ actions on boot │
└──────┬───────────┘ └───────────────────┘
│
▼
┌──────────────────┐ ┌───────────────────┐
│ ContextRegistry │ ←── │ Module provides │
│ (gathers context) │ │ domain context │
└──────┬───────────┘ └───────────────────┘
│
▼
┌──────────────────────────┐
│ AiContentGeneratorService │ ← Calls OpenAI/Claude/Gemini/Ollama
└──────┬───────────────────┘
│
▼
┌──────────────────┐
│ AiResult │ ← Returned to UI
└──────────────────┘
Core Contracts
AiCapabilityInterface
Groups related AI actions into a named capability with enable/disable control.
namespace App\Ai\Contracts;
interface AiCapabilityInterface
{
public function name(): string; // "CRM Contact Intelligence"
public function description(): string; // Human-readable description
public function actions(): array; // Array of AiActionInterface classes
public function isEnabled(): bool; // Feature flag check
}
AiActionInterface
A single executable AI operation with command patterns and permission checks.
namespace App\Ai\Contracts;
interface AiActionInterface
{
public function name(): string; // "contacts.score"
public function description(): string; // "Score a contact based on engagement"
public function patterns(): array; // Regex patterns to match commands
public function payloadSchema(): array; // Expected input structure
public function requiredPermission(): ?string; // "contact.view"
public function handle(array $payload): AiResult;
public function handleWithProgress(array $payload, callable $onProgress): AiResult;
}
AiContextProviderInterface
Provides domain-specific data to enrich AI prompts.
namespace App\Ai\Contracts;
interface AiContextProviderInterface
{
public function key(): string; // "crm_contacts"
public function context(): array; // Aggregated domain data
}
Adding AI to Your Module
Step 1: Create a Context Provider
Context providers give the AI system knowledge about your module's domain.
<?php
declare(strict_types=1);
namespace Modules\Inventory\Ai\Context;
use App\Ai\Contracts\AiContextProviderInterface;
use Modules\Inventory\Models\Product;
use Modules\Inventory\Models\Warehouse;
class InventoryContextProvider implements AiContextProviderInterface
{
public function key(): string
{
return 'inventory';
}
public function context(): array
{
return [
'total_products' => Product::count(),
'low_stock_count' => Product::where('quantity', '<', 10)->count(),
'warehouses' => Warehouse::pluck('name')->toArray(),
'categories' => Product::distinct('category')->pluck('category')->toArray(),
'total_value' => Product::sum(\DB::raw('quantity * unit_price')),
];
}
}
Step 2: Create an Action
Actions define what the AI can do. Each action matches user commands via regex patterns.
<?php
declare(strict_types=1);
namespace Modules\Inventory\Ai\Actions;
use App\Ai\Contracts\AiActionInterface;
use App\Ai\AiResult;
use App\Services\AiContentGeneratorService;
use Modules\Inventory\Models\Product;
class PredictRestockAction implements AiActionInterface
{
public function __construct(
private readonly AiContentGeneratorService $aiService,
) {}
public function name(): string
{
return 'inventory.predict_restock';
}
public function description(): string
{
return 'Predict which products need restocking based on sales velocity and current stock levels.';
}
public function patterns(): array
{
return [
'/predict\s+restock/i',
'/which\s+products?\s+need\s+restock/i',
'/low\s+stock\s+predict/i',
];
}
public function payloadSchema(): array
{
return [
'days_ahead' => ['type' => 'integer', 'default' => 30],
'warehouse_id' => ['type' => 'integer', 'optional' => true],
];
}
public function requiredPermission(): ?string
{
return 'product.view';
}
public function handle(array $payload): AiResult
{
return $this->handleWithProgress($payload, fn () => null);
}
public function handleWithProgress(array $payload, callable $onProgress): AiResult
{
$onProgress('Analyzing inventory levels...');
$daysAhead = $payload['days_ahead'] ?? 30;
$lowStockProducts = Product::query()
->where('quantity', '<', 20)
->with('salesHistory')
->get();
if ($lowStockProducts->isEmpty()) {
return AiResult::success('All products are well-stocked.', [
'products_analyzed' => Product::count(),
'recommendations' => [],
]);
}
$onProgress('Generating restock predictions...');
$context = $lowStockProducts->map(fn ($p) => [
'name' => $p->name,
'current_stock' => $p->quantity,
'avg_daily_sales' => $p->averageDailySales(),
'days_of_stock' => $p->daysOfStockRemaining(),
])->toArray();
$prompt = "Analyze these products and predict restock needs for the next {$daysAhead} days. "
. "Return JSON with 'recommendations' array, each having 'product', 'urgency' (high/medium/low), "
. "'restock_quantity', and 'reason'. Products:\n" . json_encode($context);
$response = $this->aiService->generateContent($prompt);
$recommendations = json_decode($response, true)['recommendations'] ?? [];
return AiResult::success(
"Analyzed {$lowStockProducts->count()} low-stock products.",
[
'recommendations' => $recommendations,
'days_ahead' => $daysAhead,
]
);
}
}
Step 3: Create a Capability
Capabilities group related actions and control feature flags.
<?php
declare(strict_types=1);
namespace Modules\Inventory\Ai\Capabilities;
use App\Ai\Contracts\AiCapabilityInterface;
use Modules\Inventory\Ai\Actions\PredictRestockAction;
use Modules\Inventory\Ai\Actions\CategorizePr oductAction;
use Modules\Inventory\Ai\Actions\GenerateProductDescriptionAction;
class InventoryAiCapability implements AiCapabilityInterface
{
public function name(): string
{
return 'Inventory Intelligence';
}
public function description(): string
{
return 'AI-powered inventory predictions, product categorization, and description generation.';
}
public function actions(): array
{
return [
PredictRestockAction::class,
CategorizeProductAction::class,
GenerateProductDescriptionAction::class,
];
}
public function isEnabled(): bool
{
$settings = app('inventory.settings');
return ($settings['ai_restock_prediction'] ?? false)
|| ($settings['ai_categorization'] ?? false)
|| ($settings['ai_descriptions'] ?? false);
}
}
Step 4: Register in Service Provider
<?php
declare(strict_types=1);
namespace Modules\Inventory\Providers;
use Illuminate\Support\ServiceProvider;
use App\Ai\Registry\CapabilityRegistry;
use App\Ai\Registry\ContextRegistry;
use Modules\Inventory\Ai\Capabilities\InventoryAiCapability;
use Modules\Inventory\Ai\Context\InventoryContextProvider;
class InventoryServiceProvider extends ServiceProvider
{
public function boot(): void
{
// ... other registrations ...
$this->registerAiCapabilities();
}
protected function registerAiCapabilities(): void
{
// Check if core AI system exists (graceful fallback)
if (! $this->app->bound(CapabilityRegistry::class)) {
return;
}
$this->app->booted(function () {
$this->app->make(CapabilityRegistry::class)
->register(InventoryAiCapability::class);
$this->app->make(ContextRegistry::class)
->register(InventoryContextProvider::class);
});
}
}
Directory Structure
Follow this structure in your module:
modules/{Module}/app/Ai/
├── Capabilities/
│ └── {Module}AiCapability.php ← Groups actions, feature flags
├── Context/
│ └── {Resource}ContextProvider.php ← Provides domain data
└── Actions/
└── {Resource}/
├── ScoreAction.php ← Individual AI operations
├── PredictAction.php
└── SummarizeAction.php
AiResult
Actions return AiResult objects:
use App\Ai\AiResult;
// Success
return AiResult::success('Operation completed.', ['key' => 'value']);
// Partial success (some steps completed)
return AiResult::partial('Completed 3 of 5 items.', $data, $completedSteps);
// Failure
return AiResult::failed('Could not process: insufficient data.');
AI Providers
LaraDashboard supports multiple AI providers via AiContentGeneratorService:
| Provider | Default Model | Env Variable | Best For |
|---|---|---|---|
| OpenAI | gpt-4o-mini |
OPENAI_API_KEY |
General purpose, image generation |
| Claude | claude-3-haiku |
ANTHROPIC_API_KEY |
Long-form content, analysis |
| Gemini | gemini-2.0-flash |
GEMINI_API_KEY |
Fast responses |
| Ollama | llama3.2 |
OLLAMA_BASE_URL |
Local/private, no API key needed |
Using the Service
use App\Services\AiContentGeneratorService;
class MyService
{
public function __construct(
private readonly AiContentGeneratorService $ai,
) {}
public function analyze(string $data): ?string
{
return $this->ai->generateContent(
"Analyze this data and provide insights:\n{$data}"
);
}
public function generateJson(string $prompt): ?array
{
$response = $this->ai->generateContent($prompt);
// Service handles JSON parsing from markdown code blocks
return json_decode($response, true);
}
}
Provider Configuration
Providers are configured in config/ai.php and can be overridden via database settings (ai_* keys in the settings table):
// config/ai.php
return [
'default_provider' => env('AI_DEFAULT_PROVIDER', 'openai'),
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
'model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
'max_tokens' => (int) env('OPENAI_MAX_TOKENS', 2000),
],
'anthropic' => [
'api_key' => env('ANTHROPIC_API_KEY'),
'model' => env('ANTHROPIC_MODEL', 'claude-3-haiku-20240307'),
],
'gemini' => [
'api_key' => env('GEMINI_API_KEY'),
'model' => env('GEMINI_MODEL', 'gemini-2.0-flash'),
],
'ollama' => [
'base_url' => env('OLLAMA_BASE_URL', 'http://localhost:11434'),
'model' => env('OLLAMA_MODEL', 'llama3.2'),
],
];
CRM Module AI — Reference Implementation
The CRM module is the most complete AI integration. Study it as the reference:
Capabilities (6)
| Capability | Actions | Purpose |
|---|---|---|
| ContactAiCapability | 5 | Scoring, enrichment, duplicates, segmentation, summaries |
| DealAiCapability | 5 | Scoring, summaries, pipeline forecasting, stale detection |
| TicketAiCapability | 3 | Categorization, sentiment analysis, reply suggestions |
| CampaignAiCapability | 2 | Subject line generation, email body improvement |
| InvoiceAiCapability | 3 | Payment prediction, health scoring, follow-up emails |
| DashboardAiCapability | 3 | Daily digest, dashboard insights, follow-up suggestions |
Context Providers (5)
| Provider | Data Provided |
|---|---|
| ContactContextProvider | Contact stats, types, groups, tags, recent activity |
| DealContextProvider | Pipeline metrics, deal stages, revenue data |
| TicketContextProvider | Support queue stats, categories, response times |
| CampaignContextProvider | Campaign performance, open/click rates |
| InvoiceContextProvider | Revenue metrics, payment patterns |
CrmAiService Pattern
The CRM wraps the core AiContentGeneratorService with domain-specific prompt building:
class CrmAiService
{
public function __construct(
private readonly AiContentGeneratorService $aiService,
) {}
public function scoreContact(Contact $contact): ?array
{
$prompt = $this->buildContactScoringPrompt($contact);
$response = $this->aiService->generateContent($prompt);
return $this->parseJsonResponse($response);
}
private function buildContactScoringPrompt(Contact $contact): string
{
return "Score this contact on a scale of 0-100 based on engagement...\n"
. json_encode($contact->toArray());
}
}
Progress Streaming
For long-running AI operations, use progress callbacks:
public function handleWithProgress(array $payload, callable $onProgress): AiResult
{
$onProgress('Step 1: Loading data...');
$data = $this->loadData($payload);
$onProgress('Step 2: Analyzing with AI...');
$analysis = $this->ai->generateContent($prompt);
$onProgress('Step 3: Processing results...');
$results = $this->processResults($analysis);
return AiResult::success('Analysis complete.', $results);
}
Command Logging
All AI command executions are logged to the ai_command_logs table:
| Column | Type | Description |
|---|---|---|
user_id |
FK | Who ran the command |
command |
string | Original user input |
intent |
JSON | Parsed intent with confidence |
plan |
JSON | Execution plan |
result |
JSON | AiResult data |
status |
string | success / partial / failed |
execution_time_ms |
integer | How long it took |
Best Practices
- Always check
isEnabled()— Respect feature flags in capabilities - Use progress callbacks — Give users feedback during long operations
- Parse JSON carefully — AI responses may include markdown code blocks; the service handles this
- Handle failures gracefully — Return
AiResult::failed()with a helpful message - Check permissions — Set
requiredPermission()to enforce authorization - Keep prompts focused — Smaller, specific prompts produce better results than large vague ones
- Provide rich context — Context providers help the AI understand the domain
- Check if AI system exists — Use
$this->app->bound(CapabilityRegistry::class)before registering
Next Steps
- AI Integration Guide — User-facing AI features and configuration
- Hook System — Extend AI with hooks
- Module Development — Module creation basics
- CRM Module — Reference AI implementation