jayanta / jeeves
A chat window for your own database. Users ask questions in plain English, Jeeves writes the SQL and runs it locally - the AI receives your schema structure only, never a row. Drop-in Blade widget, voice or text, any provider or a model you run yourself.
Requires
- php: ^8.2
- ext-json: *
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/validation: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^9.11|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.5|^12.0|^13.0
Suggests
- jayanta/laravel-ai-guard: Adds scoring-based prompt-injection, jailbreak and bot detection on top of the built-in input guard. Auto-detected - no wiring required. Needs a version exposing detectText(string); v2.0.0 does not, and jeeves:doctor will say so.
Provides
None
Conflicts
None
Replaces
None
README
Laravel Jeeves
A natural-language query box you can put inside your own Laravel app - for data you are not allowed to send anywhere.
Natural language to SQL (text-to-SQL, NL2SQL) as a drop-in chat window: your users ask in plain English, Jeeves writes the query, and your own server runs it. The model is sent your schema structure and nothing else.
{{-- One line. A chat thread, follow-up questions, voice input, charts. --}} <x-jeeves::widget />
Almost every "chat with your database" product works by sending rows to a model. If the data is under GDPR or DPDP, or belongs to a client who has not agreed to that, the conversation ends there.
This one sends the model your schema structure only - table names, column names, types, and the words your users use for them. It returns SQL. Your server validates that SQL, runs it locally, and formats the rows. Not one row is ever sent upstream, and that is enforced by tests, not by intent.
Three things follow that a hosted tool cannot offer:
- You can ship it to your own users. It is a Blade component inside your application, not a separate analytics seat they have to buy and log in to.
- The data-protection conversation is short. What leaves is schema structure and the question someone typed. Nothing else has a path out - see docs/SECURITY.md for how that is enforced and tested.
- It can run with nothing leaving your network at all. Point it at a model on your own hardware and even the schema stays in-house.
Any model, hosted or your own. Gemini, Claude, OpenAI, DeepSeek, Mistral, Groq, OpenRouter - or a model you run yourself on Ollama, vLLM, LM Studio or llama.cpp. One config block, no code changes.
Conversation state is real, and resolved in PHP rather than left to the model: ask "revenue by city", then "just Springfield", then "break that down by client", then rewind. Each turn knows what the last one narrowed to. People can ask by typing or by speaking, and the browser does the listening, so no audio reaches your server either.
Install
Requires PHP 8.2+ and Laravel 11, 12 or 13. Works on PostgreSQL, MySQL, MariaDB and SQLite.
On Laravel 11. It is supported and the suite runs against it, because most applications are still there. Be aware that Laravel 11 is past security support:
composer auditreports advisories that are fixed in 12.x and 13.x and in no 11.x release, because there will not be another one. That is the framework's status, not this package's, and it is worth knowing before you decide where to run something that sits next to your database.If Composer refuses to install. Recent Composer versions block advisory-affected packages during
requireandupdate, so on Laravel 11 you may see "not loaded, because they are affected by security advisories". That block comes from your own framework and applies to every package you install, not just this one. Composer reads the policy from your application'scomposer.json, so the decision is yours to make there:{ "config": { "policy": { "advisories": { "ignore": ["laravel/framework"] } } } }Upgrading to 12 or 13 is the better answer, and this package runs on both unchanged. The setting above is for when that has to wait.
composer require jayanta/jeeves php artisan jeeves:install php artisan migrate
Generated SQL needs its own database connection. It is written by a
language model, so it does not run on the connection your application writes
with — point sql.database_connection at a separate connection whose database
user holds SELECT only. php artisan jeeves:doctor checks it, and on
MySQL and PostgreSQL proves the user cannot write. The GRANT statements and
statement timeouts are in docs/CONNECTION.md.
Choose a model in .env. A model you run yourself is a first-class choice, not
a fallback:
# Local, no API key, nothing leaves your machine JEEVES_LLM_DRIVER=ollama OLLAMA_MODEL=llama3.3 # Or a hosted API JEEVES_LLM_DRIVER=gemini GEMINI_API_KEY=your-key-here
Built-in drivers: ollama, gemini, openai, claude. Any other
OpenAI-compatible service - DeepSeek, Groq, Mistral, OpenRouter, vLLM,
LM Studio, LocalAI - plugs in with a base_url and a model; see
docs/PROVIDERS.md.
Teach it your data
php artisan jeeves:discover --ai
This is the whole adaptation step. The package knows nothing about your
application: this reads your database and writes one plain PHP file per table
into config/jeeves-schemas/. Those files are the only thing that makes
it understand your domain - no code changes, no subclassing.
--ai also fills in the human layer that cannot be read from a database:
descriptions, the words your users actually say, business rules, and computed
metrics like averages. Worth doing - without it, a question like "average
amount" costs an extra API call to answer.
Then check it:
php artisan jeeves:doctor
It names the real cause of any problem and prints the exact fix. Run it first whenever something is wrong. It is read-only — it reports and changes nothing.
Everything the model is told is yours to change
discover writes a starting point, not a verdict. The files are plain PHP
config: edit them. Re-run it with --merge and your edits survive.
Nothing below needs a code change, a subclass, or a fork.
Per dataset — config/jeeves-schemas/*.php, where most tuning belongs:
| To change | Set |
|---|---|
| What a table is | description |
| What your users call it | aliases |
| What a column means | columns[].description |
| What your users call a column | columns[].aliases |
| What may be measured, grouped, filtered | aggregatable, groupable, filterable |
| Metrics that are not columns | computed_metrics with an expression |
| A rule every query must obey | required_filter |
| Business rules in plain English | llm_instructions |
| Worked examples to imitate | example_queries |
| Joins and related tables | required_join, relationships |
| Units on the numbers | unit |
Across the whole project — config/jeeves.php:
| To change | Set |
|---|---|
| The opening line of the system prompt | prompts.system_role |
| Context added to every prompt | system_instructions |
| Examples that apply to all datasets | global_examples |
| Which dataset a question routes to | query_routing |
| Row limits | sql.default_limit, sql.max_limit |
php artisan jeeves:audit-schema tells you which descriptions are missing —
and that curation is what moves accuracy, by the numbers further down this
page. Full reference: docs/SCHEMA.md.
When people type it differently
Your data spells a value one way and people type it another. Two keys in the schema file handle that, on any column - statuses, product names, countries, regions, whatever yours holds:
| Key | What happens | |
|---|---|---|
| Other names | 'value_aliases' => ['cancelled' => ['canceled', 'void']] |
A rename, spelling variant or synonym is swapped for the stored value before the query runs |
| Typos | 'correct_typos' => true |
When a filter finds nothing, the closest stored value - a different case, or an edit or two away - is tried once more |
Both are reported in the response metadata and scoped to the column and table
that declare them, and neither sends a stored value to the model. A misspelled
dataset name is covered separately, by JEEVES_FUZZY_DATASET_MATCH below.
→ docs/SCHEMA.md
Questions that must never vary. The number a report is signed off against
can be tied to reviewed SQL in pinned_queries. It runs with no model call and
gives the same answer every time, and it is still validated like any other
statement. It buys stability for those questions, not accuracy on the rest.
Ask a question
use Jayanta\Jeeves\Facades\Jeeves; $result = Jeeves::query('total revenue by region last month');
{
"status": "success",
"answer": "Revenue by region: West 2,028,763; East 1,878,404",
"speech_text": "Revenue by region. West, 2 million…", // phrased to be read aloud
"rows": [ { "region": "West", "revenue": "2028763.00" } ],
"parsed_summary": "Orders · revenue · by region · 2026-07-01 to 2026-07-31",
"parsed_query": {
"metric": "revenue", "group_by": "region",
"filters": [], "period": "2026-07-01 to 2026-07-31",
"date_from": "2026-07-01", "date_to": "2026-07-31"
}
}
Show users how the question was read. Every answer also carries
parsed_summary - the same information as one line, "Orders · revenue · by region · status is pending" - and the bundled widget puts it under each
answer. It is the difference between someone catching a misreading and
believing a number that answers a different question. Use parsed_query when
you want the structure instead.
Or over HTTP, which is what the widget uses:
POST /jeeves/text {"text": "top 5 customers by revenue"}
POST /jeeves/conversation {"session_id": "abc", "text": "only in West"}
Voice
The browser listens. Your server only ever receives text.
<x-jeeves::widget /> {{-- the microphone is already there --}}
There is nothing to configure and no audio endpoint. The widget uses the
browser's SpeechRecognition to turn speech into English text on the device,
then posts that text exactly as if it had been typed. Three things follow from
that one decision:
- It works with every model - Gemini, Claude, Ollama, anything - because by the time the model is involved it is reading a sentence, not hearing a recording.
- No audio leaves the device. Not to your server, not to a provider. There is no upload path in the package at all.
- Nothing extra to set up or pay for - no transcription service, no second API key, no added latency.
Answers carry a speech_text field phrased for reading aloud, and the widget
speaks it. Chrome, Edge and Safari support recognition; Firefox does not, so
the microphone is hidden there and people type - which is why text input is
never optional.
language picks which English accent to listen for - en-IN recognises
Indian English far more accurately than en-US does:
<x-jeeves::widget language="en-IN" />
English only, on purpose. Multilingual belongs to a separate package with a speech pipeline of its own; this one stays an English natural-language-to-SQL assistant. → docs/WIDGET.md
Who is allowed to ask
These endpoints spend your API key, so they are not open by default.
| Who gets in | |
|---|---|
A viewJeeves gate you define |
Whatever the gate says |
No gate, local or testing |
Everyone - so it works the moment you install it |
| No gate, anywhere else | Signed-in users only |
// AppServiceProvider::boot() Gate::define('viewJeeves', fn ($user) => $user->isAdmin());
Define the gate as soon as this is more than you: an ungated endpoint in production is an LLM proxy for the internet.
What it costs to run
Every question that reaches a model is a paid call, so the defaults are built to keep that number down rather than to look fast in a demo.
| Default | ||
|---|---|---|
| A repeated question | 0 calls | Cached for 24h (cache.ttl), keyed on the words and the dataset |
| A new question | 1 call | 2 if verification is on and it rewrites |
| Self-verification | on | ~200 tokens, a fraction of generation |
| Ceiling per user per day | 200 | limits.queries_per_day, HTTP 429 past it |
| Burst | 60/min | throttle:60,1 |
The daily ceiling is applied by the package rather than through
routes.middleware, so customising that array — the first thing anyone does to
make the widget public — cannot drop it by accident. Set it to null for no
ceiling, deliberately.
php artisan jeeves:cache-stats shows whether the cache is earning its keep.
Or spend nothing at all: point JEEVES_LLM_DRIVER at Ollama and the only
cost is your own hardware. → docs/CACHING.md
Misspelled dataset names (optional, off)
JEEVES_FUZZY_DATASET_MATCH=true
A question that misspells a dataset's name or alias by an edit or two is routed
there locally, with no call - "show invoces for march" reaches the dataset
aliased invoices. Words under five letters are never fuzzed (sales and
scale are one edit apart), and a misspelling two datasets match equally well
goes to the model instead of being guessed. Exact routing always wins, and this
runs before semantic matching below because an edit distance costs nothing.
Both stages apply to questions answered by SQL generation, where placing a question would otherwise cost a call of its own. A simple question on the intent route is placed by the model in the same call that reads the rest of it, so neither stage is consulted there - and neither has a call to save.
Matching by meaning, to skip the routing call (optional, off)
Routing matches words. Ask "how many houses were built" of a dataset
whose aliases are dwellings and sanctioned units and nothing matches, so the
question goes to the model just to be placed — a call before the call that
answers it.
Jeeves can ask an embedding service you run which dataset a question is closest to, and skip that first call when it is confident:
JEEVES_SEMANTIC_MATCH_ENABLED=true JEEVES_SEMANTIC_MATCH_ENDPOINT=http://127.0.0.1:8001 JEEVES_SEMANTIC_MATCH_THRESHOLD=0.3 JEEVES_SEMANTIC_MATCH_FALLBACK=llm # llm | clarification
No model ships with this package. Bundling a sentence-transformer would add
hundreds of megabytes for something most installs never switch on — the client
is about 10 KB and adds no dependency. You point it at a small service of your
own that answers POST /match-scheme with {"query": "..."} and returns
{"top_n": [{"dataset": "orders", "score": 0.42}, ...]}. scheme is accepted
in place of dataset, so a service already written against that spelling works
unchanged.
It describes your datasets, whatever they are. The service ranks a question against one short description per dataset, and those have to be yours — orders and tickets in one app, patients and claims in another. Generate them from your own schema files rather than writing them by hand:
php artisan jeeves:semantic-corpus # → storage/app/jeeves/semantic-corpus.json php artisan jeeves:semantic-corpus --stdout # or pipe it somewhere
Mount that file into your service and restart it — the corpus is embedded once at startup. Regenerate whenever a schema file changes, or the matcher keeps routing against a description of your data that is no longer true. It writes names, descriptions and the aliases your users type; it never opens a database connection, so no row can reach it.
It can only ever add a route, never remove one:
| An alias already matches | Never consulted — exact always wins |
| Confident match | Dataset settled, routing call skipped |
Below THRESHOLD |
Falls through to the model, as today |
| Service down or slow | Falls through to the model, as today |
| Names a dataset you do not have | Discarded |
| Feature off | No socket is opened |
The threshold is applied in PHP, not in the service, so retuning confidence
never means redeploying anything. Tune it against your own corpus, because
the scores depend on how much you have written: measured on MiniLM, correct
matches landed at 0.22–0.33 against terse scheme descriptions and 0.17–0.50
against a different set. 0.3 is a reasonable start, 0.5 is too strict for
short descriptions, and jeeves:semantic-corpus --stdout shows you exactly what
is being embedded.
Set FALLBACK=clarification to ask the user instead of guessing. It genuinely
gives something up: questions the model used to place on its own come back as a
prompt. llm is the default for that reason.
The request carries the question text and nothing else — the same text your LLM provider gets a line later if this stage declines. No rows, no values, not even the list of your dataset names, which is filtered on your own server.
Which tables it can see
Only the ones you have written a schema file for. The whitelist is derived from those files, not from your database, so a table with no schema file is not merely discouraged — the validator refuses any query naming it, and the model was never told it exists.
Three levers, in the order you will reach for them:
1. Delete or don't create the schema file. config/jeeves-schemas/ is the
whole list. Remove users.php and users becomes unqueryable, immediately.
2. Stop discovery from generating them at all.
// config/jeeves.php 'schema' => [ 'discover_exclude' => ['migrations', 'sessions', 'password_resets', 'audit_*'], 'discover_exclude_columns' => ['ssn', 'salary'], // columns, not tables ],
Framework tables are excluded already. Credential columns — password,
remember_token, two_factor_secret — are withheld by discovery whatever you
configure.
3. Grant only what it should read. The first two are the package's rules; this one is the database's, and it holds even if the package has a bug:
GRANT SELECT ON myapp.orders TO 'jeeves'@'%'; GRANT SELECT ON myapp.customers TO 'jeeves'@'%';
GRANT SELECT ON *.* works and defeats the point. See
docs/CONNECTION.md.
Already have schema files carrying something you would rather it did not see?
php artisan jeeves:audit-schema names them.
What it is good at, and what it is not
It works well on datasets you have described. Told that revenue is a
measure to total, that users say "client" for customer_name, and that
cancelled orders do not count, it is reliable for the questions those datasets
are meant to answer.
That last one is a rule, not a hint. A required_filter in a schema file is
enforced where the SQL is executed - a generated query that omits it is
refused rather than answered, on every route that reaches your database,
including a cached one replayed later. It is the one setting whose whole
purpose is that the answer is wrong without it, so it is not left to the model
to remember. → docs/SCHEMA.md
It is not magic. Pointed at an undescribed database and asked something vague, any text-to-SQL system will sometimes produce a confident, wrong answer.
Two benchmarks, both run against uncurated schemas, both reproducible:
| Benchmark | Score | What it is |
|---|---|---|
| Spider dev sample | 30-31/36 (83-86%) | Real questions and gold SQL from the Spider set, two unfamiliar databases |
| This package own set | 35/46 (76%) | 46 questions over a 14-table schema, up to four-table joins |
Three consecutive runs each, against Gemini 2.5 Flash, September 2026. The second is lower because it is harder: more joins, more periods, and questions like which orders have not shipped where a wrong join silently returns everything. Both numbers are published because quoting only the friendlier one is the kind of thing anybody can check in five minutes.
Read them as: roughly one question in four is wrong on an uncurated schema. Describing your schema moves it - measurably, and you can measure it yourself below - but it is a correction, not a cure.
The difficulty is not depth, which is the surprise. By join width, over the same three runs:
| Join width | Correct |
|---|---|
| 1 table | 16-17/20 |
| 2 tables | 13/16 |
| 3 tables | 2/6 |
| 4 tables | 3-4/4 |
Three-table questions failed two times in three, identically in all three runs, while four-table ones nearly all passed. So the honest warning is not "deep joins are shaky" — it is that a specific shape goes wrong, and the four-table questions here happen to run along paths the schema describes well. Six questions is a small sample; the consistency is what makes it worth stating.
Where it was weakest, from the runs that produced those numbers: superlatives, HAVING clauses, and anti-joins ("customers who have never ordered").
Most of that turned out to be a routing fault rather than a limit of the approach. Intent mode expresses a deliberate subset of SQL, and questions needing more are supposed to escalate to SQL generation — but the escalation rules had holes exactly where the benchmark said accuracy was worst:
- An anti-join needs
NOT EXISTS, which the contract cannot express at all, and no rule recognised the wording. The negation was dropped and every order came back for "which orders have not shipped". - "Highest" was not treated as "maximum". The contract names a metric and
nothing about what to do with it, so
SqlBuildersummed: "what is the highest unit price" answered 670, which is 120 + 250 + 300. The right answer is 300. - A group filter escalated as "more than 1 order" but not "more than one order".
- A schema whose
SUMtotal is aliased "revenue" — which is whatdiscover --aiwrites — disarmed the aggregate guard entirely, so "the highest revenue" was answered with the total.
Fixing those moved the package's own set from 30/46 to 32-33/46, and it now measures 35/46. What closed the last two or three is not established - later work in the same area, a server-side model change, or both - so it is reported as an observation rather than credited to a fix.
A range, not a number, because a live model is not deterministic. Four runs at the 32-33 level scored 33, 33, 33 and 32, and individual questions moved between them: "revenue by region in July 2026" passed three times and on the fourth was grouped by payment method instead of region. One run is not a measurement. Run it three times and read the spread.
The three runs behind the current figure were tighter - 35, 35, 35 on the package's own set and 30, 30, 31 on Spider - and ten of the eleven failures were the same questions every time. A stable failure is a defect with an address; a wandering one is the model. Both are worth knowing apart, and only repeated runs tell you which you have.
One genuine non-finding, worth stating because it looks like the opposite:
superlatives that ask for a row ("the car with the largest acceleration")
have gold SQL of plain GROUP BY / ORDER BY / LIMIT, which the contract
expresses perfectly well. Measured across all 164 questions in both sets,
escalating those moves five questions and buys nothing on any of them. Only the
scalar form ("what is the highest value") needs MAX(). Whatever makes
the row form fail is not routing, so it is deliberately left alone, and a test
pins that so the measurement has to be repeated before anyone changes it.
What remains genuinely hard is the three-table shape shown above - 2/6 in every run, while four-table questions nearly all pass. No amount of routing fixes that, because it is not a routing fault.
The honest framing is a fast analyst for datasets you have curated, not an
oracle for arbitrary databases. Every mitigation here follows from that: SQL is
SELECT-only and restricted to your tables, doctor catches schema drift, and
every answer states how it read the question.
You do not have to take that on trust for your own data. Two commands close the loop:
php artisan jeeves:audit-schema # what the model is still guessing php artisan jeeves:benchmark # how often it is right, on your schema
The audit names the things introspection cannot recover - a column nobody described, two columns that could both be "revenue", a table your users call something else. The benchmark runs your own questions against SQL you wrote by hand and compares the results. Curate what the audit found, run the benchmark again, and the difference is a number you produced rather than one this README asserts.
Provider conformance
Seventeen cases whose answers are arithmetic on three seeded rows - totals, filters, averages, periods, a decomposed comparison, and a conversation that narrows, drills down and rewinds:
| Model | Result |
|---|---|
| Gemini 2.5 Flash | 17/17 |
| Claude Sonnet 5 | 17/17 |
| DeepSeek v4 Flash | 17/17 |
| DeepSeek v4 Flash, via an OpenAI-compatible router | 17/17 |
gpt-oss-20b (open weights) |
17/17 |
| Mistral Large | 17/17 |
| Muse Glimmer 30B (open weights) | 15/17 |
| Nemotron 3 Super 120B (open weights) | 15/17 |
Capability does not track parameter count, and models fail differently. A 20B scored 17/17 including the multi-step decomposition ("compare July with August") that a 120B failed. Where models do miss, they miss in different places: one answers a scalar question with an unrequested breakdown - asked for the average it returns an average per client - while another handles single questions cleanly and cannot decompose a comparison into steps.
So there is no single number to rank models by for this job. The battery takes about a minute; run it against the model you actually intend to use rather than inferring from size or vendor.
Small local models are the one case to be careful with. Below roughly 20B, expect dropped filters and ignored date periods - asked for July, the whole table comes back, confidently. If you are running a model on your own hardware and a wrong number matters, measure it before you trust it.
Conversation state is the exception worth noting: narrowing, drill-down and rewind hold up even on small models, because they are resolved in PHP rather than left to the model.
The fourth row is the portability claim, measured: a service this package has
no built-in support for, reached with nothing but a base_url and a model
name.
JEEVES_CONFORMANCE=1 JEEVES_LLM_DRIVER=claude \ JEEVES_CONFORMANCE_KEY=sk-... vendor/bin/phpunit --testsuite Conformance
Run any battery more than once before believing it. On a free tier the first
pass often measures the rate limit rather than the model - add
JEEVES_CONFORMANCE_DELAY=15 to space the calls out.
How this differs from the alternatives
beyondcode/laravel-ask-database is the package most people find first, and the one this is most often compared to. It asked the same question in 2023, collected 302 stars, and was archived in February 2024. It calls OpenAI's GPT-3 specifically, needs an OpenAI key, and its own README describes it as a learning resource for prompt engineering rather than something to run.
If you arrived here after finding that one archived, this is the maintained answer to the same question - and a different shape of answer: any provider or a model on your own hardware, SELECT-only validation against a schema-derived whitelist, conversation state, and a privacy wall that is tested rather than promised.
prism-php/prism and
openai-php/laravel are
the layer below this one. They give you a clean, provider-agnostic way to
call a model from Laravel. They do not know what a dataset is, will not stop a
DROP TABLE, and have no opinion about whether a row reaches the provider.
Jeeves is a vertical built on that idea: schema introspection, a SQL
validator, a two-tier cache, conversation state, and a privacy wall. If you
want to call an LLM, use Prism. If you want to let people ask your database
questions, use this.
Hosted text-to-SQL - the analytics products with an "ask your data" box - send your schema and usually your rows to a third party, and price per seat. This runs in your application, sends schema structure only, and can run entirely offline against Ollama.
Writing it yourself. Entirely reasonable, and most of it is a weekend. The parts that are not: SELECT-only validation against a schema-derived whitelist, a cache that cannot answer one question with another question's result, rate limits reported as rate limits, and a benchmark that tells you how often you are wrong. Those took this package many adversarial review rounds and 812 tests, and every one of them exists because something went wrong first.
Documentation
| docs/SCHEMA.md | Schema files in full - metrics, aliases, joins, many tables |
| docs/API.md | Every endpoint, field and error code - plus events and token cost |
| docs/CONVERSATIONS.md | Follow-ups, drill-downs, rewind, multi-step answers |
| docs/PROVIDERS.md | Every LLM driver, and adding your own |
| docs/WIDGET.md | The bundled UI and browser voice input |
| docs/SECURITY.md | The privacy wall, SQL validation, prompt-injection guard |
| docs/CACHING.md | What is cached, when a row is reused, replacing the cache |
| docs/TROUBLESHOOTING.md | What each error means and how to fix it |
Commands
php artisan jeeves:install # publish config and migrations php artisan jeeves:discover # write schema files from your database php artisan jeeves:audit-schema # what the AI still has to guess - do this next php artisan jeeves:doctor # diagnose setup problems, print the fix php artisan jeeves:benchmark # how accurate is it on YOUR schema? php artisan jeeves:debug "…" # the exact prompt, and which route it takes php artisan jeeves:cache-stats # is the cache earning its keep? php artisan jeeves:cache-cleanup # prune it php artisan jeeves:semantic-corpus # describe your datasets for a matching service
discover → audit-schema → write the descriptions it asks for → benchmark
is the loop that moves accuracy. The audit says what the model is guessing; the
benchmark tells you what fixing that was worth, on your own data.
A benchmark question can also carry expect - a row count, a range the figure
must fall in, a name that must appear - instead of or as well as reference SQL.
That catches what comparing results cannot: a total over no rows is NULL, and
a reference that returns the same NULL would otherwise call it correct.
Contributing
vendor/bin/phpunit, vendor/bin/pint and vendor/bin/phpstan analyse must
pass, and the widget must pass node --check. New behaviour gets a test you
have watched fail; every failure a real user hits becomes a regression test.
See CONTRIBUTING.md, and SECURITY.md for
anything that should not be a public issue.
License
MIT. See LICENSE.
