Skip to content

ubxty/eloquent-ai

Repository files navigation

ELOQUENT AI
Talk to your database in plain English.
No SQL. No BI tool. Zero config to start.

PHP 8.3+ Laravel AI Optional 7-Layer Security MIT License


A plug-and-play Laravel package that lets authorized users query their database through a floating chat interface — no SQL knowledge required. Works with or without AI providers. Rich card views, CSV/JSON export, smart suggestions, temporal queries, index advisory, and a fully customizable config — all out of the box.


Why Eloquent AI?

Every team has questions about their data. Most solutions are painful:

  • BI tools (Metabase, Tableau) — require separate infra, licensing, and training
  • Raw SQL — only works for engineers, never business users
  • Generic AI chatbots — no knowledge of your schema, no security controls, hallucinate queries
  • Spreadsheet exports — stale data the moment you export it

Eloquent AI embeds a secure, schema-aware chat interface directly into your Laravel app — deployed in 3 commands, authorized via your existing gate system, and configurable to your domain vocabulary.

  Without Eloquent AI                    With Eloquent AI
┌──────────────────────┐         ┌─────────────────────────────┐
│ Business user asks   │         │ Business user types:         │
│ engineer for a query │         │ "Show me pending orders      │
│   → engineer writes  │         │  from last month"            │
│     SQL or exports   │         │   → Eloquent AI resolves,   │
│     CSV              │         │     secures, and executes   │
│   → stale by the     │         │     instantly in the app    │
│     time it lands    │         │   → Rich cards, export,     │
│                      │         │     navigation links        │
└──────────────────────┘         └─────────────────────────────┘
  Hours per query                  Seconds, self-serve

How it compares

Feature Eloquent AI Raw SQL Metabase / BI Generic AI Chat
No SQL required ⚠️ Learn tool ⚠️ Often wrong
Zero external infra ❌ Separate server ❌ API only
Works without AI provider
Schema-aware queries Manual
7-layer security Manual ⚠️
Horizon-style access gate N/A
Domain vocabulary mapping Manual ⚠️
Rich card layouts
Inline CSV / JSON export Manual
Real-time (live DB) ⚠️ Cached
Audit trail Manual ⚠️
Embeds in your app N/A ❌ iframe only
Temporal queries ("last month") Manual ⚠️
Index advisory Manual


Requirements

Dependency Version Required?
PHP ^8.3 Yes
Laravel ^12.0 / ^13.0 Yes
laravel/ai ^0.x No — only for ai / auto engine modes

The package is fully functional without any AI provider using the built-in local engine mode.


Installation

1. Require the package

composer require ubxty/eloquent-ai

Local development (path repository):

{
  "repositories": [
    {
      "type": "path",
      "url": "packages/ubxty/eloquent-ai"
    }
  ]
}
composer require ubxty/eloquent-ai:@dev

2. Run the install command

php artisan eloquent-ai:install --no-interaction

This will:

  • Publish config files (config/eloquent-ai.php, config/eloquent-ai-routes.php)
  • Publish and run database migrations (audit logs + conversations tables)
  • Scan your schema (models, relationships, columns)
  • Scan your routes (auto-generate route-to-model mappings)

3. Configure access

By default, any authenticated user can access Eloquent AI. To restrict to specific users, use the Horizon-style Gate (recommended) or config-based access control:

// app/Providers/AppServiceProvider.php
use Ubxty\EloquentAi\EloquentAiFacade as EloquentAi;

public function boot(): void
{
    EloquentAi::auth(function ($user) {
        return $user->hasRole('ADMIN');
        // or: return $user->account_type === 'ADMIN';
        // or: return in_array($user->email, ['admin@example.com']);
    });
}

4. Add to your frontend

Vue 3 SPA — add to your layout component:

<template>
  <router-view />
  <EloquentAiChat />
</template>

<script>
import EloquentAiChat from 'vendor/ubxty/eloquent-ai/resources/js/EloquentAiChat.vue';
// Or after publishing: import EloquentAiChat from './vendor/eloquent-ai/EloquentAiChat.vue';
export default { components: { EloquentAiChat } };
</script>

Blade — add the directive to any layout:

@eloquentAiChat

Or visit /eloquent-ai for the standalone full-page chat.


Quick Start

# Install
php artisan eloquent-ai:install --no-interaction

# Set engine to local (no AI needed)
# .env:
ELOQUENT_AI_ENGINE=local

# Visit /eloquent-ai and try:
# "How many users are there?"
# "Show me the latest 5 orders"
# "Count orders grouped by status"
# "Users created this month"
# "Export users as CSV"

Configuration Reference

Publish the config:

php artisan eloquent-ai:publish --config

This creates config/eloquent-ai.php — the single file that controls the entire package behavior. Every section is documented below.

Engine Mode

// .env
ELOQUENT_AI_ENGINE=auto
Mode Description AI Required?
local Built-in NaturalLanguage regex engine only. Zero external API calls. No
ai Always sends queries to the AI provider (via laravel/ai). Yes
auto Tries local first, falls back to ai for complex queries. Optional

Schema & Discovery

'schema' => [
    // How models are discovered
    'discovery_mode' => 'all', // 'all', 'trait', or 'mixed'

    // Directories to scan for Eloquent models
    'model_directories' => [app_path('Models')],

    // Only allow these models (empty = allow all discovered)
    'include_models' => [],

    // Never expose these models to the chat
    'exclude_models' => [
        App\Models\PersonalAccessToken::class,
    ],

    // Tables completely hidden from all queries
    'hidden_tables' => [
        'migrations', 'password_reset_tokens',
        'telescope_entries', 'telescope_entries_tags',
        'failed_jobs', 'cache', 'cache_locks',
    ],

    // Columns redacted — never returned in results or shown in schema
    'redacted_columns' => [
        'password', 'remember_token',
        'two_factor_secret', 'google2fa_secret',
    ],

    // Schema cache duration (seconds). 0 = no caching.
    'cache_ttl' => 3600,
],
Discovery Mode Description
all (default) Scans directories, indexes every Eloquent model. No code changes needed.
trait Only indexes models with use HasEloquentAi. Explicit opt-in.
mixed Scans all models. Trait-using models get priority and per-model config.

Access Control & Horizon-Style Gate

The package supports a 5-step authorization cascade, evaluated in order:

'access' => [
    // Middleware for page/view routes
    'middleware' => ['web', 'auth'],

    // Separate middleware for API routes (POST /ask, DELETE, etc.)
    // Falls back to 'middleware' if not set
    'api_middleware' => ['web', 'auth'],

    // Skip CSRF verification on API endpoints (recommended for SPA)
    'csrf_exempt' => true,

    // Laravel Gate name
    'gate' => 'use-eloquent-ai',

    // Spatie permission roles
    'roles' => ['ADMIN'],

    // Specific user IDs
    'allowed_users' => [],
],

Authorization cascade:

