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.
Files changed (55) hide show
  1. jev_style/__init__.py +6 -0
  2. jev_style/__main__.py +3 -0
  3. jev_style/adapter.py +135 -0
  4. jev_style/cli.py +163 -0
  5. jev_style/client.py +167 -0
  6. jev_style/data/guard_config.json +210 -0
  7. jev_style/data/guard_examples/labelled_commands.jsonl +49 -0
  8. jev_style/discovery.py +108 -0
  9. jev_style/evaluate.py +464 -0
  10. jev_style/ext/__init__.py +0 -0
  11. jev_style/ext/agent_approval.py +258 -0
  12. jev_style/fake.py +76 -0
  13. jev_style/fastpath.py +77 -0
  14. jev_style/guard.py +693 -0
  15. jev_style/guard_replay.py +159 -0
  16. jev_style/mcp_server.py +249 -0
  17. jev_style/models.py +154 -0
  18. jev_style/schema.py +210 -0
  19. jev_style/server.py +122 -0
  20. jev_style/web/common.css +99 -0
  21. jev_style/web/common.js +203 -0
  22. jev_style/web/confidence.html +72 -0
  23. jev_style/web/demo-data/chinese/manifest.json +23 -0
  24. jev_style/web/demo-data/chinese/massive_dev_subset.json +1 -0
  25. jev_style/web/demos/agent-approval/LICENSES.md +14 -0
  26. jev_style/web/demos/agent-approval/approval.css +180 -0
  27. jev_style/web/demos/agent-approval/approval.js +531 -0
  28. jev_style/web/demos/agent-approval/demo.json +9 -0
  29. jev_style/web/demos/agent-approval/index.html +96 -0
  30. jev_style/web/demos/chinese/LICENSES.md +13 -0
  31. jev_style/web/demos/chinese/cases.json +391 -0
  32. jev_style/web/demos/chinese/demo.json +9 -0
  33. jev_style/web/demos/chinese/index.html +94 -0
  34. jev_style/web/demos/chinese/zh.css +54 -0
  35. jev_style/web/demos/chinese/zh.js +313 -0
  36. jev_style/web/demos/snake/LICENSES.md +9 -0
  37. jev_style/web/demos/snake/demo.json +9 -0
  38. jev_style/web/demos/snake/index.html +145 -0
  39. jev_style/web/demos/snake/snake-core.js +291 -0
  40. jev_style/web/demos/snake/snake.css +92 -0
  41. jev_style/web/demos/snake/snake.js +604 -0
  42. jev_style/web/index.html +115 -0
  43. jev_style/web/playground/LICENSES.md +20 -0
  44. jev_style/web/playground/confidence.css +31 -0
  45. jev_style/web/playground/confidence.js +124 -0
  46. jev_style/web/playground/playground.css +92 -0
  47. jev_style/web/playground/playground.js +716 -0
  48. jev_style/web/playground/presets.json +716 -0
  49. jev_style/web/playground/share.js +58 -0
  50. jev_style-0.2.0.dist-info/METADATA +327 -0
  51. jev_style-0.2.0.dist-info/RECORD +55 -0
  52. jev_style-0.2.0.dist-info/WHEEL +4 -0
  53. jev_style-0.2.0.dist-info/entry_points.txt +2 -0
  54. jev_style-0.2.0.dist-info/licenses/LICENSE +202 -0
  55. 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
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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"]}