von-sdk 1.0.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.
von/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """von - The open-source System One decision model.
2
+
3
+ Fast, local, non-autoregressive decision primitives.
4
+ Named in homage to John von Neumann and Ludwig von Mises.
5
+ """
6
+
7
+ from .types import (
8
+ Noul,
9
+ Choice,
10
+ Score,
11
+ noul,
12
+ choice,
13
+ score,
14
+ NoulAnswer,
15
+ ChoiceAnswer,
16
+ ScoreAnswer,
17
+ SystemOneResponse,
18
+ Usage,
19
+ )
20
+ from .client import VonClient, AsyncVonClient
21
+ from .api import system_one, decide, judge, rate, set_backend
22
+ from . import presets
23
+ from . import patterns
24
+
25
+ __version__ = "1.0.0"
26
+
27
+ __all__ = [
28
+ "Noul",
29
+ "Choice",
30
+ "Score",
31
+ "noul",
32
+ "choice",
33
+ "score",
34
+ "NoulAnswer",
35
+ "ChoiceAnswer",
36
+ "ScoreAnswer",
37
+ "SystemOneResponse",
38
+ "Usage",
39
+ "VonClient",
40
+ "AsyncVonClient",
41
+ "system_one",
42
+ "decide",
43
+ "judge",
44
+ "rate",
45
+ "set_backend",
46
+ "presets",
47
+ "patterns",
48
+ ]
von/api.py ADDED
@@ -0,0 +1,83 @@
1
+ """High-level convenience API for Von."""
2
+
3
+ from typing import Any, Dict, List, Optional, Union
4
+ from .client import VonClient
5
+ from .types import (
6
+ Choice,
7
+ ChoiceAnswer,
8
+ Noul,
9
+ Question,
10
+ Score,
11
+ ScoreAnswer,
12
+ SystemOneResponse,
13
+ )
14
+
15
+ _default_client: Optional[VonClient] = None
16
+
17
+
18
+ def _get_default_client() -> VonClient:
19
+ global _default_client
20
+ if _default_client is None:
21
+ _default_client = VonClient(local=True)
22
+ return _default_client
23
+
24
+
25
+ def set_backend(backend: str):
26
+ """Set the underlying decision backend ('needle', 'modernbert', 'qwen0.5b')."""
27
+ from .engine import VonEngine
28
+ VonEngine.set_backend(backend)
29
+
30
+
31
+ def system_one(
32
+ state: Any,
33
+ questions: Dict[str, Union[Question, Dict[str, Any]]],
34
+ model: str = "von-latest",
35
+ client: Optional[VonClient] = None,
36
+ ) -> SystemOneResponse:
37
+ """Evaluate state and questions using System One."""
38
+ cli = client or _get_default_client()
39
+ return cli.system_one(state=state, questions=questions, model=model)
40
+
41
+
42
+ def decide(
43
+ state: Any,
44
+ choices: Union[List[str], Dict[str, Optional[str]]],
45
+ instructions: str = "Which option best describes the state?",
46
+ model: str = "von-latest",
47
+ ) -> ChoiceAnswer:
48
+ """Make a fast discrete decision among options."""
49
+ if isinstance(choices, list):
50
+ if len(choices) != len(set(choices)):
51
+ raise ValueError(f"Duplicate choices found in options list: {choices}")
52
+ criteria = {c: None for c in choices}
53
+ else:
54
+ criteria = choices
55
+
56
+ q = Choice(instructions=instructions, criteria=criteria)
57
+ resp = system_one(state=state, questions={"decision": q}, model=model)
58
+ return resp.answers["decision"] # type: ignore
59
+
60
+
61
+ def judge(
62
+ state: Any,
63
+ instructions: str,
64
+ criteria: Optional[Dict[str, str]] = None,
65
+ model: str = "von-latest",
66
+ ) -> float:
67
+ """Evaluate a yes/no question and return the probability (0.0 to 1.0)."""
68
+ q = Noul(instructions=instructions, criteria=criteria)
69
+ resp = system_one(state=state, questions={"judgment": q}, model=model)
70
+ answer = resp.answers["judgment"]
71
+ return getattr(answer, "noul", 0.0)
72
+
73
+
74
+ def rate(
75
+ state: Any,
76
+ criteria: List[Union[str, Dict[str, Any]]],
77
+ instructions: str = "Rate where the state falls on this scale:",
78
+ model: str = "von-latest",
79
+ ) -> ScoreAnswer:
80
+ """Evaluate a state on an ordered multi-level scale."""
81
+ q = Score(instructions=instructions, criteria=criteria)
82
+ resp = system_one(state=state, questions={"rating": q}, model=model)
83
+ return resp.answers["rating"] # type: ignore
@@ -0,0 +1,9 @@
1
+ """Backends package for Von."""
2
+
3
+ from .base import BaseBackend
4
+ from .berta_backend import BertaBackend
5
+
6
+ __all__ = [
7
+ "BaseBackend",
8
+ "BertaBackend",
9
+ ]
von/backends/base.py ADDED
@@ -0,0 +1,39 @@
1
+ """Abstract Base Class for Von Decision Backends."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, Union
5
+ from ..types import (
6
+ Choice,
7
+ ChoiceAnswer,
8
+ Noul,
9
+ NoulAnswer,
10
+ Question,
11
+ Score,
12
+ ScoreAnswer,
13
+ SystemOneResponse,
14
+ )
15
+
16
+
17
+ class BaseBackend(ABC):
18
+ """Base interface for all Von System One decision backends."""
19
+
20
+ @abstractmethod
21
+ def evaluate_choice(self, q_id: str, state_text: str, q: Choice) -> ChoiceAnswer:
22
+ pass
23
+
24
+ @abstractmethod
25
+ def evaluate_score(self, q_id: str, state_text: str, q: Score) -> ScoreAnswer:
26
+ pass
27
+
28
+ @abstractmethod
29
+ def evaluate_noul(self, q_id: str, state_text: str, q: Noul) -> NoulAnswer:
30
+ pass
31
+
32
+ @abstractmethod
33
+ def evaluate(
34
+ self,
35
+ state: Any,
36
+ questions: Dict[str, Union[Question, Dict[str, Any]]],
37
+ model: str,
38
+ ) -> SystemOneResponse:
39
+ pass
@@ -0,0 +1,328 @@
1
+ """Native bidirectional Transformer encoder backend for Von (BERT / DeBERTa family)."""
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ import torch
9
+
10
+ from ..types import (
11
+ Choice,
12
+ ChoiceAnswer,
13
+ Noul,
14
+ NoulAnswer,
15
+ Question,
16
+ Score,
17
+ ScoreAnswer,
18
+ SystemOneResponse,
19
+ Usage,
20
+ )
21
+ from .base import BaseBackend
22
+
23
+
24
+ MODEL_REGISTRY = {
25
+ "von-1.0": "checkpoints/von-modernbert-rlcd" if os.path.exists("checkpoints/von-modernbert-rlcd/config.json") else "wfzyx/von-1.0",
26
+ "modernbert": "checkpoints/von-modernbert-rlcd" if os.path.exists("checkpoints/von-modernbert-rlcd/config.json") else "wfzyx/von-1.0",
27
+ "deberta-v3": "MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli",
28
+ "deberta-xxl": "microsoft/deberta-v2-xxlarge-mnli",
29
+ }
30
+
31
+
32
+ def _format_state(state: Any) -> str:
33
+ if isinstance(state, str):
34
+ return state
35
+ try:
36
+ return json.dumps(state, indent=2, ensure_ascii=False)
37
+ except Exception:
38
+ return str(state)
39
+
40
+
41
+ def _detect_device(device_str: Optional[str] = None) -> torch.device:
42
+ d_str = (device_str or os.environ.get("VON_DEVICE", "auto")).lower().strip()
43
+
44
+ # Handle AMD ROCm / HIP aliases
45
+ if d_str in ("rocm", "hip"):
46
+ if not torch.cuda.is_available():
47
+ raise RuntimeError(
48
+ "AMD ROCm requested, but PyTorch CUDA/ROCm is not available. "
49
+ "Ensure PyTorch was installed with ROCm support."
50
+ )
51
+ return torch.device("cuda")
52
+
53
+ # Handle DirectML (Windows AMD / Intel)
54
+ if d_str in ("dml", "directml"):
55
+ try:
56
+ import torch_directml
57
+ return torch_directml.device()
58
+ except ImportError:
59
+ raise RuntimeError(
60
+ "DirectML requested, but 'torch-directml' is not installed. "
61
+ "Run 'pip install torch-directml'."
62
+ )
63
+
64
+ if d_str != "auto":
65
+ return torch.device(d_str)
66
+
67
+ # Auto-detection: First-party native accelerators only (CUDA/ROCm -> Apple Silicon MPS -> CPU).
68
+ # DirectML on Windows is supported via explicit opt-in (--device dml) to avoid third-party driver clashing.
69
+ if torch.cuda.is_available():
70
+ return torch.device("cuda")
71
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
72
+ return torch.device("mps")
73
+ return torch.device("cpu")
74
+
75
+
76
+ def get_device_description(device: torch.device) -> str:
77
+ if device.type == "cuda":
78
+ dev_name = torch.cuda.get_device_name(device) if torch.cuda.is_available() else "CUDA"
79
+ if getattr(torch.version, "hip", None) or any(w in dev_name.lower() for w in ["amd", "radeon", "instinct"]):
80
+ return f"AMD GPU [ROCm: {dev_name}]"
81
+ return f"NVIDIA GPU [CUDA: {dev_name}]"
82
+ elif device.type == "mps":
83
+ return "Apple Silicon [MPS]"
84
+ elif str(device).startswith("privateuseone"):
85
+ return "DirectML GPU [AMD/Intel]"
86
+ return "CPU"
87
+
88
+
89
+ class BertaBackend(BaseBackend):
90
+ """Native non-autoregressive decision engine powered by bidirectional BERT/DeBERTa encoders."""
91
+
92
+ def __init__(self, variant: str = "deberta-v3", device: Optional[str] = None):
93
+ var_clean = variant.lower().strip()
94
+ self.variant = var_clean
95
+ self.model_id = MODEL_REGISTRY.get(var_clean, variant)
96
+ self.device = _detect_device(device)
97
+ self._model = None
98
+ self._tokenizer = None
99
+ self._entail_idx = 0
100
+ self._lock = threading.Lock()
101
+
102
+ def _get_model_and_tok(self):
103
+ with self._lock:
104
+ if self._model is None or self._tokenizer is None:
105
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
106
+
107
+ dtype = torch.float32
108
+ if self.device.type == "cuda":
109
+ dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
110
+ elif self.device.type == "mps":
111
+ dtype = torch.float16
112
+
113
+ self._tokenizer = AutoTokenizer.from_pretrained(self.model_id)
114
+ self._model = AutoModelForSequenceClassification.from_pretrained(
115
+ self.model_id,
116
+ torch_dtype=dtype,
117
+ ).to(self.device).eval()
118
+
119
+ # Check for calibration.json if using local checkpoint
120
+ calib_path = os.path.join(self.model_id, "calibration.json")
121
+ if os.path.exists(calib_path):
122
+ try:
123
+ with open(calib_path, "r", encoding="utf-8") as f:
124
+ cdata = json.load(f)
125
+ self._default_temp = float(cdata.get("temperature", 1.0))
126
+ except Exception:
127
+ self._default_temp = 1.0
128
+ else:
129
+ self._default_temp = 1.0
130
+
131
+ # Detect entailment class index in id2label
132
+ id2label = getattr(self._model.config, "id2label", {})
133
+ for idx, lbl in id2label.items():
134
+ if "entail" in lbl.lower():
135
+ self._entail_idx = int(idx)
136
+ break
137
+ return self._model, self._tokenizer
138
+
139
+ def evaluate_choice(
140
+ self,
141
+ q_id: str,
142
+ state_text: str,
143
+ q: Choice,
144
+ temperature: float = 1.0,
145
+ **kwargs,
146
+ ) -> ChoiceAnswer:
147
+ options = list(q.criteria.keys())
148
+ if not options:
149
+ return ChoiceAnswer(choice="", probabilities={}, confidence=0.0)
150
+
151
+ model, tok = self._get_model_and_tok()
152
+
153
+ # Build (premise, hypothesis) pairs
154
+ hypotheses = []
155
+ for opt in options:
156
+ desc = q.criteria.get(opt)
157
+ text = f"{q.instructions} {desc}" if desc else f"{q.instructions} {opt}"
158
+ hypotheses.append(text)
159
+
160
+ premises = [state_text] * len(options)
161
+ inputs = tok(
162
+ premises,
163
+ hypotheses,
164
+ padding=True,
165
+ truncation=True,
166
+ max_length=512,
167
+ return_tensors="pt",
168
+ ).to(self.device)
169
+
170
+ with torch.no_grad():
171
+ logits = model(**inputs).logits
172
+ entail_scores = logits[:, self._entail_idx]
173
+ scaled = entail_scores / max(temperature, 1e-4)
174
+ probs = torch.softmax(scaled, dim=-1).cpu().tolist()
175
+
176
+ best_idx = int(torch.argmax(entail_scores).item())
177
+ best_choice = options[best_idx]
178
+
179
+ prob_dict = {opt: round(float(p), 4) for opt, p in zip(options, probs)}
180
+ sorted_p = sorted(prob_dict.values(), reverse=True)
181
+ confidence = round(max(0.0, min(1.0, sorted_p[0] - (sorted_p[1] if len(sorted_p) > 1 else 0.0))), 3)
182
+
183
+ return ChoiceAnswer(
184
+ choice=best_choice,
185
+ probabilities=prob_dict,
186
+ confidence=confidence,
187
+ )
188
+
189
+ def evaluate_score(
190
+ self,
191
+ q_id: str,
192
+ state_text: str,
193
+ q: Score,
194
+ temperature: float = 1.0,
195
+ **kwargs,
196
+ ) -> ScoreAnswer:
197
+ levels = q.criteria
198
+ if not levels:
199
+ return ScoreAnswer(score=0.0, confidence=0.0, legend={}, probabilities={})
200
+
201
+ model, tok = self._get_model_and_tok()
202
+
203
+ legend: Dict[str, str] = {}
204
+ hypotheses = []
205
+ for i, item in enumerate(levels):
206
+ idx_str = str(i)
207
+ if isinstance(item, dict):
208
+ what = item.get("what", "")
209
+ examples = item.get("examples", [])
210
+ ex_str = f" Examples: {', '.join(examples)}" if examples else ""
211
+ desc = f"{what}{ex_str}".strip()
212
+ else:
213
+ desc = str(item)
214
+ legend[idx_str] = desc
215
+ hypotheses.append(f"{q.instructions} Level {i}: {desc}")
216
+
217
+ premises = [state_text] * len(levels)
218
+ inputs = tok(
219
+ premises,
220
+ hypotheses,
221
+ padding=True,
222
+ truncation=True,
223
+ max_length=512,
224
+ return_tensors="pt",
225
+ ).to(self.device)
226
+
227
+ with torch.no_grad():
228
+ logits = model(**inputs).logits
229
+ entail_scores = logits[:, self._entail_idx]
230
+ scaled = entail_scores / max(temperature, 1e-4)
231
+ probs = torch.softmax(scaled, dim=-1).cpu().tolist()
232
+
233
+ prob_dict = {str(i): round(float(p), 4) for i, p in enumerate(probs)}
234
+ weighted_score = round(sum(i * p for i, p in enumerate(probs)), 2)
235
+
236
+ sorted_p = sorted(probs, reverse=True)
237
+ confidence = round(max(0.0, min(1.0, sorted_p[0] - (sorted_p[1] if len(sorted_p) > 1 else 0.0))), 3)
238
+
239
+ return ScoreAnswer(
240
+ score=weighted_score,
241
+ confidence=confidence,
242
+ legend=legend,
243
+ probabilities=prob_dict,
244
+ )
245
+
246
+ def evaluate_noul(
247
+ self,
248
+ q_id: str,
249
+ state_text: str,
250
+ q: Noul,
251
+ temperature: float = 1.0,
252
+ **kwargs,
253
+ ) -> NoulAnswer:
254
+ model, tok = self._get_model_and_tok()
255
+
256
+ crit = q.criteria or {}
257
+ pos_crit = crit.get("true", "")
258
+ neg_crit = crit.get("false", "")
259
+
260
+ pos_hyp = f"{q.instructions} {pos_crit or 'Condition holds true.'}".strip()
261
+ neg_hyp = f"{q.instructions} {neg_crit or 'Condition is false or not satisfied.'}".strip()
262
+
263
+ premises = [state_text, state_text]
264
+ hypotheses = [pos_hyp, neg_hyp]
265
+
266
+ inputs = tok(
267
+ premises,
268
+ hypotheses,
269
+ padding=True,
270
+ truncation=True,
271
+ max_length=512,
272
+ return_tensors="pt",
273
+ ).to(self.device)
274
+
275
+ with torch.no_grad():
276
+ logits = model(**inputs).logits
277
+ entail_scores = logits[:, self._entail_idx]
278
+ scaled = entail_scores / max(temperature, 1e-4)
279
+ probs = torch.softmax(scaled, dim=-1).cpu().tolist()
280
+
281
+ prob_true = round(max(0.0, min(1.0, probs[0])), 4)
282
+ return NoulAnswer(noul=prob_true)
283
+
284
+ def evaluate(
285
+ self,
286
+ state: Any,
287
+ questions: Dict[str, Union[Question, Dict[str, Any]]],
288
+ model: str = "von-latest",
289
+ ) -> SystemOneResponse:
290
+ state_str = _format_state(state)
291
+ answers: Dict[str, Union[NoulAnswer, ChoiceAnswer, ScoreAnswer]] = {}
292
+ total_q_chars = 0
293
+
294
+ for q_id, q_data in questions.items():
295
+ if isinstance(q_data, dict):
296
+ q_type = q_data.get("type")
297
+ if q_type == "noul":
298
+ q = Noul(**q_data)
299
+ elif q_type == "choice":
300
+ q = Choice(**q_data)
301
+ elif q_type == "score":
302
+ q = Score(**q_data)
303
+ else:
304
+ raise ValueError(f"Unknown question type: {q_type} for question '{q_id}'")
305
+ else:
306
+ q = q_data
307
+
308
+ total_q_chars += len(q.instructions)
309
+
310
+ if isinstance(q, Noul):
311
+ answers[q_id] = self.evaluate_noul(q_id, state_str, q)
312
+ elif isinstance(q, Choice):
313
+ answers[q_id] = self.evaluate_choice(q_id, state_str, q)
314
+ elif isinstance(q, Score):
315
+ answers[q_id] = self.evaluate_score(q_id, state_str, q)
316
+
317
+ input_tokens = max(1, (len(state_str) + total_q_chars) // 4)
318
+ output_tokens = len(answers) * 8
319
+ if model in ("von-latest", "von-preview", "von-1.0.0", "von-1.0", "jev-latest", "jev-preview", None):
320
+ resolved_model = "von-1.0.0"
321
+ else:
322
+ resolved_model = model
323
+
324
+ return SystemOneResponse(
325
+ model=resolved_model,
326
+ answers=answers,
327
+ usage=Usage(input_tokens=input_tokens, output_tokens=output_tokens),
328
+ )
von/cli.py ADDED
@@ -0,0 +1,190 @@
1
+ """CLI entrypoint for Von."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ import click
7
+ import uvicorn
8
+
9
+ from .api import decide as api_decide
10
+ from .api import judge as api_judge
11
+ from .api import rate as api_rate
12
+ from .api import system_one as api_system_one
13
+ from .backends.berta_backend import _detect_device, get_device_description
14
+
15
+
16
+ @click.group()
17
+ @click.version_option(version="1.0.0", prog_name="von")
18
+ def main():
19
+ """Von - Open Source System One Decision Model."""
20
+ pass
21
+
22
+
23
+ @main.command()
24
+ @click.option("--host", default="0.0.0.0", help="Host interface to bind on.")
25
+ @click.option("--port", default=8000, type=int, help="Port to listen on.")
26
+ @click.option("--backend", default="modernbert", type=click.Choice(["modernbert", "laya", "needle", "berta-v3"]), help="Decision backend to load.")
27
+ @click.option("--device", default="auto", help="Compute device: 'auto', 'cuda', 'rocm', 'mps', 'dml', 'cpu'.")
28
+ @click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
29
+ def serve(host: str, port: int, backend: str, device: str, reload: bool):
30
+ """Start the Von System One HTTP server."""
31
+ os.environ["VON_BACKEND"] = backend
32
+ if device and device != "auto":
33
+ os.environ["VON_DEVICE"] = device
34
+ dev_obj = _detect_device(device)
35
+ dev_desc = get_device_description(dev_obj)
36
+ click.echo(f"Starting Von Decision Server [{backend} on {dev_desc}] on http://{host}:{port}")
37
+ uvicorn.run("von.server:app", host=host, port=port, reload=reload)
38
+
39
+
40
+ @main.command()
41
+ @click.argument("text")
42
+ @click.option(
43
+ "-c",
44
+ "--choices",
45
+ required=True,
46
+ help="Comma-separated choices (e.g. 'billing,bug_report,feature_request').",
47
+ )
48
+ @click.option(
49
+ "-i",
50
+ "--instructions",
51
+ default="Which option best describes the input?",
52
+ help="Instructions for classification.",
53
+ )
54
+ @click.option(
55
+ "--device",
56
+ default="auto",
57
+ help="Compute device: 'auto', 'cuda', 'mps', 'cpu'.",
58
+ )
59
+ def decide(text: str, choices: str, instructions: str, device: str):
60
+ """Classify input text among discrete choices."""
61
+ if device and device != "auto":
62
+ os.environ["VON_DEVICE"] = device
63
+ opts = [c.strip() for c in choices.split(",") if c.strip()]
64
+ if not opts:
65
+ click.echo("Error: At least one choice must be provided.", err=True)
66
+ sys.exit(1)
67
+
68
+ ans = api_decide(state=text, choices=opts, instructions=instructions)
69
+ click.echo(
70
+ json.dumps(
71
+ {
72
+ "choice": ans.choice,
73
+ "confidence": ans.confidence,
74
+ "probabilities": ans.probabilities,
75
+ },
76
+ indent=2,
77
+ )
78
+ )
79
+
80
+
81
+ @main.command()
82
+ @click.argument("text")
83
+ @click.option(
84
+ "-i",
85
+ "--instructions",
86
+ required=True,
87
+ help="Boolean judgment question (e.g. 'Is the server down?').",
88
+ )
89
+ @click.option(
90
+ "--pos",
91
+ default="",
92
+ help="Explicit criteria description for True condition.",
93
+ )
94
+ @click.option(
95
+ "--neg",
96
+ default="",
97
+ help="Explicit criteria description for False condition.",
98
+ )
99
+ @click.option(
100
+ "--device",
101
+ default="auto",
102
+ help="Compute device: 'auto', 'cuda', 'mps', 'cpu'.",
103
+ )
104
+ def judge(text: str, instructions: str, pos: str, neg: str, device: str):
105
+ """Evaluate a yes/no judgment (Noul) and return the probability."""
106
+ if device and device != "auto":
107
+ os.environ["VON_DEVICE"] = device
108
+ crit = {}
109
+ if pos:
110
+ crit["true"] = pos
111
+ if neg:
112
+ crit["false"] = neg
113
+
114
+ prob = api_judge(state=text, instructions=instructions, criteria=crit or None)
115
+ click.echo(
116
+ json.dumps(
117
+ {
118
+ "type": "noul",
119
+ "instructions": instructions,
120
+ "noul": prob,
121
+ },
122
+ indent=2,
123
+ )
124
+ )
125
+
126
+
127
+ @main.command()
128
+ @click.argument("text")
129
+ @click.option(
130
+ "-l",
131
+ "--levels",
132
+ required=True,
133
+ help="Comma-separated descriptions of ordered levels from 0 to N-1.",
134
+ )
135
+ @click.option(
136
+ "-i",
137
+ "--instructions",
138
+ default="Rate where the state falls on this scale:",
139
+ help="Instructions for rating.",
140
+ )
141
+ @click.option(
142
+ "--device",
143
+ default="auto",
144
+ help="Compute device: 'auto', 'cuda', 'mps', 'cpu'.",
145
+ )
146
+ def rate(text: str, levels: str, instructions: str, device: str):
147
+ """Rate text on an ordered multi-level scale (Score)."""
148
+ if device and device != "auto":
149
+ os.environ["VON_DEVICE"] = device
150
+ lvl_list = [lvl.strip() for lvl in levels.split(",") if lvl.strip()]
151
+ if len(lvl_list) < 2:
152
+ click.echo("Error: At least two levels must be provided.", err=True)
153
+ sys.exit(1)
154
+
155
+ ans = api_rate(state=text, criteria=lvl_list, instructions=instructions)
156
+ click.echo(
157
+ json.dumps(
158
+ {
159
+ "type": "score",
160
+ "score": ans.score,
161
+ "confidence": ans.confidence,
162
+ "legend": ans.legend,
163
+ "probabilities": ans.probabilities,
164
+ },
165
+ indent=2,
166
+ )
167
+ )
168
+
169
+
170
+ @main.command()
171
+ @click.argument("request_file", type=click.Path(exists=True))
172
+ def eval(request_file: str):
173
+ """Evaluate a JSON request file containing state and questions."""
174
+ with open(request_file, "r", encoding="utf-8") as f:
175
+ data = json.load(f)
176
+
177
+ state = data.get("state")
178
+ questions = data.get("questions")
179
+ model = data.get("model", "von-latest")
180
+
181
+ if state is None or questions is None:
182
+ click.echo("Error: JSON must contain 'state' and 'questions' fields.", err=True)
183
+ sys.exit(1)
184
+
185
+ resp = api_system_one(state=state, questions=questions, model=model)
186
+ click.echo(json.dumps(resp.model_dump(), indent=2))
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()