jev-style 0.2.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.
- jev_style/__init__.py +6 -0
- jev_style/__main__.py +3 -0
- jev_style/adapter.py +135 -0
- jev_style/cli.py +163 -0
- jev_style/client.py +167 -0
- jev_style/data/guard_config.json +210 -0
- jev_style/data/guard_examples/labelled_commands.jsonl +49 -0
- jev_style/discovery.py +108 -0
- jev_style/evaluate.py +464 -0
- jev_style/ext/__init__.py +0 -0
- jev_style/ext/agent_approval.py +258 -0
- jev_style/fake.py +76 -0
- jev_style/fastpath.py +77 -0
- jev_style/guard.py +693 -0
- jev_style/guard_replay.py +159 -0
- jev_style/mcp_server.py +249 -0
- jev_style/models.py +154 -0
- jev_style/schema.py +210 -0
- jev_style/server.py +122 -0
- jev_style/web/common.css +99 -0
- jev_style/web/common.js +203 -0
- jev_style/web/confidence.html +72 -0
- jev_style/web/demo-data/chinese/manifest.json +23 -0
- jev_style/web/demo-data/chinese/massive_dev_subset.json +1 -0
- jev_style/web/demos/agent-approval/LICENSES.md +14 -0
- jev_style/web/demos/agent-approval/approval.css +180 -0
- jev_style/web/demos/agent-approval/approval.js +531 -0
- jev_style/web/demos/agent-approval/demo.json +9 -0
- jev_style/web/demos/agent-approval/index.html +96 -0
- jev_style/web/demos/chinese/LICENSES.md +13 -0
- jev_style/web/demos/chinese/cases.json +391 -0
- jev_style/web/demos/chinese/demo.json +9 -0
- jev_style/web/demos/chinese/index.html +94 -0
- jev_style/web/demos/chinese/zh.css +54 -0
- jev_style/web/demos/chinese/zh.js +313 -0
- jev_style/web/demos/snake/LICENSES.md +9 -0
- jev_style/web/demos/snake/demo.json +9 -0
- jev_style/web/demos/snake/index.html +145 -0
- jev_style/web/demos/snake/snake-core.js +291 -0
- jev_style/web/demos/snake/snake.css +92 -0
- jev_style/web/demos/snake/snake.js +604 -0
- jev_style/web/index.html +115 -0
- jev_style/web/playground/LICENSES.md +20 -0
- jev_style/web/playground/confidence.css +31 -0
- jev_style/web/playground/confidence.js +124 -0
- jev_style/web/playground/playground.css +92 -0
- jev_style/web/playground/playground.js +716 -0
- jev_style/web/playground/presets.json +716 -0
- jev_style/web/playground/share.js +58 -0
- jev_style-0.2.0.dist-info/METADATA +327 -0
- jev_style-0.2.0.dist-info/RECORD +55 -0
- jev_style-0.2.0.dist-info/WHEEL +4 -0
- jev_style-0.2.0.dist-info/entry_points.txt +2 -0
- jev_style-0.2.0.dist-info/licenses/LICENSE +202 -0
- jev_style-0.2.0.dist-info/licenses/NOTICE +16 -0
jev_style/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Jev-Style: small, calibrated, local decision models with a systemone-compatible API."""
|
|
2
|
+
__version__ = "0.2.0"
|
|
3
|
+
|
|
4
|
+
from .client import JevStyle, JevStyleError, choice, classify, configure, decide, noul, score # noqa: E402
|
|
5
|
+
|
|
6
|
+
__all__ = ["JevStyle", "JevStyleError", "choice", "classify", "configure", "decide", "noul", "score", "__version__"]
|
jev_style/__main__.py
ADDED
jev_style/adapter.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""systemone-compatible requests on top of a Jev-Style runtime (``decide_many(state, questions)``).
|
|
2
|
+
|
|
3
|
+
One request = one state and any number of questions; they go to ONE ``decide_many`` call, so the
|
|
4
|
+
state is read once. Answer arithmetic (over the runtime's calibrated probabilities):
|
|
5
|
+
|
|
6
|
+
* noul: ``noul`` = P(true)
|
|
7
|
+
* choice: ``choice`` = argmax option, ``probabilities`` = {option: p}, ``confidence``
|
|
8
|
+
* score: ``score`` = sum_i i * p_i over level indices 0..K-1, ``legend`` = {"0": label, ...},
|
|
9
|
+
``probabilities`` = {"0": p_0, ...}, ``confidence``
|
|
10
|
+
|
|
11
|
+
confidence = (k * p_max - 1) / (k - 1) for k options, clipped to [0, 1] (0 = uniform, 1 = all mass on
|
|
12
|
+
one option). Over-budget inputs are rejected with 422 ``input_budget_exceeded``; nothing is truncated.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .models import MODEL_ID
|
|
23
|
+
from .schema import ApiError, ParsedRequest, QuestionSpec, parse_request
|
|
24
|
+
|
|
25
|
+
DEFAULT_CATEGORY = "typed_official" # calibration group the model card uses for free-form typed questions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def confidence(probs: list[float]) -> float:
|
|
29
|
+
k = len(probs)
|
|
30
|
+
if k <= 1:
|
|
31
|
+
return 1.0
|
|
32
|
+
return float(min(1.0, max(0.0, (k * max(probs) - 1.0) / (k - 1.0))))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def renormalise(probs: list[float]) -> list[float]:
|
|
36
|
+
clean = [float(p) if math.isfinite(p) and p > 0 else 0.0 for p in probs]
|
|
37
|
+
s = sum(clean)
|
|
38
|
+
if s <= 0:
|
|
39
|
+
return [1.0 / len(clean)] * len(clean)
|
|
40
|
+
return [p / s for p in clean]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def model_thread() -> ThreadPoolExecutor:
|
|
44
|
+
"""One worker thread that loads the model and runs every call. MLX keeps its GPU stream per thread,
|
|
45
|
+
so the model must be used on the thread that loaded it; one worker also means one call at a time."""
|
|
46
|
+
return ThreadPoolExecutor(max_workers=1, thread_name_prefix="jev-style-model")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Adapter:
|
|
50
|
+
"""Thread-safe: every model call runs on ``worker`` (one at a time). ``runtime`` is a Jev-Style runtime
|
|
51
|
+
object, ``rt`` its module."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, runtime: Any, rt: Any, backend: str, model_id: str = MODEL_ID,
|
|
54
|
+
category: str | None = DEFAULT_CATEGORY, description: str | None = None,
|
|
55
|
+
worker: ThreadPoolExecutor | None = None):
|
|
56
|
+
self.runtime, self.rt = runtime, rt
|
|
57
|
+
self.worker = worker or model_thread()
|
|
58
|
+
self.model_id = model_id
|
|
59
|
+
self.category = category
|
|
60
|
+
self._backend = backend
|
|
61
|
+
self.description = description or "Jev-Style 0.8B Decision v3: local typed decisions (noul / choice / score)"
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def backend(self) -> str:
|
|
66
|
+
dev = getattr(self.runtime, "device", None)
|
|
67
|
+
return f"{self._backend}-{dev}" if dev else self._backend
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def max_len(self) -> int:
|
|
71
|
+
return int(getattr(getattr(self.runtime, "renderer", None), "max_len", 25_600))
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def head_max(self) -> int:
|
|
75
|
+
return int(getattr(getattr(self.runtime, "renderer", None), "head_max", 2_048))
|
|
76
|
+
|
|
77
|
+
def systemone(self, body: Any) -> dict:
|
|
78
|
+
t0 = time.perf_counter()
|
|
79
|
+
parsed = body if isinstance(body, ParsedRequest) else parse_request(body)
|
|
80
|
+
budget = getattr(self.rt, "InputBudgetError", ())
|
|
81
|
+
qerr = getattr(self.rt, "QuestionError", ())
|
|
82
|
+
with self._lock:
|
|
83
|
+
try:
|
|
84
|
+
results = self.worker.submit(self.runtime.decide_many, parsed.state,
|
|
85
|
+
[q.internal for q in parsed.questions], category=self.category).result()
|
|
86
|
+
except budget as e:
|
|
87
|
+
raise ApiError(422, "input_budget_exceeded", f"{e} (nothing truncated)") from None
|
|
88
|
+
except qerr as e:
|
|
89
|
+
raise ApiError(422, "invalid_question", str(e)) from None
|
|
90
|
+
total_ms = (time.perf_counter() - t0) * 1000
|
|
91
|
+
answers = {q.id: self._answer(q, r) for q, r in zip(parsed.questions, results)}
|
|
92
|
+
head = sum(int(r.get("head_tokens", 0)) for r in results)
|
|
93
|
+
state_tokens = int(results[0]["input_tokens"]) - int(results[0].get("head_tokens", 0)) if results else 0
|
|
94
|
+
return {"model": self.model_id, "answers": answers,
|
|
95
|
+
"usage": {"input_tokens": state_tokens + head, "state_tokens": state_tokens, "output_tokens": 0},
|
|
96
|
+
"latency_ms": round(total_ms, 1), "timing": {"total_ms": round(total_ms, 1)}, "backend": self.backend}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _answer(q: QuestionSpec, res: dict) -> dict:
|
|
100
|
+
probs = res["probabilities"]
|
|
101
|
+
names = q.options if q.type != "noul" else ["false", "true"]
|
|
102
|
+
p = renormalise([float(probs[n]) for n in names])
|
|
103
|
+
if q.type == "noul":
|
|
104
|
+
return {"type": "noul", "noul": p[1]}
|
|
105
|
+
i = max(range(len(p)), key=p.__getitem__)
|
|
106
|
+
if q.type == "choice":
|
|
107
|
+
return {"type": "choice", "choice": names[i], "confidence": confidence(p),
|
|
108
|
+
"probabilities": dict(zip(names, p))}
|
|
109
|
+
return {"type": "score", "score": float(sum(k * v for k, v in enumerate(p))), "confidence": confidence(p),
|
|
110
|
+
"legend": dict(q.legend), "probabilities": dict(zip(names, p))}
|
|
111
|
+
|
|
112
|
+
def models(self) -> dict:
|
|
113
|
+
entry = {"id": self.model_id, "context_tokens": self.max_len, "head_max_tokens": self.head_max,
|
|
114
|
+
"backend": self.backend}
|
|
115
|
+
return {"object": "list", "data": [entry],
|
|
116
|
+
# the shape the public systemone SDKs read from GET /v1/models
|
|
117
|
+
"models": [{"name": self.model_id, "release_date": "2026-09-24", "description": self.description}]}
|
|
118
|
+
|
|
119
|
+
def release_info(self) -> dict:
|
|
120
|
+
fake = self.model_id.endswith("fake")
|
|
121
|
+
return {"model_name": self.model_id, "release_kind": "fake" if fake else "release", "untrained": fake,
|
|
122
|
+
"acceptance_status": None}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_adapter(backend: str = "auto", *, fake: bool = False, **load_kw: Any) -> Adapter:
|
|
126
|
+
if fake:
|
|
127
|
+
from . import fake as fk
|
|
128
|
+
return Adapter(fk.FakeRuntime(), fk, "fake", fk.FAKE_MODEL_ID,
|
|
129
|
+
description="Fake engine: deterministic hash-based probabilities, no model")
|
|
130
|
+
from .models import load
|
|
131
|
+
worker = model_thread()
|
|
132
|
+
runtime, rt, name = worker.submit(load, backend, **load_kw).result()
|
|
133
|
+
from .fastpath import enable_prefix_sharing
|
|
134
|
+
enable_prefix_sharing(runtime, rt) # MLX: read the state once for many questions (same scores)
|
|
135
|
+
return Adapter(runtime, rt, name, worker=worker)
|
jev_style/cli.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""``jev-style`` command line.
|
|
2
|
+
|
|
3
|
+
jev-style serve start the local server (API + Playground) on :8765
|
|
4
|
+
jev-style decide "text" --noul "Is this about billing?"
|
|
5
|
+
jev-style download [--backend mlx] fetch the weights ahead of time
|
|
6
|
+
jev-style guard [--check CMD] Claude Code PreToolUse hook
|
|
7
|
+
jev-style guard-replay score the guard on labelled tool calls
|
|
8
|
+
jev-style mcp MCP server (stdio) for Claude Code, Cursor, Codex ...
|
|
9
|
+
jev-style eval data.jsonl accuracy, calibration and automation rate on your labels
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
from .models import BACKENDS
|
|
22
|
+
|
|
23
|
+
DEFAULT_PORT = 8765
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _model_args(ap: argparse.ArgumentParser) -> None:
|
|
27
|
+
ap.add_argument("--backend", default=os.environ.get("JEV_STYLE_BACKEND", "auto"), choices=BACKENDS,
|
|
28
|
+
help="auto = mlx on Apple silicon when installed, else torch ($JEV_STYLE_BACKEND)")
|
|
29
|
+
ap.add_argument("--model-dir", default=os.environ.get("JEV_STYLE_MODEL_DIR"),
|
|
30
|
+
help="use a local copy of the model repository instead of downloading ($JEV_STYLE_MODEL_DIR)")
|
|
31
|
+
ap.add_argument("--device", choices=("cuda", "mps", "cpu"),
|
|
32
|
+
help="torch device (default: $JEV_STYLE_DEVICE, else the best available)")
|
|
33
|
+
ap.add_argument("--dtype", default="float32", choices=("float32", "bfloat16", "float16"), help="torch dtype")
|
|
34
|
+
ap.add_argument("--precision", default="bf16", choices=("bf16", "8bit"), help="mlx weights")
|
|
35
|
+
ap.add_argument("--quant", default="Q8_0", choices=("F16", "Q8_0", "Q4_K_M"), help="gguf file")
|
|
36
|
+
ap.add_argument("--scorer", help="path to the jev-score binary (gguf backend)")
|
|
37
|
+
ap.add_argument("--fake", action="store_true", help="deterministic fake engine, no model (tests / UI work)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_kw(args: argparse.Namespace) -> dict[str, Any]:
|
|
41
|
+
return {"model_dir": args.model_dir, "device": args.device, "dtype": args.dtype, "precision": args.precision,
|
|
42
|
+
"quant": args.quant, "scorer": args.scorer}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_serve(args: argparse.Namespace) -> int:
|
|
46
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
47
|
+
api_key = None
|
|
48
|
+
if args.api_key_env:
|
|
49
|
+
api_key = os.environ.get(args.api_key_env, "").strip()
|
|
50
|
+
if not api_key:
|
|
51
|
+
print(f"error: environment variable {args.api_key_env} is empty or unset", file=sys.stderr)
|
|
52
|
+
return 2
|
|
53
|
+
from .server import build_app
|
|
54
|
+
print("loading the model (the first run downloads it) ..." if not args.fake else "fake engine", flush=True)
|
|
55
|
+
app = build_app(args.backend, fake=args.fake, api_key=api_key, **({} if args.fake else _load_kw(args)))
|
|
56
|
+
a = app.state.adapter
|
|
57
|
+
print(f"Jev-Style server: model={a.model_id} backend={a.backend} context={a.max_len} "
|
|
58
|
+
f"auth={'on' if api_key else 'off'} -> http://{args.host}:{args.port}/", flush=True)
|
|
59
|
+
for e in app.state.ext_errors:
|
|
60
|
+
print(f"warning: extension {e['slug']} not loaded: {e['error']}", file=sys.stderr)
|
|
61
|
+
import uvicorn
|
|
62
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_download(args: argparse.Namespace) -> int:
|
|
67
|
+
from .models import download
|
|
68
|
+
print(download(args.backend, precision=args.precision, quant=args.quant))
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cmd_decide(args: argparse.Namespace) -> int:
|
|
73
|
+
from .client import JevStyle, JevStyleError, choice, noul, score
|
|
74
|
+
state: Any = sys.stdin.read() if args.state == "-" else args.state
|
|
75
|
+
if args.json_state:
|
|
76
|
+
state = json.loads(state)
|
|
77
|
+
qs: dict[str, dict] = {}
|
|
78
|
+
for i, text in enumerate(args.noul or []):
|
|
79
|
+
qs[f"noul_{i + 1}"] = noul(text)
|
|
80
|
+
for i, spec in enumerate(args.choice or []):
|
|
81
|
+
text, _, opts = spec.partition("::")
|
|
82
|
+
qs[f"choice_{i + 1}"] = choice(text, [o.strip() for o in opts.split(",") if o.strip()])
|
|
83
|
+
for i, spec in enumerate(args.score or []):
|
|
84
|
+
text, _, levels = spec.partition("::")
|
|
85
|
+
qs[f"score_{i + 1}"] = score(text, [x.strip() for x in levels.split(",") if x.strip()])
|
|
86
|
+
if not qs:
|
|
87
|
+
print("error: give at least one --noul, --choice or --score", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
url = args.url or os.environ.get("JEV_STYLE_URL")
|
|
90
|
+
js = JevStyle(base_url=url) if url else JevStyle(backend=args.backend, fake=args.fake,
|
|
91
|
+
**({} if args.fake else _load_kw(args)))
|
|
92
|
+
try:
|
|
93
|
+
out = js.decide(state, qs)
|
|
94
|
+
except JevStyleError as e:
|
|
95
|
+
print(f"error: {e}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_guard(argv: list[str]) -> int:
|
|
102
|
+
from . import guard
|
|
103
|
+
try:
|
|
104
|
+
return guard.main(argv)
|
|
105
|
+
except SystemExit:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as exc: # last resort: ask, never block or allow by accident
|
|
108
|
+
sys.stdout.write(json.dumps({"hookSpecificOutput": {
|
|
109
|
+
"hookEventName": "PreToolUse", "permissionDecision": "ask",
|
|
110
|
+
"permissionDecisionReason": f"Jev-Style guard failed ({exc.__class__.__name__}); asking instead."}}) + "\n")
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: list[str] | None = None) -> int:
|
|
115
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
116
|
+
# subcommands with their own parsers
|
|
117
|
+
if argv and argv[0] == "guard":
|
|
118
|
+
return cmd_guard(argv[1:])
|
|
119
|
+
if argv and argv[0] == "guard-replay":
|
|
120
|
+
from .guard_replay import main as replay
|
|
121
|
+
return replay(argv[1:])
|
|
122
|
+
if argv and argv[0] == "eval":
|
|
123
|
+
from .evaluate import main as ev
|
|
124
|
+
return ev(argv[1:])
|
|
125
|
+
if argv and argv[0] == "mcp":
|
|
126
|
+
from .mcp_server import main as mcp
|
|
127
|
+
return mcp(argv[1:])
|
|
128
|
+
|
|
129
|
+
ap = argparse.ArgumentParser(prog="jev-style", description="Small, calibrated, local decision models.",
|
|
130
|
+
epilog="also: jev-style eval | guard | guard-replay | mcp (each has its own --help)")
|
|
131
|
+
ap.add_argument("--version", action="version", version=__version__)
|
|
132
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
133
|
+
|
|
134
|
+
s = sub.add_parser("serve", help="start the local server (API + Playground)")
|
|
135
|
+
_model_args(s)
|
|
136
|
+
s.add_argument("--host", default="127.0.0.1")
|
|
137
|
+
s.add_argument("--port", type=int, default=int(os.environ.get("JEV_STYLE_PORT", DEFAULT_PORT)),
|
|
138
|
+
help=f"default $JEV_STYLE_PORT or {DEFAULT_PORT}")
|
|
139
|
+
s.add_argument("--api-key-env", metavar="NAME", help="require 'Authorization: Bearer <key>', key from env NAME")
|
|
140
|
+
s.set_defaults(func=cmd_serve)
|
|
141
|
+
|
|
142
|
+
d = sub.add_parser("download", help="download the weights for a backend")
|
|
143
|
+
d.add_argument("--backend", default=os.environ.get("JEV_STYLE_BACKEND", "auto"), choices=BACKENDS)
|
|
144
|
+
d.add_argument("--precision", default="bf16", choices=("bf16", "8bit"))
|
|
145
|
+
d.add_argument("--quant", default="Q8_0", choices=("F16", "Q8_0", "Q4_K_M"))
|
|
146
|
+
d.set_defaults(func=cmd_download)
|
|
147
|
+
|
|
148
|
+
q = sub.add_parser("decide", help="answer questions about a text from the command line")
|
|
149
|
+
q.add_argument("state", help="the text to judge ('-' reads stdin)")
|
|
150
|
+
q.add_argument("--json-state", action="store_true", help="parse the state as JSON")
|
|
151
|
+
q.add_argument("--noul", action="append", metavar="STATEMENT", help="yes/no question (repeatable)")
|
|
152
|
+
q.add_argument("--choice", action="append", metavar="Q::a,b,c", help="multiple choice (repeatable)")
|
|
153
|
+
q.add_argument("--score", action="append", metavar="Q::low,mid,high", help="ordered scale, lowest first")
|
|
154
|
+
q.add_argument("--url", help="use a running server instead of loading the model ($JEV_STYLE_URL)")
|
|
155
|
+
_model_args(q)
|
|
156
|
+
q.set_defaults(func=cmd_decide)
|
|
157
|
+
|
|
158
|
+
args = ap.parse_args(argv)
|
|
159
|
+
return args.func(args)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
raise SystemExit(main())
|
jev_style/client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Python client for Jev-Style: in-process, or against any systemone-compatible server.
|
|
2
|
+
|
|
3
|
+
from jev_style import JevStyle, noul, choice, score
|
|
4
|
+
js = JevStyle(fake=True) # in-process fake engine (no model)
|
|
5
|
+
js = JevStyle(backend="auto") # in-process real model (downloads it once)
|
|
6
|
+
js = JevStyle.from_pretrained("chaoliangUNSW/Jev-Style-0.8B-Decision-v3-GGUF") # pick the build by repo id
|
|
7
|
+
js = JevStyle(base_url="http://127.0.0.1:8765") # over HTTP: jev-style serve, or another server
|
|
8
|
+
# that implements POST /v1/systemone
|
|
9
|
+
out = js.decide("I was charged twice.", {
|
|
10
|
+
"billing": noul("Is this about billing?"),
|
|
11
|
+
"team": choice("Which team?", {"billing": None, "tech": "bugs, outages"}),
|
|
12
|
+
"urgency": score("How urgent?", ["low", "medium", "high"]),
|
|
13
|
+
})
|
|
14
|
+
out["answers"]["team"]["choice"]
|
|
15
|
+
|
|
16
|
+
``decide`` returns exactly the HTTP response body of ``POST /v1/systemone`` and raises
|
|
17
|
+
``JevStyleError`` (status, code, message, question) for the contract's error bodies.
|
|
18
|
+
|
|
19
|
+
For scripts there are module-level shortcuts that share one lazily created client
|
|
20
|
+
(``$JEV_STYLE_URL`` if set, else the local model with ``$JEV_STYLE_BACKEND`` or ``auto``)::
|
|
21
|
+
|
|
22
|
+
import jev_style
|
|
23
|
+
jev_style.classify("Where is my parcel?", ["billing", "shipping", "tech"])["label"] # 'shipping'
|
|
24
|
+
jev_style.decide("I was charged twice.", {"billing": noul("This is about billing.")})
|
|
25
|
+
jev_style.configure(base_url="http://127.0.0.1:8765") # optional: choose the engine yourself
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import threading
|
|
31
|
+
from typing import Any, Iterable, Mapping
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JevStyleError(Exception):
|
|
35
|
+
def __init__(self, status: int, code: str, message: str, question: str | None = None, body: dict | None = None):
|
|
36
|
+
super().__init__(f"{status} {code}: {message}" + (f" (question {question!r})" if question else ""))
|
|
37
|
+
self.status, self.code, self.message, self.question, self.body = status, code, message, question, body or {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def noul(instructions: Any, criteria: Mapping[str, Any] | None = None) -> dict:
|
|
41
|
+
q = {"type": "noul", "instructions": instructions}
|
|
42
|
+
if criteria is not None:
|
|
43
|
+
q["criteria"] = dict(criteria)
|
|
44
|
+
return q
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def choice(instructions: Any, options: Mapping[str, Any] | Iterable[str]) -> dict:
|
|
48
|
+
crit = dict(options) if isinstance(options, Mapping) else {str(o): None for o in options}
|
|
49
|
+
return {"type": "choice", "instructions": instructions, "criteria": crit}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def score(instructions: Any, levels: Iterable[Any]) -> dict:
|
|
53
|
+
return {"type": "score", "instructions": instructions, "criteria": list(levels)}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class JevStyle:
|
|
57
|
+
def __init__(self, base_url: str | None = None, backend: str = "auto", fake: bool = False,
|
|
58
|
+
api_key: str | None = None, timeout: float = 120.0, http_client: Any = None, **load_kw: Any):
|
|
59
|
+
self.base_url = base_url.rstrip("/") if base_url else None
|
|
60
|
+
self.api_key = api_key if api_key is not None else os.environ.get("JEV_STYLE_API_KEY")
|
|
61
|
+
self.timeout = timeout
|
|
62
|
+
self._http = http_client
|
|
63
|
+
self._adapter = None
|
|
64
|
+
if self.base_url is None and http_client is None:
|
|
65
|
+
from .adapter import build_adapter
|
|
66
|
+
self._adapter = build_adapter(backend, fake=fake, **load_kw)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_pretrained(cls, repo_id: str, *, trust_remote_code: bool = False, **load_kw: Any) -> "JevStyle":
|
|
70
|
+
"""Load a Jev-Style release in-process by its Hub repo id; the repo decides the backend (a ``-GGUF`` repo
|
|
71
|
+
runs on llama.cpp, ``-MLX`` on MLX, the main repo on PyTorch). Keyword arguments go to the loader
|
|
72
|
+
(``precision="8bit"``, ``quant="Q4_K_M"``, ``device="cpu"``, ``model_dir=...``)."""
|
|
73
|
+
return cls(repo=repo_id, trust_remote_code=trust_remote_code, **load_kw)
|
|
74
|
+
|
|
75
|
+
# -- transport ---------------------------------------------------------------------------
|
|
76
|
+
def _client(self):
|
|
77
|
+
if self._http is None:
|
|
78
|
+
import httpx
|
|
79
|
+
self._http = httpx.Client(base_url=self.base_url, timeout=self.timeout)
|
|
80
|
+
return self._http
|
|
81
|
+
|
|
82
|
+
def _headers(self) -> dict:
|
|
83
|
+
return {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _raise(resp) -> None:
|
|
87
|
+
try:
|
|
88
|
+
body = resp.json()
|
|
89
|
+
except ValueError:
|
|
90
|
+
body = {}
|
|
91
|
+
err = body.get("error") if isinstance(body, dict) else None
|
|
92
|
+
err = err if isinstance(err, dict) else {}
|
|
93
|
+
raise JevStyleError(resp.status_code, err.get("code", "http_error"), err.get("message", resp.text[:500]),
|
|
94
|
+
err.get("question"), body)
|
|
95
|
+
|
|
96
|
+
# -- API ---------------------------------------------------------------------------------
|
|
97
|
+
def decide(self, state: Any, questions: Mapping[str, dict], model: str | None = None) -> dict:
|
|
98
|
+
body: dict = {"state": state, "questions": dict(questions)}
|
|
99
|
+
if model:
|
|
100
|
+
body["model"] = model
|
|
101
|
+
if self._adapter is not None:
|
|
102
|
+
from .schema import ApiError
|
|
103
|
+
try:
|
|
104
|
+
return self._adapter.systemone(body)
|
|
105
|
+
except ApiError as e:
|
|
106
|
+
raise JevStyleError(e.status, e.code, e.message, e.question, e.body()) from None
|
|
107
|
+
resp = self._client().post("/v1/systemone", json=body, headers=self._headers())
|
|
108
|
+
if resp.status_code != 200:
|
|
109
|
+
self._raise(resp)
|
|
110
|
+
return resp.json()
|
|
111
|
+
|
|
112
|
+
def models(self) -> dict:
|
|
113
|
+
if self._adapter is not None:
|
|
114
|
+
return self._adapter.models()
|
|
115
|
+
resp = self._client().get("/v1/models", headers=self._headers())
|
|
116
|
+
if resp.status_code != 200:
|
|
117
|
+
self._raise(resp)
|
|
118
|
+
return resp.json()
|
|
119
|
+
|
|
120
|
+
def close(self) -> None:
|
|
121
|
+
if self._http is not None and hasattr(self._http, "close"):
|
|
122
|
+
self._http.close()
|
|
123
|
+
|
|
124
|
+
def __enter__(self) -> "JevStyle":
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, *exc: Any) -> None:
|
|
128
|
+
self.close()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# -- module-level shortcuts ------------------------------------------------------------------------
|
|
132
|
+
_default: JevStyle | None = None
|
|
133
|
+
_default_lock = threading.Lock()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def configure(**kw: Any) -> JevStyle:
|
|
137
|
+
"""Set the client the shortcuts use (same arguments as ``JevStyle``). Returns it."""
|
|
138
|
+
global _default
|
|
139
|
+
client = JevStyle(**kw)
|
|
140
|
+
with _default_lock:
|
|
141
|
+
old, _default = _default, client
|
|
142
|
+
if old is not None:
|
|
143
|
+
old.close()
|
|
144
|
+
return client
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def default_client() -> JevStyle:
|
|
148
|
+
global _default
|
|
149
|
+
with _default_lock:
|
|
150
|
+
if _default is None:
|
|
151
|
+
url = os.environ.get("JEV_STYLE_URL")
|
|
152
|
+
backend = os.environ.get("JEV_STYLE_BACKEND", "auto")
|
|
153
|
+
_default = JevStyle(base_url=url) if url else JevStyle(backend=backend)
|
|
154
|
+
return _default
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def decide(state: Any, questions: Mapping[str, dict], model: str | None = None) -> dict:
|
|
158
|
+
"""``JevStyle.decide`` on the shared client."""
|
|
159
|
+
return default_client().decide(state, questions, model=model)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def classify(state: Any, labels: Mapping[str, Any] | Iterable[str],
|
|
163
|
+
instructions: Any = "Which label fits best?") -> dict:
|
|
164
|
+
"""Pick one label. ``labels`` is a list of names or {name: description}.
|
|
165
|
+
Returns {"label", "confidence", "probabilities"}."""
|
|
166
|
+
ans = decide(state, {"label": choice(instructions, labels)})["answers"]["label"]
|
|
167
|
+
return {"label": ans["choice"], "confidence": ans["confidence"], "probabilities": ans["probabilities"]}
|