{"id":44123464,"url":"https://github.com/behindsolution/laragrep","last_synced_at":"2026-03-08T04:01:40.817Z","repository":{"id":319542304,"uuid":"1078965776","full_name":"behindSolution/laragrep","owner":"behindSolution","description":"used to get information from database using natural language","archived":false,"fork":false,"pushed_at":"2026-03-07T22:41:06.000Z","size":284,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-07T23:49:51.875Z","etag":null,"topics":["ai","api","backend","laravel","php"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/behindSolution.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-18T19:52:40.000Z","updated_at":"2026-03-07T22:40:55.000Z","dependencies_parsed_at":"2025-10-19T12:14:54.015Z","dependency_job_id":"3ef79adb-9dec-46c5-98ea-98afd3a9aec2","html_url":"https://github.com/behindSolution/laragrep","commit_stats":null,"previous_names":["jeffleyd/laragrep","behindsolution/laragrep"],"tags_count":28,"template":false,"template_full_name":null,"purl":"pkg:github/behindSolution/laragrep","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/behindSolution%2Flaragrep","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/behindSolution%2Flaragrep/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/behindSolution%2Flaragrep/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/behindSolution%2Flaragrep/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/behindSolution","download_url":"https://codeload.github.com/behindSolution/laragrep/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/behindSolution%2Flaragrep/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30244556,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T00:58:18.660Z","status":"online","status_checked_at":"2026-03-08T02:00:06.215Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","api","backend","laravel","php"],"created_at":"2026-02-08T20:26:51.081Z","updated_at":"2026-03-08T04:01:40.798Z","avatar_url":"https://github.com/behindSolution.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"asset/logo_laragrep.png\" alt=\"LaraGrep\" width=\"400\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://packagist.org/packages/behindsolution/laragrep\"\u003e\u003cimg src=\"https://img.shields.io/packagist/v/behindsolution/laragrep.svg\" alt=\"Latest Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/behindSolution/laragrep/actions/workflows/tests.yml\"\u003e\u003cimg src=\"https://github.com/behindSolution/laragrep/actions/workflows/tests.yml/badge.svg\" alt=\"Tests\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  Transform natural language questions into safe, parameterized SQL queries using AI.\u003cbr\u003e\n  LaraGrep uses an \u003cstrong\u003eagent loop\u003c/strong\u003e — the AI executes queries, sees the results,\u003cbr\u003e\n  and iteratively reasons until it can provide a final answer.\n\u003c/p\u003e\n\n---\n\n## Quick Start\n\n### 1. Install\n\n```bash\ncomposer require behindsolution/laragrep\n```\n\n### 2. Publish config and migrations\n\n```bash\nphp artisan vendor:publish --tag=laragrep-config\nphp artisan vendor:publish --tag=laragrep-migrations\n```\n\n### 3. Create the SQLite database and run migrations\n\nLaraGrep stores conversations, monitor logs, and recipes in a separate SQLite database by default, keeping everything isolated from your main database.\n\nCreate the file and run migrations:\n\n```bash\n# Linux / macOS\ntouch database/laragrep.sqlite\n\n# Windows\ntype nul \u003e database\\laragrep.sqlite\n```\n\nAdd a `laragrep` connection to your `config/database.php`:\n\n```php\n'connections' =\u003e [\n    // ... your existing connections\n\n    'laragrep' =\u003e [\n        'driver' =\u003e 'sqlite',\n        'database' =\u003e database_path('laragrep.sqlite'),\n        'foreign_key_constraints' =\u003e true,\n    ],\n],\n```\n\nThen point LaraGrep to it in your `.env`:\n\n```env\nLARAGREP_CONVERSATION_CONNECTION=laragrep\nLARAGREP_MONITOR_CONNECTION=laragrep\nLARAGREP_RECIPES_CONNECTION=laragrep\n```\n\nRun the migrations:\n\n```bash\nphp artisan migrate\n```\n\n\u003e Already using SQLite as your main database? You can skip the connection setup — LaraGrep will use the default `sqlite` connection as-is.\n\n### 4. Add your API key to `.env`\n\n```env\nLARAGREP_PROVIDER=openai\nLARAGREP_API_KEY=sk-...\nLARAGREP_MODEL=gpt-4o-mini\n```\n\n### 5. Define your tables in `config/laragrep.php`\n\n```php\nuse LaraGrep\\Config\\Table;\nuse LaraGrep\\Config\\Column;\nuse LaraGrep\\Config\\Relationship;\n\n'contexts' =\u003e [\n    'default' =\u003e [\n        // ...\n        'tables' =\u003e [\n            Table::make('users')\n                -\u003edescription('Registered users.')\n                -\u003ecolumns([\n                    Column::id(),\n                    Column::string('name'),\n                    Column::string('email'),\n                    Column::timestamp('created_at'),\n                ]),\n\n            Table::make('orders')\n                -\u003edescription('Customer orders.')\n                -\u003ecolumns([\n                    Column::id(),\n                    Column::bigInteger('user_id')-\u003eunsigned(),\n                    Column::decimal('total', 10, 2),\n                    Column::enum('status', ['pending', 'paid', 'cancelled']),\n                    Column::timestamp('created_at'),\n                ])\n                -\u003erelationships([\n                    Relationship::belongsTo('users', 'user_id'),\n                ]),\n        ],\n    ],\n],\n```\n\n### 6. Ask your first question\n\n```bash\ncurl -X POST http://localhost/laragrep \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\": \"How many users registered this week?\"}'\n```\n\n```json\n{\n    \"summary\": \"There were 42 new registrations this week.\",\n    \"conversation_id\": \"550e8400-e29b-41d4-a716-446655440000\"\n}\n```\n\nThat's it. LaraGrep validates, executes, and answers automatically.\n\n---\n\n## Monitor\n\nLaraGrep includes a built-in monitoring dashboard. Enable it to track every query, error, token usage, and performance metric.\n\n### Enable\n\n```env\nLARAGREP_MONITOR_ENABLED=true\n```\n\nAccess the dashboard at **`GET /laragrep/monitor`**:\n\n- **Logs** — Filterable list of all queries with status, duration, iterations, and token estimates\n- **Overview** — Aggregate stats: success rate, errors, token usage, daily charts, top scopes\n- **Detail** — Full agent loop trace for each query: SQL, bindings, results, AI reasoning\n\nProtect it with middleware:\n\n```php\n// config/laragrep.php\n'monitor' =\u003e [\n    'enabled' =\u003e true,\n    'middleware' =\u003e ['auth:sanctum'],\n],\n```\n\n---\n\n## Async Mode\n\nThe 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.\n\n### Enable\n\n```env\nLARAGREP_ASYNC_ENABLED=true\nLARAGREP_ASYNC_QUEUE_CONNECTION=redis\n```\n\nRequires a real queue driver (`redis`, `database`, `sqs`, etc.). LaraGrep will throw an exception at boot if the queue connection uses the `sync` driver.\n\nWhen enabled, **all requests** become async — the frontend doesn't decide, the backend does.\n\n### How It Works\n\n```\nPOST /laragrep { \"question\": \"...\" }\n\n-\u003e 202 Accepted\n{\n    \"query_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"channel\": \"laragrep.550e8400-e29b-41d4-a716-446655440000\"\n}\n```\n\nThe agent loop runs in a background job. When it finishes, the result is delivered via **broadcasting** (WebSocket) and/or **polling** (GET endpoint).\n\n### Polling\n\n```bash\nGET /laragrep/queries/{query_id}\n```\n\nReturns the current status:\n\n```json\n{ \"status\": \"processing\" }\n```\n\nWhile the AI is working, the response includes a progress message describing the current step:\n\n```json\n{ \"status\": \"processing\", \"progress\": \"Counting users registered this week\" }\n```\n\nOr when completed:\n\n```json\n{\n    \"status\": \"completed\",\n    \"summary\": \"There were 42 new registrations this week.\",\n    \"conversation_id\": \"...\",\n    \"recipe_id\": 42\n}\n```\n\nOr on failure:\n\n```json\n{ \"status\": \"failed\", \"error\": \"Sorry, something went wrong...\" }\n```\n\n### Broadcasting (Optional)\n\nIf you have Laravel broadcasting configured (Reverb, Pusher, Soketi, Ably), LaraGrep broadcasts two events on the channel returned in the response:\n\n| Event | Payload |\n|---|---|\n| `laragrep.answer.progress` | `queryId`, `iteration`, `message` |\n| `laragrep.answer.ready` | `queryId`, `summary`, `conversationId`, `recipeId` |\n| `laragrep.answer.failed` | `queryId`, `error` |\n\n**Frontend example (Laravel Echo):**\n\n```js\nEcho.channel(response.channel)\n    .listen('.laragrep.answer.progress', (e) =\u003e {\n        showProgress(e.message); // \"Counting users registered this week\"\n    })\n    .listen('.laragrep.answer.ready', (e) =\u003e {\n        showAnswer(e.summary);\n    })\n    .listen('.laragrep.answer.failed', (e) =\u003e {\n        showError(e.error);\n    });\n```\n\nFor private channels, set `LARAGREP_ASYNC_PRIVATE=true` and register the channel authorization in your `routes/channels.php`:\n\n```php\nBroadcast::channel('laragrep.{queryId}', function ($user, $queryId) {\n    return true; // your authorization logic\n});\n```\n\nBroadcasting is entirely optional — polling via GET works without any broadcasting setup. If you only want polling, make sure broadcasting is disabled in your `.env`:\n\n```env\nBROADCAST_CONNECTION=null\n```\n\n### Completed records cleanup\n\nAsync records are automatically cleaned up after 24 hours (configurable via `LARAGREP_ASYNC_RETENTION_HOURS`).\n\n---\n\n## How It Works\n\nUnlike simple text-to-SQL tools, LaraGrep uses an **agent loop**:\n\n1. You ask a question in natural language\n2. The AI analyzes the schema and decides which queries to run\n3. LaraGrep validates and executes the queries safely\n4. The AI sees the results and decides: run more queries, or provide the final answer\n5. Repeat until the AI has enough data to answer (up to `max_iterations`)\n\nThis means the AI can build on previous results, self-correct, break down complex analysis into steps, and batch independent queries in a single iteration.\n\n```\n\"How many users and how many orders do I have?\"\n\n  -\u003e AI: Sends 2 queries in one batch (independent)        (1 API call)\n  -\u003e AI: Sees both results, provides the final answer       (1 API call)\n```\n\n---\n\n## Configuration\n\n### AI Provider\n\n**OpenAI:**\n\n```env\nLARAGREP_PROVIDER=openai\nLARAGREP_API_KEY=sk-...\nLARAGREP_MODEL=gpt-4o-mini\n```\n\n**Anthropic:**\n\n```env\nLARAGREP_PROVIDER=anthropic\nLARAGREP_API_KEY=sk-ant-...\nLARAGREP_MODEL=claude-sonnet-4-20250514\n```\n\n**Ollama (local):**\n\n```env\nLARAGREP_PROVIDER=openai\nLARAGREP_API_KEY=ollama\nLARAGREP_MODEL=qwen3-coder:30b\nLARAGREP_BASE_URL=http://localhost:11434/v1/chat/completions\n```\n\nOllama 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.\n\n### Fallback Provider\n\nIf the primary provider fails (timeout, rate limit, API down), LaraGrep can automatically retry with a fallback:\n\n```env\nLARAGREP_FALLBACK_PROVIDER=anthropic\nLARAGREP_FALLBACK_API_KEY=sk-ant-...\nLARAGREP_FALLBACK_MODEL=claude-sonnet-4-20250514\n```\n\nWorks 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.\n\n### Schema Loading Mode\n\n| Mode     | Behavior                                              |\n|----------|-------------------------------------------------------|\n| `manual` | Only use tables defined in config (default)           |\n| `auto`   | Auto-load from `information_schema` (MySQL/MariaDB/PostgreSQL) |\n| `merged` | Auto-load first, then overlay config definitions      |\n\n```env\nLARAGREP_SCHEMA_MODE=manual\n```\n\n- **manual** is the safest — no accidental schema exposure.\n- **auto** is ideal for quick setup when all tables are fair game.\n- **merged** lets you auto-load and then add descriptions, relationships, or extra tables on top.\n\n### Table Definitions\n\nDefine tables using fluent classes with IDE autocomplete:\n\n```php\nuse LaraGrep\\Config\\Table;\nuse LaraGrep\\Config\\Column;\nuse LaraGrep\\Config\\Relationship;\n\nTable::make('orders')\n    -\u003edescription('Customer orders.')\n    -\u003ecolumns([\n        Column::id(),\n        Column::bigInteger('user_id')-\u003eunsigned()-\u003edescription('FK to users.id.'),\n        Column::decimal('total', 10, 2)-\u003edescription('Order total.'),\n        Column::enum('status', ['pending', 'paid', 'cancelled']),\n        Column::json('metadata')\n            -\u003edescription('Order metadata')\n            -\u003etemplate(['shipping_method' =\u003e 'express', 'tracking_code' =\u003e 'BR123456789']),\n        Column::timestamp('created_at'),\n    ])\n    -\u003erelationships([\n        Relationship::belongsTo('users', 'user_id'),\n    ]),\n```\n\n**Supported column types:** `id()`, `bigInteger()`, `integer()`, `smallInteger()`, `tinyInteger()`, `string()`, `text()`, `decimal()`, `float()`, `boolean()`, `date()`, `dateTime()`, `timestamp()`, `json()`, `enum()`.\n\n**Modifiers:** `-\u003eunsigned()`, `-\u003enullable()`, `-\u003edescription()`.\n\nFor JSON columns, `-\u003etemplate()` provides an example structure so the AI knows how to query with `JSON_EXTRACT`.\n\n#### Organizing Large Schemas\n\nFor projects with many tables, extract each definition into its own class:\n\n```php\n// app/LaraGrep/Tables/OrdersTable.php\nnamespace App\\LaraGrep\\Tables;\n\nuse LaraGrep\\Config\\Table;\nuse LaraGrep\\Config\\Column;\nuse LaraGrep\\Config\\Relationship;\n\nclass OrdersTable\n{\n    public static function define(): Table\n    {\n        return Table::make('orders')\n            -\u003edescription('Customer orders.')\n            -\u003ecolumns([\n                Column::id(),\n                Column::bigInteger('user_id')-\u003eunsigned(),\n                Column::decimal('total', 10, 2),\n                Column::timestamp('created_at'),\n            ])\n            -\u003erelationships([\n                Relationship::belongsTo('users', 'user_id'),\n            ]);\n    }\n}\n```\n\n```php\n// config/laragrep.php\n'tables' =\u003e [\n    \\App\\LaraGrep\\Tables\\UsersTable::define(),\n    \\App\\LaraGrep\\Tables\\OrdersTable::define(),\n    \\App\\LaraGrep\\Tables\\ProductsTable::define(),\n],\n```\n\n### Multi-Connection Tables\n\nWhen some tables live in a different database, use `-\u003econnection()` to tell LaraGrep which connection to use for queries on that table:\n\n```php\n'tables' =\u003e [\n    Table::make('users')\n        -\u003edescription('Registered users.')\n        -\u003ecolumns([\n            Column::id(),\n            Column::string('name'),\n            Column::string('email'),\n        ]),\n\n    Table::make('analytics_events')\n        -\u003edescription('Columnar analytics store.')\n        -\u003econnection('clickhouse', 'ClickHouse')\n        -\u003ecolumns([\n            Column::string('event_name'),\n            Column::timestamp('event_time'),\n            Column::bigInteger('user_id'),\n        ]),\n],\n```\n\nThe 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.\n\n```php\n// Connection only (same engine as the primary database)\n-\u003econnection('replica')\n\n// Connection + engine (different SQL dialect)\n-\u003econnection('clickhouse', 'ClickHouse')\n```\n\nWhen the AI encounters tables on different connections, it will:\n\n1. **Generate engine-compatible SQL** for each table\n2. **Include the connection name** in each query entry so the executor runs it on the right database\n3. **Avoid cross-connection JOINs** — instead, it queries each database separately and combines the results in the final answer\n\n### Multi-Tenant / Dynamic Connections\n\nIn 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:\n\n```php\n'contexts' =\u003e [\n    'default' =\u003e [\n        'connection' =\u003e fn () =\u003e 'tenant_' . tenant()-\u003eid,\n        'tables' =\u003e [\n            Table::make('users')-\u003ecolumns([...]),\n            Table::make('orders')-\u003ecolumns([...]),\n        ],\n    ],\n],\n```\n\nThe 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.\n\nYou can mix dynamic and static connections. For example, tenant tables on a dynamic connection and shared tables on a fixed central database:\n\n```php\n'contexts' =\u003e [\n    'default' =\u003e [\n        'connection' =\u003e fn () =\u003e app('tenant')-\u003egetConnectionName(),\n        'tables' =\u003e [\n            Table::make('orders')-\u003ecolumns([...]),\n\n            Table::make('plans')\n                -\u003econnection('central')\n                -\u003ecolumns([...]),\n        ],\n    ],\n],\n```\n\nTable-level connections also accept closures:\n\n```php\nTable::make('orders')-\u003econnection(fn () =\u003e 'tenant_' . tenant()-\u003eid)\n```\n\n### Named Scopes (Contexts)\n\nWork with multiple databases or table sets:\n\n```php\n'contexts' =\u003e [\n    'default' =\u003e [\n        'connection' =\u003e env('LARAGREP_CONNECTION'),\n        'tables' =\u003e [...],\n    ],\n    'analytics' =\u003e [\n        'connection' =\u003e 'analytics_db',\n        'schema_mode' =\u003e 'auto',\n        'database' =\u003e ['type' =\u003e 'MariaDB 10.6', 'name' =\u003e 'analytics'],\n        'exclude_tables' =\u003e ['migrations', 'jobs'],\n    ],\n],\n```\n\nSelect a scope via the URL: `POST /laragrep/analytics`\n\n### Query Protection\n\n```env\nLARAGREP_MAX_ROWS=20\nLARAGREP_MAX_QUERY_TIME=3\n```\n\n- **max_rows** — Automatically injects `LIMIT` into queries that don't have one. Default: `20`. Set to `0` to disable.\n- **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.\n\n### Agent Loop\n\n```env\nLARAGREP_MAX_ITERATIONS=10\n```\n\nSimple questions typically resolve in 1-2 iterations. Complex analytical questions may need more. Higher values increase capability but also cost.\n\n### Smart Schema\n\nFor large databases, LaraGrep can make an initial AI call to identify only the relevant tables, reducing token usage across all iterations.\n\n```env\nLARAGREP_SMART_SCHEMA=20\n```\n\nActivates automatically when the table count reaches the threshold. With 200 tables and only 5 relevant, this reduces token usage by ~60%.\n\n### Question Clarification\n\nWhen 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.\n\n**Enable:**\n\n```env\nLARAGREP_CLARIFICATION_ENABLED=true\n```\n\n**Define rules per context:**\n\n```php\n'contexts' =\u003e [\n    'default' =\u003e [\n        'clarification_rules' =\u003e [\n            'Always ask for a date range when the question involves time-based data',\n            'Always ask which store/branch if not specified',\n        ],\n        'tables' =\u003e [...],\n    ],\n],\n```\n\n**Flow:**\n\n```\nUser question → AI checks against rules → Missing context?\n  ├─ YES → Returns clarification questions (no agent loop runs)\n  └─ NO  → Proceeds to answerQuestion() normally\n```\n\n**Clarification response:**\n\n```json\n{\n    \"action\": \"clarification\",\n    \"questions\": [\"What date range?\", \"Which store?\"],\n    \"original_question\": \"Show me the sales\",\n    \"conversation_id\": \"uuid\"\n}\n```\n\nThe 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.\n\n**Answering clarification questions:**\n\n```bash\ncurl -X POST http://localhost/laragrep \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"question\": \"Show me the sales\",\n    \"conversation_id\": \"uuid-from-clarification-response\",\n    \"clarification_answers\": [\n      {\"question\": \"What date range?\", \"answer\": \"January 2026\"},\n      {\"question\": \"Which store?\", \"answer\": \"Store Centro\"}\n    ]\n  }'\n```\n\nThe 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.\n\nIf reformulation fails for any reason, the original question is used as fallback — the agent loop still runs.\n\n**Programmatic usage:**\n\n```php\n$laraGrep = app(LaraGrep::class);\n\n$clarification = $laraGrep-\u003eclarifyQuestion('Show me the sales', 'default');\n\nif ($clarification !== null) {\n    // Collect answers from the user, then reformulate\n    $reformulated = $laraGrep-\u003ereformulateQuestion(\n        'Show me the sales',\n        [\n            ['question' =\u003e 'What date range?', 'answer' =\u003e 'January 2026'],\n            ['question' =\u003e 'Which store?', 'answer' =\u003e 'Store Centro'],\n        ],\n        'default',\n    );\n\n    $answer = $laraGrep-\u003eanswerQuestion($reformulated);\n} else {\n    // Question is clear — proceed normally\n    $answer = $laraGrep-\u003eanswerQuestion('Show me the sales');\n}\n```\n\n**Token usage impact:**\n\nThe 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:\n\n| Call | Input (typical) | Output (typical) |\n|---|---|---|\n| Clarification | ~200-400 tokens | ~15-40 tokens |\n| Reformulation | ~150-300 tokens | ~15-30 tokens |\n| Smart Schema Filter | ~150-300 tokens | ~15-30 tokens |\n| Agent Loop (per iteration) | ~500-2000+ tokens | ~100-300 tokens |\n\nWhen 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\u0026A pairs (no schema), producing a plain text question.\n\nWith the feature disabled or no `clarification_rules` defined, zero API calls are made — `clarifyQuestion()` returns `null` immediately.\n\n### Conversation Persistence\n\nMulti-turn conversations are enabled by default. Previous questions and answers are sent as context for follow-ups.\n\n```env\nLARAGREP_CONVERSATION_ENABLED=true\nLARAGREP_CONVERSATION_CONNECTION=sqlite\nLARAGREP_CONVERSATION_MAX_MESSAGES=10\nLARAGREP_CONVERSATION_RETENTION_DAYS=10\n```\n\n### Route Protection\n\n```php\n'route' =\u003e [\n    'prefix' =\u003e 'laragrep',\n    'middleware' =\u003e ['auth:sanctum'],\n],\n```\n\n---\n\n## Usage\n\n### API Endpoint\n\n```\nPOST /laragrep/{scope?}\n```\n\n**Basic request:**\n\n```bash\ncurl -X POST http://localhost/laragrep \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\": \"How many users registered this week?\"}'\n```\n\n**With authentication and options:**\n\n```bash\ncurl -X POST http://localhost/laragrep \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_TOKEN\" \\\n  -d '{\n    \"question\": \"How many users registered this week?\",\n    \"conversation_id\": \"optional-uuid-for-follow-ups\",\n    \"debug\": true\n  }'\n```\n\n**Using a named scope:**\n\n```bash\ncurl -X POST http://localhost/laragrep/analytics \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"question\": \"What are the top 5 products by revenue?\"}'\n```\n\n**Debug response** (when `debug: true`):\n\n```json\n{\n    \"summary\": \"There were 42 new registrations this week.\",\n    \"conversation_id\": \"550e8400-e29b-41d4-a716-446655440000\",\n    \"steps\": [\n        {\n            \"query\": \"SELECT COUNT(*) as total FROM users WHERE created_at \u003e= ?\",\n            \"bindings\": [\"2025-01-20\"],\n            \"results\": [{\"total\": 42}],\n            \"reason\": \"Counting users registered in the current week\"\n        }\n    ],\n    \"debug\": {\n        \"queries\": [\n            {\"query\": \"SELECT COUNT(*) ...\", \"bindings\": [\"...\"], \"time\": 1.23}\n        ],\n        \"iterations\": 1\n    }\n}\n```\n\n### Programmatic Usage\n\n```php\nuse LaraGrep\\LaraGrep;\n\n$laraGrep = app(LaraGrep::class);\n\n$answer = $laraGrep-\u003eanswerQuestion(\n    question: 'How many orders were placed today?',\n    scope: 'default',\n);\n\necho $answer['summary'];\n```\n\n### Formatting Results\n\nUse `formatResult()` to transform raw query results into structured formats via AI.\n\n**Query format** — a single consolidated SQL query for export:\n\n```php\n$answer = $laraGrep-\u003eanswerQuestion('Weekly sales by region');\n\n$result = $laraGrep-\u003eformatResult($answer, 'query');\n// [\n//     'title' =\u003e 'Weekly Sales by Region',\n//     'headers' =\u003e ['Region', 'Total Sales', 'Order Count'],\n//     'query' =\u003e 'SELECT r.name as region, SUM(o.total) ... GROUP BY r.name',\n//     'bindings' =\u003e ['2026-02-01'],\n// ]\n```\n\nReturns the SQL itself, no `LIMIT`. Use it with Laravel's streaming tools:\n\n```php\n// Stream with cursor\nforeach (DB::cursor($result['query'], $result['bindings']) as $row) {\n    // process row\n}\n\n// Chunk for batch processing\nDB::table(DB::raw(\"({$result['query']}) as sub\"))\n    -\u003esetBindings($result['bindings'])\n    -\u003echunk(1000, function ($rows) {\n        // process chunk\n    });\n```\n\n**Notification format** — ready-to-render content for email, Slack, or webhooks:\n\n```php\n$notification = $laraGrep-\u003eformatResult($answer, 'notification');\n// [\n//     'title' =\u003e 'Weekly Sales Report',\n//     'html' =\u003e '\u003cp\u003eSales this week totaled...\u003c/p\u003e\u003ctable\u003e...\u003c/table\u003e',\n//     'text' =\u003e 'Sales this week totaled...\\nProduct | Revenue...',\n// ]\n```\n\n### Saved Queries (Recipes)\n\nAuto-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.\n\n**Enable:**\n\n```env\nLARAGREP_RECIPES_ENABLED=true\n```\n\nAfter enabling, publish and run the migration for the `laragrep_recipes` table.\n\n**API response with recipe:**\n\n```json\n{\n    \"summary\": \"Sales this week totaled...\",\n    \"conversation_id\": \"uuid\",\n    \"recipe_id\": 42\n}\n```\n\n**Dispatch a recipe:**\n\n```bash\ncurl -X POST http://localhost/laragrep/recipes/42/dispatch \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"format\": \"notification\", \"period\": \"now\"}'\n```\n\nThe `period` parameter controls timing:\n- `\"now\"` — immediate execution (default)\n- `\"2026-02-10 08:00:00\"` — scheduled for a specific date/time\n\nLaraGrep fires a `RecipeDispatched` event. Your app handles the rest via a listener:\n\n```php\n// app/Listeners/HandleRecipeDispatch.php\nuse LaraGrep\\Events\\RecipeDispatched;\n\npublic function handle(RecipeDispatched $event)\n{\n    $job = new ProcessRecipeJob($event-\u003erecipe, $event-\u003eformat, $event-\u003euserId);\n\n    if ($event-\u003eperiod === 'now') {\n        dispatch($job);\n    } else {\n        dispatch($job)-\u003edelay(Carbon::parse($event-\u003eperiod));\n    }\n}\n```\n\n```php\n// app/Jobs/ProcessRecipeJob.php\nuse LaraGrep\\LaraGrep;\n\npublic function handle(LaraGrep $laraGrep)\n{\n    $answer = $laraGrep-\u003ereplayRecipe($this-\u003erecipe);\n    $result = $laraGrep-\u003eformatResult($answer, $this-\u003eformat);\n\n    // Send email, generate Excel, post to Slack, etc.\n}\n```\n\n**Programmatic usage:**\n\n```php\nuse LaraGrep\\LaraGrep;\n\n$laraGrep = app(LaraGrep::class);\n\n// First run\n$answer = $laraGrep-\u003eanswerQuestion('Weekly sales by region');\n$recipe = $laraGrep-\u003eextractRecipe($answer, 'Weekly sales by region', 'default');\n\n// Later — replay with fresh data\n$freshAnswer = $laraGrep-\u003ereplayRecipe($recipe);\n$notification = $laraGrep-\u003eformatResult($freshAnswer, 'notification');\n```\n\n\u003e **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.\n\n---\n\n## Extending\n\n### Custom AI Client\n\nImplement `LaraGrep\\Contracts\\AiClientInterface` and rebind in a service provider:\n\n```php\n$this-\u003eapp-\u003esingleton(AiClientInterface::class, fn () =\u003e new MyCustomClient());\n```\n\n### Custom Metadata Loader\n\nLaraGrep auto-detects MySQL/MariaDB and PostgreSQL. For other databases, implement `LaraGrep\\Contracts\\MetadataLoaderInterface`:\n\n```php\n$this-\u003eapp-\u003esingleton(MetadataLoaderInterface::class, fn ($app) =\u003e new MySqliteSchemaLoader($app['db']));\n```\n\n### Custom Conversation Store\n\nImplement `LaraGrep\\Contracts\\ConversationStoreInterface` for Redis, file-based storage, etc.:\n\n```php\n$this-\u003eapp-\u003esingleton(ConversationStoreInterface::class, fn () =\u003e new RedisConversationStore());\n```\n\n---\n\n## Environment Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `LARAGREP_PROVIDER` | `openai` | AI provider (`openai`, `anthropic`) |\n| `LARAGREP_API_KEY` | — | API key for the AI provider |\n| `LARAGREP_MODEL` | `gpt-4o-mini` | Model identifier |\n| `LARAGREP_BASE_URL` | — | Override API endpoint URL |\n| `LARAGREP_MAX_TOKENS` | `1024` | Max response tokens |\n| `LARAGREP_TIMEOUT` | `300` | HTTP timeout in seconds |\n| `LARAGREP_FALLBACK_PROVIDER` | — | Fallback AI provider |\n| `LARAGREP_FALLBACK_API_KEY` | — | Fallback API key |\n| `LARAGREP_FALLBACK_MODEL` | — | Fallback model identifier |\n| `LARAGREP_FALLBACK_BASE_URL` | — | Fallback API endpoint URL |\n| `LARAGREP_MAX_ITERATIONS` | `10` | Max query iterations per question |\n| `LARAGREP_MAX_ROWS` | `20` | Max rows per query (auto LIMIT) |\n| `LARAGREP_MAX_QUERY_TIME` | `3` | Max query execution time (seconds) |\n| `LARAGREP_SMART_SCHEMA` | — | Table count threshold for smart filtering |\n| `LARAGREP_CLARIFICATION_ENABLED` | `false` | Enable pre-query question clarification |\n| `LARAGREP_SCHEMA_MODE` | `manual` | Schema loading mode |\n| `LARAGREP_USER_LANGUAGE` | `en` | AI response language |\n| `LARAGREP_RESPONSE_FORMAT` | `html` | Summary format: `html`, `markdown`, or `text` |\n| `LARAGREP_CONNECTION` | — | Database connection name |\n| `LARAGREP_DATABASE_TYPE` | — | DB type hint for AI |\n| `LARAGREP_DATABASE_NAME` | `DB_DATABASE` | DB name hint for AI |\n| `LARAGREP_EXCLUDE_TABLES` | — | Comma-separated tables to hide |\n| `LARAGREP_DEBUG` | `false` | Enable debug mode |\n| `LARAGREP_ROUTE_PREFIX` | `laragrep` | API route prefix |\n| `LARAGREP_CONVERSATION_ENABLED` | `true` | Enable conversation persistence |\n| `LARAGREP_CONVERSATION_CONNECTION` | `sqlite` | DB connection for conversations |\n| `LARAGREP_CONVERSATION_MAX_MESSAGES` | `10` | Max messages per conversation |\n| `LARAGREP_CONVERSATION_RETENTION_DAYS` | `10` | Auto-delete conversations after days |\n| `LARAGREP_MONITOR_ENABLED` | `false` | Enable monitoring dashboard |\n| `LARAGREP_MONITOR_CONNECTION` | `sqlite` | DB connection for monitor logs |\n| `LARAGREP_MONITOR_TABLE` | `laragrep_logs` | Table name for monitor logs |\n| `LARAGREP_MONITOR_RETENTION_DAYS` | `30` | Auto-delete logs after days |\n| `LARAGREP_RECIPES_ENABLED` | `false` | Enable recipe auto-save |\n| `LARAGREP_RECIPES_CONNECTION` | `sqlite` | DB connection for recipes |\n| `LARAGREP_RECIPES_TABLE` | `laragrep_recipes` | Table name for recipes |\n| `LARAGREP_RECIPES_RETENTION_DAYS` | `30` | Auto-delete recipes after days |\n| `LARAGREP_ASYNC_ENABLED` | `false` | Enable async mode |\n| `LARAGREP_ASYNC_CONNECTION` | `laragrep` | DB connection for async table |\n| `LARAGREP_ASYNC_TABLE` | `laragrep_async` | Table name for async records |\n| `LARAGREP_ASYNC_RETENTION_HOURS` | `24` | Auto-delete records after hours |\n| `LARAGREP_ASYNC_QUEUE` | `default` | Queue name for async jobs |\n| `LARAGREP_ASYNC_QUEUE_CONNECTION` | — | Queue connection (falls back to default) |\n| `LARAGREP_ASYNC_CHANNEL_PREFIX` | `laragrep` | Broadcasting channel prefix |\n| `LARAGREP_ASYNC_PRIVATE` | `false` | Use private broadcasting channels |\n\n---\n\n## Security\n\n- Only `SELECT` queries are generated and executed — mutations are rejected.\n- All queries use parameterized bindings to prevent SQL injection.\n- Table references are validated against the known schema metadata.\n- The agent loop is capped at `max_iterations` to prevent runaway costs.\n- Protect the endpoint with middleware (e.g., `auth:sanctum`).\n\n## Testing\n\n```bash\n./vendor/bin/phpunit\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbehindsolution%2Flaragrep","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbehindsolution%2Flaragrep","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbehindsolution%2Flaragrep/lists"}