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 +48 -0
- von/api.py +83 -0
- von/backends/__init__.py +9 -0
- von/backends/base.py +39 -0
- von/backends/berta_backend.py +328 -0
- von/cli.py +190 -0
- von/client.py +113 -0
- von/engine.py +98 -0
- von/patterns.py +182 -0
- von/presets.py +129 -0
- von/server.py +81 -0
- von/types.py +89 -0
- von_sdk-1.0.0.dist-info/METADATA +17 -0
- von_sdk-1.0.0.dist-info/RECORD +17 -0
- von_sdk-1.0.0.dist-info/WHEEL +4 -0
- von_sdk-1.0.0.dist-info/entry_points.txt +2 -0
- von_sdk-1.0.0.dist-info/licenses/LICENSE.md +69 -0
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
|
von/backends/__init__.py
ADDED
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()
|