Priority Method How
1 EloquentAi::auth() Horizon-style callback (highest priority)
2 Gate Gate::allows('use-eloquent-ai', $user)
3 Allowed users in_array($user->id, config('eloquent-ai.access.allowed_users'))
4 Roles $user->hasRole(config('eloquent-ai.access.roles')) (Spatie)
5 Allow all If nothing is configured, any authenticated user can access

Horizon-style Gate (recommended):

// app/Providers/AppServiceProvider.php
use Ubxty\EloquentAi\EloquentAiFacade as EloquentAi;

public function boot(): void
{
    EloquentAi::auth(function ($user) {
        return $user->hasRole('ADMIN');
    });
}

Auth strategies for different setups:

Strategy Config
Session-based (Blade/Vue SPA) middleware: ['web', 'auth']
Sanctum SPA cookies api_middleware: ['web', 'auth:sanctum']
Sanctum API tokens api_middleware: ['api', 'auth:sanctum'] + bearerToken prop
Passport api_middleware: ['api', 'auth:api'] + bearerToken prop

Query Constraints

'query' => [
    'max_rows' => 100,              // Max records per query
    'max_execution_time' => 10,     // Seconds (gracefully skipped if DB doesn't support)
    'allow_aggregates' => true,     // COUNT, SUM, AVG, MIN, MAX
    'allow_subqueries' => false,    // Nested queries
    'allow_full_table_scan' => false, // SELECT * without WHERE (aggregates exempt)
    'allow_write_operations' => false, // INSERT, UPDATE, DELETE (always false = safety)
    'eager_load_depth' => 2,        // Max relationship nesting depth
    'complexity_score_limit' => 50, // Reject overly complex query plans
],

Natural Language Customization

This is where the package becomes truly powerful — you can map your entire domain vocabulary so users can query using business terms, not database column names.

Model Aliases & Display Names

Map how users refer to your tables:

'natural_language' => [
    'model_aliases' => [
        'staff'     => App\Models\User::class,
        'employees' => App\Models\User::class,
        'clients'   => App\Models\Customer::class,
        'tickets'   => App\Models\SupportTicket::class,
        'docs'      => App\Models\Document::class, // "docs" = documents
    ],
],

Now "How many docs do we have?" queries the documents table.

Rich Model Configuration

Go beyond simple aliases — define display names, priorities, hidden columns, and example queries per model:

'natural_language' => [
    'models' => [
        App\Models\User::class => [
            'display_name' => 'User',
            'aliases'      => ['staff', 'employee', 'team member', 'member'],
            'priority'     => 100,  // Higher = shown first in suggestions
            'hidden_columns' => ['password', 'remember_token', 'two_factor_secret'],
            'example_queries' => [
                'How many users are active?',
                'Show me the latest 10 users',
                'Count users by account type',
            ],
        ],
        App\Models\Order::class => [
            'display_name' => 'Order',
            'aliases'      => ['orders', 'purchases', 'transactions'],
            'priority'     => 90,
            'example_queries' => [
                'How many orders are there?',
                'Show latest 5 orders',
                'Count orders by status',
            ],
        ],
        App\Models\Product::class => [
            'display_name' => 'Product',
            'aliases'      => ['products', 'items', 'catalog', 'listings'],
            'priority'     => 80,
            'example_queries' => [
                'How many products are there?',
                'Show products where status is active',
            ],
        ],
    ],
],

Column Aliases

Map business terms to actual database columns, per model:

'column_aliases' => [
    App\Models\User::class => [
        'role'   => 'account_type',
        'phone'  => 'contact_phone',
        'joined' => 'created_at',
    ],
    App\Models\Location::class => [
        'address' => 'street_address',
        'city'    => 'address_city',
        'state'   => 'address_state',
        'zip'     => 'postal_code',
        'lat'     => 'latitude',
        'lng'     => 'longitude',
    ],
],

Now "Show locations in city Denver" works — city resolves to address_city.

Keyword Filters

Map business keywords to predefined WHERE clauses — no column knowledge needed by the end user:

'keyword_filters' => [
    App\Models\User::class => [
        'active'   => [['field' => 'active', 'operator' => '=', 'value' => 1]],
        'inactive' => [['field' => 'active', 'operator' => '=', 'value' => 0]],
        'admins'   => [['field' => 'account_type', 'operator' => '=', 'value' => 'admin']],
    ],
    App\Models\Order::class => [
        'approved'    => [['field' => 'status', 'operator' => '=', 'value' => 'approved']],
        'rejected'    => [['field' => 'status', 'operator' => '=', 'value' => 'rejected']],
        'in progress' => [['field' => 'status', 'operator' => '=', 'value' => 'in_progress']],
        'pending'     => [['field' => 'status', 'operator' => '=', 'value' => 'pending']],
        'recent'      => [['field' => 'created_at', 'operator' => '>=', 'value' => '-30 days']],
    ],
    App\Models\Product::class => [
        'active'      => [['field' => 'active', 'operator' => '=', 'value' => 1]],
        'low_stock'   => [['field' => 'stock_quantity', 'operator' => '<=', 'value' => 10]],
    ],
],

Now "How many active users?" → User::where('active', 1)->count().
"Show me recent reports" → Report::where('created_at', '>=', 30 days ago)->get().
Relative dates like -30 days are resolved at query time.

Virtual Columns

Computed columns that don't exist in the database — defined as SQL expressions, model scopes, or PHP callbacks:

'virtual_columns' => [
    App\Models\User::class => [
        'initials' => [
            'type'        => 'expression',
            'expression'  => "UPPER(CONCAT(LEFT(first_name, 1), LEFT(last_name, 1)))",
            'description' => 'User initials from first and last name',
        ],
        'full_name' => [
            'type'        => 'expression',
            'expression'  => "CONCAT(first_name, ' ', last_name)",
            'description' => 'Combined first and last name',
        ],
    ],
    App\Models\Location::class => [
        'nearby' => [
            'type'        => 'scope',
            'scope'       => 'nearCoordinates',
            'description' => 'Filter locations by proximity to coordinates',
        ],
    ],
],

Now "Find user with initials RJ" → User::whereRaw("UPPER(CONCAT(LEFT(first_name, 1), LEFT(last_name, 1))) = ?", ['RJ'])->get().

Virtual column types:

Type How it works
expression Raw SQL via whereRaw() with parameterized binding
scope Calls model scope (e.g. ->nearLocation($value))
callback Custom PHP closure fn($query, $value) => ...

Response Display

Field Visibility

By default, the package auto-hides technical database noise from chat responses:

'response' => [
    'field_visibility' => [
        'hide_uuid_fields'         => true,  // Auto-detect & hide UUID-valued columns
        'hide_foreign_keys'        => true,  // Hide *_id, *_uuid columns
        'hide_timestamp_fields'    => false,  // created_at, updated_at (shown by default)
        'hide_soft_delete_fields'  => true,  // deleted_at
        'always_show'              => [],     // Force show these columns regardless
        'always_hide'              => [],     // Force hide these columns regardless

        // Per-model overrides
        'per_model' => [
            App\Models\User::class => [
                'hide' => ['google2fa_secret', 'fa2_enabled'],
                'show' => ['name', 'email', 'account_type'],
            ],
        ],
    ],
],

