sqljev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqljev/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """sqljev: ask your SQL rows questions in plain language, answered by Laya (open weights) or Jev."""
2
+ from .core import (BACKENDS, FUNCTIONS, Jev, JevError, __version__, default_engine, laya_question,
3
+ make_config, to_row_json)
4
+
5
+ __all__ = ["BACKENDS", "FUNCTIONS", "Jev", "JevError", "__version__", "default_engine", "laya_question",
6
+ "make_config", "to_row_json"]
sqljev/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
sqljev/aws_lambda.py ADDED
@@ -0,0 +1,16 @@
1
+ """AWS Lambda handler for Redshift Lambda UDFs (CREATE EXTERNAL FUNCTION ... LAMBDA 'sqljev').
2
+
3
+ Handler: sqljev.aws_lambda.handler. Configure with SQLJEV_* environment variables on the function, e.g.
4
+ SQLJEV_BACKEND=gateway + SQLJEV_API_URL=https://your-gateway/v1/eval, or SQLJEV_BACKEND=jev + TYPESAFE_API_KEY.
5
+ The engine (and its answer cache) lives as long as the Lambda container does.
6
+ """
7
+ import json
8
+
9
+ from .core import default_engine
10
+ from .gateway import handle_redshift
11
+
12
+
13
+ def handler(event, context=None):
14
+ if isinstance(event, (str, bytes)):
15
+ event = json.loads(event)
16
+ return json.dumps(handle_redshift(default_engine(), event))
sqljev/cli.py ADDED
@@ -0,0 +1,516 @@
1
+ """sqljev command line: plain-language questions over any SQLAlchemy database, the gateway, and the
2
+ fine-tuning loop (dataset -> fine-tune Laya -> eval).
3
+
4
+ sqljev query URL "SELECT * FROM tickets" --where "the customer is angry" --limit 20
5
+ sqljev query URL "SELECT * FROM tickets" --rank "the customer is angry" --limit 10
6
+ sqljev query URL "SELECT * FROM tickets" --choice "which team?" --options billing,technical,sales
7
+ sqljev materialize URL "SELECT id, body FROM tickets" --key id --prob "the customer is angry" --into t_angry
8
+ sqljev dataset URL "SELECT * FROM tickets" --label team --choice "which team?" -o tickets.jsonl
9
+ sqljev eval tickets.test.jsonl
10
+ sqljev gateway --port 8765
11
+ """
12
+ import argparse
13
+ import csv
14
+ import hashlib
15
+ import json
16
+ import sys
17
+ import time
18
+
19
+ from . import core
20
+ from .core import Jev, JevError, check_question, laya_question, to_row_json
21
+
22
+
23
+ # ---------------------------------------------------------------- database access (SQLAlchemy)
24
+
25
+ def _engine(url):
26
+ try:
27
+ import sqlalchemy
28
+ except ImportError:
29
+ raise JevError("sqljev: database access needs SQLAlchemy: pip install 'sqljev[db]'")
30
+ return sqlalchemy.create_engine(url)
31
+
32
+
33
+ def stream_rows(url, sql, chunk=1000):
34
+ """Rows as dicts, streamed with a server-side cursor where the driver supports it."""
35
+ import sqlalchemy
36
+ eng = _engine(url)
37
+ with eng.connect() as conn:
38
+ res = conn.execution_options(stream_results=True, yield_per=chunk).execute(sqlalchemy.text(sql))
39
+ for m in res.mappings():
40
+ yield dict(m)
41
+
42
+
43
+ def _model_view(columns, exclude=()):
44
+ """The part of a row the model sees: --columns selects, the label column is always excluded."""
45
+ cols = [c.strip() for c in columns.split(",")] if columns else None
46
+
47
+ def view(row):
48
+ return {k: v for k, v in row.items() if (cols is None or k in cols) and k not in exclude}
49
+ return view
50
+
51
+
52
+ # ---------------------------------------------------------------- question selection shared by commands
53
+
54
+ def _question(a, need=True):
55
+ picked = [(k, getattr(a, k)) for k in ("where", "rank", "prob", "choice", "score", "noul")
56
+ if getattr(a, k, None)]
57
+ if len(picked) != 1:
58
+ if not need:
59
+ return None
60
+ raise JevError("sqljev: give exactly one of --where / --rank / --prob / --choice / --score")
61
+ mode, text = picked[0]
62
+ kind = {"choice": "choice", "score": "score"}.get(mode, "noul")
63
+ opts = a.options if kind == "choice" else getattr(a, "levels", None) if kind == "score" else None
64
+ if kind == "choice" and opts is None and mode == "choice" and getattr(a, "label", None):
65
+ return mode, text, kind, None # dataset: options come from the label column
66
+ kind, opts = check_question(kind, opts)
67
+ return mode, text, kind, opts
68
+
69
+
70
+ def _settings(a):
71
+ return {k: getattr(a, k) for k in ("backend", "api_url", "model", "device", "batch_size", "concurrency",
72
+ "max_rows", "max_len") if getattr(a, k, None) is not None} | \
73
+ ({"drop_nulls": False} if getattr(a, "keep_nulls", False) else {})
74
+
75
+
76
+ def _value(kind, a, opts):
77
+ if a is None:
78
+ return None
79
+ if kind == "noul":
80
+ return a["noul"]
81
+ if kind == "choice":
82
+ return a["choice"]
83
+ return a["score"]
84
+
85
+
86
+ # ---------------------------------------------------------------- output
87
+
88
+ def _fmt(v, width=40):
89
+ if isinstance(v, float):
90
+ return "%.3f" % v
91
+ s = "" if v is None else str(v)
92
+ s = s.replace("\n", " ")
93
+ return s if len(s) <= width else s[:width - 1] + "…"
94
+
95
+
96
+ def write_rows(rows, fmt, out=None):
97
+ out = out or sys.stdout
98
+ rows = list(rows)
99
+ if not rows:
100
+ print("(0 rows)", file=sys.stderr)
101
+ return
102
+ cols = list(rows[0].keys())
103
+ if fmt == "jsonl":
104
+ for r in rows:
105
+ out.write(json.dumps(r, default=str, ensure_ascii=False) + "\n")
106
+ elif fmt == "csv":
107
+ w = csv.DictWriter(out, fieldnames=cols)
108
+ w.writeheader()
109
+ w.writerows(rows)
110
+ else:
111
+ cells = [[_fmt(r.get(c)) for c in cols] for r in rows]
112
+ widths = [max(len(c), *(len(row[i]) for row in cells)) for i, c in enumerate(cols)]
113
+ out.write(" | ".join(c.ljust(w) for c, w in zip(cols, widths)) + "\n")
114
+ out.write("-+-".join("-" * w for w in widths) + "\n")
115
+ for row in cells:
116
+ out.write(" | ".join(v.ljust(w) for v, w in zip(row, widths)) + "\n")
117
+ print("(%d row%s)" % (len(rows), "" if len(rows) == 1 else "s"), file=sys.stderr)
118
+
119
+
120
+ # ---------------------------------------------------------------- commands
121
+
122
+ def cmd_query(a):
123
+ mode, text, kind, opts = _question(a)
124
+ jev = Jev(**_settings(a))
125
+ view = _model_view(a.columns)
126
+ rows = stream_rows(a.url, a.sql)
127
+ col = {"where": "jev_prob", "rank": "jev_prob", "prob": "jev_prob", "choice": "jev_choice",
128
+ "score": "jev_score"}[mode]
129
+ t0 = time.time()
130
+ if mode == "where":
131
+ # Streaming: rows are judged in order with a bounded read-ahead, so --limit stops the scan early.
132
+ threshold = a.threshold if a.threshold is not None else jev.cfg["threshold"]
133
+
134
+ def passing():
135
+ n = 0
136
+ for row, ans in jev.evaluate_iter(rows, text, kind, opts, key=view):
137
+ p = ans["noul"]
138
+ if p >= threshold:
139
+ yield {**row, col: p}
140
+ n += 1
141
+ if a.limit and n >= a.limit:
142
+ return
143
+ write_rows(passing(), a.format)
144
+ else:
145
+ all_rows = list(rows)
146
+ answers = jev.evaluate([view(r) for r in all_rows], text, kind, opts)
147
+ out = [{**r, col: _value(kind, ans, opts)} for r, ans in zip(all_rows, answers)]
148
+ if mode == "rank" or (mode == "score" and a.sort):
149
+ out.sort(key=lambda r: -(r[col] or 0))
150
+ write_rows(out[:a.limit] if a.limit else out, a.format)
151
+ _report(jev, t0, a)
152
+
153
+
154
+ def cmd_materialize(a):
155
+ """Judge every row and write (key, value, answer) into a table, joinable in any database."""
156
+ import sqlalchemy as sa
157
+ mode, text, kind, opts = _question(a)
158
+ jev = Jev(**_settings(a))
159
+ view = _model_view(a.columns)
160
+ rows = list(stream_rows(a.url, a.sql))
161
+ if rows and a.key not in rows[0]:
162
+ raise JevError("sqljev: key column %r is not in the query result" % a.key)
163
+ t0 = time.time()
164
+ answers = jev.evaluate([view(r) for r in rows], text, kind, opts)
165
+ col = "jev_" + ("choice" if kind == "choice" else "score" if kind == "score" else "prob")
166
+ key_type = sa.BigInteger() if rows and isinstance(rows[0][a.key], int) else sa.String(255)
167
+ eng = _engine(a.url)
168
+ meta = sa.MetaData()
169
+ schema, _, name = a.into.rpartition(".")
170
+ table = sa.Table(name, meta, sa.Column(a.key, key_type, primary_key=True),
171
+ sa.Column(col, sa.String(255) if kind == "choice" else sa.Float()),
172
+ sa.Column("jev_answer", sa.Text()), schema=schema or None)
173
+ with eng.begin() as conn:
174
+ if a.replace:
175
+ table.drop(conn, checkfirst=True)
176
+ table.create(conn, checkfirst=True)
177
+ payload = [{a.key: r[a.key], col: _value(kind, ans, opts), "jev_answer": json.dumps(ans)}
178
+ for r, ans in zip(rows, answers)]
179
+ for i in range(0, len(payload), 1000):
180
+ conn.execute(table.insert(), payload[i:i + 1000])
181
+ print("sqljev: wrote %d rows to %s (%s)" % (len(rows), a.into, col), file=sys.stderr)
182
+ _report(jev, t0, a)
183
+
184
+
185
+ def _truthy(v):
186
+ if isinstance(v, bool):
187
+ return v
188
+ s = str(v).strip().lower()
189
+ if s in ("1", "true", "t", "yes", "y"):
190
+ return True
191
+ if s in ("0", "false", "f", "no", "n"):
192
+ return False
193
+ raise JevError("sqljev: label %r is not a boolean" % (v,))
194
+
195
+
196
+ def cmd_dataset(a):
197
+ """Labelled SQL rows -> Laya training/eval data, using exactly the state and question sqljev asks at
198
+ query time, so a checkpoint fine-tuned on it sees what it will be asked."""
199
+ mode, text, kind, opts = _question(a)
200
+ view = _model_view(a.columns, exclude={a.label})
201
+ rows = list(stream_rows(a.url, a.sql))
202
+ if not rows:
203
+ raise JevError("sqljev: the query returned no rows")
204
+ if a.label not in rows[0]:
205
+ raise JevError("sqljev: label column %r is not in the query result" % a.label)
206
+ if kind == "choice" and opts is None:
207
+ opts = sorted({str(r[a.label]) for r in rows if r[a.label] is not None})
208
+ kind, opts = check_question(kind, opts)
209
+ q = {"q": laya_question(kind, text, opts)}
210
+ base = a.output[:-6] if a.output.endswith(".jsonl") else a.output
211
+ outs = {"train": open(base + ".train.jsonl", "w"), "test": open(base + ".test.jsonl", "w")} \
212
+ if a.test_fraction else {"all": open(a.output, "w")}
213
+ counts = {k: 0 for k in outs}
214
+ skipped = 0
215
+ for r in rows:
216
+ lab = r[a.label]
217
+ if lab is None:
218
+ skipped += 1
219
+ continue
220
+ if kind == "noul":
221
+ expected = _truthy(lab)
222
+ gold = {"label": "true" if expected else "false",
223
+ "probabilities": {"true": float(expected), "false": float(not expected)}}
224
+ elif kind == "choice":
225
+ expected = str(lab)
226
+ if expected not in opts:
227
+ skipped += 1
228
+ continue
229
+ gold = {"label": expected, "probabilities": {o: float(o == expected) for o in opts}}
230
+ else:
231
+ idx = opts.index(str(lab)) if str(lab) in opts else int(lab)
232
+ expected = idx
233
+ gold = {"label": idx, "probabilities": {str(i): float(i == idx) for i in range(len(opts))}}
234
+ state = json.loads(to_row_json(view(r), not a.keep_nulls))
235
+ rid = hashlib.sha1(json.dumps(state, sort_keys=True, default=str).encode()).hexdigest()[:16]
236
+ split = "all"
237
+ if a.test_fraction:
238
+ split = "test" if int(rid[:8], 16) / 0xFFFFFFFF < a.test_fraction else "train"
239
+ if a.format == "typed-decisions":
240
+ rec = {"id": rid, "workflow": a.workflow, "state": json.dumps(state, ensure_ascii=False),
241
+ "questions": json.dumps(q), "gold": json.dumps({"q": gold})}
242
+ else:
243
+ rec = {"state": state, "questions": q, "expected": {"q": expected}, "tags": [a.workflow]}
244
+ outs[split].write(json.dumps(rec, ensure_ascii=False, default=str) + "\n")
245
+ counts[split] += 1
246
+ for f in outs.values():
247
+ f.close()
248
+ print("sqljev: %s%s" % (", ".join("%s %d" % kv for kv in counts.items()),
249
+ ", skipped %d (no/unknown label)" % skipped if skipped else ""), file=sys.stderr)
250
+
251
+
252
+ def cmd_eval(a):
253
+ """Accuracy of the configured backend / checkpoint on a dataset file (from `sqljev dataset`)."""
254
+ from .finetune import accuracy
255
+ s = _settings(a)
256
+ res = accuracy(a.file, **s)
257
+ print(json.dumps({"backend": s.get("backend", "local"), "model": s.get("model"), **res}))
258
+ if a.min_accuracy is not None and res["accuracy"] < a.min_accuracy:
259
+ sys.exit(1)
260
+
261
+
262
+ def cmd_finetune(a):
263
+ from .finetune import finetune
264
+ finetune(a.train, a.out, base=a.base, epochs=a.epochs, micro_batch=a.micro_batch, grad_accum=a.grad_accum,
265
+ train_layers=a.train_layers, device=a.device, seed=a.seed, log=lambda m: print(m, file=sys.stderr, flush=True))
266
+ print("sqljev: use it with --model %s (or SQLJEV_MODEL=%s)" % (a.out, a.out), file=sys.stderr)
267
+
268
+
269
+ def cmd_publish(a):
270
+ import os
271
+ from .finetune import publish
272
+ metrics = json.loads(a.metrics) if a.metrics else None
273
+ url = publish(a.dir, a.repo, token=os.environ.get("HF_TOKEN"), private=not a.public, metrics=metrics)
274
+ print("sqljev: published %s\nsqljev: every database can now use SQLJEV_MODEL=%s" % (url, a.repo), file=sys.stderr)
275
+
276
+
277
+ def cmd_gateway(a):
278
+ from .gateway import serve
279
+ serve(a.host, a.port, Jev(**_settings(a)), certfile=a.certfile, keyfile=a.keyfile)
280
+
281
+
282
+ def _quote(dialect, ident):
283
+ q = {"mssql": ("[", "]"), "mysql": ("`", "`"), "mariadb": ("`", "`")}.get(dialect, ('"', '"'))
284
+ return q[0] + ident.replace(q[1], q[1] * 2) + q[1]
285
+
286
+
287
+ def _lit(s):
288
+ return "'" + s.replace("'", "''") + "'"
289
+
290
+
291
+ class _Dialect:
292
+ """What `sqljev judge` needs from each database: resolve the source, a row -> JSON expression computed by
293
+ the database itself, and the jev.answers / jev_answers statements of sql/<db>/install.sql."""
294
+
295
+ def __init__(self, conn, source, columns):
296
+ import sqlalchemy as sa
297
+ self.sa, self.conn, self.name = sa, conn, conn.dialect.name
298
+ cols = [c.strip() for c in columns.split(",")] if columns else None
299
+ if self.name == "mssql":
300
+ self.table = conn.execute(sa.text(
301
+ "SELECT QUOTENAME(OBJECT_SCHEMA_NAME(OBJECT_ID(:s))) + N'.' + QUOTENAME(OBJECT_NAME(OBJECT_ID(:s)))"),
302
+ {"s": source}).scalar()
303
+ inner = ", ".join("t." + _quote(self.name, c) for c in cols) if cols else "t.*"
304
+ self.row = "(SELECT %s FOR JSON PATH, WITHOUT_ARRAY_WRAPPER)" % inner
305
+ self.hash, self.answers, self.qkey = "jev.row_hash(%s)", "jev.answers", "SELECT jev.question_key(:q, :k, :o)"
306
+ elif self.name == "postgresql":
307
+ self.table = conn.execute(sa.text("SELECT to_regclass(:s)::text"), {"s": source}).scalar()
308
+ self.row = ("jsonb_build_object(%s)" % ", ".join("%s, t.%s" % (_lit(c), _quote(self.name, c)) for c in cols)
309
+ if cols else "to_jsonb(t)")
310
+ self.hash, self.answers = "jev.row_hash(%s)", "jev.answers"
311
+ self.qkey = "SELECT jev.question_key(:q, :k, CAST(:o AS text[]))"
312
+ elif self.name in ("mysql", "mariadb"):
313
+ schema, _, tbl = source.rpartition(".")
314
+ found = conn.execute(sa.text(
315
+ "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = COALESCE(:sc, DATABASE()) "
316
+ "AND TABLE_NAME = :t ORDER BY ORDINAL_POSITION"), {"sc": schema or None, "t": tbl}).scalars().all()
317
+ self.table = ".".join(_quote(self.name, x) for x in ([schema] if schema else []) + [tbl]) if found else None
318
+ use = [c for c in found if c in cols] if cols else found
319
+ self.row = "JSON_OBJECT(%s)" % ", ".join("%s, t.%s" % (_lit(c), _quote(self.name, c)) for c in use)
320
+ self.hash, self.answers, self.qkey = "jev_row_hash(%s)", "jev_answers", "SELECT jev_question_key(:q, :k, :o)"
321
+ else:
322
+ raise JevError("sqljev: judge supports SQL Server, PostgreSQL, MySQL and MariaDB (got %s); for other "
323
+ "databases use `sqljev materialize`" % self.name)
324
+ if not self.table:
325
+ raise JevError("sqljev: %r is not a table or view" % source)
326
+
327
+ def question_key(self, text, kind, opts):
328
+ o = opts if self.name == "postgresql" else (json.dumps(opts) if opts else None)
329
+ return self.conn.execute(self.sa.text(self.qkey), {"q": text, "k": kind, "o": o}).scalar()
330
+
331
+ def pending(self, qkey, where):
332
+ """(row_hash, row_json) for rows with no answer yet, de-duplicated by content."""
333
+ sql = ("SELECT %s AS h, x.j AS j FROM (SELECT %s AS j FROM %s AS t%s) AS x WHERE NOT EXISTS "
334
+ "(SELECT 1 FROM %s AS a WHERE a.question_key = :qk AND a.row_hash = %s)"
335
+ % (self.hash % "x.j", self.row, self.table, " WHERE " + where if where else "", self.answers,
336
+ self.hash % "x.j"))
337
+ todo = {}
338
+ for h, j in self.conn.execute(self.sa.text(sql), {"qk": qkey}):
339
+ todo.setdefault(bytes(h), j if isinstance(j, str) else json.dumps(j))
340
+ return todo
341
+
342
+ def store(self, qkey, pairs):
343
+ sql = {"mssql": "INSERT jev.answers (question_key, row_hash, answer) VALUES (:qk, :h, :a)",
344
+ "postgresql": "INSERT INTO jev.answers (question_key, row_hash, answer) VALUES (:qk, :h, CAST(:a AS jsonb))"
345
+ " ON CONFLICT DO NOTHING"}.get(
346
+ self.name, "INSERT IGNORE INTO jev_answers (question_key, row_hash, answer) VALUES (:qk, :h, :a)")
347
+ rows = [{"qk": qkey, "h": h, "a": json.dumps(a)} for h, a in pairs]
348
+ for i in range(0, len(rows), 500):
349
+ self.conn.execute(self.sa.text(sql), rows[i:i + 500])
350
+
351
+ def usage(self, mode, text, opts):
352
+ fn = ("jev." if self.name in ("mssql", "postgresql") else "jev_") + \
353
+ {"choice": "choice", "score": "score"}.get(mode, "prob")
354
+ n = "N" if self.name == "mssql" else ""
355
+ args = [self.row, n + _lit(text)]
356
+ if opts:
357
+ args.append("ARRAY[%s]" % ", ".join(_lit(o) for o in opts) if self.name == "postgresql"
358
+ else n + _lit(json.dumps(opts)))
359
+ return "%s(%s)" % (fn, ", ".join(args))
360
+
361
+
362
+ def cmd_judge(a):
363
+ """Judge every row of a table that has no answer yet and store the answers in the database, where the
364
+ lookup functions of sql/<db>/install.sql read them. The database computes each row's JSON and hash itself,
365
+ so its functions find exactly these answers, and a row edited later is judged again on the next run."""
366
+ mode, text, kind, opts = _question(a)
367
+ jev = Jev(**_settings(a))
368
+ t0 = time.time()
369
+ with _engine(a.url).begin() as conn:
370
+ d = _Dialect(conn, a.source, a.columns)
371
+ qkey = d.question_key(text, kind, opts)
372
+ todo = d.pending(qkey, a.filter)
373
+ answers = jev.evaluate(list(todo.values()), text, kind, opts)
374
+ d.store(qkey, zip(todo, answers))
375
+ print("sqljev: judged %d new rows of %s" % (len(todo), d.table), file=sys.stderr)
376
+ print("sqljev: read them with %s" % d.usage(mode, text, opts), file=sys.stderr)
377
+ _report(jev, t0, a)
378
+
379
+
380
+ def _report(jev, t0, a):
381
+ if a.stats:
382
+ s = jev.stats()
383
+ s["seconds"] = round(time.time() - t0, 2)
384
+ print("sqljev: " + json.dumps(s), file=sys.stderr)
385
+
386
+
387
+ # ---------------------------------------------------------------- argument parsing
388
+
389
+ def _common(p):
390
+ g = p.add_argument_group("model")
391
+ g.add_argument("--backend", choices=core.BACKENDS, help="default: local (Laya in-process); env SQLJEV_BACKEND")
392
+ g.add_argument("--api-url", help="gateway / laya-serve / Jev endpoint")
393
+ g.add_argument("--model", help="Laya checkpoint (english, multilingual, typed-decisions), a fine-tuned "
394
+ "checkpoint directory or Hub id, or a Jev model id")
395
+ g.add_argument("--device", help="cpu, cuda, mps (local backend)")
396
+ g.add_argument("--batch-size", type=int, help="rows per forward pass / request")
397
+ g.add_argument("--concurrency", type=int)
398
+ g.add_argument("--max-len", type=int, help="Laya token budget per row (multilingual reads up to 8192)")
399
+ g.add_argument("--max-rows", type=int, help="spend guard: refuse to send more rows than this")
400
+ g.add_argument("--keep-nulls", action="store_true", help="send NULL columns to the model too")
401
+ g.add_argument("--stats", action="store_true", help="print engine stats to stderr")
402
+
403
+
404
+ def _questions(p, dataset=False):
405
+ g = p.add_argument_group("question (exactly one)")
406
+ if dataset:
407
+ g.add_argument("--noul", metavar="CONDITION", help="yes/no condition; label column holds true/false")
408
+ else:
409
+ g.add_argument("--where", metavar="CONDITION", help="keep rows that satisfy the condition (streams)")
410
+ g.add_argument("--rank", metavar="CONDITION", help="order rows by probability, most likely first")
411
+ g.add_argument("--prob", metavar="CONDITION", help="add jev_prob for every row")
412
+ g.add_argument("--choice", metavar="QUESTION", help="classify each row into one of --options")
413
+ g.add_argument("--score", metavar="QUESTION", help="rate each row along ordered --levels")
414
+ g.add_argument("--options", type=core.parse_options, help="comma list or JSON array")
415
+ g.add_argument("--levels", type=core.parse_options, help="comma list or JSON array, lowest first")
416
+ p.add_argument("--columns", help="comma list of columns the model sees (default: all)")
417
+
418
+
419
+ def main(argv=None):
420
+ p = argparse.ArgumentParser(prog="sqljev", description="Ask your SQL rows questions in plain language.")
421
+ p.add_argument("--version", action="version", version="sqljev " + core.__version__)
422
+ sub = p.add_subparsers(dest="cmd", required=True)
423
+
424
+ q = sub.add_parser("query", help="run a SELECT and filter / rank / classify its rows")
425
+ q.add_argument("url", help="SQLAlchemy URL, e.g. postgresql://..., mssql+pyodbc://..., snowflake://...")
426
+ q.add_argument("sql")
427
+ _questions(q)
428
+ q.add_argument("--threshold", type=float)
429
+ q.add_argument("--limit", type=int)
430
+ q.add_argument("--sort", action="store_true", help="with --score: highest first")
431
+ q.add_argument("--format", choices=("table", "csv", "jsonl"), default="table")
432
+ _common(q)
433
+ q.set_defaults(fn=cmd_query)
434
+
435
+ m = sub.add_parser("materialize", help="write judgments into a table, keyed by a column")
436
+ m.add_argument("url")
437
+ m.add_argument("sql")
438
+ m.add_argument("--key", required=True)
439
+ m.add_argument("--into", required=True, help="table name ([schema.]table)")
440
+ m.add_argument("--replace", action="store_true")
441
+ _questions(m)
442
+ _common(m)
443
+ m.set_defaults(fn=cmd_materialize)
444
+
445
+ d = sub.add_parser("dataset", help="labelled rows -> Laya fine-tuning / eval JSONL")
446
+ d.add_argument("url")
447
+ d.add_argument("sql")
448
+ d.add_argument("--label", required=True, help="column holding the ground truth (never shown to the model)")
449
+ d.add_argument("-o", "--output", required=True)
450
+ d.add_argument("--format", choices=("laya-evals", "typed-decisions"), default="laya-evals")
451
+ d.add_argument("--test-fraction", type=float, default=0.0, help="write OUTPUT.train/.test.jsonl")
452
+ d.add_argument("--workflow", default="sql")
453
+ d.add_argument("--keep-nulls", action="store_true")
454
+ _questions(d, dataset=True)
455
+ d.set_defaults(fn=cmd_dataset)
456
+
457
+ e = sub.add_parser("eval", help="accuracy of a backend / checkpoint on a dataset file")
458
+ e.add_argument("file")
459
+ e.add_argument("--min-accuracy", type=float)
460
+ _common(e)
461
+ e.set_defaults(fn=cmd_eval)
462
+
463
+ f = sub.add_parser("finetune", help="fine-tune Laya on a `sqljev dataset` file (one GPU)")
464
+ f.add_argument("train", help="training JSONL from `sqljev dataset`")
465
+ f.add_argument("--out", required=True, help="output checkpoint directory")
466
+ f.add_argument("--base", default="convaiinnovations/laya",
467
+ help="english (default), multilingual, typed-decisions, a Hub id or a local checkpoint")
468
+ f.add_argument("--epochs", type=int, default=3)
469
+ f.add_argument("--micro-batch", type=int, default=8)
470
+ f.add_argument("--grad-accum", type=int, default=8)
471
+ f.add_argument("--train-layers", type=int, help="low-memory mode: train only the top N encoder layers + head")
472
+ f.add_argument("--device")
473
+ f.add_argument("--seed", type=int, default=0)
474
+ f.set_defaults(fn=cmd_finetune)
475
+
476
+ pb = sub.add_parser("publish", help="upload a fine-tuned checkpoint to the Hugging Face Hub (HF_TOKEN)")
477
+ pb.add_argument("dir")
478
+ pb.add_argument("--repo", required=True, help="e.g. your-org/laya-tickets")
479
+ pb.add_argument("--public", action="store_true", help="default: private")
480
+ pb.add_argument("--metrics", help='JSON, e.g. \'{"base": 0.41, "finetuned": 0.93}\'')
481
+ pb.set_defaults(fn=cmd_publish)
482
+
483
+ g = sub.add_parser("gateway", help="serve the HTTP gateway for SQL Server, Snowflake, BigQuery, Redshift")
484
+ g.add_argument("--host", default="127.0.0.1")
485
+ g.add_argument("--port", type=int, default=8765)
486
+ g.add_argument("--certfile", help="serve HTTPS with this certificate (PEM)")
487
+ g.add_argument("--keyfile", help="private key for --certfile")
488
+ _common(g)
489
+ g.set_defaults(fn=cmd_gateway)
490
+
491
+ s = sub.add_parser("judge", help="judge a table into jev answers (SQL Server, PostgreSQL, MySQL, MariaDB)")
492
+ s.add_argument("url", help="mssql+pymssql://..., postgresql://..., mysql+pymysql://..., mariadb+pymysql://...")
493
+ s.add_argument("--source", required=True, help="table or view, e.g. dbo.tickets / public.tickets / tickets")
494
+ s.add_argument("--where", dest="filter", help="filter in the database's SQL, applied before judging")
495
+ s.add_argument("--columns", help="comma list of columns the model sees (default: all)")
496
+ g2 = s.add_argument_group("question (exactly one)")
497
+ g2.add_argument("--prob", metavar="CONDITION")
498
+ g2.add_argument("--choice", metavar="QUESTION")
499
+ g2.add_argument("--score", metavar="QUESTION")
500
+ g2.add_argument("--options", type=core.parse_options)
501
+ g2.add_argument("--levels", type=core.parse_options)
502
+ _common(s)
503
+ s.set_defaults(fn=cmd_judge)
504
+
505
+ a = p.parse_args(argv)
506
+ try:
507
+ a.fn(a)
508
+ except JevError as e:
509
+ print(str(e), file=sys.stderr)
510
+ sys.exit(2)
511
+ except BrokenPipeError:
512
+ pass
513
+
514
+
515
+ if __name__ == "__main__":
516
+ main()