laya-cli 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.
- laya_cli/__init__.py +3 -0
- laya_cli/cli.py +1332 -0
- laya_cli-0.1.0.dist-info/METADATA +182 -0
- laya_cli-0.1.0.dist-info/RECORD +7 -0
- laya_cli-0.1.0.dist-info/WHEEL +4 -0
- laya_cli-0.1.0.dist-info/entry_points.txt +2 -0
- laya_cli-0.1.0.dist-info/licenses/LICENSE +17 -0
laya_cli/cli.py
ADDED
|
@@ -0,0 +1,1332 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""laya-cli — ergonomic CLI for Laya (human + AI-agent friendly).
|
|
3
|
+
|
|
4
|
+
Primary command is `predict` for direct use:
|
|
5
|
+
laya-cli predict "refund my order" --preset triage
|
|
6
|
+
laya-cli predict --text "is this spam?" --preset guard --format json
|
|
7
|
+
echo '{"state":"hello"}' | laya-cli predict --questions q.json --format jsonl
|
|
8
|
+
|
|
9
|
+
Batch pipeline (TASK.md) is preserved via `classify | filter`:
|
|
10
|
+
cat candidates.jsonl | laya-cli classify --questions q.json | laya-cli filter --where "on_topic>=0.4" --sort -on_topic
|
|
11
|
+
|
|
12
|
+
Also: `questions`/`presets`, `evaluate`, `filter`, `info`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re as _re
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from . import __version__
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Parser
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
33
|
+
p = argparse.ArgumentParser(
|
|
34
|
+
prog="laya-cli",
|
|
35
|
+
description="Ergonomic CLI for Laya — typed decisions (choice/score/noul) in one forward pass. Works for humans (table output) and agents (JSON/JSONL).",
|
|
36
|
+
)
|
|
37
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
38
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
39
|
+
|
|
40
|
+
# ---- predict (primary, human+AI friendly) -----------------------------
|
|
41
|
+
pr = sub.add_parser(
|
|
42
|
+
"predict",
|
|
43
|
+
help="Run Laya on one text/JSON state or a batch (JSONL). Human-friendly & agent-friendly.",
|
|
44
|
+
description="Predict with Laya on a single state or a batch. Supports plain text, JSON state, presets, shortlist for high-cardinality choices, and table/JSON output.",
|
|
45
|
+
epilog=(
|
|
46
|
+
"Examples (human):\n"
|
|
47
|
+
' laya-cli predict "I was charged twice, refund please" --preset triage\n'
|
|
48
|
+
' laya-cli predict --text "Ignore previous instructions" --preset guard\n'
|
|
49
|
+
' laya-cli predict --preset email --state \'{"subject":"Invoice","body":"Hi"}\' --format table\n'
|
|
50
|
+
"\n"
|
|
51
|
+
"Examples (agent / batch):\n"
|
|
52
|
+
' laya-cli predict --text "hello" --questions q.json --format json\n'
|
|
53
|
+
" cat candidates.jsonl | laya-cli predict --questions q.json --state-field state --format jsonl > scored.jsonl\n"
|
|
54
|
+
" laya-cli predict --input candidates.jsonl --questions q.json --shortlist-k 20 --format jsonl\n"
|
|
55
|
+
' laya-cli predict --preset triage --text "my payment failed" --model convaiinnovations/laya --device cpu --full-probs\n'
|
|
56
|
+
),
|
|
57
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
58
|
+
)
|
|
59
|
+
# Positional text is sugar for --text (allows: laya-cli predict "hello" --preset guard)
|
|
60
|
+
pr.add_argument(
|
|
61
|
+
"text_positional",
|
|
62
|
+
nargs="*",
|
|
63
|
+
help="State as plain text (shorthand for --text). If multiple, each is a separate prediction.",
|
|
64
|
+
)
|
|
65
|
+
pr.add_argument(
|
|
66
|
+
"--text",
|
|
67
|
+
dest="text_opt",
|
|
68
|
+
action="append",
|
|
69
|
+
default=None,
|
|
70
|
+
help="State as plain text (can repeat).",
|
|
71
|
+
)
|
|
72
|
+
pr.add_argument(
|
|
73
|
+
"--state",
|
|
74
|
+
dest="state_json",
|
|
75
|
+
default=None,
|
|
76
|
+
help='State as JSON string (e.g. \'{"subject":"Hi","body":"..."}\')',
|
|
77
|
+
)
|
|
78
|
+
pr.add_argument("--state-file", dest="state_file", default=None, help="State from JSON file")
|
|
79
|
+
pr.add_argument(
|
|
80
|
+
"--input",
|
|
81
|
+
dest="input_path",
|
|
82
|
+
default=None,
|
|
83
|
+
help="Batch input: JSONL file (each line JSON) or - for stdin. Each object’s field --state-field is used as state.",
|
|
84
|
+
)
|
|
85
|
+
pr.add_argument(
|
|
86
|
+
"--state-field",
|
|
87
|
+
default="state",
|
|
88
|
+
help="For --input batch: field holding state text (default: state). Taken verbatim, no concatenation.",
|
|
89
|
+
)
|
|
90
|
+
pr.add_argument(
|
|
91
|
+
"--questions",
|
|
92
|
+
default=None,
|
|
93
|
+
help="Path to questions.json (dict of {id: {type, instructions, criteria}})",
|
|
94
|
+
)
|
|
95
|
+
pr.add_argument(
|
|
96
|
+
"--questions-inline",
|
|
97
|
+
dest="questions_inline",
|
|
98
|
+
default=None,
|
|
99
|
+
help="Questions as JSON string (merges with --questions/--preset)",
|
|
100
|
+
)
|
|
101
|
+
pr.add_argument(
|
|
102
|
+
"--preset",
|
|
103
|
+
default=None,
|
|
104
|
+
help="Built-in preset: triage | email | guard | moderation | router (can combine with --questions)",
|
|
105
|
+
)
|
|
106
|
+
pr.add_argument(
|
|
107
|
+
"--model",
|
|
108
|
+
default="convaiinnovations/laya",
|
|
109
|
+
help="HF repo id (default: convaiinnovations/laya)",
|
|
110
|
+
)
|
|
111
|
+
pr.add_argument(
|
|
112
|
+
"--subfolder",
|
|
113
|
+
default=None,
|
|
114
|
+
help="Subfolder checkpoint: multilingual / typed-decisions / '' (bundle root). Only that subfolder is downloaded.",
|
|
115
|
+
)
|
|
116
|
+
pr.add_argument(
|
|
117
|
+
"--device",
|
|
118
|
+
default=None,
|
|
119
|
+
choices=["cpu", "mps", "cuda"],
|
|
120
|
+
help="Device to force (default: auto: cuda > mps > cpu)",
|
|
121
|
+
)
|
|
122
|
+
pr.add_argument(
|
|
123
|
+
"--router",
|
|
124
|
+
action="store_true",
|
|
125
|
+
help="Use laya.Router(preload=True) — recommended for multilingual (auto-detects script/language per request).",
|
|
126
|
+
)
|
|
127
|
+
pr.add_argument(
|
|
128
|
+
"--lang",
|
|
129
|
+
default=None,
|
|
130
|
+
help="Force language for Router (e.g. en, fr, de). Without --lang Router only recognises {en,fr,de,es,pt,it,nl} among Latin scripts.",
|
|
131
|
+
)
|
|
132
|
+
pr.add_argument(
|
|
133
|
+
"--shortlist-k",
|
|
134
|
+
dest="shortlist_k",
|
|
135
|
+
type=int,
|
|
136
|
+
default=None,
|
|
137
|
+
help="Enable shortlist for high-cardinality choice: keep top K labels via embeddings (uses agent encoder via embed_fn_from_agent).",
|
|
138
|
+
)
|
|
139
|
+
pr.add_argument(
|
|
140
|
+
"--full-probs",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Include full probability distributions in flattened output",
|
|
143
|
+
)
|
|
144
|
+
pr.add_argument(
|
|
145
|
+
"--format",
|
|
146
|
+
dest="out_format",
|
|
147
|
+
choices=["json", "jsonl", "table"],
|
|
148
|
+
default=None,
|
|
149
|
+
help="Output format: json (single object), jsonl (one per line), table (human). Default: json for single, jsonl for batch.",
|
|
150
|
+
)
|
|
151
|
+
pr.add_argument("--pretty", action="store_true", help="Pretty-print JSON")
|
|
152
|
+
pr.add_argument(
|
|
153
|
+
"--flatten",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="For batch JSONL: flatten answers into top-level fields like classify does ({id}, {id}_p, {id}_confidence) instead of nested answers.",
|
|
156
|
+
)
|
|
157
|
+
pr.add_argument(
|
|
158
|
+
"--prepend-field",
|
|
159
|
+
default=None,
|
|
160
|
+
help="For batch: prepend field to state text as '<value> <state>'. Disabled by default (mixing metadata degraded on_topic scores 0.1-0.3).",
|
|
161
|
+
)
|
|
162
|
+
pr.add_argument("--output", default=None, help="Output file (default: stdout)")
|
|
163
|
+
|
|
164
|
+
# ---- classify (legacy batch, kept for TASK.md compat) -----------------
|
|
165
|
+
c = sub.add_parser(
|
|
166
|
+
"classify",
|
|
167
|
+
help="Batch classify JSONL from stdin via Laya (legacy, use predict for new code).",
|
|
168
|
+
description="Classify JSONL from stdin via Laya; append flattened answers. Kept for pipeline compatibility (px | laya-cli classify). For new code prefer `predict`.",
|
|
169
|
+
epilog=(
|
|
170
|
+
"Examples:\n"
|
|
171
|
+
" cat candidates.jsonl | laya-cli classify --questions questions.json > scored.jsonl\n"
|
|
172
|
+
' px videos --queries "..." --state --dedupe keep-first \\\n'
|
|
173
|
+
" | laya-cli classify --questions questions.json \\\n"
|
|
174
|
+
' | laya-cli filter --where "on_topic>=0.4" --sort -on_topic > shortlist.jsonl\n'
|
|
175
|
+
),
|
|
176
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
177
|
+
)
|
|
178
|
+
c.add_argument("--questions", required=True, help="Path to questions.json")
|
|
179
|
+
c.add_argument(
|
|
180
|
+
"--state-field",
|
|
181
|
+
default="state",
|
|
182
|
+
help="Field name holding the state text (default: state). Taken verbatim.",
|
|
183
|
+
)
|
|
184
|
+
c.add_argument(
|
|
185
|
+
"--model",
|
|
186
|
+
default="convaiinnovations/laya",
|
|
187
|
+
help="HF repo id (default: convaiinnovations/laya)",
|
|
188
|
+
)
|
|
189
|
+
c.add_argument(
|
|
190
|
+
"--subfolder",
|
|
191
|
+
default=None,
|
|
192
|
+
help="Subfolder checkpoint: multilingual / typed-decisions / ''",
|
|
193
|
+
)
|
|
194
|
+
c.add_argument("--device", default=None, choices=["cpu", "mps", "cuda"], help="Device to force")
|
|
195
|
+
c.add_argument("--router", action="store_true", help="Use laya.Router(preload=True)")
|
|
196
|
+
c.add_argument("--lang", default=None, help="Force language for Router")
|
|
197
|
+
c.add_argument("--prepend-field", default=None, help="Optional field to prepend to state text")
|
|
198
|
+
c.add_argument("--full-probs", action="store_true", help="Include full distribution ({id}_probs)")
|
|
199
|
+
|
|
200
|
+
# ---- filter -----------------------------------------------------------
|
|
201
|
+
f = sub.add_parser(
|
|
202
|
+
"filter",
|
|
203
|
+
help="Lightweight post-filter/sorter over JSONL.",
|
|
204
|
+
description="Filter and sort JSONL from stdin. Replaces per-case build_shortlist*.py.",
|
|
205
|
+
epilog=(
|
|
206
|
+
"Examples:\n"
|
|
207
|
+
' cat scored.jsonl | laya-cli filter --where "on_topic>=0.4" --sort -on_topic\n'
|
|
208
|
+
' cat scored.jsonl | laya-cli filter --where "on_topic>=0.4,is_spam!=1" --sort -on_topic,+id\n'
|
|
209
|
+
),
|
|
210
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
211
|
+
)
|
|
212
|
+
f.add_argument(
|
|
213
|
+
"--where",
|
|
214
|
+
default=None,
|
|
215
|
+
help='Filter expression(s), comma = AND. Ops: == != = > < >= <=. Example: "on_topic>=0.4"',
|
|
216
|
+
)
|
|
217
|
+
f.add_argument(
|
|
218
|
+
"--sort",
|
|
219
|
+
default=None,
|
|
220
|
+
help='Sort keys, comma-separated, "-" prefix = descending. Example: "-on_topic,+id"',
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# ---- questions / presets ----------------------------------------------
|
|
224
|
+
q = sub.add_parser(
|
|
225
|
+
"questions",
|
|
226
|
+
aliases=["presets"],
|
|
227
|
+
help="Show or list built-in preset questions.json.",
|
|
228
|
+
description="Presets are direct passthroughs of laya.*_questions() — triage, email, guard, moderation, router.",
|
|
229
|
+
epilog=(
|
|
230
|
+
"Examples:\n"
|
|
231
|
+
" laya-cli questions list\n"
|
|
232
|
+
" laya-cli questions triage > questions.json\n"
|
|
233
|
+
' laya-cli predict --preset triage --text "my payment failed" # use without file\n'
|
|
234
|
+
" laya-cli questions guard | laya-cli predict --input /dev/stdin --state-field prompt # pipe\n"
|
|
235
|
+
),
|
|
236
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
237
|
+
)
|
|
238
|
+
q.add_argument(
|
|
239
|
+
"preset",
|
|
240
|
+
nargs="?",
|
|
241
|
+
default=None,
|
|
242
|
+
help="Preset name: triage | email | guard | moderation | router | list",
|
|
243
|
+
)
|
|
244
|
+
q.add_argument(
|
|
245
|
+
"--format",
|
|
246
|
+
dest="q_format",
|
|
247
|
+
choices=["json", "table"],
|
|
248
|
+
default="json",
|
|
249
|
+
help="Output format (default: json)",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# ---- evaluate ---------------------------------------------------------
|
|
253
|
+
e = sub.add_parser(
|
|
254
|
+
"evaluate",
|
|
255
|
+
help="Evaluate on a labelled JSONL set and print accuracy / threshold report.",
|
|
256
|
+
description="Evaluate before shipping: run classify on a labelled set and report accuracy per question, pass rate at threshold, and precision @ threshold.",
|
|
257
|
+
epilog=(
|
|
258
|
+
"Examples:\n"
|
|
259
|
+
" laya-cli evaluate --questions questions.json --labeled labeled.jsonl --label-field label --threshold 0.5\n"
|
|
260
|
+
" laya-cli evaluate --questions q.json --labeled dev.jsonl --field on_topic --state-field state\n"
|
|
261
|
+
),
|
|
262
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
263
|
+
)
|
|
264
|
+
e.add_argument("--questions", required=True, help="Path to questions.json")
|
|
265
|
+
e.add_argument(
|
|
266
|
+
"--labeled",
|
|
267
|
+
required=True,
|
|
268
|
+
help="Path to labelled JSONL (each line: {state_field: text, label_field: true_label})",
|
|
269
|
+
)
|
|
270
|
+
e.add_argument("--state-field", default="state", help="Field holding the state text (default: state)")
|
|
271
|
+
e.add_argument(
|
|
272
|
+
"--label-field",
|
|
273
|
+
default="label",
|
|
274
|
+
help="Field holding the ground-truth label (default: label)",
|
|
275
|
+
)
|
|
276
|
+
e.add_argument("--field", default=None, help="Optional single question id to evaluate (default: all)")
|
|
277
|
+
e.add_argument("--threshold", type=float, default=0.5, help="Threshold for pass/precision (default: 0.5)")
|
|
278
|
+
e.add_argument("--model", default="convaiinnovations/laya", help="HF repo id")
|
|
279
|
+
e.add_argument("--subfolder", default=None, help="Subfolder checkpoint")
|
|
280
|
+
e.add_argument("--device", default=None, choices=["cpu", "mps", "cuda"], help="Device to force")
|
|
281
|
+
e.add_argument("--router", action="store_true", help="Use laya.Router(preload=True)")
|
|
282
|
+
e.add_argument("--lang", default=None, help="Force language for Router")
|
|
283
|
+
e.add_argument("--prepend-field", default=None, help="Optional field to prepend to state text")
|
|
284
|
+
|
|
285
|
+
# ---- info -------------------------------------------------------------
|
|
286
|
+
inf = sub.add_parser(
|
|
287
|
+
"info",
|
|
288
|
+
help="Show model, device and checkpoint info.",
|
|
289
|
+
description="Show environment, available devices, and (if cached) checkpoint config.",
|
|
290
|
+
)
|
|
291
|
+
inf.add_argument("--model", default="convaiinnovations/laya", help="HF repo id to inspect")
|
|
292
|
+
inf.add_argument("--subfolder", default=None, help="Subfolder to inspect")
|
|
293
|
+
|
|
294
|
+
return p
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# Shared helpers
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _load_questions(path: str) -> dict[str, Any]:
|
|
303
|
+
with open(path, encoding="utf-8") as f:
|
|
304
|
+
data = json.load(f)
|
|
305
|
+
if not isinstance(data, dict) or not data:
|
|
306
|
+
print(f"error: questions file {path!r} must be a non-empty dict", file=sys.stderr)
|
|
307
|
+
sys.exit(2)
|
|
308
|
+
for qid, qdef in data.items():
|
|
309
|
+
if not isinstance(qdef, dict) or "type" not in qdef:
|
|
310
|
+
print(f"error: question {qid!r} must have a 'type' field", file=sys.stderr)
|
|
311
|
+
sys.exit(2)
|
|
312
|
+
if qdef["type"] not in ("choice", "score", "noul"):
|
|
313
|
+
print(f"error: question {qid!r} has unknown type {qdef['type']!r}", file=sys.stderr)
|
|
314
|
+
sys.exit(2)
|
|
315
|
+
return data
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _load_preset(name: str) -> dict[str, Any]:
|
|
319
|
+
mapping = {
|
|
320
|
+
"triage": "triage_questions",
|
|
321
|
+
"email": "email_questions",
|
|
322
|
+
"guard": "guard_questions",
|
|
323
|
+
"moderation": "moderation_questions",
|
|
324
|
+
"router": "router_questions",
|
|
325
|
+
}
|
|
326
|
+
key = name.strip().lower()
|
|
327
|
+
if key not in mapping:
|
|
328
|
+
print(
|
|
329
|
+
f"error: unknown preset {name!r}; choose from {', '.join(sorted(mapping))}",
|
|
330
|
+
file=sys.stderr,
|
|
331
|
+
)
|
|
332
|
+
sys.exit(2)
|
|
333
|
+
try:
|
|
334
|
+
import laya
|
|
335
|
+
except ImportError as exc:
|
|
336
|
+
print(f"error: laya not installed: {exc}", file=sys.stderr)
|
|
337
|
+
sys.exit(2)
|
|
338
|
+
fn = getattr(laya, mapping[key])
|
|
339
|
+
return fn()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _load_questions_merged(args) -> dict[str, Any]:
|
|
343
|
+
"""Merge --preset, --questions, --questions-inline (preset < file < inline)."""
|
|
344
|
+
merged: dict[str, Any] = {}
|
|
345
|
+
if getattr(args, "preset", None):
|
|
346
|
+
merged.update(_load_preset(args.preset))
|
|
347
|
+
if getattr(args, "questions", None):
|
|
348
|
+
file_q = _load_questions(args.questions)
|
|
349
|
+
merged.update(file_q)
|
|
350
|
+
inline = getattr(args, "questions_inline", None)
|
|
351
|
+
if inline:
|
|
352
|
+
try:
|
|
353
|
+
inline_q = json.loads(inline)
|
|
354
|
+
except json.JSONDecodeError as exc:
|
|
355
|
+
print(f"error: invalid --questions-inline JSON: {exc}", file=sys.stderr)
|
|
356
|
+
sys.exit(2)
|
|
357
|
+
if not isinstance(inline_q, dict):
|
|
358
|
+
print("error: --questions-inline must be a JSON object (dict)", file=sys.stderr)
|
|
359
|
+
sys.exit(2)
|
|
360
|
+
merged.update(inline_q)
|
|
361
|
+
if not merged:
|
|
362
|
+
print(
|
|
363
|
+
"error: no questions provided. Use --preset or --questions or --questions-inline",
|
|
364
|
+
file=sys.stderr,
|
|
365
|
+
)
|
|
366
|
+
sys.exit(2)
|
|
367
|
+
# validate merged
|
|
368
|
+
for qid, qdef in merged.items():
|
|
369
|
+
if not isinstance(qdef, dict) or "type" not in qdef:
|
|
370
|
+
print(f"error: question {qid!r} must have a 'type' field", file=sys.stderr)
|
|
371
|
+
sys.exit(2)
|
|
372
|
+
if qdef["type"] not in ("choice", "score", "noul"):
|
|
373
|
+
print(f"error: question {qid!r} has unknown type {qdef['type']!r}", file=sys.stderr)
|
|
374
|
+
sys.exit(2)
|
|
375
|
+
return merged
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _init_agent(args) -> Any:
|
|
379
|
+
"""Load Laya agent or Router exactly once; warm up; emit timing to stderr."""
|
|
380
|
+
t0 = time.time()
|
|
381
|
+
if getattr(args, "router", False):
|
|
382
|
+
if not getattr(args, "lang", None):
|
|
383
|
+
print(
|
|
384
|
+
"[laya-cli] warning: --router is active without --lang. "
|
|
385
|
+
"As of laya 0.3.4 the Router detector only recognises {en,fr,de,es,pt,it,nl} "
|
|
386
|
+
"among Latin-script languages; Polish, Czech, Turkish, Swedish etc. are "
|
|
387
|
+
"silently routed to the English checkpoint and may answer confidently and wrongly. "
|
|
388
|
+
"Pass --lang when you know it.",
|
|
389
|
+
file=sys.stderr,
|
|
390
|
+
)
|
|
391
|
+
try:
|
|
392
|
+
from laya import Router
|
|
393
|
+
except ImportError as exc:
|
|
394
|
+
print(f"error: laya not installed: {exc}", file=sys.stderr)
|
|
395
|
+
sys.exit(2)
|
|
396
|
+
kwargs: dict[str, Any] = {"preload": True}
|
|
397
|
+
if getattr(args, "device", None):
|
|
398
|
+
kwargs["device"] = args.device
|
|
399
|
+
agent = Router(**kwargs)
|
|
400
|
+
try:
|
|
401
|
+
dummy_q = {"_warmup": {"type": "noul", "instructions": "Is this warmup text relevant?"}}
|
|
402
|
+
agent.predict("warmup text for kernel compilation", dummy_q, lang=getattr(args, "lang", None)) if getattr(
|
|
403
|
+
args, "lang", None
|
|
404
|
+
) else agent.predict("warmup text for kernel compilation", dummy_q)
|
|
405
|
+
except Exception:
|
|
406
|
+
pass
|
|
407
|
+
dt = time.time() - t0
|
|
408
|
+
print(
|
|
409
|
+
f"[laya-cli] Router loaded in {dt:.1f}s (device={getattr(agent, 'device', '?')})",
|
|
410
|
+
file=sys.stderr,
|
|
411
|
+
)
|
|
412
|
+
return agent
|
|
413
|
+
else:
|
|
414
|
+
try:
|
|
415
|
+
import laya
|
|
416
|
+
except ImportError as exc:
|
|
417
|
+
print(f"error: laya not installed: {exc}", file=sys.stderr)
|
|
418
|
+
sys.exit(2)
|
|
419
|
+
kwargs: dict[str, Any] = {}
|
|
420
|
+
if getattr(args, "device", None):
|
|
421
|
+
kwargs["device"] = args.device
|
|
422
|
+
if getattr(args, "subfolder", None):
|
|
423
|
+
kwargs["subfolder"] = args.subfolder
|
|
424
|
+
agent = laya.load(getattr(args, "model", "convaiinnovations/laya"), **kwargs)
|
|
425
|
+
try:
|
|
426
|
+
dummy_q = getattr(args, "_warmup_questions", None)
|
|
427
|
+
if dummy_q is None:
|
|
428
|
+
dummy_q = {"_warmup": {"type": "noul", "instructions": "Is this warmup text relevant?"}}
|
|
429
|
+
agent.predict("warmup text for kernel compilation", dummy_q)
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
dt = time.time() - t0
|
|
433
|
+
print(
|
|
434
|
+
f"[laya-cli] model {getattr(args, 'model', 'convaiinnovations/laya')!r} loaded in {dt:.1f}s (device={getattr(agent, 'device', '?')})",
|
|
435
|
+
file=sys.stderr,
|
|
436
|
+
)
|
|
437
|
+
return agent
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _apply_answers(row: dict[str, Any], answers: dict[str, Any], questions: dict[str, Any], full_probs: bool) -> None:
|
|
441
|
+
for qid, qdef in questions.items():
|
|
442
|
+
ans = answers.get(qid)
|
|
443
|
+
if ans is None:
|
|
444
|
+
continue
|
|
445
|
+
qtype = qdef.get("type")
|
|
446
|
+
if qtype == "choice":
|
|
447
|
+
row[qid] = ans.get("choice")
|
|
448
|
+
probs = ans.get("probabilities", {})
|
|
449
|
+
chosen = ans.get("choice")
|
|
450
|
+
p = probs.get(chosen) if isinstance(probs, dict) else None
|
|
451
|
+
if p is not None:
|
|
452
|
+
row[f"{qid}_p"] = p
|
|
453
|
+
if full_probs:
|
|
454
|
+
row[f"{qid}_probs"] = probs
|
|
455
|
+
if "confidence" in ans:
|
|
456
|
+
row[f"{qid}_confidence"] = ans["confidence"]
|
|
457
|
+
elif qtype == "score":
|
|
458
|
+
row[f"{qid}_score"] = ans.get("score")
|
|
459
|
+
if "confidence" in ans:
|
|
460
|
+
row[f"{qid}_confidence"] = ans["confidence"]
|
|
461
|
+
if full_probs and "probabilities" in ans:
|
|
462
|
+
row[f"{qid}_probs"] = ans["probabilities"]
|
|
463
|
+
elif qtype == "noul":
|
|
464
|
+
row[qid] = ans.get("noul")
|
|
465
|
+
if "confidence" in ans:
|
|
466
|
+
row[f"{qid}_confidence"] = ans["confidence"]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _collect_states_predict(args) -> list[tuple[Any, Any]]:
|
|
470
|
+
"""Return list of (original_row_or_none, state) for predict."""
|
|
471
|
+
states: list[tuple[Any, Any]] = []
|
|
472
|
+
|
|
473
|
+
# Explicit batch input
|
|
474
|
+
if getattr(args, "input_path", None):
|
|
475
|
+
path = args.input_path
|
|
476
|
+
fh = sys.stdin if path == "-" else open(path, encoding="utf-8")
|
|
477
|
+
try:
|
|
478
|
+
for line in fh:
|
|
479
|
+
line = line.strip()
|
|
480
|
+
if not line:
|
|
481
|
+
continue
|
|
482
|
+
try:
|
|
483
|
+
row = json.loads(line)
|
|
484
|
+
except json.JSONDecodeError:
|
|
485
|
+
# treat as raw text line
|
|
486
|
+
row = {"state": line}
|
|
487
|
+
if not isinstance(row, dict):
|
|
488
|
+
row = {"state": str(row)}
|
|
489
|
+
raw = row.get(args.state_field, "")
|
|
490
|
+
if raw is None:
|
|
491
|
+
raw = ""
|
|
492
|
+
if isinstance(raw, (dict, list)):
|
|
493
|
+
state_text = json.dumps(raw, ensure_ascii=False)
|
|
494
|
+
else:
|
|
495
|
+
state_text = str(raw)
|
|
496
|
+
if getattr(args, "prepend_field", None):
|
|
497
|
+
extra = row.get(args.prepend_field)
|
|
498
|
+
if extra is not None and str(extra).strip() != "":
|
|
499
|
+
extra_s = (
|
|
500
|
+
json.dumps(extra, ensure_ascii=False) if isinstance(extra, (dict, list)) else str(extra)
|
|
501
|
+
)
|
|
502
|
+
state_text = f"{extra_s} {state_text}"
|
|
503
|
+
# keep original row for output
|
|
504
|
+
states.append(
|
|
505
|
+
(
|
|
506
|
+
row,
|
|
507
|
+
state_text
|
|
508
|
+
if not isinstance(row.get(args.state_field), (dict, list)) or isinstance(raw, (dict, list))
|
|
509
|
+
else state_text,
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
# For cases where state field was dict/list originally, state_text already serialized
|
|
513
|
+
# but we remember original row
|
|
514
|
+
states[-1] = (row, state_text)
|
|
515
|
+
finally:
|
|
516
|
+
if fh is not sys.stdin:
|
|
517
|
+
fh.close()
|
|
518
|
+
return states
|
|
519
|
+
|
|
520
|
+
# Positional + --text
|
|
521
|
+
texts: list[str] = []
|
|
522
|
+
if getattr(args, "text_positional", None):
|
|
523
|
+
texts.extend([t for t in args.text_positional if t is not None and str(t) != ""])
|
|
524
|
+
if getattr(args, "text_opt", None):
|
|
525
|
+
# text_opt is list of strings (action append)
|
|
526
|
+
for t in args.text_opt:
|
|
527
|
+
if t is not None:
|
|
528
|
+
texts.append(t)
|
|
529
|
+
if texts:
|
|
530
|
+
for t in texts:
|
|
531
|
+
states.append((None, t))
|
|
532
|
+
return states
|
|
533
|
+
|
|
534
|
+
# --state JSON string
|
|
535
|
+
if getattr(args, "state_json", None):
|
|
536
|
+
try:
|
|
537
|
+
parsed = json.loads(args.state_json)
|
|
538
|
+
states.append((None, parsed))
|
|
539
|
+
except json.JSONDecodeError:
|
|
540
|
+
states.append((None, args.state_json))
|
|
541
|
+
return states
|
|
542
|
+
|
|
543
|
+
if getattr(args, "state_file", None):
|
|
544
|
+
with open(args.state_file, encoding="utf-8") as f:
|
|
545
|
+
content = f.read().strip()
|
|
546
|
+
try:
|
|
547
|
+
parsed = json.loads(content)
|
|
548
|
+
states.append((None, parsed))
|
|
549
|
+
except json.JSONDecodeError:
|
|
550
|
+
states.append((None, content))
|
|
551
|
+
return states
|
|
552
|
+
|
|
553
|
+
# Fallback: stdin as single state (human pipe) or batch if JSONL
|
|
554
|
+
if not sys.stdin.isatty():
|
|
555
|
+
data = sys.stdin.read()
|
|
556
|
+
if not data.strip():
|
|
557
|
+
return states
|
|
558
|
+
# Try JSONL first: multiple lines JSON?
|
|
559
|
+
lines = [line for line in data.splitlines() if line.strip() != ""]
|
|
560
|
+
# If single line and it looks like JSON object, treat as state JSON
|
|
561
|
+
if len(lines) == 1:
|
|
562
|
+
try:
|
|
563
|
+
parsed = json.loads(lines[0])
|
|
564
|
+
if isinstance(parsed, dict) and args.state_field in parsed:
|
|
565
|
+
raw = parsed.get(args.state_field, "")
|
|
566
|
+
state_text = json.dumps(raw, ensure_ascii=False) if isinstance(raw, (dict, list)) else str(raw)
|
|
567
|
+
states.append((parsed, state_text))
|
|
568
|
+
elif isinstance(parsed, dict):
|
|
569
|
+
# use whole object as state (Laya supports dict state)
|
|
570
|
+
states.append((None, parsed))
|
|
571
|
+
else:
|
|
572
|
+
states.append((None, parsed))
|
|
573
|
+
return states
|
|
574
|
+
except json.JSONDecodeError:
|
|
575
|
+
states.append((None, lines[0]))
|
|
576
|
+
return states
|
|
577
|
+
# Multiple lines: try batch JSONL
|
|
578
|
+
has_json = False
|
|
579
|
+
for line in lines:
|
|
580
|
+
try:
|
|
581
|
+
row = json.loads(line)
|
|
582
|
+
if isinstance(row, dict):
|
|
583
|
+
has_json = True
|
|
584
|
+
raw = row.get(args.state_field, "")
|
|
585
|
+
st = json.dumps(raw, ensure_ascii=False) if isinstance(raw, (dict, list)) else str(raw)
|
|
586
|
+
states.append((row, st))
|
|
587
|
+
else:
|
|
588
|
+
states.append((None, str(row)))
|
|
589
|
+
except json.JSONDecodeError:
|
|
590
|
+
states.append((None, line))
|
|
591
|
+
if has_json:
|
|
592
|
+
return states
|
|
593
|
+
# Fallback: treat whole stdin as single text
|
|
594
|
+
if not states:
|
|
595
|
+
states.append((None, data.strip()))
|
|
596
|
+
return states
|
|
597
|
+
|
|
598
|
+
return states
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _write_output(data: Any, args, is_batch: bool = False) -> None:
|
|
602
|
+
out_path = getattr(args, "output", None)
|
|
603
|
+
fh = open(out_path, "w", encoding="utf-8") if out_path else sys.stdout
|
|
604
|
+
try:
|
|
605
|
+
fmt = getattr(args, "out_format", None)
|
|
606
|
+
if fmt is None:
|
|
607
|
+
fmt = "jsonl" if is_batch else "json"
|
|
608
|
+
pretty = getattr(args, "pretty", False)
|
|
609
|
+
if fmt == "table":
|
|
610
|
+
# table is only for single currently; for batch we output per-row table sections
|
|
611
|
+
if is_batch:
|
|
612
|
+
for item in data:
|
|
613
|
+
_print_table_single(item, fh)
|
|
614
|
+
fh.write("\n")
|
|
615
|
+
else:
|
|
616
|
+
_print_table_single(data, fh)
|
|
617
|
+
elif fmt == "jsonl":
|
|
618
|
+
if is_batch:
|
|
619
|
+
for item in data:
|
|
620
|
+
fh.write(json.dumps(item, ensure_ascii=False) + "\n")
|
|
621
|
+
else:
|
|
622
|
+
fh.write(json.dumps(data, ensure_ascii=False) + "\n")
|
|
623
|
+
else: # json
|
|
624
|
+
if is_batch:
|
|
625
|
+
# when batch but format json -> output JSON array
|
|
626
|
+
json.dump(data, fh, ensure_ascii=False, indent=2 if pretty else None)
|
|
627
|
+
fh.write("\n")
|
|
628
|
+
else:
|
|
629
|
+
json.dump(data, fh, ensure_ascii=False, indent=2 if pretty else None)
|
|
630
|
+
fh.write("\n")
|
|
631
|
+
finally:
|
|
632
|
+
if fh is not sys.stdout:
|
|
633
|
+
fh.close()
|
|
634
|
+
else:
|
|
635
|
+
fh.flush()
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _print_table_single(result: dict[str, Any], fh) -> None:
|
|
639
|
+
answers = result.get("answers", {})
|
|
640
|
+
routing = result.get("routing")
|
|
641
|
+
usage = result.get("usage", {})
|
|
642
|
+
fh.write("Answers:\n")
|
|
643
|
+
# header
|
|
644
|
+
fh.write(f" {'question':<18} {'type':<7} {'answer':<22} {'confidence':<10} {'details'}\n")
|
|
645
|
+
fh.write(" " + "-" * 80 + "\n")
|
|
646
|
+
for qid, ans in answers.items():
|
|
647
|
+
qtype = ans.get("type", "")
|
|
648
|
+
if qtype == "choice":
|
|
649
|
+
detail = f"{ans.get('choice')} p={ans.get('probabilities', {}).get(ans.get('choice'), '')}"
|
|
650
|
+
answer = str(ans.get("choice"))
|
|
651
|
+
elif qtype == "score":
|
|
652
|
+
answer = str(ans.get("score"))
|
|
653
|
+
detail = f"probs={ans.get('probabilities')}"
|
|
654
|
+
elif qtype == "noul":
|
|
655
|
+
answer = str(ans.get("noul"))
|
|
656
|
+
detail = f"P(true)={ans.get('noul')}"
|
|
657
|
+
else:
|
|
658
|
+
answer = str(ans)
|
|
659
|
+
detail = ""
|
|
660
|
+
fh.write(f" {qid:<18} {qtype:<7} {answer:<22} {ans.get('confidence', ''):<10} {detail}\n")
|
|
661
|
+
# show full probs on next line if choice
|
|
662
|
+
if qtype == "choice" and "probabilities" in ans:
|
|
663
|
+
fh.write(f" probs: {json.dumps(ans['probabilities'], ensure_ascii=False)}\n")
|
|
664
|
+
if routing:
|
|
665
|
+
fh.write(f"\nRouting: {routing.get('model')} — {routing.get('reason')}\n")
|
|
666
|
+
if usage:
|
|
667
|
+
fh.write(f"Usage: {usage}\n")
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
# Command impls
|
|
672
|
+
# ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def cmd_predict(args: argparse.Namespace) -> None:
|
|
676
|
+
questions = _load_questions_merged(args)
|
|
677
|
+
args._warmup_questions = questions
|
|
678
|
+
agent = _init_agent(args)
|
|
679
|
+
|
|
680
|
+
states = _collect_states_predict(args)
|
|
681
|
+
if not states:
|
|
682
|
+
print(
|
|
683
|
+
"error: no state provided. Use TEXT positional, --text, --state, --state-file or --input / stdin",
|
|
684
|
+
file=sys.stderr,
|
|
685
|
+
)
|
|
686
|
+
sys.exit(2)
|
|
687
|
+
|
|
688
|
+
# Decide output mode
|
|
689
|
+
is_batch = len(states) > 1 or getattr(args, "input_path", None) is not None
|
|
690
|
+
# If --input was used, we already know it's batch
|
|
691
|
+
# For predict via stdin single JSONL line, we treat as single vs batch based on count
|
|
692
|
+
|
|
693
|
+
results: list[dict[str, Any]] = []
|
|
694
|
+
for orig, state in states:
|
|
695
|
+
# state can be str/dict/list — pass as-is to Laya
|
|
696
|
+
try:
|
|
697
|
+
if getattr(args, "shortlist_k", None) is not None:
|
|
698
|
+
# shortlist path
|
|
699
|
+
try:
|
|
700
|
+
from laya import embed_fn_from_agent, predict_shortlist
|
|
701
|
+
except ImportError as exc:
|
|
702
|
+
print(f"error: shortlist requires laya shortlist module: {exc}", file=sys.stderr)
|
|
703
|
+
sys.exit(2)
|
|
704
|
+
embed_fn = embed_fn_from_agent(agent)
|
|
705
|
+
k = int(args.shortlist_k)
|
|
706
|
+
# predict_shortlist expects agent, state, questions, embed_fn, k
|
|
707
|
+
if getattr(args, "router", False):
|
|
708
|
+
kwargs = {}
|
|
709
|
+
if getattr(args, "lang", None):
|
|
710
|
+
kwargs["lang"] = args.lang
|
|
711
|
+
result = predict_shortlist(agent, state, questions, embed_fn, k=k, **kwargs)
|
|
712
|
+
else:
|
|
713
|
+
result = predict_shortlist(agent, state, questions, embed_fn, k=k)
|
|
714
|
+
else:
|
|
715
|
+
if getattr(args, "router", False):
|
|
716
|
+
kwargs = {}
|
|
717
|
+
if getattr(args, "lang", None):
|
|
718
|
+
kwargs["lang"] = args.lang
|
|
719
|
+
result = agent.predict(state, questions, **kwargs)
|
|
720
|
+
else:
|
|
721
|
+
result = agent.predict(state, questions)
|
|
722
|
+
except Exception as exc:
|
|
723
|
+
print(
|
|
724
|
+
f"[laya-cli] error: predict failed for state {str(state)[:80]!r}: {exc}",
|
|
725
|
+
file=sys.stderr,
|
|
726
|
+
)
|
|
727
|
+
result = {"error": str(exc), "state": state, "answers": {}}
|
|
728
|
+
|
|
729
|
+
# For batch with --flatten, mimic classify output
|
|
730
|
+
if is_batch and getattr(args, "flatten", False):
|
|
731
|
+
# orig is original row dict (or None)
|
|
732
|
+
row = dict(orig) if isinstance(orig, dict) else {"state": state} if orig is None else {"state": str(state)}
|
|
733
|
+
# ensure state field preserved
|
|
734
|
+
if isinstance(orig, dict) and args.state_field not in row and isinstance(state, str):
|
|
735
|
+
row[args.state_field] = state
|
|
736
|
+
answers = result.get("answers", {})
|
|
737
|
+
_apply_answers(row, answers, questions, full_probs=getattr(args, "full_probs", False))
|
|
738
|
+
# also keep routing/usage if Router
|
|
739
|
+
if "routing" in result:
|
|
740
|
+
row["_routing"] = result["routing"]
|
|
741
|
+
results.append(row)
|
|
742
|
+
elif is_batch:
|
|
743
|
+
# Batch but not flattened: output per-row result with answers
|
|
744
|
+
out_row = {}
|
|
745
|
+
if isinstance(orig, dict):
|
|
746
|
+
out_row = dict(orig)
|
|
747
|
+
else:
|
|
748
|
+
out_row = {"state": state}
|
|
749
|
+
out_row["answers"] = result.get("answers", {})
|
|
750
|
+
if "routing" in result:
|
|
751
|
+
out_row["routing"] = result["routing"]
|
|
752
|
+
if "usage" in result:
|
|
753
|
+
out_row["usage"] = result["usage"]
|
|
754
|
+
if "shortlist" in result:
|
|
755
|
+
out_row["shortlist"] = result["shortlist"]
|
|
756
|
+
if "error" in result:
|
|
757
|
+
out_row["error"] = result["error"]
|
|
758
|
+
results.append(out_row)
|
|
759
|
+
else:
|
|
760
|
+
# single: result is top-level
|
|
761
|
+
if getattr(args, "flatten", False):
|
|
762
|
+
# flatten single as well
|
|
763
|
+
row = (
|
|
764
|
+
{"state": state}
|
|
765
|
+
if not isinstance(state, dict)
|
|
766
|
+
else dict(state)
|
|
767
|
+
if isinstance(state, dict)
|
|
768
|
+
else {"state": str(state)}
|
|
769
|
+
)
|
|
770
|
+
answers = result.get("answers", {})
|
|
771
|
+
_apply_answers(row, answers, questions, full_probs=getattr(args, "full_probs", False))
|
|
772
|
+
results = row # type: ignore
|
|
773
|
+
else:
|
|
774
|
+
results.append(result) # type: ignore
|
|
775
|
+
|
|
776
|
+
if is_batch:
|
|
777
|
+
_write_output(results, args, is_batch=True)
|
|
778
|
+
print(f"[laya-cli] predict: {len(states)} states", file=sys.stderr)
|
|
779
|
+
else:
|
|
780
|
+
single = results[0] if isinstance(results, list) else results
|
|
781
|
+
_write_output(single, args, is_batch=False)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def cmd_classify(args: argparse.Namespace) -> None:
|
|
785
|
+
questions = _load_questions(args.questions)
|
|
786
|
+
args._warmup_questions = questions
|
|
787
|
+
agent = _init_agent(args)
|
|
788
|
+
n_in = n_out = 0
|
|
789
|
+
for line in sys.stdin:
|
|
790
|
+
line = line.strip()
|
|
791
|
+
if not line:
|
|
792
|
+
continue
|
|
793
|
+
try:
|
|
794
|
+
row = json.loads(line)
|
|
795
|
+
except json.JSONDecodeError as exc:
|
|
796
|
+
print(f"[laya-cli] warning: skipping invalid JSON line: {exc}", file=sys.stderr)
|
|
797
|
+
continue
|
|
798
|
+
if not isinstance(row, dict):
|
|
799
|
+
print("[laya-cli] warning: skipping non-object JSON line", file=sys.stderr)
|
|
800
|
+
continue
|
|
801
|
+
raw_state = row.get(args.state_field, "")
|
|
802
|
+
if raw_state is None:
|
|
803
|
+
raw_state = ""
|
|
804
|
+
if isinstance(raw_state, (dict, list)):
|
|
805
|
+
state_text = json.dumps(raw_state, ensure_ascii=False)
|
|
806
|
+
else:
|
|
807
|
+
state_text = str(raw_state)
|
|
808
|
+
if args.prepend_field:
|
|
809
|
+
extra = row.get(args.prepend_field)
|
|
810
|
+
if extra is not None and str(extra).strip() != "":
|
|
811
|
+
extra_s = json.dumps(extra, ensure_ascii=False) if isinstance(extra, (dict, list)) else str(extra)
|
|
812
|
+
state_text = f"{extra_s} {state_text}"
|
|
813
|
+
n_in += 1
|
|
814
|
+
try:
|
|
815
|
+
if args.router:
|
|
816
|
+
kwargs = {}
|
|
817
|
+
if args.lang:
|
|
818
|
+
kwargs["lang"] = args.lang
|
|
819
|
+
result = agent.predict(state_text, questions, **kwargs)
|
|
820
|
+
else:
|
|
821
|
+
result = agent.predict(state_text, questions)
|
|
822
|
+
except Exception as exc:
|
|
823
|
+
print(f"[laya-cli] error: predict failed on line {n_in}: {exc}", file=sys.stderr)
|
|
824
|
+
row["_laya_error"] = str(exc)
|
|
825
|
+
sys.stdout.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
826
|
+
sys.stdout.flush()
|
|
827
|
+
continue
|
|
828
|
+
answers = result.get("answers", result)
|
|
829
|
+
if not isinstance(answers, dict):
|
|
830
|
+
answers = {}
|
|
831
|
+
_apply_answers(row, answers, questions, full_probs=args.full_probs)
|
|
832
|
+
n_out += 1
|
|
833
|
+
sys.stdout.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
834
|
+
sys.stdout.flush()
|
|
835
|
+
print(f"[laya-cli] classify: {n_in} in, {n_out} out", file=sys.stderr)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
# ---------------------------------------------------------------------------
|
|
839
|
+
# filter helpers
|
|
840
|
+
# ---------------------------------------------------------------------------
|
|
841
|
+
|
|
842
|
+
_WHERE_RE = _re.compile(r"^\s*([A-Za-z0-9_.\-]+)\s*(>=|<=|==|!=|>|<|=)\s*(.+?)\s*$")
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _parse_where(expr: str):
|
|
846
|
+
if not expr:
|
|
847
|
+
return []
|
|
848
|
+
parts = [p.strip() for p in expr.split(",") if p.strip() != ""]
|
|
849
|
+
filters = []
|
|
850
|
+
for p in parts:
|
|
851
|
+
m = _WHERE_RE.match(p)
|
|
852
|
+
if not m:
|
|
853
|
+
print(
|
|
854
|
+
f"error: invalid --where expression {p!r} (expected like 'field>=0.4')",
|
|
855
|
+
file=sys.stderr,
|
|
856
|
+
)
|
|
857
|
+
sys.exit(2)
|
|
858
|
+
field, op, raw_val = m.groups()
|
|
859
|
+
if op == "=":
|
|
860
|
+
op = "=="
|
|
861
|
+
v_str = raw_val.strip()
|
|
862
|
+
if len(v_str) >= 2 and v_str[0] == v_str[-1] and v_str[0] in ('"', "'"):
|
|
863
|
+
value = v_str[1:-1]
|
|
864
|
+
else:
|
|
865
|
+
try:
|
|
866
|
+
if "." in v_str or "e" in v_str.lower():
|
|
867
|
+
value = float(v_str)
|
|
868
|
+
else:
|
|
869
|
+
value = int(v_str)
|
|
870
|
+
except ValueError:
|
|
871
|
+
value = v_str
|
|
872
|
+
filters.append((field, op, value))
|
|
873
|
+
return filters
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _parse_sort(expr: str | None):
|
|
877
|
+
if not expr:
|
|
878
|
+
return []
|
|
879
|
+
parts = [p.strip() for p in expr.split(",") if p.strip() != ""]
|
|
880
|
+
keys = []
|
|
881
|
+
for p in parts:
|
|
882
|
+
if p.startswith("-"):
|
|
883
|
+
keys.append((p[1:], -1))
|
|
884
|
+
elif p.startswith("+"):
|
|
885
|
+
keys.append((p[1:], 1))
|
|
886
|
+
else:
|
|
887
|
+
keys.append((p, 1))
|
|
888
|
+
return keys
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _row_get(row: dict[str, Any], field: str):
|
|
892
|
+
return row.get(field)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _passes_filters(row: dict[str, Any], filters) -> bool:
|
|
896
|
+
for field, op, val in filters:
|
|
897
|
+
cur = _row_get(row, field)
|
|
898
|
+
if cur is None:
|
|
899
|
+
return False
|
|
900
|
+
if isinstance(val, (int, float)) and isinstance(cur, str):
|
|
901
|
+
try:
|
|
902
|
+
cur = float(cur) if "." in cur else int(cur)
|
|
903
|
+
except ValueError:
|
|
904
|
+
return False
|
|
905
|
+
try:
|
|
906
|
+
if op == "==":
|
|
907
|
+
if cur != val:
|
|
908
|
+
return False
|
|
909
|
+
elif op == "!=":
|
|
910
|
+
if cur == val:
|
|
911
|
+
return False
|
|
912
|
+
elif op == ">":
|
|
913
|
+
if not (cur > val):
|
|
914
|
+
return False
|
|
915
|
+
elif op == "<":
|
|
916
|
+
if not (cur < val):
|
|
917
|
+
return False
|
|
918
|
+
elif op == ">=":
|
|
919
|
+
if not (cur >= val):
|
|
920
|
+
return False
|
|
921
|
+
elif op == "<=":
|
|
922
|
+
if not (cur <= val):
|
|
923
|
+
return False
|
|
924
|
+
except TypeError:
|
|
925
|
+
return False
|
|
926
|
+
return True
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def cmd_filter(args: argparse.Namespace) -> None:
|
|
930
|
+
filters = _parse_where(args.where)
|
|
931
|
+
sort_keys = _parse_sort(args.sort)
|
|
932
|
+
rows: list[dict[str, Any]] = []
|
|
933
|
+
need_sort = len(sort_keys) > 0
|
|
934
|
+
if need_sort:
|
|
935
|
+
for line in sys.stdin:
|
|
936
|
+
line = line.strip()
|
|
937
|
+
if not line:
|
|
938
|
+
continue
|
|
939
|
+
try:
|
|
940
|
+
row = json.loads(line)
|
|
941
|
+
except json.JSONDecodeError:
|
|
942
|
+
continue
|
|
943
|
+
if not isinstance(row, dict):
|
|
944
|
+
continue
|
|
945
|
+
if filters and not _passes_filters(row, filters):
|
|
946
|
+
continue
|
|
947
|
+
rows.append(row)
|
|
948
|
+
import functools
|
|
949
|
+
|
|
950
|
+
def cmp(a, b):
|
|
951
|
+
for field, direction in sort_keys:
|
|
952
|
+
av = _row_get(a, field)
|
|
953
|
+
bv = _row_get(b, field)
|
|
954
|
+
if av is None and bv is None:
|
|
955
|
+
continue
|
|
956
|
+
if av is None:
|
|
957
|
+
return 1
|
|
958
|
+
if bv is None:
|
|
959
|
+
return -1
|
|
960
|
+
try:
|
|
961
|
+
if av < bv:
|
|
962
|
+
return -direction
|
|
963
|
+
if av > bv:
|
|
964
|
+
return direction
|
|
965
|
+
except TypeError:
|
|
966
|
+
sa, sb = str(av), str(bv)
|
|
967
|
+
if sa < sb:
|
|
968
|
+
return -direction
|
|
969
|
+
if sa > sb:
|
|
970
|
+
return direction
|
|
971
|
+
return 0
|
|
972
|
+
|
|
973
|
+
rows.sort(key=functools.cmp_to_key(cmp))
|
|
974
|
+
for r in rows:
|
|
975
|
+
sys.stdout.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
976
|
+
sys.stdout.flush()
|
|
977
|
+
print(f"[laya-cli] filter: {len(rows)} rows (sorted by {args.sort})", file=sys.stderr)
|
|
978
|
+
else:
|
|
979
|
+
n = 0
|
|
980
|
+
for line in sys.stdin:
|
|
981
|
+
line = line.strip()
|
|
982
|
+
if not line:
|
|
983
|
+
continue
|
|
984
|
+
try:
|
|
985
|
+
row = json.loads(line)
|
|
986
|
+
except json.JSONDecodeError:
|
|
987
|
+
continue
|
|
988
|
+
if not isinstance(row, dict):
|
|
989
|
+
continue
|
|
990
|
+
if filters and not _passes_filters(row, filters):
|
|
991
|
+
continue
|
|
992
|
+
sys.stdout.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
993
|
+
n += 1
|
|
994
|
+
sys.stdout.flush()
|
|
995
|
+
print(f"[laya-cli] filter: {n} rows", file=sys.stderr)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
# ---------------------------------------------------------------------------
|
|
999
|
+
# questions
|
|
1000
|
+
# ---------------------------------------------------------------------------
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def cmd_questions(args: argparse.Namespace) -> None:
|
|
1004
|
+
preset = args.preset
|
|
1005
|
+
if preset is None or preset.strip().lower() in ("list", "ls", "--list"):
|
|
1006
|
+
# list presets
|
|
1007
|
+
presets = {
|
|
1008
|
+
"triage": "Support ticket triage (intent, urgency, frustration, churn)",
|
|
1009
|
+
"email": "Inbound email triage & threat filtering (category, spam, phishing)",
|
|
1010
|
+
"guard": "Real-time LLM input guardrails (jailbreak, injection, harm)",
|
|
1011
|
+
"moderation": "Content safety & moderation (toxic, harassment, threat, spam)",
|
|
1012
|
+
"router": "Intelligent model routing (difficulty, domain, tools, sensitivity)",
|
|
1013
|
+
}
|
|
1014
|
+
if getattr(args, "q_format", "json") == "table":
|
|
1015
|
+
print("Available presets:")
|
|
1016
|
+
for k, desc in presets.items():
|
|
1017
|
+
print(f" {k:<12} {desc}")
|
|
1018
|
+
else:
|
|
1019
|
+
json.dump(presets, sys.stdout, ensure_ascii=False, indent=2)
|
|
1020
|
+
sys.stdout.write("\n")
|
|
1021
|
+
sys.stdout.flush()
|
|
1022
|
+
return
|
|
1023
|
+
key = preset.strip().lower()
|
|
1024
|
+
mapping = {
|
|
1025
|
+
"triage": "triage_questions",
|
|
1026
|
+
"email": "email_questions",
|
|
1027
|
+
"guard": "guard_questions",
|
|
1028
|
+
"moderation": "moderation_questions",
|
|
1029
|
+
"router": "router_questions",
|
|
1030
|
+
}
|
|
1031
|
+
if key not in mapping:
|
|
1032
|
+
print(
|
|
1033
|
+
f"error: unknown preset {preset!r}; choose from {', '.join(sorted(mapping))} or 'list'",
|
|
1034
|
+
file=sys.stderr,
|
|
1035
|
+
)
|
|
1036
|
+
sys.exit(2)
|
|
1037
|
+
try:
|
|
1038
|
+
import laya
|
|
1039
|
+
except ImportError as exc:
|
|
1040
|
+
print(f"error: laya not installed: {exc}", file=sys.stderr)
|
|
1041
|
+
sys.exit(2)
|
|
1042
|
+
fn = getattr(laya, mapping[key])
|
|
1043
|
+
qs = fn()
|
|
1044
|
+
if getattr(args, "q_format", "json") == "table":
|
|
1045
|
+
print(f"Preset: {key}")
|
|
1046
|
+
for qid, qdef in qs.items():
|
|
1047
|
+
crit = qdef.get("criteria")
|
|
1048
|
+
print(f" {qid} ({qdef.get('type')}): {qdef.get('instructions')}")
|
|
1049
|
+
if isinstance(crit, dict):
|
|
1050
|
+
for k, v in crit.items():
|
|
1051
|
+
print(f" - {k}: {v}")
|
|
1052
|
+
elif isinstance(crit, list):
|
|
1053
|
+
for i, c in enumerate(crit):
|
|
1054
|
+
print(f" {i}: {c}")
|
|
1055
|
+
else:
|
|
1056
|
+
json.dump(qs, sys.stdout, ensure_ascii=False, indent=2)
|
|
1057
|
+
sys.stdout.write("\n")
|
|
1058
|
+
sys.stdout.flush()
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
# ---------------------------------------------------------------------------
|
|
1062
|
+
# evaluate
|
|
1063
|
+
# ---------------------------------------------------------------------------
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def cmd_evaluate(args: argparse.Namespace) -> None:
|
|
1067
|
+
questions = _load_questions(args.questions)
|
|
1068
|
+
if args.field:
|
|
1069
|
+
if args.field not in questions:
|
|
1070
|
+
print(
|
|
1071
|
+
f"error: --field {args.field!r} not in questions.json (available: {', '.join(questions)})",
|
|
1072
|
+
file=sys.stderr,
|
|
1073
|
+
)
|
|
1074
|
+
sys.exit(2)
|
|
1075
|
+
questions = {args.field: questions[args.field]}
|
|
1076
|
+
labeled_path = args.labeled
|
|
1077
|
+
if not os.path.exists(labeled_path):
|
|
1078
|
+
print(f"error: labeled file not found: {labeled_path!r}", file=sys.stderr)
|
|
1079
|
+
sys.exit(2)
|
|
1080
|
+
|
|
1081
|
+
class _FakeArgs:
|
|
1082
|
+
pass
|
|
1083
|
+
|
|
1084
|
+
fake = _FakeArgs()
|
|
1085
|
+
fake.model = args.model
|
|
1086
|
+
fake.subfolder = args.subfolder
|
|
1087
|
+
fake.device = args.device
|
|
1088
|
+
fake.router = args.router
|
|
1089
|
+
fake.lang = args.lang
|
|
1090
|
+
fake._warmup_questions = questions
|
|
1091
|
+
agent = _init_agent(fake)
|
|
1092
|
+
rows: list[dict[str, Any]] = []
|
|
1093
|
+
with open(labeled_path, encoding="utf-8") as f:
|
|
1094
|
+
for idx, line in enumerate(f, 1):
|
|
1095
|
+
line = line.strip()
|
|
1096
|
+
if not line:
|
|
1097
|
+
continue
|
|
1098
|
+
try:
|
|
1099
|
+
row = json.loads(line)
|
|
1100
|
+
except json.JSONDecodeError as exc:
|
|
1101
|
+
print(f"[laya-cli] warning: skipping invalid JSON line {idx}: {exc}", file=sys.stderr)
|
|
1102
|
+
continue
|
|
1103
|
+
rows.append(row)
|
|
1104
|
+
total = len(rows)
|
|
1105
|
+
if total == 0:
|
|
1106
|
+
print("No labeled examples found.", file=sys.stderr)
|
|
1107
|
+
sys.exit(2)
|
|
1108
|
+
stats: dict[str, dict[str, Any]] = {}
|
|
1109
|
+
for qid, qdef in questions.items():
|
|
1110
|
+
stats[qid] = {
|
|
1111
|
+
"type": qdef.get("type"),
|
|
1112
|
+
"correct": 0,
|
|
1113
|
+
"passing": 0,
|
|
1114
|
+
"correct_and_passing": 0,
|
|
1115
|
+
"n": 0,
|
|
1116
|
+
}
|
|
1117
|
+
threshold = args.threshold
|
|
1118
|
+
for row in rows:
|
|
1119
|
+
raw_state = row.get(args.state_field, "")
|
|
1120
|
+
if raw_state is None:
|
|
1121
|
+
raw_state = ""
|
|
1122
|
+
if isinstance(raw_state, (dict, list)):
|
|
1123
|
+
state_text = json.dumps(raw_state, ensure_ascii=False)
|
|
1124
|
+
else:
|
|
1125
|
+
state_text = str(raw_state)
|
|
1126
|
+
if args.prepend_field:
|
|
1127
|
+
extra = row.get(args.prepend_field)
|
|
1128
|
+
if extra is not None and str(extra).strip() != "":
|
|
1129
|
+
extra_s = json.dumps(extra, ensure_ascii=False) if isinstance(extra, (dict, list)) else str(extra)
|
|
1130
|
+
state_text = f"{extra_s} {state_text}"
|
|
1131
|
+
try:
|
|
1132
|
+
if args.router:
|
|
1133
|
+
kwargs = {}
|
|
1134
|
+
if args.lang:
|
|
1135
|
+
kwargs["lang"] = args.lang
|
|
1136
|
+
result = agent.predict(state_text, questions, **kwargs)
|
|
1137
|
+
else:
|
|
1138
|
+
result = agent.predict(state_text, questions)
|
|
1139
|
+
except Exception as exc:
|
|
1140
|
+
print(f"[laya-cli] warning: predict failed on row: {exc}", file=sys.stderr)
|
|
1141
|
+
continue
|
|
1142
|
+
answers = result.get("answers", result)
|
|
1143
|
+
true_val = row.get(args.label_field)
|
|
1144
|
+
for qid, qdef in questions.items():
|
|
1145
|
+
ans = answers.get(qid)
|
|
1146
|
+
if ans is None:
|
|
1147
|
+
continue
|
|
1148
|
+
qtype = qdef.get("type")
|
|
1149
|
+
if len(questions) > 1 and qid in row and qid != args.label_field:
|
|
1150
|
+
gt = row.get(qid)
|
|
1151
|
+
else:
|
|
1152
|
+
gt = true_val
|
|
1153
|
+
is_correct = False
|
|
1154
|
+
passing = False
|
|
1155
|
+
if qtype == "choice":
|
|
1156
|
+
pred = ans.get("choice")
|
|
1157
|
+
probs = ans.get("probabilities", {})
|
|
1158
|
+
p = probs.get(pred, 0) if isinstance(probs, dict) else 0
|
|
1159
|
+
is_correct = (str(pred) == str(gt)) if gt is not None else False
|
|
1160
|
+
passing = float(p) >= threshold
|
|
1161
|
+
elif qtype == "noul":
|
|
1162
|
+
p_true = float(ans.get("noul", 0))
|
|
1163
|
+
if isinstance(gt, bool):
|
|
1164
|
+
gt_bool = gt
|
|
1165
|
+
elif isinstance(gt, (int, float)):
|
|
1166
|
+
gt_bool = bool(gt)
|
|
1167
|
+
elif isinstance(gt, str):
|
|
1168
|
+
gt_bool = gt.strip().lower() in ("true", "1", "yes", "y", "t")
|
|
1169
|
+
else:
|
|
1170
|
+
gt_bool = bool(gt) if gt is not None else False
|
|
1171
|
+
pred_bool = p_true >= threshold
|
|
1172
|
+
is_correct = pred_bool == gt_bool
|
|
1173
|
+
passing = max(p_true, 1 - p_true) >= threshold
|
|
1174
|
+
elif qtype == "score":
|
|
1175
|
+
pred_score = float(ans.get("score", 0))
|
|
1176
|
+
try:
|
|
1177
|
+
gt_score = float(gt) if gt is not None else None
|
|
1178
|
+
except (ValueError, TypeError):
|
|
1179
|
+
gt_score = None
|
|
1180
|
+
if gt_score is not None:
|
|
1181
|
+
is_correct = round(pred_score) == round(gt_score)
|
|
1182
|
+
passing = float(ans.get("confidence", 0)) >= threshold
|
|
1183
|
+
else:
|
|
1184
|
+
continue
|
|
1185
|
+
st = stats[qid]
|
|
1186
|
+
st["n"] += 1
|
|
1187
|
+
if is_correct:
|
|
1188
|
+
st["correct"] += 1
|
|
1189
|
+
if passing:
|
|
1190
|
+
st["passing"] += 1
|
|
1191
|
+
if is_correct:
|
|
1192
|
+
st["correct_and_passing"] += 1
|
|
1193
|
+
out_lines = []
|
|
1194
|
+
out_lines.append("laya-cli evaluate report")
|
|
1195
|
+
out_lines.append(f" questions: {', '.join(stats.keys())}")
|
|
1196
|
+
out_lines.append(f" labeled file: {labeled_path}")
|
|
1197
|
+
out_lines.append(f" state field: {args.state_field!r}, label field: {args.label_field!r}, threshold: {threshold}")
|
|
1198
|
+
out_lines.append(f" examples: {total}")
|
|
1199
|
+
out_lines.append("")
|
|
1200
|
+
for qid, st in stats.items():
|
|
1201
|
+
n = st["n"]
|
|
1202
|
+
if n == 0:
|
|
1203
|
+
continue
|
|
1204
|
+
acc = st["correct"] / n if n else 0
|
|
1205
|
+
pass_rate = st["passing"] / n if n else 0
|
|
1206
|
+
prec = (st["correct_and_passing"] / st["passing"]) if st["passing"] else 0
|
|
1207
|
+
esc = 1 - pass_rate
|
|
1208
|
+
out_lines.append(f" [{qid}] type={st['type']}")
|
|
1209
|
+
out_lines.append(f" accuracy: {acc:.1%} ({st['correct']}/{n})")
|
|
1210
|
+
out_lines.append(f" passing @ {threshold}: {pass_rate:.1%} ({st['passing']}/{n})")
|
|
1211
|
+
out_lines.append(
|
|
1212
|
+
f" precision @ {threshold} (correct among passing): {prec:.1%} ({st['correct_and_passing']}/{st['passing']})"
|
|
1213
|
+
if st["passing"]
|
|
1214
|
+
else f" precision @ {threshold}: n/a (no passing)"
|
|
1215
|
+
)
|
|
1216
|
+
out_lines.append(f" escalation rate (1 - passing): {esc:.1%} — share that would go to LLM/human")
|
|
1217
|
+
out_lines.append("")
|
|
1218
|
+
report = "\n".join(out_lines)
|
|
1219
|
+
sys.stdout.write(report + "\n")
|
|
1220
|
+
sys.stdout.flush()
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def cmd_info(args: argparse.Namespace) -> None:
|
|
1224
|
+
import platform
|
|
1225
|
+
|
|
1226
|
+
print(f"laya-cli {__version__}")
|
|
1227
|
+
print(f"python: {platform.python_version()} ({sys.executable})")
|
|
1228
|
+
try:
|
|
1229
|
+
import torch
|
|
1230
|
+
|
|
1231
|
+
print(f"torch: {torch.__version__}")
|
|
1232
|
+
print(f" cuda available: {torch.cuda.is_available()}")
|
|
1233
|
+
if torch.cuda.is_available():
|
|
1234
|
+
print(f" cuda devices: {torch.cuda.device_count()}")
|
|
1235
|
+
try:
|
|
1236
|
+
import torch.backends.mps as mps
|
|
1237
|
+
|
|
1238
|
+
print(f" mps available: {mps.is_available()}")
|
|
1239
|
+
except Exception:
|
|
1240
|
+
pass
|
|
1241
|
+
except ImportError:
|
|
1242
|
+
print("torch: not installed")
|
|
1243
|
+
try:
|
|
1244
|
+
import laya
|
|
1245
|
+
|
|
1246
|
+
print(f"laya: {getattr(laya, '__version__', 'unknown')}")
|
|
1247
|
+
print(f"model: {args.model}" + (f" subfolder={args.subfolder}" if args.subfolder else ""))
|
|
1248
|
+
try:
|
|
1249
|
+
from huggingface_hub import scan_cache_dir
|
|
1250
|
+
|
|
1251
|
+
cache = scan_cache_dir()
|
|
1252
|
+
repos = [r.repo_id for r in cache.repos]
|
|
1253
|
+
if args.model in repos:
|
|
1254
|
+
print(" cached: yes")
|
|
1255
|
+
else:
|
|
1256
|
+
print(" cached: not found (will download on first predict)")
|
|
1257
|
+
except Exception:
|
|
1258
|
+
pass
|
|
1259
|
+
except ImportError as e:
|
|
1260
|
+
print(f"laya: not installed ({e})")
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _preprocess_argv(argv: list[str]) -> list[str]:
|
|
1264
|
+
known_flags = {
|
|
1265
|
+
"--questions",
|
|
1266
|
+
"--state-field",
|
|
1267
|
+
"--model",
|
|
1268
|
+
"--subfolder",
|
|
1269
|
+
"--device",
|
|
1270
|
+
"--router",
|
|
1271
|
+
"--lang",
|
|
1272
|
+
"--prepend-field",
|
|
1273
|
+
"--full-probs",
|
|
1274
|
+
"--where",
|
|
1275
|
+
"--sort",
|
|
1276
|
+
"--labeled",
|
|
1277
|
+
"--label-field",
|
|
1278
|
+
"--field",
|
|
1279
|
+
"--threshold",
|
|
1280
|
+
"--help",
|
|
1281
|
+
"-h",
|
|
1282
|
+
"--version",
|
|
1283
|
+
"--text",
|
|
1284
|
+
"--state",
|
|
1285
|
+
"--state-file",
|
|
1286
|
+
"--input",
|
|
1287
|
+
"--questions-inline",
|
|
1288
|
+
"--preset",
|
|
1289
|
+
"--shortlist-k",
|
|
1290
|
+
"--format",
|
|
1291
|
+
"--pretty",
|
|
1292
|
+
"--flatten",
|
|
1293
|
+
"--output",
|
|
1294
|
+
"--q_format",
|
|
1295
|
+
}
|
|
1296
|
+
out: list[str] = []
|
|
1297
|
+
i = 0
|
|
1298
|
+
while i < len(argv):
|
|
1299
|
+
tok = argv[i]
|
|
1300
|
+
out.append(tok)
|
|
1301
|
+
if tok == "--sort" and i + 1 < len(argv):
|
|
1302
|
+
nxt = argv[i + 1]
|
|
1303
|
+
if nxt not in known_flags and (nxt.startswith("-") or nxt.startswith("+")):
|
|
1304
|
+
out[-1] = f"--sort={nxt}"
|
|
1305
|
+
i += 1
|
|
1306
|
+
i += 1
|
|
1307
|
+
return out
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def main() -> None:
|
|
1311
|
+
sys.argv = _preprocess_argv(sys.argv)
|
|
1312
|
+
parser = _build_parser()
|
|
1313
|
+
args = parser.parse_args()
|
|
1314
|
+
if args.cmd == "predict":
|
|
1315
|
+
cmd_predict(args)
|
|
1316
|
+
elif args.cmd == "classify":
|
|
1317
|
+
cmd_classify(args)
|
|
1318
|
+
elif args.cmd == "filter":
|
|
1319
|
+
cmd_filter(args)
|
|
1320
|
+
elif args.cmd in ("questions", "presets"):
|
|
1321
|
+
cmd_questions(args)
|
|
1322
|
+
elif args.cmd == "evaluate":
|
|
1323
|
+
cmd_evaluate(args)
|
|
1324
|
+
elif args.cmd == "info":
|
|
1325
|
+
cmd_info(args)
|
|
1326
|
+
else:
|
|
1327
|
+
parser.print_help()
|
|
1328
|
+
sys.exit(2)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
if __name__ == "__main__":
|
|
1332
|
+
main()
|