How UUID detection works: If a column value matches the UUID regex (/^[0-9a-f]{8}-[0-9a-f]{4}-/i), the column is automatically hidden — including the id column when it's a UUID primary key. Numeric auto-increment IDs (like id: 42) are kept because they're meaningful references.

Rich Field Renderers

Transform raw database values into rich, interactive displays:

'response' => [
    'field_renderers' => [
        // Global renderers (apply to all models)
        'email'  => ['type' => 'email'],     // → mailto: link
        'phone'  => ['type' => 'phone'],     // → tel: link
        'status' => ['type' => 'status', 'options' => [
            'colors' => [
                'active'      => 'green',
                'approved'    => 'green',
                'pending'     => 'yellow',
                'in_progress' => 'blue',
                'rejected'    => 'red',
                'inactive'    => 'gray',
            ],
        ]],
        'is_active' => ['type' => 'boolean', 'options' => [
            'true_label'  => 'Active',
            'false_label' => 'Inactive',
        ]],
    ],

    // Per-model renderers
    'field_renderers_per_model' => [
        App\Models\Location::class => [
            'latitude' => ['type' => 'coordinates', 'options' => [
                'lng_field' => 'longitude',
                'label'     => 'View on Map',
            ]],
            'address' => ['type' => 'address'],
        ],
        App\Models\Employee::class => [
            'current_latitude' => ['type' => 'coordinates', 'options' => [
                'lng_field' => 'current_longitude',
                'label'     => 'View Location',
            ]],
        ],
    ],
],

Available renderer types:

Type Output Example
coordinates Google Maps link 📍 View on Maphttps://maps.google.com/?q=40.7,-74.0
address Google Maps search link 🗺️ View Maphttps://maps.google.com/?q=123+Main+St
email Clickable mailto link 📧 user@example.com
phone Clickable tel link 📞 (555) 123-4567
status Color-coded badge 🟢 Active, 🔴 Rejected, 🟡 Pending
boolean Human-readable label ✅ Active / ❌ Inactive
currency Formatted money $1,234.56
percentage Percentage format 85.5%
date_relative Relative timestamp 3 days ago
filesize Human-readable size 2.4 MB
json Pretty-printed JSON Expandable JSON block
truncate Truncated with "..." First 100 chars + "..."
url Clickable link 🔗 Open Link
custom Custom formatting Your own callback

Card Layouts

Replace plain tables with rich, visually structured cards for specific models:

'response' => [
    'card_auto_detect' => true, // Auto-generate cards from common fields

    'card_layouts' => [
        App\Models\User::class => [
            'title'        => 'name',
            'subtitle'     => '{email}',
            'badges'       => [
                ['field' => 'account_type', 'colors' => [
                    '1' => '#e74c3c', '2' => '#3498db', '3' => '#2ecc71',
                ]],
                ['field' => 'active', 'colors' => [
                    '1' => '#2ecc71', '0' => '#95a5a6',
                ]],
            ],
            'details'      => ['phone', 'created_at'],
            'accent_color' => '#667eea',
        ],
        App\Models\Product::class => [
            'title'    => 'name',
            'subtitle' => '{category}, {sku}',
            'badges'   => [
                ['field' => 'active', 'colors' => ['1' => '#2ecc71', '0' => '#e74c3c']],
            ],
            'details'  => ['price', 'stock_quantity'],
            'actions'  => [
                ['label' => 'View Product', 'url' => '/products/{id}'],
            ],
            'accent_color' => '#2ecc71',
        ],
    ],
],

Card layout options:

Option Type Description
title string Field name for the card heading
subtitle string Field name or template with {field} placeholders
badges array Pill-shaped highlights with optional color maps
details array Key/value pairs in the card body (rich fields supported)
actions array Buttons with template URLs {field}
accent_color string/map Static #hex or field-based color map for left border
avatar string Field name containing avatar/image URL
image string Field name containing a display image URL
custom_view string Fully custom Vue/Blade component name

Display modes (auto-selected):

Mode When
table No card layout configured, or aggregate results
cards Multiple records with a card layout
single-card One record with a card layout
custom custom_view is specified in the card layout

When card_auto_detect is true and no explicit layout exists, the package examines the model's columns for common field names (name, title, email, status, created_at) and auto-generates an appropriate card layout.

Custom Views

For full control, point a model to a custom Vue component or Blade partial:

'card_layouts' => [
    App\Models\Order::class => [
        'custom_view' => 'order-detail-card', // Vue component name or Blade partial
    ],
],

Publish the Vue stubs to customize:

php artisan eloquent-ai:publish --vue

Export Configuration

Map the chat's "export" command to your existing export controllers/routes:

'export' => [
    'enabled' => true,

    // Allow inline CSV/JSON generation from query results
    'allow_inline' => true,

    // Supported formats
    'formats' => ['csv', 'json'],

    // Max rows for inline export (protects memory)
    'max_inline_rows' => 5000,

    // Map models to existing export endpoints
    'mapped_exports' => [
        App\Models\Order::class => [
            'controller' => 'App\\Http\\Controllers\\Export\\OrderExportController',
            'method'     => 'exportOrders',
            'route_name' => 'export.orders',
            'formats'    => ['csv', 'xlsx', 'pdf'],
            'params'     => ['status', 'date_from', 'date_to'],
            'description' => 'Export orders with filters',
            'frontend_url' => '/orders?export=true',
        ],
        App\Models\User::class => [
            'controller' => 'App\\Http\\Controllers\\Export\\UserExportController',
            'method'     => 'exportUsers',
            'route_name' => 'export.users',
            'formats'    => ['csv', 'xlsx'],
            'params'     => ['role', 'active'],
            'description' => 'Export user data',
        ],
    ],
],

How export works:

  1. User says "export users as CSV" or "export this as CSV" (after viewing results)
  2. If the model has a mapped_exports entry → returns the existing export URL + page link
  3. If no mapping exists and allow_inline is true → generates CSV/JSON inline from last query results
  4. If export is disabled → returns an error message

Rate Limiting

'rate_limit' => [
    'enabled' => true,
    'max_queries_per_minute' => 20,
    'max_queries_per_hour'   => 200,
    'max_queries_per_day'    => 1000,
],

Audit Logging

Every query is logged with the user, timestamp, query plan, execution time, and optional results:

'audit' => [
    'enabled'          => true,
    'channel'          => 'stack',
    'log_queries'      => true,
    'log_ai_responses' => true,
    'log_results'      => false, // Be careful with PII
    'retention_days'   => 90,
],

Prune old entries:

php artisan eloquent-ai:audit-prune --days=90

Route Scanning & URL Generation

The package scans your Laravel routes and generates clickable navigation links in query results:

'routes' => [
    'enabled'        => true,
    'auto_discover'  => true,
    'config_file'    => config_path('eloquent-ai-routes.php'),
    'include_patterns' => ['*'],
    'exclude_patterns' => ['telescope/*', 'horizon/*', 'eloquent-ai/*'],
    'frontend_base_url' => env('ELOQUENT_AI_FRONTEND_URL'),
    'url_format'     => 'laravel', // 'laravel', 'vue-router', 'inertia'
],

After scanning (php artisan eloquent-ai:scan-routes), results are cached in config/eloquent-ai-routes.php. Edit this file to fine-tune route-to-model mappings — the chat will then generate clickable links to relevant records in your app.

Multi-Tenancy

'multi_tenancy' => [
    'enabled'         => false,
    'strategy'        => 'column', // 'column' or 'database'
    'tenant_column'   => 'team_id',
    'tenant_resolver' => null,      // Custom resolver class
],

Publishing Assets

Granular control over published assets:

php artisan eloquent-ai:publish --config      # Config files only
php artisan eloquent-ai:publish --migrations   # Migration files only
php artisan eloquent-ai:publish --views        # Blade views (customize chat page)
php artisan eloquent-ai:publish --vue          # Vue 3 components (customize widget)
php artisan eloquent-ai:publish --css          # CSS styles
php artisan eloquent-ai:publish --all          # Everything
php artisan eloquent-ai:publish --all --force  # Overwrite existing
Asset Published To
Config config/eloquent-ai.php, config/eloquent-ai-routes.php
Migrations database/migrations/
Blade Views resources/views/vendor/eloquent-ai/
Vue Widget resources/js/vendor/eloquent-ai/
CSS resources/css/vendor/eloquent-ai/

Using the Chat Interface

Full-Page Chat (Blade)

Navigate to /eloquent-ai — a standalone dark-themed chat page served automatically behind the access gate.

Blade Directive

Add the floating chat widget to any Blade layout:

{{-- In your layout file --}}
@eloquentAiChat

This renders a self-contained floating chat bubble with all JS/CSS inline — no build step needed. Only visible to authorized users (respects the access gate).

Vue 3 Floating Widget

Import into your Vue 3 app:

<template>
  <router-view />
  <EloquentAiChat
    base-url="/eloquent-ai"
    admin-check="IS_ADMIN"
    user-cookie="login_user"
  />
</template>

<script>
import EloquentAiChat from 'vendor/ubxty/eloquent-ai/resources/js/EloquentAiChat.vue';
// Or after publishing: import EloquentAiChat from './vendor/eloquent-ai/EloquentAiChat.vue';
export default { components: { EloquentAiChat } };
</script>

Props:

Prop Type Default Description
baseUrl String /eloquent-ai API endpoint prefix
adminCheck String / Function / null 'IS_ADMIN' Cookie property or function (user) => bool. null = show to all.
userCookie String login_user Cookie containing JSON user data
position String bottom-right bottom-right or bottom-left
bearerToken String / null null API token for Sanctum/Passport auth (adds Authorization: Bearer <token> header)

Custom admin check:

<EloquentAiChat :admin-check="(user) => user.account_type === 'ADMIN' || user.IS_SUB_ADMIN" />

API token auth (for non-cookie setups):

<EloquentAiChat bearer-token="your-api-token" :admin-check="null" />

Features:

  • Server-side authorization check via /eloquent-ai/auth-check endpoint
  • Sends its own CSRF token (doesn't inherit host app's axios config)
  • Rich card rendering, export buttons, navigation links
  • Dashboard link in the header
  • Persistent conversation across page navigations

API-Only (Headless)

Use the REST API directly from any frontend (React, Angular, mobile, etc.):

POST   /eloquent-ai/ask                           Ask a question
POST   /eloquent-ai/ask  {"question":"export..."}  Export data
GET    /eloquent-ai/suggest?q=show+me              Get autocomplete suggestions
GET    /eloquent-ai/auth-check                     Check if current user is authorized
GET    /eloquent-ai/schema                         Get schema info + stats
GET    /eloquent-ai/dashboard                      Get dashboard data (model cards, FAQs, quick queries)
GET    /eloquent-ai/index-advisor                  Get index suggestions
GET    /eloquent-ai/stream?question=&conversation_id=  SSE streaming
GET    /eloquent-ai/conversations/{id}             Get conversation history
DELETE /eloquent-ai/conversations/{id}             Clear conversation
GET    /eloquent-ai/export/download?file=          Download inline export file
GET    /eloquent-ai/dashboard/view                 Standalone dashboard page (Blade)

Example:

const response = await fetch('/eloquent-ai/ask', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    question: 'How many users signed up this month?',
    conversation_id: null,
  }),
});

const { success, data, conversation_id } = await response.json();
// data.records    = [{ count: 42 }]
// data.meta       = { explanation, total_rows, execution_time_ms, model, display }
// data.meta.display = { mode: 'table' | 'cards' | 'single-card', cards: [...] }
// data.meta.export  = { type, url, filename }  (for export responses)
// data.follow_up_suggestions = ["Show users by day", "List new users"]
// data.navigation_links = [{ url: '/users/1', label: 'View User #1' }]

Database Dashboard & FAQs

The package includes a built-in dashboard at /eloquent-ai/dashboard/view (or JSON at /eloquent-ai/dashboard) that provides:

  • Summary stats — total models, tables, fields, relationships
  • Model cards — each model with display name, table, key fields, relationships, scopes
  • Auto-generated FAQs — "What is the Users table?", "How many orders?", "Which tables support soft deletes?"
  • Quick query cards — clickable examples that navigate to the chat with pre-filled queries
  • Index suggestions — top performance recommendations

The dashboard link appears in the chat widget header (database icon). All dashboard routes are behind the same access gate.


Export System

Inline Export

Ask the chat to export results:

"Export users as CSV"
"Export this as JSON"        ← exports the results from the previous query
"Download sites as CSV"

The package generates a temporary file and returns a download link. Inline exports are limited to export.max_inline_rows (default: 5000).

Mapped Export

For models with configured mapped_exports, the chat returns:

  • A link to the existing export endpoint/controller
  • A link to the frontend export page (if frontend_url is configured)
  • Available filter parameters
"Export reports as CSV"
→ "Reports can be exported from the existing export page: /reports?export=true
   Available filters: status, date_from, date_to, company_id"

The HasEloquentAi Trait

Add use HasEloquentAi to any model for fine-grained per-model control:

use Ubxty\EloquentAi\Traits\HasEloquentAi;

class Order extends Model
{
    use HasEloquentAi;

