Skip to main content

nedb-cast-slm

A 3.3M-parameter language model that turns a sentence into a database query — and can prove where it came from.

from cast import Cast

caster = Cast.from_pretrained("runs/v1")

caster.cast("top 5 stylists in winter park")
# FROM stylists WHERE city = "winter park" LIMIT 5

caster.cast("what caused these checkpoints")
# FROM checkpoints TRACE caused_by

No GPU. No API key. No network call. It trained from scratch on two CPU cores in about half an hour, and it fits in a file smaller than most JavaScript bundles.


Why this exists

Every "natural language to query" feature you've used is a round-trip to someone else's datacenter. You type a sentence, it goes to a frontier model, a query comes back, you pay per token, and you wait. For a query language with ten clauses and six operators, that is an absurd amount of machinery.

NQL — the NEDB query language — is small enough to learn completely. So we taught a small model to do it, and left the frontier models for problems that actually need them.

The interesting part is not the model. It's what the model is attached to.


The two-way attachment

nedb-cast-slm doesn't merely target NEDB. NEDB is on both ends of it.

Direction 1 — the engine writes its own training data

NEDB ships a parser: nedb/query.py. It takes NQL text and returns a plan dict.

That parser is doing three jobs here that would otherwise cost money and time:

It is the data generator. Sample a random plan, render it as NQL, render a human paraphrase. Perfect labels, infinite supply, zero annotation cost. We built 200,000 examples in 16.5 seconds.

It is the grader. We don't score string equality — we score whether the parsed plan matches. FROM orders WHERE total > 99 and from orders where total>99 are the same query, and the model gets full credit for both. Anything the engine can't parse fails deterministically.

It is the gate. Not one example enters the corpus unless it round-trips through the real parser back to a canonically identical plan. A generator bug crashes the build instead of quietly teaching the model invalid syntax.

Most text-to-DSL projects hand-write a verifier and then hope it's right. We didn't write one. It already shipped, and it's the same code the database uses in production.

Direction 2 — the model records its own lineage

Every artifact of training is a document in NEDB, chained by caused_by:

datasets  ──caused_by──▶  training_runs  ──caused_by──▶  checkpoints  ──caused_by──▶  evals

Which buys three things you cannot get from a folder of .pt files:

db.query("FROM evals TRACE caused_by")
# → the checkpoint, the run, and the exact dataset behind this score

TRACE caused_by — full lineage of any checkpoint. Which data trained it, which run produced it, which eval graded it. One query.

AS OF <seq> — replay the exact corpus any checkpoint was trained on. Reproducibility as a query instead of a spreadsheet.

verify() — the ledger is hash-chained and tamper-evident. Nobody edits an eval score after the fact.

The dataset id is a BLAKE2b hash over the generator source, the config, and the resulting pairs. Same seed and unchanged code produce the same id, so re-running writes nothing — verified across three separate processes. Change one line of the paraphraser and the id changes, because it is now a different dataset and should never be confused for the old one.

Models that show their work. That's the whole idea.


Results

Trained on 2 vCPU (Xeon @ 2.9GHz), 4GB RAM, no GPU. Metrics are plan-dict exact match against the real parser, on plans held out from training.

Parameters 3.34M
Vocabulary 581 tokens
Training 14,000 steps in 40.9 min on 2 vCPU
Throughput 7,081 tok/s (CPU)
Final eval loss 0.0099
Valid NQL rate 100.0% (eval) · 98.8% (holdout)
Exact plan match 92.3% (eval) · 63.1% (holdout)

Two numbers, both reported, because they measure different things. Eval uses phrasings drawn from the same distribution as training — that's the competence number. Holdout uses structures the model has never seen (inverted clause order, question framing, appositive asides) built from in-vocabulary words — that's the compositional-generalisation number, and it is deliberately adversarial.

Accuracy by clause, so a weak clause can't hide behind a good average:

clause eval holdout
TRACE ... REVERSE 97.7% 71.0%
TRACE 96.5% 68.1%
TRAVERSE 93.3% 61.4%
VALID AS OF 92.6% 58.4%
WHERE (single) 91.2% 56.6%
LIMIT 91.1% 60.2%
SEARCH 90.5% 58.5%
AS OF 88.3% 69.4%
ORDER BY 87.7% 51.1%
WHERE (multi-predicate) 85.1% 61.2%
GROUP BY 80.2% 52.9%
aggregate 77.0% 55.0%

What it gets wrong

Being specific, because "72.8%" without a failure mode is marketing:

Duplicated predicates on comma-appositive structures. The clearest holdout failure:

"list products, price cheaper than 182, aggregated by category with sum of price"
  pred: WHERE price < 182.0 AND price < 182.0 GROUP BY category SUM price   ← duplicated
  gold: WHERE price < 182.0 GROUP BY category SUM price

It gets the semantics right and emits the predicate twice — it reads the closing comma as a conjunction, because that construction appears only in holdout. This is a real compositional limit, not a measurement artifact.

Long digit runs. "blocks above height 400000"WHERE height > 4000. Numbers are tokenized digit-by-digit, so copying a 6-digit literal means six sequential correct predictions with no positional anchor.

Clause attachment under reordering. When a sort clause is interposed between the subject and its conditions, fields occasionally attach to the wrong clause.

It is honest about the boundary: this model interprets short prompts into a constrained grammar. It does not write code, and nothing at 3.3M parameters will.


