dsh-router-laya 2.1.0
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.
- package/LICENSE +176 -0
- package/NOTICE +19 -0
- package/README.md +108 -0
- package/bin/setup.mjs +297 -0
- package/client.js +436 -0
- package/cordis.patch.yml +9 -0
- package/index.js +870 -0
- package/install.mjs +38 -0
- package/package.json +49 -0
- package/service/finetuned_judge.py +324 -0
- package/service/intent_parser.py +1012 -0
- package/service/laya/__init__.py +51 -0
- package/service/laya/agent.py +447 -0
- package/service/laya/common.py +280 -0
- package/service/laya/email.py +90 -0
- package/service/laya/lang.py +324 -0
- package/service/laya/presets.py +187 -0
- package/service/laya/pyproject.toml +38 -0
- package/service/laya/router.py +447 -0
- package/service/laya_router.py +320 -0
- package/service/requirements.lock.txt +27 -0
- package/service/start_router.ps1 +102 -0
- package/service/start_router.sh +144 -0
- package/weights/fetch.mjs +219 -0
- package/weights/manifest.json +36 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Laya: Fast, non-autoregressive System 1 decision engine with calibrated probabilities."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent, RLAgent, load
|
|
4
|
+
from .common import (
|
|
5
|
+
QTYPES,
|
|
6
|
+
QTYPE_NAMES,
|
|
7
|
+
confidence_from_probs,
|
|
8
|
+
ece_score,
|
|
9
|
+
proper_reward,
|
|
10
|
+
render_options,
|
|
11
|
+
td_lambda_targets,
|
|
12
|
+
)
|
|
13
|
+
from .email import clean_email_body, email_state
|
|
14
|
+
from .lang import analyse as detect_language
|
|
15
|
+
from .lang import detect_script, is_english
|
|
16
|
+
from .presets import (
|
|
17
|
+
email_questions,
|
|
18
|
+
guard_questions,
|
|
19
|
+
moderation_questions,
|
|
20
|
+
router_questions,
|
|
21
|
+
triage_questions,
|
|
22
|
+
)
|
|
23
|
+
from .router import DEFAULT_MODELS, RouteDecision, Router
|
|
24
|
+
|
|
25
|
+
__version__ = "0.3.7"
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Agent",
|
|
28
|
+
"RLAgent",
|
|
29
|
+
"load",
|
|
30
|
+
"Router",
|
|
31
|
+
"RouteDecision",
|
|
32
|
+
"DEFAULT_MODELS",
|
|
33
|
+
"detect_language",
|
|
34
|
+
"detect_script",
|
|
35
|
+
"is_english",
|
|
36
|
+
"clean_email_body",
|
|
37
|
+
"email_questions",
|
|
38
|
+
"email_state",
|
|
39
|
+
"guard_questions",
|
|
40
|
+
"moderation_questions",
|
|
41
|
+
"router_questions",
|
|
42
|
+
"triage_questions",
|
|
43
|
+
"proper_reward",
|
|
44
|
+
"td_lambda_targets",
|
|
45
|
+
"ece_score",
|
|
46
|
+
"confidence_from_probs",
|
|
47
|
+
"render_options",
|
|
48
|
+
"QTYPES",
|
|
49
|
+
"QTYPE_NAMES",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""High-level inference runtime for laya System 1 decision models."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Any, Dict, Optional, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from .common import (
|
|
11
|
+
QTYPES,
|
|
12
|
+
TEMP_MAX,
|
|
13
|
+
TEMP_MIN,
|
|
14
|
+
amp_dtype,
|
|
15
|
+
build_model,
|
|
16
|
+
build_sequence,
|
|
17
|
+
clamp_temperature,
|
|
18
|
+
collate_items,
|
|
19
|
+
confidence_from_probs,
|
|
20
|
+
render_options,
|
|
21
|
+
temp_bucket,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _fix_tokenizer_config(path: str):
|
|
26
|
+
"""Ensure tokenizer_config.json can be loaded across all transformers versions."""
|
|
27
|
+
cfg_file = os.path.join(path, "tokenizer", "tokenizer_config.json")
|
|
28
|
+
if not os.path.exists(cfg_file):
|
|
29
|
+
return
|
|
30
|
+
try:
|
|
31
|
+
with open(cfg_file) as f:
|
|
32
|
+
tcfg = json.load(f)
|
|
33
|
+
changed = False
|
|
34
|
+
if tcfg.get("tokenizer_class") in (None, "TokenizersBackend"):
|
|
35
|
+
tcfg["tokenizer_class"] = "PreTrainedTokenizerFast"
|
|
36
|
+
tcfg.pop("backend", None)
|
|
37
|
+
tcfg.pop("is_local", None)
|
|
38
|
+
changed = True
|
|
39
|
+
# Checkpoints built on the mmBERT/Gemma tokenizer store extra_special_tokens as a list;
|
|
40
|
+
# transformers expects a mapping and raises "'list' object has no attribute 'keys'",
|
|
41
|
+
# which makes AutoTokenizer -- and so the whole model -- fail to load.
|
|
42
|
+
extra = tcfg.get("extra_special_tokens")
|
|
43
|
+
if isinstance(extra, list):
|
|
44
|
+
tcfg["extra_special_tokens"] = {"extra_%d" % i: t for i, t in enumerate(extra)}
|
|
45
|
+
changed = True
|
|
46
|
+
if changed:
|
|
47
|
+
with open(cfg_file, "w") as f:
|
|
48
|
+
json.dump(tcfg, f, indent=2)
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _verify_compatibility(model: torch.nn.Module, cfg: Dict, weights: Dict[str, torch.Tensor], model_id: str):
|
|
54
|
+
"""Verify that the loaded checkpoint weights and config strictly match the expected architecture."""
|
|
55
|
+
# 1. Verify required configuration attributes
|
|
56
|
+
required_cfg = ["encoder", "head_layers"]
|
|
57
|
+
missing_cfg = [k for k in required_cfg if k not in cfg]
|
|
58
|
+
if missing_cfg:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Incompatible model config for {model_id!r}: missing configuration keys {missing_cfg}. "
|
|
61
|
+
f"Ensure this is a valid RL Agent decision model."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 2. Check for required component prefixes
|
|
65
|
+
required_prefixes = ("encoder.", "type_emb.", "scorer.", "act_head.")
|
|
66
|
+
for prefix in required_prefixes:
|
|
67
|
+
if not any(k.startswith(prefix) for k in weights.keys()):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Incompatible model weights for {model_id!r}: checkpoint is missing '{prefix}' parameters. "
|
|
70
|
+
f"Expected an RL Agent decision model with encoder and decision heads."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 3. Check for parameter shape mismatches
|
|
74
|
+
model_sd = model.state_dict()
|
|
75
|
+
shape_mismatches = []
|
|
76
|
+
missing_keys = []
|
|
77
|
+
|
|
78
|
+
for name, param in model.named_parameters():
|
|
79
|
+
if name not in weights:
|
|
80
|
+
missing_keys.append(name)
|
|
81
|
+
elif tuple(weights[name].shape) != tuple(param.shape):
|
|
82
|
+
shape_mismatches.append(f" - {name}: expected {tuple(param.shape)}, found {tuple(weights[name].shape)}")
|
|
83
|
+
|
|
84
|
+
if shape_mismatches:
|
|
85
|
+
err_details = "\n".join(shape_mismatches[:5])
|
|
86
|
+
if len(shape_mismatches) > 5:
|
|
87
|
+
err_details += f"\n ... and {len(shape_mismatches) - 5} more mismatched layers."
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Model architecture mismatch for {model_id!r}:\n{err_details}\n"
|
|
90
|
+
f"The checkpoint weights do not match the configured model architecture."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if missing_keys:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Model weights incomplete for {model_id!r}: missing {len(missing_keys)} parameter tensors "
|
|
96
|
+
f"(e.g. {missing_keys[:3]})."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Agent:
|
|
101
|
+
"""System 1 decision model runtime: fast, non-autoregressive, calibrated decisions."""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
model_id_or_path: str = "convaiinnovations/laya",
|
|
106
|
+
device: Optional[str] = None,
|
|
107
|
+
token: Optional[str] = None,
|
|
108
|
+
subfolder: Optional[str] = None,
|
|
109
|
+
):
|
|
110
|
+
"""Load a Laya checkpoint.
|
|
111
|
+
|
|
112
|
+
`subfolder` selects one checkpoint from a repo that bundles several, e.g.
|
|
113
|
+
`Agent("convaiinnovations/laya", subfolder="multilingual")`. Only that subfolder is
|
|
114
|
+
downloaded, so bundling does not cost every user the whole family.
|
|
115
|
+
"""
|
|
116
|
+
from safetensors.torch import load_file
|
|
117
|
+
from transformers import AutoTokenizer
|
|
118
|
+
try:
|
|
119
|
+
from transformers.initialization import no_init_weights
|
|
120
|
+
except ImportError: # Transformers 4.x
|
|
121
|
+
from transformers.modeling_utils import no_init_weights
|
|
122
|
+
|
|
123
|
+
model_dir = model_id_or_path
|
|
124
|
+
if not os.path.exists(model_dir):
|
|
125
|
+
if model_id_or_path.startswith(("/", "./", "../")) or os.path.isabs(model_id_or_path):
|
|
126
|
+
raise FileNotFoundError(
|
|
127
|
+
f"Local model path not found: {model_id_or_path!r}. "
|
|
128
|
+
f"Check that the directory exists and that training saved the model successfully."
|
|
129
|
+
)
|
|
130
|
+
from huggingface_hub import snapshot_download
|
|
131
|
+
|
|
132
|
+
# Restrict root checkpoints too: the default repo also contains sibling
|
|
133
|
+
# checkpoints, which an unfiltered snapshot would unnecessarily download.
|
|
134
|
+
prefix = f"{subfolder}/" if subfolder else ""
|
|
135
|
+
kw = {
|
|
136
|
+
"token": token or os.environ.get("HF_TOKEN"),
|
|
137
|
+
"allow_patterns": [prefix + name for name in (
|
|
138
|
+
"rl_agent_config.json", "model.safetensors", "tokenizer/*", "encoder/*",
|
|
139
|
+
)],
|
|
140
|
+
}
|
|
141
|
+
model_dir = snapshot_download(model_id_or_path, **kw)
|
|
142
|
+
|
|
143
|
+
if subfolder:
|
|
144
|
+
model_dir = os.path.join(model_dir, subfolder)
|
|
145
|
+
if not os.path.isdir(model_dir):
|
|
146
|
+
raise FileNotFoundError(
|
|
147
|
+
f"Subfolder {subfolder!r} not found in {model_id_or_path!r}."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
_fix_tokenizer_config(model_dir)
|
|
151
|
+
|
|
152
|
+
cfg_path = os.path.join(model_dir, "rl_agent_config.json")
|
|
153
|
+
if not os.path.exists(cfg_path):
|
|
154
|
+
raise FileNotFoundError(
|
|
155
|
+
f"Incompatible model: {model_id_or_path!r} does not contain 'rl_agent_config.json'. "
|
|
156
|
+
f"Make sure you are loading a compatible RL Agent model (e.g. 'convaiinnovations/rl-agent')."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
with open(cfg_path) as f:
|
|
160
|
+
self.cfg = json.load(f)
|
|
161
|
+
|
|
162
|
+
weights_path = os.path.join(model_dir, "model.safetensors")
|
|
163
|
+
if not os.path.exists(weights_path):
|
|
164
|
+
raise FileNotFoundError(
|
|
165
|
+
f"Incompatible model: 'model.safetensors' not found in {model_id_or_path!r}."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 1. Device resolution with automatic fallback
|
|
169
|
+
if device is not None:
|
|
170
|
+
target_device = torch.device(device)
|
|
171
|
+
if target_device.type == "cuda" and not torch.cuda.is_available():
|
|
172
|
+
print("Warning: CUDA requested but not available. Falling back to CPU.")
|
|
173
|
+
self.device = torch.device("cpu")
|
|
174
|
+
elif target_device.type == "mps" and not (hasattr(torch.backends, "mps") and torch.backends.mps.is_available()):
|
|
175
|
+
print("Warning: MPS requested but not available. Falling back to CPU.")
|
|
176
|
+
self.device = torch.device("cpu")
|
|
177
|
+
else:
|
|
178
|
+
self.device = target_device
|
|
179
|
+
else:
|
|
180
|
+
if torch.cuda.is_available():
|
|
181
|
+
self.device = torch.device("cuda")
|
|
182
|
+
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
183
|
+
self.device = torch.device("mps")
|
|
184
|
+
else:
|
|
185
|
+
self.device = torch.device("cpu")
|
|
186
|
+
|
|
187
|
+
tok_dir = os.path.join(model_dir, "tokenizer")
|
|
188
|
+
self.tok = AutoTokenizer.from_pretrained(tok_dir if os.path.exists(tok_dir) else self.cfg.get("encoder"))
|
|
189
|
+
|
|
190
|
+
enc_dir = os.path.join(model_dir, "encoder")
|
|
191
|
+
# The checkpoint supplies every parameter; skip random/base-model weights.
|
|
192
|
+
with no_init_weights():
|
|
193
|
+
self.model = build_model(self.cfg, encoder_dir=enc_dir if os.path.exists(enc_dir) else None,
|
|
194
|
+
pretrained=False)
|
|
195
|
+
|
|
196
|
+
# Load weights and verify architectural compatibility
|
|
197
|
+
weights = load_file(weights_path)
|
|
198
|
+
_verify_compatibility(self.model, self.cfg, weights, model_id_or_path)
|
|
199
|
+
|
|
200
|
+
self.model.load_state_dict(weights, strict=True)
|
|
201
|
+
|
|
202
|
+
# ModernBERT's reference_compile defaults to "auto" and will torch.compile the encoder.
|
|
203
|
+
# That is a loss for the batch sizes Laya runs (a handful of questions per call) and can
|
|
204
|
+
# hang on some platforms, so keep the eager path.
|
|
205
|
+
try:
|
|
206
|
+
self.model.encoder.config.reference_compile = False
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
# Keep what the checkpoint shipped for inspection, but only ever apply clamped values:
|
|
211
|
+
# some buckets are fitted to sharpen rather than soften (see clamp_temperature).
|
|
212
|
+
self.temperature_raw = self.cfg.get("temperature", [1.0, 1.0, 1.0])
|
|
213
|
+
self.temperature_by_options_raw = self.cfg.get("temperature_by_options", {})
|
|
214
|
+
self.temperature = [clamp_temperature(t) for t in self.temperature_raw]
|
|
215
|
+
self.temperature_by_options = {k: clamp_temperature(v)
|
|
216
|
+
for k, v in self.temperature_by_options_raw.items()}
|
|
217
|
+
entries = [(k, v, self.temperature_by_options[k]) for k, v in self.temperature_by_options_raw.items()]
|
|
218
|
+
entries += [("temperature[%d]" % i, t, self.temperature[i]) for i, t in enumerate(self.temperature_raw)]
|
|
219
|
+
rejected = []
|
|
220
|
+
for name, raw, applied in entries:
|
|
221
|
+
try:
|
|
222
|
+
if float(raw) == applied:
|
|
223
|
+
continue
|
|
224
|
+
except (TypeError, ValueError):
|
|
225
|
+
# Invalid entries already have a neutral fallback; diagnostics must not
|
|
226
|
+
# repeat the failed conversion or prevent the checkpoint from loading.
|
|
227
|
+
pass
|
|
228
|
+
rejected.append("%s=%r -> %g" % (name, raw, applied))
|
|
229
|
+
if rejected:
|
|
230
|
+
warnings.warn(
|
|
231
|
+
"laya: this checkpoint ships invalid temperatures or values outside [%g, %g]; "
|
|
232
|
+
"using %s. Treat confidence from the affected entries as uncalibrated."
|
|
233
|
+
% (TEMP_MIN, TEMP_MAX, ", ".join(rejected)),
|
|
234
|
+
RuntimeWarning, stacklevel=2)
|
|
235
|
+
self.dtype = amp_dtype(self.cfg.get("amp_dtype", "fp16"))
|
|
236
|
+
|
|
237
|
+
if self.device.type == "cuda" and torch.cuda.get_device_capability(self.device)[0] < 8:
|
|
238
|
+
self.dtype = torch.float16
|
|
239
|
+
elif self.device.type in ("cpu", "mps"):
|
|
240
|
+
self.dtype = torch.float32
|
|
241
|
+
|
|
242
|
+
# 2. Place on device with graceful fallback to CPU on memory error
|
|
243
|
+
fell_back_from = fell_back_why = None
|
|
244
|
+
try:
|
|
245
|
+
self.model.to(self.device).eval()
|
|
246
|
+
except (RuntimeError, torch.cuda.OutOfMemoryError) as e:
|
|
247
|
+
if self.device.type != "cpu":
|
|
248
|
+
# Record what actually went wrong: the reason matters more than the symptom,
|
|
249
|
+
# and it is the only place the underlying exception is ever surfaced.
|
|
250
|
+
fell_back_from, fell_back_why = self.device, e
|
|
251
|
+
self.device = torch.device("cpu")
|
|
252
|
+
self.dtype = torch.float32
|
|
253
|
+
self.model.to(self.device).eval()
|
|
254
|
+
else:
|
|
255
|
+
raise e
|
|
256
|
+
|
|
257
|
+
if fell_back_from is not None:
|
|
258
|
+
print(
|
|
259
|
+
"\n[laya] Warning: could not place the model on %s, so it is running on CPU.\n"
|
|
260
|
+
" Reason: %s\n"
|
|
261
|
+
" Inference will be roughly 10-15x slower (~200-500 ms rather than ~35 ms).\n"
|
|
262
|
+
" If this is a newer NVIDIA GPU (Blackwell / RTX 50-series), your PyTorch build\n"
|
|
263
|
+
" may not support its CUDA architecture:\n"
|
|
264
|
+
" pip install --pre torch --index-url https://download.pytorch.org/whl/nightly/cu128\n"
|
|
265
|
+
" See https://pytorch.org/get-started/locally/\n"
|
|
266
|
+
% (fell_back_from, fell_back_why), flush=True)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def _check_question(qid: str, qdef: Any) -> None:
|
|
270
|
+
"""Reject a question that cannot be answered, naming it and what to fix.
|
|
271
|
+
|
|
272
|
+
`render_options` reads `criteria` in the shape the question's type expects and the decision
|
|
273
|
+
head needs at least one option, so a malformed definition used to surface from three frames
|
|
274
|
+
down as something that names neither the question nor the problem: `AttributeError:
|
|
275
|
+
'NoneType' object has no attribute 'items'`, `KeyError: 'bool'`, or a `selected index k out
|
|
276
|
+
of range` raised inside the model for a question that ended up with no options at all.
|
|
277
|
+
"""
|
|
278
|
+
if not isinstance(qdef, dict):
|
|
279
|
+
raise ValueError("question %r: definition must be a dict, got %s"
|
|
280
|
+
% (qid, type(qdef).__name__))
|
|
281
|
+
t = qdef.get("type")
|
|
282
|
+
if t not in QTYPES:
|
|
283
|
+
raise ValueError("question %r: unknown type %r; use one of %s" % (qid, t, sorted(QTYPES)))
|
|
284
|
+
if "instructions" not in qdef:
|
|
285
|
+
raise ValueError("question %r: no 'instructions'; add the text the model should answer" % (qid,))
|
|
286
|
+
crit = qdef.get("criteria")
|
|
287
|
+
if t == "choice":
|
|
288
|
+
if not isinstance(crit, (dict, list)):
|
|
289
|
+
raise ValueError("question %r: a choice question takes 'criteria' as a dict of "
|
|
290
|
+
"label -> description, or a list of labels" % (qid,))
|
|
291
|
+
if not crit:
|
|
292
|
+
raise ValueError("question %r: a choice question needs at least one criterion" % (qid,))
|
|
293
|
+
elif t == "score":
|
|
294
|
+
if not isinstance(crit, list):
|
|
295
|
+
raise ValueError("question %r: a score question takes 'criteria' as a list of level "
|
|
296
|
+
"descriptions, index 0 first" % (qid,))
|
|
297
|
+
if not crit:
|
|
298
|
+
raise ValueError("question %r: a score question needs at least one level" % (qid,))
|
|
299
|
+
elif crit is not None and not isinstance(crit, dict):
|
|
300
|
+
raise ValueError("question %r: a noul question takes 'criteria' as a dict with optional "
|
|
301
|
+
"'true'/'false' descriptions, or omits it" % (qid,))
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _to_internal(qdef: Dict) -> Dict:
|
|
305
|
+
t = qdef["type"]
|
|
306
|
+
crit = qdef.get("criteria")
|
|
307
|
+
if t == "choice" and isinstance(crit, list):
|
|
308
|
+
crit = {c: None for c in crit}
|
|
309
|
+
elif t == "noul" and isinstance(crit, dict):
|
|
310
|
+
# Align upstream 0.3.7: normalise keys so render_options'
|
|
311
|
+
# crit.get("false")/get("true") can see boolean or mixed-case criteria.
|
|
312
|
+
crit = {str(k).lower(): v for k, v in crit.items()}
|
|
313
|
+
ins = qdef["instructions"]
|
|
314
|
+
if not isinstance(ins, str):
|
|
315
|
+
ins = json.dumps(ins)
|
|
316
|
+
return {"t": t, "ins": ins, "crit": crit}
|
|
317
|
+
|
|
318
|
+
@torch.no_grad()
|
|
319
|
+
def system_one(self, state: Union[str, dict, list], questions: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
|
320
|
+
"""Evaluate typed questions across state in a single, parallel forward pass.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
state: Text string, JSON dict, or conversation turn list.
|
|
324
|
+
questions: Dictionary mapping question_id -> question definition.
|
|
325
|
+
- choice: {"type": "choice", "instructions": "...", "criteria": {"optA": "...", ...}}
|
|
326
|
+
- score: {"type": "score", "instructions": "...", "criteria": ["lvl0", "lvl1", ...]}
|
|
327
|
+
- noul: {"type": "noul", "instructions": "..."}
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Dictionary with answers, probabilities, calibrated confidence, and token usage.
|
|
331
|
+
Empty questions return empty answers and zero token usage without tokenization
|
|
332
|
+
or a model forward pass.
|
|
333
|
+
"""
|
|
334
|
+
ids = list(questions.keys())
|
|
335
|
+
if not ids:
|
|
336
|
+
return {
|
|
337
|
+
"model": "laya-rl-agent",
|
|
338
|
+
"answers": {},
|
|
339
|
+
"usage": {"input_tokens": 0, "output_tokens": 0},
|
|
340
|
+
}
|
|
341
|
+
items = []
|
|
342
|
+
max_len = self.cfg.get("max_len", 512)
|
|
343
|
+
head_max_len = self.cfg.get("head_max_len", 192)
|
|
344
|
+
|
|
345
|
+
for qid in ids:
|
|
346
|
+
self._check_question(qid, questions[qid])
|
|
347
|
+
q = self._to_internal(questions[qid])
|
|
348
|
+
seq, markers = build_sequence(self.tok, state, q, max_len, head_max_len)
|
|
349
|
+
if len(markers) != len(render_options(q)):
|
|
350
|
+
raise ValueError("question %r options exceed head_max_len=%d" % (qid, head_max_len))
|
|
351
|
+
items.append({"ids": seq, "markers": markers, "qtype": QTYPES[q["t"]]})
|
|
352
|
+
|
|
353
|
+
b = collate_items([items], self.tok.pad_token_id)
|
|
354
|
+
use_amp = self.device.type == "cuda"
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
with torch.autocast(device_type=self.device.type, dtype=self.dtype, enabled=use_amp):
|
|
358
|
+
logits, act = self.model(
|
|
359
|
+
b["input_ids"].to(self.device),
|
|
360
|
+
b["attention_mask"].to(self.device),
|
|
361
|
+
b["marker_pos"].to(self.device),
|
|
362
|
+
b["marker_mask"].to(self.device),
|
|
363
|
+
b["qtype"].to(self.device),
|
|
364
|
+
)
|
|
365
|
+
except (RuntimeError, torch.cuda.OutOfMemoryError) as e:
|
|
366
|
+
if self.device.type != "cpu" and ("memory" in str(e).lower() or "cuda" in str(e).lower()):
|
|
367
|
+
print("Warning: GPU memory exceeded during inference. Falling back to CPU...")
|
|
368
|
+
self.device = torch.device("cpu")
|
|
369
|
+
self.dtype = torch.float32
|
|
370
|
+
self.model.to(self.device)
|
|
371
|
+
logits, act = self.model(
|
|
372
|
+
b["input_ids"].to(self.device),
|
|
373
|
+
b["attention_mask"].to(self.device),
|
|
374
|
+
b["marker_pos"].to(self.device),
|
|
375
|
+
b["marker_mask"].to(self.device),
|
|
376
|
+
b["qtype"].to(self.device),
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
raise e
|
|
380
|
+
|
|
381
|
+
logits = logits.float().cpu().numpy()
|
|
382
|
+
act = torch.softmax(act.float(), -1).cpu().numpy()
|
|
383
|
+
|
|
384
|
+
answers = {}
|
|
385
|
+
n_tokens = int(b["attention_mask"].sum())
|
|
386
|
+
|
|
387
|
+
for r, qid in enumerate(ids):
|
|
388
|
+
q = self._to_internal(questions[qid])
|
|
389
|
+
k = len(items[r]["markers"])
|
|
390
|
+
qt = QTYPES[q["t"]]
|
|
391
|
+
t_scale = self.temperature_by_options.get(temp_bucket(qt, k), self.temperature[qt])
|
|
392
|
+
z = logits[r, :k] / t_scale
|
|
393
|
+
p = np.exp(z - z.max())
|
|
394
|
+
p = p / p.sum()
|
|
395
|
+
|
|
396
|
+
conf_score = round(confidence_from_probs(p, k), 4)
|
|
397
|
+
ext = {"act_probability": round(float(act[r, 0]), 4)}
|
|
398
|
+
|
|
399
|
+
if q["t"] == "choice":
|
|
400
|
+
keys = list(q["crit"].keys())
|
|
401
|
+
answers[qid] = {
|
|
402
|
+
"type": "choice",
|
|
403
|
+
"choice": keys[int(p.argmax())],
|
|
404
|
+
"probabilities": {kk: round(float(v), 4) for kk, v in zip(keys, p)},
|
|
405
|
+
"confidence": conf_score,
|
|
406
|
+
"action": ext,
|
|
407
|
+
}
|
|
408
|
+
elif q["t"] == "score":
|
|
409
|
+
exp_score = float((np.arange(k) * p).sum())
|
|
410
|
+
answers[qid] = {
|
|
411
|
+
"type": "score",
|
|
412
|
+
"score": round(exp_score, 4),
|
|
413
|
+
"legend": {str(i): c for i, c in enumerate(q["crit"])},
|
|
414
|
+
"probabilities": {str(i): round(float(v), 4) for i, v in enumerate(p)},
|
|
415
|
+
"confidence": conf_score,
|
|
416
|
+
"action": ext,
|
|
417
|
+
}
|
|
418
|
+
else:
|
|
419
|
+
answers[qid] = {
|
|
420
|
+
"type": "noul",
|
|
421
|
+
"noul": round(float(p[1]), 4),
|
|
422
|
+
"confidence": round(max(float(p[1]), 1.0 - float(p[1])), 4),
|
|
423
|
+
"action": ext,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
"model": "laya-rl-agent",
|
|
428
|
+
"answers": answers,
|
|
429
|
+
"usage": {"input_tokens": n_tokens, "output_tokens": 0},
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
predict = system_one
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
RLAgent = Agent
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def load(model_id_or_path: str = "convaiinnovations/laya", device: Optional[str] = None,
|
|
439
|
+
token: Optional[str] = None, subfolder: Optional[str] = None) -> Agent:
|
|
440
|
+
"""Load a Laya agent.
|
|
441
|
+
|
|
442
|
+
`subfolder` picks one checkpoint out of a repo that bundles several:
|
|
443
|
+
|
|
444
|
+
laya.load("convaiinnovations/laya") # English (repo root)
|
|
445
|
+
laya.load("convaiinnovations/laya", subfolder="multilingual")
|
|
446
|
+
"""
|
|
447
|
+
return Agent(model_id_or_path, device=device, token=token, subfolder=subfolder)
|