    public static function eloquentAiLabel(): string { return 'Purchase Order'; }
    public static function eloquentAiAliases(): array { return ['PO', 'purchase']; }
    public static function eloquentAiHiddenColumns(): array { return ['internal_notes']; }
    public static function eloquentAiRelationships(): ?array { return ['customer', 'items']; }
    public static function eloquentAiScopes(): ?array { return ['active', 'recent']; }
    public static function eloquentAiBaseQuery(): ?\Closure
    {
        return fn ($q) => $q->where('tenant_id', auth()->user()->tenant_id);
    }
    public static function eloquentAiMaxRows(): ?int { return 500; }
}
Method Default Purpose
eloquentAiLabel() Class basename Display name in chat
eloquentAiAliases() [] Extra aliases users can type
eloquentAiHiddenColumns() [] Columns hidden for this model
eloquentAiRelationships() null (auto-discover) Whitelist queryable relationships
eloquentAiScopes() null (auto-discover) Whitelist exposed scopes
eloquentAiBaseQuery() null Always-applied WHERE constraint
eloquentAiMaxRows() null (use global) Per-model row limit
eloquentAiReadOnly() true Always true (read-only engine)
eloquentAiQueries() [] Custom query handlers (see Custom Query Handlers)

The trait integrates across the full pipeline: ModelDiscoveryModelReflectorQueryBuilderEngine (applies base query + max rows) → ModelResolver (registers aliases) → MessageFormatter (uses labels in responses).


Custom Query Handlers

Standard pattern matching handles common queries (count, list, find, aggregate), but some questions require custom business logic — "which inspectors are online?", "what's today's schedule?", "show system overview". Custom Query Handlers let models define their own question-to-logic mappings that the chatbot invokes directly.

There are three ways to register custom queries, in order of priority:

Method 1: PHP 8 Attributes

Decorate static methods on your model with the #[EloquentAiQuery] attribute:

use Ubxty\EloquentAi\Attributes\EloquentAiQuery;
use Ubxty\EloquentAi\Traits\HasEloquentAi;

class User extends Model
{
    use HasEloquentAi;

    #[EloquentAiQuery(
        phrases: ['inspectors online', 'who is online', 'online inspectors'],
        description: 'Shows inspectors currently active in the system',
        category: 'monitoring',
        priority: 90,
    )]
    public static function getOnlineInspectors(): Collection
    {
        return static::query()
            ->whereHas('activeSessions')
            ->where('role', 'inspector')
            ->select('name', 'email', 'phone')
            ->get();
    }
}

Attribute parameters:

Parameter Type Default Description
phrases array<string> [] Natural language phrases that trigger this handler
description string '' Human-readable description shown in suggestions
category ?string null Category for grouping (displayed in help)
showInSuggestions bool true Whether to show in help / suggestion lists
priority int 50 Higher = matched first (0-100)

Method 2: Trait Method

Define an eloquentAiQueries() static method on models using the HasEloquentAi trait:

class User extends Model
{
    use HasEloquentAi;

    public static function eloquentAiQueries(): array
    {
        return [
            'online_inspectors' => [
                'phrases' => ['inspectors online', 'who is online'],
                'description' => 'Shows online inspectors with active sessions',
                'category' => 'monitoring',
                'priority' => 90,
                'handler' => fn () => static::query()
                    ->whereHas('activeSessions')
                    ->select('name', 'email', 'phone')
                    ->get(),
            ],
            'inspector_locations' => [
                'phrases' => ['where are inspectors', 'inspector locations'],
                'description' => 'Last known GPS location of active inspectors',
                'method' => 'getInspectorLocations', // calls User::getInspectorLocations()
            ],
        ];
    }
}

Each query entry supports:

Key Required Description
phrases Yes Array of trigger phrases
description No Shown in suggestions
handler No* Closure that returns results
method No* Static method name on the model
category No Grouping category
priority No Match priority (default: 50)
show_in_suggestions No Show in help (default: true)

*Provide either handler (closure) or method (static method name).

Method 3: Config-Based Handlers

For queries not tied to a specific model, or to keep logic in config:

// config/eloquent-ai.php
'custom_queries' => [

    'system_overview' => [
        'phrases' => ['system overview', 'dashboard summary', 'system stats'],
        'description' => 'High-level system statistics',
        'category' => 'admin',
        'priority' => 80,
        'handler' => fn () => [
            'total_users' => User::count(),
            'active_sessions' => UserSession::where('is_active', true)->count(),
            'reports_today' => Report::whereDate('created_at', today())->count(),
        ],
    ],

    'pending_reports' => [
        'phrases' => ['pending reports', 'reports pending review'],
        'description' => 'Reports awaiting company review',
        'model' => Report::class,
        'handler' => fn () => Report::where('status', 'pending')
            ->with('inspector:id,name')
            ->select('id', 'title', 'inspector_id', 'created_at')
            ->latest()
            ->limit(20)
            ->get(),
        'priority' => 85,
    ],

],