Install

pip install nedb-cast-slm

Use

from cast import Cast
from nedb import NEDB

caster = Cast.from_pretrained("runs/v1")
db = NEDB("./mydb")

# NQL text
caster.cast("invoices that are overdue limit 10")
# 'FROM invoices WHERE status = "overdue" LIMIT 10'

# parsed plan — raises if the model emitted something invalid
caster.plan("memories with importance 5 grouped by category count")
# {'from': 'memories', 'where': [['importance','=',5]], 'group_by': 'category', ...}

# non-raising variant, for UIs that want to show the failure
caster.try_plan("...")
# {'ok': True, 'nql': '...', 'plan': {...}}

# cast and execute in one step
caster.run("paid orders over $99", db)

plan() raising on invalid output is intentional. A query planner that silently returns almost the right query is worse than one that admits it failed.

Train your own

python -m cast.cli generate --train 200000     # build + content-address the corpus
python -m cast.cli tokenizer                   # fit the vocab
python -m cast.cli train --steps 14000         # train (resumable)
python -m cast.cli eval --split holdout        # score with the real parser
python -m cast.cli lineage                     # TRACE the ledger

Every step writes to the NEDB ledger. train checkpoints continuously and resumes exactly where it stopped — built that way because this was developed in an ephemeral container that could be reclaimed mid-run.


How it's built

cast/grammar.py     six synthetic domains (shop, salon, chain, agent, crm, ops)
cast/sampler.py     random valid plan → canonical NQL
cast/paraphrase.py  plan → varied human phrasing
cast/dataset.py     corpus build, content addressing, NEDB ledger
cast/tokenizer.py   516-token word-level vocab, digit-split numbers
cast/model.py       ~3.3M param causal transformer (d=256, L=4, H=4, T=128)
cast/train.py       prompt-masked loss, bucketed batching, resumable
cast/evaluate.py    plan-equality grading, per-clause breakdown
cast/inference.py   the public Cast API

Six domains, not one. A model trained only on orders.total is useless in a studio where every user has their own collections. Training across six unrelated schemas forces it to learn query shape rather than memorize field names.

Prompt-masked loss. Loss is computed only on tokens after <sep>. Training on the prompt too would spend scarce capacity learning to model English, which is not the job.

Bucketed batching. Sequences average 39 tokens but max at 125. Padding everything to 128 wasted ~60% of compute; bucketing by length made training 2.4x faster than the original estimate.

516 tokens. A tiny vocab keeps the embedding table cheap, leaving the parameter budget for layers that actually reason. UNK rate is 0.0000%.


The Rust core is real

Inference also runs as a zero-dependency Rust crate (rust/nedb-cast-core) — no Python, no PyTorch. Verified in CI against the PyTorch reference on every push:

parity: 20 prompts, max |Δlogit| = 7.629e-6  (tol 1.0e-4)
decode parity: 20 / 20 exact
test result: ok. 5 passed (unit) · 4 passed (parity) · 1 passed (doctest)

Max logit deviation 7.6e-06, and all 20 decoded NQL strings byte-identical. That's what makes crates.io / npm / PyPI distribution legitimate rather than a Python shim in a trenchcoat. See rust/README.md.

Read next

  • docs/LORE.md — the bugs. A 55-point accuracy swing that was never the model's fault, and the "obvious" diagnosis that was dead wrong twice.
  • docs/ARCHITECTURE.md — why each design choice, and what we rejected.
  • docs/PROVENANCE.md — the ledger schema and the lineage queries in full.

Credits

Built by Interchained LLC on NEDB — the versioned, bi-temporal, causally-provable embedded database that makes the provenance half of this project possible.

Developed on HyperAgent. Interchained is a Founding 500 member of HyperAgent and received 20,000 HyperAgent inference credits.

Lightning strikes, thunder roars, code appears.

License

BUSL-1.1

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nedb_cast_slm-10.30.90.tar.gz (50.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nedb_cast_slm-10.30.90-py3-none-any.whl (47.9 kB view details)

Uploaded Python 3

File details

Details for the file nedb_cast_slm-10.30.90.tar.gz.

File metadata

  • Download URL: nedb_cast_slm-10.30.90.tar.gz
  • Upload date:
  • Size: 50.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.15

File hashes

Hashes for nedb_cast_slm-10.30.90.tar.gz
Algorithm Hash digest
SHA256 2aeb5273e65d899d95da5f6dddb063f25d7df7d67731ae6b0b0cea2fadf758bc
MD5 a84864e4128af963e1ef1ddb04b09176
BLAKE2b-256 256a013cbeb54f89fd4561a41dc75338257cea2bc176de49bc093a07aa3c6df7

See more details on using hashes here.

File details

Details for the file nedb_cast_slm-10.30.90-py3-none-any.whl.

File metadata

File hashes

Hashes for nedb_cast_slm-10.30.90-py3-none-any.whl
Algorithm Hash digest
SHA256 ab875781703322c0e3bfdb4e06b3ec92fb97fd7009cf58b67dfa14b632b403c7
MD5 6fe49c437bcb41682267a1651071693f
BLAKE2b-256 cd73ac95e369554f8645bcd7ddd4cea94abb720c9755af1a61eca18fe1af7d0a

See more details on using hashes here.

Release history Release notifications | RSS feed

10.30.92

2 files

10.30.91

2 files

This release

10.30.90 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page