PHPackages                             behindsolution/laragrep - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. behindsolution/laragrep

ActiveLibrary[API Development](/categories/api)

behindsolution/laragrep
=======================

Transform natural language questions into safe parameterized SQL queries using AI.

v2.0.1(2mo ago)6852MITPHPPHP ^8.1CI passing

Since Oct 18Pushed 2mo agoCompare

[ Source](https://github.com/behindSolution/laragrep)[ Packagist](https://packagist.org/packages/behindsolution/laragrep)[ RSS](/packages/behindsolution-laragrep/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (10)Versions (52)Used By (0)

 [![LaraGrep](asset/logo_laragrep.png)](asset/logo_laragrep.png)

 [![Latest Version](https://camo.githubusercontent.com/6a3a1eb330d4c40ab3a23d63b70b830a5c9c18c5a66bcbf9c7927abf332f7bd9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f626568696e64736f6c7574696f6e2f6c617261677265702e737667)](https://packagist.org/packages/behindsolution/laragrep) [![Tests](https://github.com/behindSolution/laragrep/actions/workflows/tests.yml/badge.svg)](https://github.com/behindSolution/laragrep/actions/workflows/tests.yml)

 Transform natural language questions into safe, parameterized SQL queries using AI.
 LaraGrep uses an **agent loop** — the AI executes queries, sees the results,
 and iteratively reasons until it can provide a final answer.

---

Quick Start
-----------

[](#quick-start)

### 1. Install

[](#1-install)

```
composer require behindsolution/laragrep
```

### 2. Publish config and migrations

[](#2-publish-config-and-migrations)

```
php artisan vendor:publish --tag=laragrep-config
php artisan vendor:publish --tag=laragrep-migrations
```

### 3. Create the SQLite database and run migrations

[](#3-create-the-sqlite-database-and-run-migrations)

LaraGrep stores conversations, monitor logs, and recipes in a separate SQLite database by default, keeping everything isolated from your main database.

Create the file and run migrations:

```
# Linux / macOS
touch database/laragrep.sqlite

# Windows
type nul > database\laragrep.sqlite
```

Add a `laragrep` connection to your `config/database.php`:

```
'connections' => [
    // ... your existing connections

    'laragrep' => [
        'driver' => 'sqlite',
        'database' => database_path('laragrep.sqlite'),
        'foreign_key_constraints' => true,
    ],
],
```

Then point LaraGrep to it in your `.env`:

```
LARAGREP_CONVERSATION_CONNECTION=laragrep
LARAGREP_MONITOR_CONNECTION=laragrep
LARAGREP_RECIPES_CONNECTION=laragrep
```

Run the migrations:

```
php artisan migrate
```

> Already using SQLite as your main database? You can skip the connection setup — LaraGrep will use the default `sqlite` connection as-is.

### 4. Add your API key to `.env`

[](#4-add-your-api-key-to-env)

```
LARAGREP_PROVIDER=openai
LARAGREP_API_KEY=sk-...
LARAGREP_MODEL=gpt-4o-mini
```

### 5. Define your tables in `config/laragrep.php`

[](#5-define-your-tables-in-configlaragrepphp)

```
use LaraGrep\Config\Table;
use LaraGrep\Config\Column;
use LaraGrep\Config\Relationship;

'contexts' => [
    'default' => [
        // ...
        'tables' => [
            Table::make('users')
                ->description('Registered users.')
                ->columns([
                    Column::id(),
                    Column::string('name'),
                    Column::string('email'),
                    Column::timestamp('created_at'),
                ]),

            Table::make('orders')
                ->description('Customer orders.')
                ->columns([
                    Column::id(),
                    Column::bigInteger('user_id')->unsigned(),
                    Column::decimal('total', 10, 2),
                    Column::enum('status', ['pending', 'paid', 'cancelled']),
                    Column::timestamp('created_at'),
                ])
                ->relationships([
                    Relationship::belongsTo('users', 'user_id'),
                ]),
        ],
    ],
],
```

### 6. Ask your first question

[](#6-ask-your-first-question)

```
curl -X POST http://localhost/laragrep \
  -H "Content-Type: application/json" \
  -d '{"question": "How many users registered this week?"}'
```

```
{
    "summary": "There were 42 new registrations this week.",
    "conversation_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

That's it. LaraGrep validates, executes, and answers automatically.

---

Monitor
-------

[](#monitor)

LaraGrep includes a built-in monitoring dashboard. Enable it to track every query, error, token usage, and performance metric.

### Enable

[](#enable)

```
LARAGREP_MONITOR_ENABLED=true
```

Access the dashboard at **`GET /laragrep/monitor`**:

- **Logs** — Filterable list of all queries with status, duration, iterations, and token estimates
- **Overview** — Aggregate stats: success rate, errors, token usage, daily charts, top scopes
- **Detail** — Full agent loop trace for each query: SQL, bindings, results, AI reasoning

Protect it with middleware:

```
// config/laragrep.php
'monitor' => [
    'enabled' => true,
    'middleware' => ['auth:sanctum'],
],
```

---

Async Mode
----------

[](#async-mode)

The agent loop can take 30-100+ seconds with multiple iterations, easily exceeding PHP or Nginx timeouts. Async mode dispatches the processing to a queue job and returns immediately.

### Enable

[](#enable-1)

```
LARAGREP_ASYNC_ENABLED=true
LARAGREP_ASYNC_QUEUE_CONNECTION=redis
```

Requires a real queue driver (`redis`, `database`, `sqs`, etc.). LaraGrep will throw an exception at boot if the queue connection uses the `sync` driver.

When enabled, **all requests** become async — the frontend doesn't decide, the backend does.

### How It Works

[](#how-it-works)

```
POST /laragrep { "question": "..." }

-> 202 Accepted
{
    "query_id": "550e8400-e29b-41d4-a716-446655440000",
    "channel": "laragrep.550e8400-e29b-41d4-a716-446655440000"
}

```

The agent loop runs in a background job. When it finishes, the result is delivered via **broadcasting** (WebSocket) and/or **polling** (GET endpoint).

### Polling

[](#polling)

```
GET /laragrep/queries/{query_id}
```

Returns the current status:

```
{ "status": "processing" }
```

While the AI is working, the response includes a progress message describing the current step:

```
{ "status": "processing", "progress": "Counting users registered this week" }
```

Or when completed:

```
{
    "status": "completed",
    "summary": "There were 42 new registrations this week.",
    "conversation_id": "...",
    "recipe_id": 42
}
```

Or on failure:

```
{ "status": "failed", "error": "Sorry, something went wrong..." }
```

### Broadcasting (Optional)

[](#broadcasting-optional)

If you have Laravel broadcasting configured (Reverb, Pusher, Soketi, Ably), LaraGrep broadcasts two events on the channel returned in the response:

EventPayload`laragrep.answer.progress``queryId`, `iteration`, `message``laragrep.answer.ready``queryId`, `summary`, `conversationId`, `recipeId``laragrep.answer.failed``queryId`, `error`**Frontend example (Laravel Echo):**

```
Echo.channel(response.channel)
    .listen('.laragrep.answer.progress', (e) => {
        showProgress(e.message); // "Counting users registered this week"
    })
    .listen('.laragrep.answer.ready', (e) => {
        showAnswer(e.summary);
    })
    .listen('.laragrep.answer.failed', (e) => {
        showError(e.error);
    });
```

For private channels, set `LARAGREP_ASYNC_PRIVATE=true` and register the channel authorization in your `routes/channels.php`:

```
Broadcast::channel('laragrep.{queryId}', function ($user, $queryId) {
    return true; // your authorization logic
});
```

Broadcasting is entirely optional — polling via GET works without any broadcasting setup. If you only want polling, make sure broadcasting is disabled in your `.env`:

```
BROADCAST_CONNECTION=null
```

### Completed records cleanup

[](#completed-records-cleanup)

Async records are automatically cleaned up after 24 hours (configurable via `LARAGREP_ASYNC_RETENTION_HOURS`).

---

How It Works
------------

[](#how-it-works-1)

Unlike simple text-to-SQL tools, LaraGrep uses an **agent loop**:

1. You ask a question in natural language
2. The AI analyzes the schema and decides which queries to run
3. LaraGrep validates and executes the queries safely
4. The AI sees the results and decides: run more queries, or provide the final answer
5. Repeat until the AI has enough data to answer (up to `max_iterations`)

This means the AI can build on previous results, self-correct, break down complex analysis into steps, and batch independent queries in a single iteration.

```
"How many users and how many orders do I have?"

  -> AI: Sends 2 queries in one batch (independent)        (1 API call)
  -> AI: Sees both results, provides the final answer       (1 API call)

```

---

Configuration
-------------

[](#configuration)

### AI Provider

[](#ai-provider)

**OpenAI:**

```
LARAGREP_PROVIDER=openai
LARAGREP_API_KEY=sk-...
LARAGREP_MODEL=gpt-4o-mini
```

**Anthropic:**

```
LARAGREP_PROVIDER=anthropic
LARAGREP_API_KEY=sk-ant-...
LARAGREP_MODEL=claude-sonnet-4-20250514
```

**Ollama (local):**

```
LARAGREP_PROVIDER=openai
LARAGREP_API_KEY=ollama
LARAGREP_MODEL=qwen3-coder:30b
LARAGREP_BASE_URL=http://localhost:11434/v1/chat/completions
```

Ollama exposes an OpenAI-compatible API, so it works with the `openai` provider. The API key can be any non-empty string. This keeps your data fully local.

### Fallback Provider

[](#fallback-provider)

If the primary provider fails (timeout, rate limit, API down), LaraGrep can automatically retry with a fallback:

```
LARAGREP_FALLBACK_PROVIDER=anthropic
LARAGREP_FALLBACK_API_KEY=sk-ant-...
LARAGREP_FALLBACK_MODEL=claude-sonnet-4-20250514
```

Works in any direction — OpenAI primary with Anthropic fallback, or vice versa. When the primary succeeds, the fallback is never called. No cooldown, no circuit breaker — just tries in order.

### Schema Loading Mode

[](#schema-loading-mode)

ModeBehavior`manual`Only use tables defined in config (default)`auto`Auto-load from `information_schema` (MySQL/MariaDB/PostgreSQL)`merged`Auto-load first, then overlay config definitions```
LARAGREP_SCHEMA_MODE=manual
```

- **manual** is the safest — no accidental schema exposure.
- **auto** is ideal for quick setup when all tables are fair game.
- **merged** lets you auto-load and then add descriptions, relationships, or extra tables on top.

### Table Definitions

[](#table-definitions)

Define tables using fluent classes with IDE autocomplete:

```
use LaraGrep\Config\Table;
use LaraGrep\Config\Column;
use LaraGrep\Config\Relationship;

Table::make('orders')
    ->description('Customer orders.')
    ->columns([
        Column::id(),
        Column::bigInteger('user_id')->unsigned()->description('FK to users.id.'),
        Column::decimal('total', 10, 2)->description('Order total.'),
        Column::enum('status', ['pending', 'paid', 'cancelled']),
        Column::json('metadata')
            ->description('Order metadata')
            ->template(['shipping_method' => 'express', 'tracking_code' => 'BR123456789']),
        Column::timestamp('created_at'),
    ])
    ->relationships([
        Relationship::belongsTo('users', 'user_id'),
    ]),
```

**Supported column types:** `id()`, `bigInteger()`, `integer()`, `smallInteger()`, `tinyInteger()`, `string()`, `text()`, `decimal()`, `float()`, `boolean()`, `date()`, `dateTime()`, `timestamp()`, `json()`, `enum()`.

**Modifiers:** `->unsigned()`, `->nullable()`, `->description()`.

For JSON columns, `->template()` provides an example structure so the AI knows how to query with `JSON_EXTRACT`.

#### Organizing Large Schemas

[](#organizing-large-schemas)

For projects with many tables, extract each definition into its own class:

```
// app/LaraGrep/Tables/OrdersTable.php
namespace App\LaraGrep\Tables;

use LaraGrep\Config\Table;
use LaraGrep\Config\Column;
use LaraGrep\Config\Relationship;

class OrdersTable
{
    public static function define(): Table
    {
        return Table::make('orders')
            ->description('Customer orders.')
            ->columns([
                Column::id(),
                Column::bigInteger('user_id')->unsigned(),
                Column::decimal('total', 10, 2),
                Column::timestamp('created_at'),
            ])
            ->relationships([
                Relationship::belongsTo('users', 'user_id'),
            ]);
    }
}
```

```
// config/laragrep.php
'tables' => [
    \App\LaraGrep\Tables\UsersTable::define(),
    \App\LaraGrep\Tables\OrdersTable::define(),
    \App\LaraGrep\Tables\ProductsTable::define(),
],
```

### Multi-Connection Tables

[](#multi-connection-tables)

When some tables live in a different database, use `->connection()` to tell LaraGrep which connection to use for queries on that table:

```
'tables' => [
    Table::make('users')
        ->description('Registered users.')
        ->columns([
            Column::id(),
            Column::string('name'),
            Column::string('email'),
        ]),

    Table::make('analytics_events')
        ->description('Columnar analytics store.')
        ->connection('clickhouse', 'ClickHouse')
        ->columns([
            Column::string('event_name'),
            Column::timestamp('event_time'),
            Column::bigInteger('user_id'),
        ]),
],
```

The second parameter is optional and describes the database engine. This is important when the external database uses a different SQL dialect (e.g., ClickHouse, PostgreSQL, SQLite) — the AI will generate compatible syntax for each table.

```
// Connection only (same engine as the primary database)
->connection('replica')

// Connection + engine (different SQL dialect)
->connection('clickhouse', 'ClickHouse')
```

When the AI encounters tables on different connections, it will:

1. **Generate engine-compatible SQL** for each table
2. **Include the connection name** in each query entry so the executor runs it on the right database
3. **Avoid cross-connection JOINs** — instead, it queries each database separately and combines the results in the final answer

### Multi-Tenant / Dynamic Connections

[](#multi-tenant--dynamic-connections)

In multi-tenant applications where each tenant has its own database, the connection name is only known at runtime. Pass a `Closure` instead of a string to resolve the connection dynamically:

```
'contexts' => [
    'default' => [
        'connection' => fn () => 'tenant_' . tenant()->id,
        'tables' => [
            Table::make('users')->columns([...]),
            Table::make('orders')->columns([...]),
        ],
    ],
],
```

The closure is evaluated per-request, so it works in HTTP (middleware sets the tenant), queue jobs, and artisan commands — as long as your tenant context is available.

You can mix dynamic and static connections. For example, tenant tables on a dynamic connection and shared tables on a fixed central database:

```
'contexts' => [
    'default' => [
        'connection' => fn () => app('tenant')->getConnectionName(),
        'tables' => [
            Table::make('orders')->columns([...]),

            Table::make('plans')
                ->connection('central')
                ->columns([...]),
        ],
    ],
],
```

Table-level connections also accept closures:

```
Table::make('orders')->connection(fn () => 'tenant_' . tenant()->id)
```

### Named Scopes (Contexts)

[](#named-scopes-contexts)

Work with multiple databases or table sets:

```
'contexts' => [
    'default' => [
        'connection' => env('LARAGREP_CONNECTION'),
        'tables' => [...],
    ],
    'analytics' => [
        'connection' => 'analytics_db',
        'schema_mode' => 'auto',
        'database' => ['type' => 'MariaDB 10.6', 'name' => 'analytics'],
        'exclude_tables' => ['migrations', 'jobs'],
    ],
],
```

Select a scope via the URL: `POST /laragrep/analytics`

### Query Protection

[](#query-protection)

```
LARAGREP_MAX_ROWS=20
LARAGREP_MAX_QUERY_TIME=3
```

- **max\_rows** — Automatically injects `LIMIT` into queries that don't have one, and rejects explicit `LIMIT` values that exceed this cap. The AI is informed of the limit in its prompt. Default: `20`. Set to `0` to disable.
- **max\_query\_time** — Maximum execution time per query in seconds. Kills slow queries before they block the database. Default: `3`. Supports MySQL, MariaDB, PostgreSQL, and SQLite.

### Agent Loop

[](#agent-loop)

```
LARAGREP_MAX_ITERATIONS=10
```

Simple questions typically resolve in 1-2 iterations. Complex analytical questions may need more. Higher values increase capability but also cost.

### Smart Schema

[](#smart-schema)

For large databases, LaraGrep can make an initial AI call to identify only the relevant tables, reducing token usage across all iterations.

```
LARAGREP_SMART_SCHEMA=20
```

Activates automatically when the table count reaches the threshold. With 200 tables and only 5 relevant, this reduces token usage by ~60%.

### Question Clarification

[](#question-clarification)

When users ask vague questions ("Show me the sales"), the AI may guess filters or return overly generic results. Clarification adds a pre-agent-loop step that analyzes the question against developer-defined rules and asks for missing context before proceeding.

**Enable:**

```
LARAGREP_CLARIFICATION_ENABLED=true
```

**Custom clarification prompt:**

Add extra instructions to the clarification system prompt — for example, to prevent the AI from mentioning internal table names in its questions:

```
LARAGREP_CLARIFY_SYSTEM_PROMPT="Never mention database table names, column names, or any technical database terms in your clarification questions. Use only business-friendly language."
```

**Define rules per context:**

```
'contexts' => [
    'default' => [
        'clarification_rules' => [
            'Always ask for a date range when the question involves time-based data',
            'Always ask which store/branch if not specified',
        ],
        'tables' => [...],
    ],
],
```

**Flow:**

```
User question → AI checks against rules → Missing context?
  ├─ YES → Returns clarification questions (no agent loop runs)
  └─ NO  → Proceeds to answerQuestion() normally

```

**Clarification response:**

```
{
    "action": "clarification",
    "questions": ["What date range?", "Which store?"],
    "suggestions": [
        {"label": "Monthly Sales Report", "url": "/reports/sales"}
    ],
    "original_question": "Show me the sales",
    "conversation_id": "uuid"
}
```

The frontend can display these questions, collect the answers, and resubmit with `clarification_answers`. LaraGrep will call the AI to reformulate the original question into a precise, self-contained question and then run the agent loop with it automatically.

**Answering clarification questions:**

```
curl -X POST http://localhost/laragrep \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Show me the sales",
    "conversation_id": "uuid-from-clarification-response",
    "clarification_answers": [
      {"question": "What date range?", "answer": "January 2026"},
      {"question": "Which store?", "answer": "Store Centro"}
    ]
  }'
```

The AI reformulates the vague question into something like "Show me the sales for Store Centro in January 2026" and proceeds with the agent loop. The reformulated question is what gets stored in conversation history and recipes.

If reformulation fails for any reason, the original question is used as fallback — the agent loop still runs.

**Programmatic usage:**

```
$laraGrep = app(LaraGrep::class);

// Pass conversationId so the AI considers conversation history
$clarification = $laraGrep->clarifyQuestion('Show me the sales', 'default', $conversationId);

if ($clarification !== null) {
    // Collect answers from the user, then reformulate
    $reformulated = $laraGrep->reformulateQuestion(
        'Show me the sales',
        [
            ['question' => 'What date range?', 'answer' => 'January 2026'],
            ['question' => 'Which store?', 'answer' => 'Store Centro'],
        ],
        'default',
    );

    $answer = $laraGrep->answerQuestion($reformulated);
} else {
    // Question is clear — proceed normally
    $answer = $laraGrep->answerQuestion('Show me the sales');
}
```

**Token usage impact:**

The clarification call is lightweight — it sends only table names and descriptions (no columns or relationships), plus the rules and question. Compared to the agent loop:

CallInput (typical)Output (typical)Clarification~200-400 tokens~15-40 tokensSuggestion Filter~100-200 tokens~10-20 tokensReformulation~150-300 tokens~15-30 tokensSmart Schema Filter~150-300 tokens~15-30 tokensAgent Loop (per iteration)~500-2000+ tokens~100-300 tokensWhen the question is clear ("proceed"), the overhead is a single lightweight API call (~200-400 input tokens). When clarification is triggered, it **saves** the entire agent loop cost (potentially 3-10 iterations) by catching vague questions early. The reformulation call is equally lightweight — it only sends the original question and Q&amp;A pairs (no schema), producing a plain text question.

**Conversation-aware clarification:**

When a `conversation_id` is provided, the clarification step receives the conversation history. This prevents false positives — for example, "Please continue" in an ongoing conversation won't trigger clarification because the AI sees the full context.

**Page suggestions:**

If your application has existing dashboards or reports that overlap with common questions, you can list them as suggestions. When the user's question matches a suggestion, the AI includes it as a natural clarification question — e.g., *"Would you like me to query the data now, or would you prefer to check the existing Incidents Dashboard (/dashboard/incidents)?"*

```
'contexts' => [
    'default' => [
        'suggestions' => [
            ['label' => 'Incidents Dashboard', 'description' => 'Shows incidents per company with period filters', 'url' => '/dashboard/incidents'],
            ['label' => 'Monthly Sales Report', 'description' => 'Pre-built report with sales by region and product', 'url' => '/reports/sales'],
        ],
        'clarification_rules' => [...],
        'tables' => [...],
    ],
],
```

Each suggestion needs a `label` (what the user sees), a `description` (what the AI uses to judge relevance), and a `url`. When clarification is triggered and suggestions are configured, a separate lightweight AI call filters which suggestions are relevant to the question. Only matching suggestions are included in the response — if none match, the `suggestions` field is omitted. The frontend can render them as clickable links alongside the clarification questions.

With the feature disabled or no `clarification_rules` defined, zero API calls are made — `clarifyQuestion()` returns `null` immediately.

### Conversation Persistence

[](#conversation-persistence)

Multi-turn conversations are enabled by default. Previous questions and answers are sent as context for follow-ups.

```
LARAGREP_CONVERSATION_ENABLED=true
LARAGREP_CONVERSATION_CONNECTION=sqlite
LARAGREP_CONVERSATION_MAX_MESSAGES=10
LARAGREP_CONVERSATION_RETENTION_DAYS=10
```

### Route Protection

[](#route-protection)

```
'route' => [
    'prefix' => 'laragrep',
    'middleware' => ['auth:sanctum'],
],
```

---

Usage
-----

[](#usage)

### API Endpoint

[](#api-endpoint)

```
POST /laragrep/{scope?}

```

**Basic request:**

```
curl -X POST http://localhost/laragrep \
  -H "Content-Type: application/json" \
  -d '{"question": "How many users registered this week?"}'
```

**With authentication and options:**

```
curl -X POST http://localhost/laragrep \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "question": "How many users registered this week?",
    "conversation_id": "optional-uuid-for-follow-ups",
    "debug": true
  }'
```

**Using a named scope:**

```
curl -X POST http://localhost/laragrep/analytics \
  -H "Content-Type: application/json" \
  -d '{"question": "What are the top 5 products by revenue?"}'
```

**Debug response** (when `debug: true`):

```
{
    "summary": "There were 42 new registrations this week.",
    "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
    "steps": [
        {
            "query": "SELECT COUNT(*) as total FROM users WHERE created_at >= ?",
            "bindings": ["2025-01-20"],
            "results": [{"total": 42}],
            "reason": "Counting users registered in the current week"
        }
    ],
    "debug": {
        "queries": [
            {"query": "SELECT COUNT(*) ...", "bindings": ["..."], "time": 1.23}
        ],
        "iterations": 1
    }
}
```

### Programmatic Usage

[](#programmatic-usage)

```
use LaraGrep\LaraGrep;

$laraGrep = app(LaraGrep::class);

$answer = $laraGrep->answerQuestion(
    question: 'How many orders were placed today?',
    scope: 'default',
);

echo $answer['summary'];
```

### Formatting Results

[](#formatting-results)

Use `formatResult()` to transform raw query results into structured formats via AI.

**Query format** — a single consolidated SQL query for export:

```
$answer = $laraGrep->answerQuestion('Weekly sales by region');

$result = $laraGrep->formatResult($answer, 'query');
// [
//     'title' => 'Weekly Sales by Region',
//     'headers' => ['Region', 'Total Sales', 'Order Count'],
//     'query' => 'SELECT r.name as region, SUM(o.total) ... GROUP BY r.name',
//     'bindings' => ['2026-02-01'],
// ]
```

Returns the SQL itself, no `LIMIT`. Use it with Laravel's streaming tools:

```
// Stream with cursor
foreach (DB::cursor($result['query'], $result['bindings']) as $row) {
    // process row
}

// Chunk for batch processing
DB::table(DB::raw("({$result['query']}) as sub"))
    ->setBindings($result['bindings'])
    ->chunk(1000, function ($rows) {
        // process chunk
    });
```

**Notification format** — ready-to-render content for email, Slack, or webhooks:

```
$notification = $laraGrep->formatResult($answer, 'notification');
// [
//     'title' => 'Weekly Sales Report',
//     'html' => 'Sales this week totaled......',
//     'text' => 'Sales this week totaled...\nProduct | Revenue...',
// ]
```

### Saved Queries (Recipes)

[](#saved-queries-recipes)

Auto-save a "recipe" after each answer — the question, scope, and queries that worked. The response includes a `recipe_id` for exports, notifications, or scheduled re-execution.

**Enable:**

```
LARAGREP_RECIPES_ENABLED=true
```

After enabling, publish and run the migration for the `laragrep_recipes` table.

**API response with recipe:**

```
{
    "summary": "Sales this week totaled...",
    "conversation_id": "uuid",
    "recipe_id": 42
}
```

**Dispatch a recipe:**

```
curl -X POST http://localhost/laragrep/recipes/42/dispatch \
  -H "Content-Type: application/json" \
  -d '{"format": "notification", "period": "now"}'
```

The `period` parameter controls timing:

- `"now"` — immediate execution (default)
- `"2026-02-10 08:00:00"` — scheduled for a specific date/time

LaraGrep fires a `RecipeDispatched` event. Your app handles the rest via a listener:

```
// app/Listeners/HandleRecipeDispatch.php
use LaraGrep\Events\RecipeDispatched;

public function handle(RecipeDispatched $event)
{
    $job = new ProcessRecipeJob($event->recipe, $event->format, $event->userId);

    if ($event->period === 'now') {
        dispatch($job);
    } else {
        dispatch($job)->delay(Carbon::parse($event->period));
    }
}
```

```
// app/Jobs/ProcessRecipeJob.php
use LaraGrep\LaraGrep;

public function handle(LaraGrep $laraGrep)
{
    $answer = $laraGrep->replayRecipe($this->recipe);
    $result = $laraGrep->formatResult($answer, $this->format);

    // Send email, generate Excel, post to Slack, etc.
}
```

**Programmatic usage:**

```
use LaraGrep\LaraGrep;

$laraGrep = app(LaraGrep::class);

// First run
$answer = $laraGrep->answerQuestion('Weekly sales by region');
$recipe = $laraGrep->extractRecipe($answer, 'Weekly sales by region', 'default');

// Later — replay with fresh data
$freshAnswer = $laraGrep->replayRecipe($recipe);
$notification = $laraGrep->formatResult($freshAnswer, 'notification');
```

> **With monitor enabled?** Inject `LaraGrep\Monitor\MonitorRecorder` instead of `LaraGrep`. It wraps the same methods (`answerQuestion`, `replayRecipe`, `formatResult`) and automatically records every execution in the dashboard. When the monitor is disabled, `MonitorRecorder` resolves to `null` — so use `LaraGrep` as the safe default.

---

Extending
---------

[](#extending)

### Custom AI Client

[](#custom-ai-client)

Implement `LaraGrep\Contracts\AiClientInterface` and rebind in a service provider:

```
$this->app->singleton(AiClientInterface::class, fn () => new MyCustomClient());
```

### Custom Metadata Loader

[](#custom-metadata-loader)

LaraGrep auto-detects MySQL/MariaDB and PostgreSQL. For other databases, implement `LaraGrep\Contracts\MetadataLoaderInterface`:

```
$this->app->singleton(MetadataLoaderInterface::class, fn ($app) => new MySqliteSchemaLoader($app['db']));
```

### Custom Conversation Store

[](#custom-conversation-store)

Implement `LaraGrep\Contracts\ConversationStoreInterface` for Redis, file-based storage, etc.:

```
$this->app->singleton(ConversationStoreInterface::class, fn () => new RedisConversationStore());
```

---

Environment Variables
---------------------

[](#environment-variables)

VariableDefaultDescription`LARAGREP_PROVIDER``openai`AI provider (`openai`, `anthropic`)`LARAGREP_API_KEY`—API key for the AI provider`LARAGREP_MODEL``gpt-4o-mini`Model identifier`LARAGREP_BASE_URL`—Override API endpoint URL`LARAGREP_MAX_TOKENS``1024`Max response tokens`LARAGREP_TIMEOUT``300`HTTP timeout in seconds`LARAGREP_FALLBACK_PROVIDER`—Fallback AI provider`LARAGREP_FALLBACK_API_KEY`—Fallback API key`LARAGREP_FALLBACK_MODEL`—Fallback model identifier`LARAGREP_FALLBACK_BASE_URL`—Fallback API endpoint URL`LARAGREP_MAX_ITERATIONS``10`Max query iterations per question`LARAGREP_MAX_ROWS``20`Max rows per query (auto LIMIT)`LARAGREP_MAX_QUERY_TIME``3`Max query execution time (seconds)`LARAGREP_SMART_SCHEMA`—Table count threshold for smart filtering`LARAGREP_CLARIFICATION_ENABLED``false`Enable pre-query question clarification`LARAGREP_CLARIFY_SYSTEM_PROMPT`—Extra instructions for clarification prompt`LARAGREP_SCHEMA_MODE``manual`Schema loading mode`LARAGREP_USER_LANGUAGE``en`AI response language`LARAGREP_RESPONSE_FORMAT``html`Summary format: `html`, `markdown`, or `text``LARAGREP_CONNECTION`—Database connection name`LARAGREP_DATABASE_TYPE`—DB type hint for AI`LARAGREP_DATABASE_NAME``DB_DATABASE`DB name hint for AI`LARAGREP_EXCLUDE_TABLES`—Comma-separated tables to hide`LARAGREP_DEBUG``false`Enable debug mode`LARAGREP_ROUTE_PREFIX``laragrep`API route prefix`LARAGREP_CONVERSATION_ENABLED``true`Enable conversation persistence`LARAGREP_CONVERSATION_CONNECTION``sqlite`DB connection for conversations`LARAGREP_CONVERSATION_MAX_MESSAGES``10`Max messages per conversation`LARAGREP_CONVERSATION_RETENTION_DAYS``10`Auto-delete conversations after days`LARAGREP_MONITOR_ENABLED``false`Enable monitoring dashboard`LARAGREP_MONITOR_CONNECTION``sqlite`DB connection for monitor logs`LARAGREP_MONITOR_TABLE``laragrep_logs`Table name for monitor logs`LARAGREP_MONITOR_RETENTION_DAYS``30`Auto-delete logs after days`LARAGREP_RECIPES_ENABLED``false`Enable recipe auto-save`LARAGREP_RECIPES_CONNECTION``sqlite`DB connection for recipes`LARAGREP_RECIPES_TABLE``laragrep_recipes`Table name for recipes`LARAGREP_RECIPES_RETENTION_DAYS``30`Auto-delete recipes after days`LARAGREP_ASYNC_ENABLED``false`Enable async mode`LARAGREP_ASYNC_CONNECTION``laragrep`DB connection for async table`LARAGREP_ASYNC_TABLE``laragrep_async`Table name for async records`LARAGREP_ASYNC_RETENTION_HOURS``24`Auto-delete records after hours`LARAGREP_ASYNC_QUEUE``default`Queue name for async jobs`LARAGREP_ASYNC_QUEUE_CONNECTION`—Queue connection (falls back to default)`LARAGREP_ASYNC_CHANNEL_PREFIX``laragrep`Broadcasting channel prefix`LARAGREP_ASYNC_PRIVATE``false`Use private broadcasting channels---

Security
--------

[](#security)

- Only `SELECT` queries are generated and executed — mutations are rejected.
- All queries use parameterized bindings to prevent SQL injection.
- Table references are validated against the known schema metadata.
- The agent loop is capped at `max_iterations` to prevent runaway costs.
- Protect the endpoint with middleware (e.g., `auth:sanctum`).

Testing
-------

[](#testing)

```
./vendor/bin/phpunit
```

License
-------

[](#license)

MIT

###  Health Score

46

—

FairBetter than 93% of packages

Maintenance87

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity57

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

47

Last Release

63d ago

Major Versions

v1.9.9 → v2.0.02026-03-12

### Community

Maintainers

![](https://www.gravatar.com/avatar/203483dacaa229cdffe458fcad421eb5d3f592fa9a0256de34d4803f4befd1e7?d=identicon)[jeffleyd](/maintainers/jeffleyd)

---

Top Contributors

[![jeffleyd](https://avatars.githubusercontent.com/u/45333690?v=4)](https://github.com/jeffleyd "jeffleyd (122 commits)")

---

Tags

aiapibackendlaravelphp

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/behindsolution-laragrep/health.svg)

```
[![Health](https://phpackages.com/badges/behindsolution-laragrep/health.svg)](https://phpackages.com/packages/behindsolution-laragrep)
```

###  Alternatives

[spatie/laravel-query-builder

Easily build Eloquent queries from API requests

4.4k26.9M220](/packages/spatie-laravel-query-builder)[essa/api-tool-kit

set of tools to build an api with laravel

52680.5k](/packages/essa-api-tool-kit)[flat3/lodata

OData v4.01 Producer for Laravel

96320.9k](/packages/flat3-lodata)[api-platform/laravel

API Platform support for Laravel

59126.4k6](/packages/api-platform-laravel)[simplestats-io/laravel-client

Client for SimpleStats!

4515.5k](/packages/simplestats-io-laravel-client)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