Config-based queries support the same keys as the trait method, plus:

  • model — optional model class (used for field filtering/rendering)
  • Closures in config are not cached (they're reloaded each request)

How Resolution Works

  1. Boot — On first request, CustomQueryResolver scans all models for #[EloquentAiQuery] attributes and eloquentAiQueries() methods, caches the registry, then loads config queries.
  2. Match — The user's question is scored against all registered phrases using exact match (1.0), substring containment (0.8+), and word overlap (0.5-0.75).
  3. Priority — Custom queries with score ≥ 0.6 are tried before standard pattern matching. If pattern matching fails, custom queries with score ≥ 0.5 are tried as a fallback.
  4. Execute — The matched handler is called and results are normalized, filtered, rendered (field types, cards), and returned.
  5. Cache — The attribute/trait registry is cached (respects schema.cache_ttl). Use php artisan eloquent-ai:scan-schema --fresh to clear.

Artisan Commands

Command Description
eloquent-ai:install Full installation: publish configs, run migrations, scan schema & routes
eloquent-ai:publish Publish specific assets: --config, --migrations, --views, --vue, --css, --all
eloquent-ai:scan-schema Re-scan and cache database schema (models, columns, relationships)
eloquent-ai:scan-routes Re-scan routes and generate config/eloquent-ai-routes.php
eloquent-ai:index-advisor Analyze schema and suggest database indexes for performance
eloquent-ai:test-connection Verify AI provider connectivity (for ai/auto modes)
eloquent-ai:audit-prune Prune old audit log entries (--days=90)

Programmatic Usage (Facade)

use Ubxty\EloquentAi\EloquentAiFacade as EloquentAi;

// Ask a question
$result = EloquentAi::ask('How many active users are there?');
// Returns: ['success' => true, 'data' => [...], 'conversation_id' => '...']

// Get suggestions
$suggestions = EloquentAi::suggest('show me');

// Check engine mode
$mode = EloquentAi::engineMode(); // 'local', 'ai', or 'auto'

// Scan schema
$graph = EloquentAi::scanSchema();

// Horizon-style auth gate
EloquentAi::auth(fn ($user) => $user->hasRole('ADMIN'));

// Before/after hooks
EloquentAi::beforeQuery(function ($question, $user) {
    Log::info("User {$user->id} asked: {$question}");
    return false; // Return false to block the query
});

EloquentAi::afterQuery(function ($response, $user) {
    // Post-processing, notifications, etc.
});

How It Works

Architecture Diagram

User Question ("How many users created this year?")
     │
     ▼
┌──────────────────┐
│  Input Sanitize  │  ← Strip SQL injection, XSS, command injection
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Pre-Normalizer  │  ← "were created this year" → "where created_at this year"
│  + Tokenizer     │  ← "day plans" → "dayplans" (multi-word alias)
└────────┬─────────┘
         │
         ▼
┌───────────────────────┐
│   Engine Router       │
│  (local / ai / auto)  │
└────────┬──────────────┘
         │
    ┌────┴─────┐
    ▼          ▼
┌───────┐  ┌───────┐
│ Local │  │  AI   │  ← laravel/ai Agent (optional)
│Engine │  │Engine │
└───┬───┘  └───┬───┘
    │          │
    ▼          ▼
┌──────────────────┐
│   Query Plan     │  ← { model, operation, filters, intent, ... }
└────────┬─────────┘
         │
    ┌────┴─────────────────┐
    │  Intent Router       │
    ├──────────────────────┤
    │ greeting → welcome   │
    │ help → query guide   │
    │ export → ExportHandler│
    │ query → continue ↓   │
    └────────┬─────────────┘
             │
             ▼
┌──────────────────┐
│  Security Layer  │  ← Table filter, Column filter, Row-level security,
│                  │     Complexity scoring, Rate limiting
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Query Builder   │  ← Builds Eloquent query (never raw SQL)
│  + Virtual Cols  │  ← keyword_filters, virtual_columns applied
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│    Execute       │  ← Uses cursor for large result sets, read-only connection
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Response Filter │  ← Hide UUIDs, FKs, soft deletes
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Field Renderer  │  ← coords→map, email→mailto, status→badge
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Card Renderer   │  ← Table or rich card layout
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│  Format + Audit  │  ← Suggestions, navigation links, conversation log
└──────────────────┘

Local Engine (No AI)

The built-in NaturalLanguageEngine parses queries through a multi-stage pipeline:

  1. Noise word stripping — removes filler ("the", "individual", "records", "please")
  2. Pre-normalization — converts natural phrasing to structured triggers ("were created this year" → "where created_at this year")
  3. Multi-word tokenization — "day plans" → "dayplans" (pre-tokenized so regex stays simple)
  4. Pattern matching — 30+ regex patterns for intents: count, aggregate, list, find, group, latest, oldest, export, greeting, help
  5. Temporal parsing — resolves 20+ time phrases to date ranges
  6. Keyword filter resolution — "active users" → keyword "active" → WHERE active = 1
  7. Column + virtual column resolution — fuzzy matching, aliases, computed fields

Recognized intents:

  • Counting: "how many users", "total number of orders", "count all sites"
  • Aggregates: "sum of totals", "average price", "minimum age", "maximum score"
  • Listing: "show me users", "list all orders", "give me the products", "find all runs"
  • Filtering: "users where status is active", "orders with total > 100", "active users"
  • Temporal: "users created this year", "reports submitted today", "orders from last month"
  • Sorting: "latest orders", "oldest users", "users ordered by name"
  • Grouping: "count users by account type", "reports grouped by status"
  • Finding: "find user 5", "find user with initials R"
  • Relations: "users with orders", "orders with their products"
  • Export: "export users as CSV", "download this as JSON"
  • Greetings: "hi", "hello", "thanks", "help"
  • Combined: "show me the latest 10 active users ordered by created date"

AI Engine

Uses laravel/ai to send the question + schema context to an AI provider. The AI returns a structured JSON query plan which is validated, security-checked, and converted to Eloquent. Never executes raw SQL.

Auto Mode

Tries the local engine first. If it can't parse the query (returns null / clarification_required), seamlessly falls back to the AI engine. Best of both worlds — fast for common queries, smart for complex ones.

Pre-Normalizer & Temporal Parser

The pre-normalizer converts natural English phrasing into structured triggers the pattern matcher understands:

Natural phrase Normalized to Result
"were created this year" "where created_at this year" WHERE created_at >= '2026-01-01'
"submitted today" "where created_at today" WHERE DATE(created_at) = today
"from last month" "where created_at last month" WHERE created_at BETWEEN '2026-02-01' AND '2026-02-28'
"are active" "where active" Resolved via keyword_filters config
"in 2025" "where created_at in 2025" WHERE YEAR(created_at) = 2025
"modified yesterday" "where updated_at yesterday" WHERE DATE(updated_at) = yesterday

Supported temporal phrases: today, yesterday, this week, last week, this month, last month, this year, last year, this quarter, last quarter, past 7 days, past 30 days, past 90 days, recent, in {YYYY}.

Multi-Word Alias Tokenization

Multi-word model names like "day plans", "inspection images", "site inspections" are pre-tokenized to single tokens before pattern matching. This keeps the regex patterns simple and reliable:

"how many day plans" → tokenize "day plans" → "dayplans" → match → de-tokenize → DayPlan model

Query Examples

These all work out of the box with the local engine:

Natural Language What It Generates
"How many users are there?" User::count()
"How many active users?" User::where('active', 1)->count() (via keyword filter)
"How many reports were submitted today?" Report::whereDate('created_at', today())->count()
"How many runs were created this year?" Run::where('created_at', '>=', '2026-01-01')->count()
"Sum of order totals" Order::sum('total')
"Average user age" User::avg('age')
"Show me the latest 10 users" User::latest()->limit(10)->get()
"List orders where status is pending" Order::where('status', 'pending')->get()
"Find user 5" User::find(5)
"Find user with initials R" User::whereRaw("UPPER(CONCAT(LEFT(name, 1))) = ?", ['R'])
"Count orders grouped by status" Order::groupBy('status')->selectRaw(...)->get()
"Users created last month" User::whereBetween('created_at', [Feb 1, Feb 28])->get()
"Orders with total between 100 and 500" Order::whereBetween('total', [100, 500])->get()
"Show users with their orders" User::with('orders')->get()
"Export users as CSV" Generates CSV download link
"Export this as JSON" Exports previous query results as JSON
"Where is inspector John?" Returns lat/lng with Google Maps link
"Show me approved reports" Report::where('status', 'approved')->get()
"What sites do we have?" Site::limit(100)->get()
"What companies do we have?" Company::limit(100)->get()
"Oldest 5 products" Product::oldest()->limit(5)->get()

Security

The package implements 7 security layers in depth:

Layer What How
1. Authentication Only auth'd users Configurable middleware + Gate
2. Input Sanitization Block injection Strips SQL keywords, XSS, command injection from input
3. Schema ACL Control visibility Hidden tables, redacted columns, model allow/deny lists
4. AI Validation Structured output only AI returns JSON query plans, never freeform SQL
5. Query Complexity Block expensive queries Complexity scoring, reject if score > limit
6. Execution Constraints Limit scope Max rows, timeout, read-only connection, no write operations
7. Audit Logging Full trail Every query logged with user, timestamp, plan, result, execution time

Critical production settings:

// config/eloquent-ai.php
'database_connection' => 'readonly',        // Use a read-only DB connection
'query' => [
    'allow_write_operations' => false,       // NEVER enable in production
    'allow_full_table_scan' => false,        // Require filters/aggregates
    'complexity_score_limit' => 50,          // Block complex queries
    'max_rows' => 100,                       // Limit result size
],

Virtual column safety: All virtual column expressions use parameterized whereRaw() bindings — never string interpolation.

Aggregate function validation: Only count, sum, avg, min, max are allowed. Column names are validated against the schema before use in DB::raw().


Performance & Index Advisor

Performance Optimizations

The package is designed to handle databases with millions of records:

  • Cursor-based iteration — large result sets use cursor() instead of get() to avoid loading everything into memory
  • Schema caching — schema is scanned once and cached for the configured TTL (default: 1 hour)
  • Execution timeout — gracefully handled (skipped on databases that don't support MAX_EXECUTION_TIME)
  • Complexity scoring — queries with too many joins/filters are rejected before execution
  • Aggregate exemptionCOUNT(*), SUM(), AVG() are exempt from "full table scan" checks since they don't load rows into memory

Index Advisor

The built-in IndexAdvisor analyzes your schema and suggests database indexes that would speed up common query patterns:

php artisan eloquent-ai:index-advisor

Output:

╔══════════════════════════════════════════════════════════════╗
║ Eloquent AI — Index Advisor                                  ║
╠══════════════════════════════════════════════════════════════╣
║ High Priority                                                ║
║  • users.role — frequently filtered, no index                ║
║  • orders.status — used in GROUP BY, no index                ║
║  • products.active — boolean filter, high cardinality table  ║
╠══════════════════════════════════════════════════════════════╣
║ Medium Priority                                              ║
║  • orders.created_at — range queries on large table          ║
║  • customers.country — frequent WHERE clause, no index       ║
╚══════════════════════════════════════════════════════════════╝

The advisor is also available via the API at GET /eloquent-ai/index-advisor and shown on the dashboard.


Plugins

Register custom plugins to extend response processing:

// config/eloquent-ai.php
'plugins' => [
    App\Plugins\ChartPlugin::class,
    App\Plugins\NotificationPlugin::class,
],

Implement the PluginInterface:

use Ubxty\EloquentAi\Plugins\Contracts\PluginInterface;

class ChartPlugin implements PluginInterface
{
    public function name(): string { return 'chart'; }
    public function description(): string { return 'Generates chart data from grouped results'; }

    public function shouldHandle(array $queryPlan, array $results): bool
    {
        return ($queryPlan['operation'] ?? '') === 'group_count';
    }

    public function handle(array $queryPlan, array $results): array
    {
        return [
            'chart' => [
                'type'   => 'bar',
                'labels' => array_column($results, 'group'),
                'values' => array_column($results, 'count'),
            ],
        ];
    }
}

Events

The package dispatches events at each stage of the pipeline:

Event When Payload
SchemaScanned Schema scan completed $graph, $modelCount, $tableCount
QueryReceived Question submitted $question, $userId
QueryPlanGenerated Engine produced a plan $queryPlan
QueryPlanRejected Security blocked a query $queryPlan, $reason
QueryExecuted Query ran successfully $formattedResult, $executionTimeMs
QueryFailed Query execution failed $model, $error, $queryPlan
// EventServiceProvider
use Ubxty\EloquentAi\Events\QueryExecuted;

Event::listen(QueryExecuted::class, function ($event) {
    Log::info('Eloquent AI query', [
        'execution_time_ms' => $event->executionTimeMs,
    ]);
});

Testing

Facade Fake

use Ubxty\EloquentAi\EloquentAiFacade as EloquentAi;

public function test_dashboard_shows_user_count(): void
{
    EloquentAi::fake();
    EloquentAi::fakeResponse(['total_rows' => 42]);

    $result = EloquentAi::ask('How many users?');

    EloquentAi::assertQueried('How many users?');
    $this->assertEquals(42, $result['data']['meta']['total_rows']);
}

Package Test Suite

The package ships with a comprehensive test suite (215+ tests, 489+ assertions) covering:

Test File Coverage
PatternMatcherTest 33+ query patterns: count, list, find, aggregate, group, latest, oldest, export, greeting
NaturalLanguageEngineTest Full pipeline: normalize → match → plan → temporal → keyword filters
FilterParserTest Conditions: equals, greater/less, between, contains, null, temporal
ModelResolverTest Alias resolution, display labels, priorities, multi-word aliases
ColumnResolverTest Column matching, fuzzy matching, virtual columns
SuggestionEngineTest Overview, per-model, grouped columns, example queries
QueryBuilderEngineTest All operation types, filters, sorting, eager loading
QueryValidatorTest Column validation, redacted columns, operator validation
QueryComplexityScorerTest Complexity scoring, threshold rejection
ResultFormatterTest Record formatting, aggregate formatting
MessageFormatterTest Response structure, display labels, navigation
ConversationManagerTest Conversation CRUD, history, expiry
SchemaGraphTest Model/table/column indexing, relationships
TableFilterTest Hidden table filtering
ColumnFilterTest Redacted column filtering
RowLevelSecurityTest Row-level WHERE injection
QueryAuditorTest Security auditing, blocked queries
IndexAdvisorTest Index suggestions, priority ranking

Troubleshooting

"You are not authorized to use Eloquent AI"

  • Check your EloquentAi::auth() callback, or config/eloquent-ai.phpaccess settings
  • Ensure role names match exactly (case-sensitive: 'ADMIN''Admin')

"CSRF token mismatch"

  • Ensure access.csrf_exempt is true in config (default)
  • Or add the XSRF-TOKEN cookie header from your SPA

"I couldn't understand that query"

  • Check natural_language.model_aliases for your table
  • Add keyword_filters for domain-specific terms
  • Add column_aliases if users reference columns by different names
  • The local engine handles 30+ patterns — check the Query Examples section

"AI provider not available"

  • Install laravel/ai: composer require laravel/ai
  • Or switch to local mode: ELOQUENT_AI_ENGINE=local

Schema not detecting models

  • Run php artisan eloquent-ai:scan-schema
  • Check schema.model_directories and schema.exclude_models

"Query complexity too high"

  • Increase query.complexity_score_limit in config
  • Or simplify the question (fewer joins, filters)

UUIDs / foreign keys showing in results

  • Check response.field_visibility.hide_uuid_fields is true
  • Add model-specific overrides in field_visibility.per_model

No suggestions showing

  • Run php artisan eloquent-ai:scan-schema
  • Check natural_language.suggestions.enabled is true
  • Add example_queries in the models config for custom suggestions

"MAX_EXECUTION_TIME" error

  • This is gracefully handled — the package catches and skips this on MariaDB / older MySQL

Export not working

  • Check export.enabled is true
  • For mapped exports, ensure the controller and route name are correct
  • For inline exports, check export.allow_inline is true

Roadmap

  • CSV/JSON export ✅ Implemented
  • Rich card views ✅ Implemented
  • Index advisor ✅ Implemented
  • Temporal queries ✅ Implemented
  • Virtual columns ✅ Implemented
  • Keyword filters ✅ Implemented
  • Database dashboard ✅ Implemented
  • Field renderers ✅ Implemented (maps, email, phone, status badges)
  • Saved queries & templates
  • Scheduled reports (cron-based)
  • Chart/graph rendering plugin
  • Excel/PDF export formats
  • Team sharing & collaborative dashboards
  • Slack/Discord bot integration
  • Write operations (with approval workflow)
  • Predictive analytics & anomaly detection
  • Admin panel for query history & analytics
  • Vector search integration
  • GraphQL endpoint
  • White-label / embeddable SDK

Full File Reference

packages/ubxty/eloquent-ai/
├── composer.json
├── README.md
├── LICENSE
├── config/
│   ├── eloquent-ai.php              # Main configuration (all options)
│   └── eloquent-ai-routes.php       # Auto-generated route-model mappings
├── database/migrations/
│   ├── ..._create_eloquent_ai_audit_logs_table.php
│   └── ..._create_eloquent_ai_conversations_table.php
├── resources/
│   ├── js/
│   │   ├── EloquentAiChat.vue       # Vue 3 floating chat widget
│   │   └── EloquentAiDashboard.vue  # Vue 3 dashboard component
│   ├── css/
│   │   └── eloquent-ai.css          # Standalone CSS for non-Vue apps
│   └── views/
│       ├── chat.blade.php           # Full-page chat (Blade)
│       ├── widget.blade.php         # Embeddable widget (Blade directive)
│       └── dashboard.blade.php      # Full-page dashboard (Blade)
├── routes/
│   └── eloquent-ai.php              # Package routes
└── src/
    ├── EloquentAiServiceProvider.php # Auto-discovery service provider
    ├── EloquentAiFacade.php          # Facade with Horizon-style auth()
    ├── Traits/
    │   └── HasEloquentAi.php         # Model opt-in trait
    ├── Schema/                       # Schema intelligence engine
    │   ├── SchemaScanner.php         # Orchestrates schema discovery
    │   ├── SchemaGraph.php           # In-memory schema representation
    │   ├── SchemaCache.php           # Cache layer
    │   ├── ModelDiscovery.php        # Finds Eloquent models
    │   ├── ModelReflector.php        # Extracts model metadata
    │   ├── DatabaseIntrospector.php  # Raw DB schema introspection
    │   └── ColumnClassifier.php      # Classifies column types
    ├── NaturalLanguage/              # Local engine (no AI needed)
    │   ├── NaturalLanguageEngine.php # Main engine: normalize → match → plan
    │   ├── PatternMatcher.php        # 30+ regex patterns for query intents
    │   ├── ModelResolver.php         # Maps aliases → model classes
    │   ├── ColumnResolver.php        # Maps aliases → column names
    │   ├── FilterParser.php          # Parses conditions + temporal phrases
    │   └── SuggestionEngine.php      # Smart contextual suggestions
    ├── AI/                           # AI engine (optional, via laravel/ai)
    │   ├── AIManager.php             # AI provider manager
    │   ├── Agents/
    │   │   └── DatabaseQueryAgent.php # laravel/ai structured agent
    │   ├── PromptBuilder.php         # Builds AI prompts with schema context
    │   ├── ContextBuilder.php        # Builds schema context for prompts
    │   └── ResponseParser.php        # Parses AI responses into query plans
    ├── Query/                        # Query building & execution
    │   ├── QueryPlan.php             # Structured query plan object
    │   ├── QueryValidator.php        # Validates plan against schema
    │   ├── QueryComplexityScorer.php # Scores query complexity
    │   ├── QueryBuilderEngine.php    # Builds Eloquent query from plan
    │   ├── QueryExecutor.php         # Executes query with safety constraints
    │   ├── ResultFormatter.php       # Formats raw results
    │   ├── ResponseFieldFilter.php   # Hides UUIDs, FKs, timestamps
    │   ├── FieldRenderer.php         # Rich field transforms (maps, badges)
    │   └── CardRenderer.php          # Card layout rendering
    ├── Chat/                         # Chat interface layer
    │   ├── ChatService.php           # Main orchestrator
    │   ├── ChatController.php        # HTTP endpoints
    │   ├── DashboardController.php   # Dashboard API + page
    │   ├── ConversationManager.php   # Conversation state management
    │   ├── StreamHandler.php         # SSE streaming support
    │   └── MessageFormatter.php      # Response formatting + suggestions
    ├── Export/
    │   └── ExportHandler.php         # CSV/JSON export + mapped exports
    ├── Security/                     # 7-layer security
    │   ├── AccessControl.php         # Gate + role authorization
    │   ├── InputSanitizer.php        # SQL/XSS injection prevention
    │   ├── TableFilter.php           # Hidden table enforcement
    │   ├── ColumnFilter.php          # Redacted column enforcement
    │   ├── RowLevelSecurity.php      # Row-level WHERE injection
    │   ├── QueryAuditor.php          # Audit logging
    │   └── RateLimiter.php           # Rate limiting
    ├── Performance/
    │   └── IndexAdvisor.php          # Database index suggestions
    ├── Routes/                       # Route scanning & URL generation
    │   ├── RouteScanner.php          # Scans Laravel routes
    │   ├── RouteModelMapper.php      # Maps routes to models
    │   ├── RouteConfigGenerator.php  # Generates route config file
    │   ├── UrlGenerator.php          # Generates navigation URLs
    │   └── Contracts/
    │       └── UrlResolverInterface.php
    ├── Plugins/
    │   ├── PluginManager.php         # Plugin lifecycle manager
    │   └── Contracts/
    │       └── PluginInterface.php
    ├── Events/                       # Pipeline events
    │   ├── SchemaScanned.php
    │   ├── QueryReceived.php
    │   ├── QueryPlanGenerated.php
    │   ├── QueryPlanRejected.php
    │   ├── QueryExecuted.php
    │   └── QueryFailed.php
    ├── Http/
    │   ├── Middleware/
    │   │   └── EloquentAiMiddleware.php
    │   └── Requests/
    │       └── AskQuestionRequest.php
    └── Console/                      # Artisan commands
        ├── InstallCommand.php
        ├── PublishCommand.php
        ├── ScanSchemaCommand.php
        ├── ScanRoutesCommand.php
        ├── IndexAdvisorCommand.php
        ├── TestConnectionCommand.php
        └── AuditPruneCommand.php

Contributing

Bug reports and pull requests are welcome at github.com/ubxty/eloquent-ai.

  • Fork the repository
  • Create a feature branch (git checkout -b feature/my-feature)
  • Add tests for your change
  • Run vendor/bin/pint --dirty to fix code style
  • Open a pull request

Changelog

See CHANGELOG.md for a full list of changes per release.


License

MIT — see LICENSE.md.


Credits

Author: Ravdeep Singhinfo.ubxty@gmail.com

Built with:

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors