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,447 @@
|
|
|
1
|
+
"""Route a request to the Laya checkpoint best suited to it.
|
|
2
|
+
|
|
3
|
+
Three checkpoints, measured on a shared benchmark (17,416 questions, one T4, identical questions
|
|
4
|
+
per model -- see the repository's benchmark notebook):
|
|
5
|
+
|
|
6
|
+
english convaiinnovations/laya 421M ModernBERT-large, 512 tokens
|
|
7
|
+
multilingual convaiinnovations/laya-multilingual 322M mmBERT-base, 1024 tokens, 100+ langs
|
|
8
|
+
typed-decisions convaiinnovations/laya-typed-decisions 421M ModernBERT-large, 1024 tokens,
|
|
9
|
+
fine-tuned on the typed-decisions
|
|
10
|
+
workflows
|
|
11
|
+
|
|
12
|
+
Why routing is worth it -- accuracy by language family:
|
|
13
|
+
|
|
14
|
+
english multilingual
|
|
15
|
+
MASSIVE intent en 0.783 0.657 <- English checkpoint wins
|
|
16
|
+
MASSIVE intent non-en 0.306 0.451
|
|
17
|
+
XNLI en 0.860 0.843
|
|
18
|
+
XNLI non-en 0.521 0.731 <- +21 points for multilingual
|
|
19
|
+
English suites 0.684 0.619
|
|
20
|
+
|
|
21
|
+
The English checkpoint does not gently degrade off English, it collapses: on 20-option MASSIVE
|
|
22
|
+
intent it scores 0.100 on Hindi and 0.103 on Korean, against 0.050 for random guessing -- and it
|
|
23
|
+
reports high confidence while doing so (ECE 0.855 on Hindi). Script detection is therefore the
|
|
24
|
+
primary routing signal.
|
|
25
|
+
|
|
26
|
+
`typed-decisions` is never selected automatically unless you opt in with
|
|
27
|
+
`auto_task_detection=True` or pass `task="typed_decisions"`: it is fine-tuned on four specific
|
|
28
|
+
synthetic workflows and should not be a silent default.
|
|
29
|
+
"""
|
|
30
|
+
import gc
|
|
31
|
+
import os
|
|
32
|
+
import threading
|
|
33
|
+
from typing import Any, Dict, List, Optional, Union
|
|
34
|
+
|
|
35
|
+
from .lang import analyse
|
|
36
|
+
|
|
37
|
+
# The hub repo bundles all three checkpoints; only the requested subfolder is downloaded.
|
|
38
|
+
BUNDLE_REPO = "convaiinnovations/laya"
|
|
39
|
+
DEFAULT_MODELS = {
|
|
40
|
+
"english": (BUNDLE_REPO, None),
|
|
41
|
+
"multilingual": (BUNDLE_REPO, "multilingual"),
|
|
42
|
+
"typed-decisions": (BUNDLE_REPO, "typed-decisions"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# The same checkpoints also live in their own repos, for anyone who prefers them.
|
|
46
|
+
STANDALONE_MODELS = {
|
|
47
|
+
"english": "convaiinnovations/laya",
|
|
48
|
+
"multilingual": "convaiinnovations/laya-multilingual",
|
|
49
|
+
"typed-decisions": "convaiinnovations/laya-typed-decisions",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _repo_str(spec):
|
|
54
|
+
"""Human-readable id for a model spec: 'repo' or 'repo/subfolder'."""
|
|
55
|
+
repo, sub = _split(spec)
|
|
56
|
+
return "%s/%s" % (repo, sub) if sub else repo
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _split(spec):
|
|
60
|
+
"""Normalise a model spec to (repo_or_path, subfolder)."""
|
|
61
|
+
if isinstance(spec, (tuple, list)):
|
|
62
|
+
repo, sub = (list(spec) + [None])[:2]
|
|
63
|
+
return repo, sub
|
|
64
|
+
return spec, None
|
|
65
|
+
|
|
66
|
+
# Aliases people are likely to type.
|
|
67
|
+
_ALIASES = {
|
|
68
|
+
"en": "english", "laya": "english", "default": "english",
|
|
69
|
+
"multi": "multilingual", "ml": "multilingual", "laya-multilingual": "multilingual",
|
|
70
|
+
"typed": "typed-decisions", "typed_decisions": "typed-decisions",
|
|
71
|
+
"laya-typed-decisions": "typed-decisions", "decisions": "typed-decisions",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Question-id signatures of the four typed-decisions workflows, used only when
|
|
75
|
+
# auto_task_detection is enabled.
|
|
76
|
+
_TYPED_DECISION_WORKFLOWS = {
|
|
77
|
+
"agent_trace_observability": {"action", "needs_review", "outcome", "risk", "urgency"},
|
|
78
|
+
"customer_service": {"action", "category", "churn_risk", "needs_human", "urgency"},
|
|
79
|
+
"invoice_processing": {"discrepancy_severity", "disposition", "duplicate", "matches_order", "urgency"},
|
|
80
|
+
"security_incidents": {"credential_compromise", "disposition", "severity", "true_positive", "urgency"},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RouteDecision(dict):
|
|
85
|
+
"""The routing outcome: which model, why, and what was detected.
|
|
86
|
+
|
|
87
|
+
Behaves as a dict so it serialises straight into an API response.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def model(self) -> str:
|
|
92
|
+
return self["model"]
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def reason(self) -> str:
|
|
96
|
+
return self["reason"]
|
|
97
|
+
|
|
98
|
+
def __repr__(self):
|
|
99
|
+
return "RouteDecision(model=%r, reason=%r)" % (self["model"], self["reason"])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalise_name(name: str) -> str:
|
|
103
|
+
key = str(name).strip().lower()
|
|
104
|
+
key = _ALIASES.get(key, key)
|
|
105
|
+
if key not in DEFAULT_MODELS:
|
|
106
|
+
raise ValueError("unknown model %r; choose one of %s (or an alias: %s)"
|
|
107
|
+
% (name, sorted(DEFAULT_MODELS), sorted(_ALIASES)))
|
|
108
|
+
return key
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def match_typed_decisions_workflow(questions: Dict[str, Any]) -> Optional[str]:
|
|
112
|
+
"""Name of the typed-decisions workflow whose question ids these are, else None.
|
|
113
|
+
|
|
114
|
+
Requires an exact id-set match, so an unrelated schema that happens to contain 'urgency'
|
|
115
|
+
is never captured.
|
|
116
|
+
"""
|
|
117
|
+
ids = set(questions or {})
|
|
118
|
+
for wf, sig in _TYPED_DECISION_WORKFLOWS.items():
|
|
119
|
+
if ids == sig:
|
|
120
|
+
return wf
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Subtags that mean "the English checkpoint can read this". Routing needs one bit -- is this
|
|
125
|
+
# English Latin text, or something the English checkpoint cannot read -- not a language id, so
|
|
126
|
+
# every other code resolves to the multilingual checkpoint.
|
|
127
|
+
_ENGLISH_SUBTAGS = ("en", "eng", "english")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _english_from_code(value: Any) -> Optional[bool]:
|
|
131
|
+
"""True/False for a language code, or None when the code identifies nothing.
|
|
132
|
+
|
|
133
|
+
Accepts the forms a caller is likely to have to hand: `"en"`, `"EN"`, `"en-US"`, the
|
|
134
|
+
POSIX `"en_US"` (which `$LANG` holds), and `"en_US.UTF-8"`. `None` here means "no usable
|
|
135
|
+
hint", which is what lets a language-identification model abstain.
|
|
136
|
+
"""
|
|
137
|
+
if value is None:
|
|
138
|
+
return None
|
|
139
|
+
code = str(value).strip().lower()
|
|
140
|
+
if not code:
|
|
141
|
+
return None
|
|
142
|
+
code = code.split(".", 1)[0] # en_US.UTF-8 -> en_US
|
|
143
|
+
primary = code.replace("_", "-").split("-", 1)[0] # en_US -> en
|
|
144
|
+
if not primary:
|
|
145
|
+
return None
|
|
146
|
+
return primary in _ENGLISH_SUBTAGS
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Router:
|
|
150
|
+
"""Lazily loads Laya checkpoints and sends each request to the right one.
|
|
151
|
+
|
|
152
|
+
from laya import Router
|
|
153
|
+
|
|
154
|
+
r = Router()
|
|
155
|
+
r.predict({"message": "Mein Konto wurde zweimal belastet"}, questions) # -> multilingual
|
|
156
|
+
r.predict({"message": "I was charged twice"}, questions) # -> english
|
|
157
|
+
r.predict(state, questions, model="typed-decisions") # explicit
|
|
158
|
+
|
|
159
|
+
Models are downloaded and built on first use. `max_loaded` caps how many stay resident
|
|
160
|
+
(least-recently-used is evicted), because all three together are ~1.16B parameters.
|
|
161
|
+
|
|
162
|
+
The default is 2, because automatic routing only ever chooses between `english` and
|
|
163
|
+
`multilingual`: a cap of one rebuilds the checkpoint it just evicted on every script switch,
|
|
164
|
+
which is seconds per request on exactly the traffic the Router exists for. Traffic that only
|
|
165
|
+
ever sees one language never builds the second checkpoint, so the default costs it nothing.
|
|
166
|
+
Lower it to 1 for a memory-constrained host, and raise it to 3 (or preload) when
|
|
167
|
+
`auto_task_detection`, an explicit `model=` or an explicit `task=` can reach
|
|
168
|
+
`typed-decisions` as well.
|
|
169
|
+
|
|
170
|
+
For a server or a demo, preload instead: a cold load costs seconds, while detection costs
|
|
171
|
+
microseconds, so even the default still pays a load the first time a language appears.
|
|
172
|
+
|
|
173
|
+
r = Router(preload=True) # all three resident, routing is free
|
|
174
|
+
r = Router(preload=True, device="cuda")
|
|
175
|
+
r.preload(["english", "multilingual"]) # or just the two you serve
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
models: Optional[Dict[str, str]] = None,
|
|
181
|
+
device: Optional[str] = None,
|
|
182
|
+
token: Optional[str] = None,
|
|
183
|
+
max_loaded: int = 2,
|
|
184
|
+
default: str = "english",
|
|
185
|
+
auto_task_detection: bool = False,
|
|
186
|
+
standalone_repos: bool = False,
|
|
187
|
+
preload: bool = False,
|
|
188
|
+
lang_guess: Optional[Any] = None,
|
|
189
|
+
):
|
|
190
|
+
self.models = dict(STANDALONE_MODELS if standalone_repos else DEFAULT_MODELS)
|
|
191
|
+
if models:
|
|
192
|
+
self.models.update({normalise_name(k): v for k, v in models.items()})
|
|
193
|
+
self.device = device
|
|
194
|
+
self.token = token or os.environ.get("HF_TOKEN")
|
|
195
|
+
self.max_loaded = max(1, int(max_loaded))
|
|
196
|
+
self.default = normalise_name(default)
|
|
197
|
+
self.auto_task_detection = bool(auto_task_detection)
|
|
198
|
+
# An opt-in language hint installed for every request: a code, or a callable taking the
|
|
199
|
+
# state and returning one (or None to abstain). Checked before the built-in detection,
|
|
200
|
+
# never before an explicit `model`, `task` or `lang`. The default path is unchanged, so
|
|
201
|
+
# the heuristic stays dependency-free; this is the seam for a real LID model.
|
|
202
|
+
self.lang_guess = lang_guess
|
|
203
|
+
self._agents: Dict[str, Any] = {}
|
|
204
|
+
self._order: List[str] = [] # least-recently-used first
|
|
205
|
+
# Re-entrant lock guarding model lifecycle (load/unload/attach/preload) and the
|
|
206
|
+
# LRU bookkeeping. RLock so the public methods can call the private `_touch`/`_evict`
|
|
207
|
+
# helpers without deadlocking. Inference (`Agent.system_one`) is deliberately left
|
|
208
|
+
# outside the lock so concurrent predictions share a checkpoint without serialising.
|
|
209
|
+
self._lock = threading.RLock()
|
|
210
|
+
if preload:
|
|
211
|
+
self.preload()
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------ loading
|
|
214
|
+
def load(self, name: str):
|
|
215
|
+
"""Return the Agent for `name`, downloading and building it on first use.
|
|
216
|
+
|
|
217
|
+
Concurrent callers share a single Agent instead of building duplicates.
|
|
218
|
+
"""
|
|
219
|
+
key = normalise_name(name)
|
|
220
|
+
with self._lock:
|
|
221
|
+
if key in self._agents:
|
|
222
|
+
self._touch(key)
|
|
223
|
+
return self._agents[key]
|
|
224
|
+
from .agent import Agent
|
|
225
|
+
repo, sub = _split(self.models[key])
|
|
226
|
+
agent = Agent(repo, device=self.device, token=self.token, subfolder=sub)
|
|
227
|
+
self._agents[key] = agent
|
|
228
|
+
self._order.append(key)
|
|
229
|
+
self._evict()
|
|
230
|
+
return agent
|
|
231
|
+
|
|
232
|
+
def _touch(self, key: str):
|
|
233
|
+
with self._lock:
|
|
234
|
+
if key in self._order:
|
|
235
|
+
self._order.remove(key)
|
|
236
|
+
self._order.append(key)
|
|
237
|
+
|
|
238
|
+
def _evict(self):
|
|
239
|
+
with self._lock:
|
|
240
|
+
evicted = False
|
|
241
|
+
while len(self._order) > self.max_loaded:
|
|
242
|
+
victim = self._order.pop(0)
|
|
243
|
+
agent = self._agents.pop(victim, None)
|
|
244
|
+
if agent is not None:
|
|
245
|
+
evicted = True
|
|
246
|
+
del agent
|
|
247
|
+
if len(self._order) < len(self._agents): # keep the two views consistent
|
|
248
|
+
for k in list(self._agents):
|
|
249
|
+
if k not in self._order:
|
|
250
|
+
agent = self._agents.pop(k, None)
|
|
251
|
+
if agent is not None:
|
|
252
|
+
evicted = True
|
|
253
|
+
del agent
|
|
254
|
+
if evicted:
|
|
255
|
+
gc.collect()
|
|
256
|
+
try:
|
|
257
|
+
import torch
|
|
258
|
+
if torch.cuda.is_available():
|
|
259
|
+
torch.cuda.empty_cache()
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
def attach(self, name: str, agent: Any):
|
|
264
|
+
"""Register an already-built Agent under `name` instead of loading a second copy.
|
|
265
|
+
|
|
266
|
+
Useful when the process has a checkpoint loaded for other reasons: a demo that already
|
|
267
|
+
built `convaiinnovations/laya` can hand it to the router rather than pay for -- and hold
|
|
268
|
+
in memory -- a duplicate 421M parameters.
|
|
269
|
+
"""
|
|
270
|
+
key = normalise_name(name)
|
|
271
|
+
with self._lock:
|
|
272
|
+
self._agents[key] = agent
|
|
273
|
+
self._touch(key)
|
|
274
|
+
self.max_loaded = max(self.max_loaded, len(self._agents))
|
|
275
|
+
return agent
|
|
276
|
+
|
|
277
|
+
def preload(self, names: Optional[List[str]] = None):
|
|
278
|
+
"""Download and build checkpoints up front so no request ever pays a model load.
|
|
279
|
+
|
|
280
|
+
A cold load costs seconds; language detection costs microseconds. With every
|
|
281
|
+
checkpoint resident, routing is effectively free -- which is what you want in a
|
|
282
|
+
server or a demo. `max_loaded` is raised to fit both the requested checkpoints and
|
|
283
|
+
all already-resident agents, so incremental preloading does not evict either.
|
|
284
|
+
"""
|
|
285
|
+
names = [normalise_name(n) for n in (names or list(self.models))]
|
|
286
|
+
with self._lock:
|
|
287
|
+
self.max_loaded = max(self.max_loaded, len(set(names) | set(self._agents)))
|
|
288
|
+
for n in names:
|
|
289
|
+
if n not in self._agents: # an attached agent is already built
|
|
290
|
+
self.load(n)
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
def unload(self, name: Optional[str] = None):
|
|
294
|
+
"""Free one model, or all of them."""
|
|
295
|
+
with self._lock:
|
|
296
|
+
if name is None:
|
|
297
|
+
self._agents.clear()
|
|
298
|
+
self._order.clear()
|
|
299
|
+
else:
|
|
300
|
+
key = normalise_name(name)
|
|
301
|
+
agent = self._agents.pop(key, None)
|
|
302
|
+
if key in self._order:
|
|
303
|
+
self._order.remove(key)
|
|
304
|
+
del agent
|
|
305
|
+
gc.collect()
|
|
306
|
+
try:
|
|
307
|
+
import torch
|
|
308
|
+
if torch.cuda.is_available():
|
|
309
|
+
torch.cuda.empty_cache()
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def loaded(self) -> List[str]:
|
|
315
|
+
with self._lock:
|
|
316
|
+
return list(self._order)
|
|
317
|
+
|
|
318
|
+
def _resolve_hint(self, hint: Any, state: Union[str, dict, list, None]) -> Optional[bool]:
|
|
319
|
+
"""True/False for a hint about whether the English checkpoint can read `state`.
|
|
320
|
+
|
|
321
|
+
`hint` is either a language code or a callable taking the state. Anything the hint
|
|
322
|
+
cannot answer returns None, which makes `route` fall through to detection rather than
|
|
323
|
+
picking a checkpoint on no evidence.
|
|
324
|
+
"""
|
|
325
|
+
if hint is None:
|
|
326
|
+
return None
|
|
327
|
+
if callable(hint):
|
|
328
|
+
hint = hint(state)
|
|
329
|
+
return _english_from_code(hint)
|
|
330
|
+
|
|
331
|
+
# ------------------------------------------------------------------ routing
|
|
332
|
+
def route(
|
|
333
|
+
self,
|
|
334
|
+
state: Union[str, dict, list, None],
|
|
335
|
+
questions: Optional[Dict[str, Any]] = None,
|
|
336
|
+
model: Optional[str] = None,
|
|
337
|
+
task: Optional[str] = None,
|
|
338
|
+
lang: Optional[str] = None,
|
|
339
|
+
lang_guess: Optional[Any] = None,
|
|
340
|
+
) -> RouteDecision:
|
|
341
|
+
"""Decide which checkpoint to use, without loading or running anything.
|
|
342
|
+
|
|
343
|
+
Precedence: explicit `model` > explicit `task` > detected workflow (opt-in) >
|
|
344
|
+
explicit `lang` > `lang_guess` > detected script/language > default.
|
|
345
|
+
|
|
346
|
+
`lang_guess` is an opt-in hint -- a language code or a callable taking the state --
|
|
347
|
+
checked after an explicit `lang` and before the built-in detection. It only answers
|
|
348
|
+
"can the English checkpoint read this?", so any non-English code routes to the
|
|
349
|
+
multilingual checkpoint. A hint that resolves to nothing falls through to detection,
|
|
350
|
+
which lets a language-identification model abstain. Pass one here, or set
|
|
351
|
+
`Router(lang_guess=...)` to apply it to every request.
|
|
352
|
+
"""
|
|
353
|
+
if model is not None:
|
|
354
|
+
key = normalise_name(model)
|
|
355
|
+
return RouteDecision(model=key, repo=_repo_str(self.models[key]), reason="explicit model=%r" % model,
|
|
356
|
+
detection=None, workflow=None)
|
|
357
|
+
|
|
358
|
+
if task is not None:
|
|
359
|
+
key = normalise_name("typed-decisions" if str(task).lower().replace("-", "_") == "typed_decisions" else task)
|
|
360
|
+
return RouteDecision(model=key, repo=_repo_str(self.models[key]), reason="explicit task=%r" % task,
|
|
361
|
+
detection=None, workflow=None)
|
|
362
|
+
|
|
363
|
+
workflow = match_typed_decisions_workflow(questions or {})
|
|
364
|
+
if workflow and self.auto_task_detection:
|
|
365
|
+
return RouteDecision(model="typed-decisions", repo=_repo_str(self.models["typed-decisions"]),
|
|
366
|
+
reason="question ids match the %r typed-decisions workflow" % workflow,
|
|
367
|
+
detection=None, workflow=workflow)
|
|
368
|
+
|
|
369
|
+
if lang is not None:
|
|
370
|
+
key = "english" if _english_from_code(lang) else "multilingual"
|
|
371
|
+
return RouteDecision(model=key, repo=_repo_str(self.models[key]), reason="explicit lang=%r" % lang,
|
|
372
|
+
detection=None, workflow=workflow)
|
|
373
|
+
|
|
374
|
+
# Caller-supplied hint, per-call first then the one installed on the Router. Only a hint
|
|
375
|
+
# that actually answers the question routes here; anything else falls through.
|
|
376
|
+
for source, hint in (("lang_guess", lang_guess), ("Router(lang_guess=...)", self.lang_guess)):
|
|
377
|
+
resolved = self._resolve_hint(hint, state)
|
|
378
|
+
if resolved is not None:
|
|
379
|
+
key = "english" if resolved else "multilingual"
|
|
380
|
+
return RouteDecision(
|
|
381
|
+
model=key, repo=_repo_str(self.models[key]),
|
|
382
|
+
reason="%s: the caller identified this as %s text" % (
|
|
383
|
+
source, "English" if resolved else "non-English"),
|
|
384
|
+
detection=None, workflow=workflow)
|
|
385
|
+
|
|
386
|
+
det = analyse(state)
|
|
387
|
+
if det["script"] == "unknown":
|
|
388
|
+
key = self.default
|
|
389
|
+
reason = "no letters detected in state; using default (%s)" % key
|
|
390
|
+
elif det["script"] != "latin":
|
|
391
|
+
key = "multilingual"
|
|
392
|
+
reason = "non-Latin script (%s, %.0f%% of letters); the English checkpoint cannot read it" % (
|
|
393
|
+
det["script"], 100 * float(det["non_latin_fraction"]))
|
|
394
|
+
elif not det["is_english"]:
|
|
395
|
+
key = "multilingual"
|
|
396
|
+
if det["language"]:
|
|
397
|
+
reason = "Latin script but language looks like %r, not English" % det["language"]
|
|
398
|
+
else:
|
|
399
|
+
# Unidentified Latin-script language: routed on the non-English letters alone,
|
|
400
|
+
# because no stopword list here covers it.
|
|
401
|
+
reason = ("Latin script, language not identified but %.0f%% non-English letters; "
|
|
402
|
+
"not safe for the English checkpoint" % (100 * float(det["diacritic_rate"])))
|
|
403
|
+
elif det["language_undecided"]:
|
|
404
|
+
# Nothing identifies the language: too short, or only content words ("Quero cancelar",
|
|
405
|
+
# "Esqueci minha senha"). That is no evidence of English either, so it takes the same
|
|
406
|
+
# `default` as a state with no letters. A deployment that serves mostly non-English
|
|
407
|
+
# traffic sets `Router(default="multilingual")`; the stock default keeps it English.
|
|
408
|
+
key = self.default
|
|
409
|
+
reason = ("Latin script, language not identified and no non-English letters; "
|
|
410
|
+
"using default (%s)" % key)
|
|
411
|
+
else:
|
|
412
|
+
key = "english"
|
|
413
|
+
reason = "English Latin text"
|
|
414
|
+
return RouteDecision(model=key, repo=_repo_str(self.models[key]), reason=reason,
|
|
415
|
+
detection=det, workflow=workflow)
|
|
416
|
+
|
|
417
|
+
# ------------------------------------------------------------------ running
|
|
418
|
+
def predict(
|
|
419
|
+
self,
|
|
420
|
+
state: Union[str, dict, list],
|
|
421
|
+
questions: Dict[str, Any],
|
|
422
|
+
model: Optional[str] = None,
|
|
423
|
+
task: Optional[str] = None,
|
|
424
|
+
lang: Optional[str] = None,
|
|
425
|
+
lang_guess: Optional[Any] = None,
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""Route, then answer every question in one forward pass on the chosen checkpoint.
|
|
428
|
+
|
|
429
|
+
The result is the usual `system_one` payload plus a `routing` key recording the decision.
|
|
430
|
+
"""
|
|
431
|
+
decision = self.route(state, questions, model=model, task=task, lang=lang, lang_guess=lang_guess)
|
|
432
|
+
agent = self.load(decision["model"])
|
|
433
|
+
result = agent.system_one(state, questions)
|
|
434
|
+
result["routing"] = dict(decision)
|
|
435
|
+
return result
|
|
436
|
+
|
|
437
|
+
def __enter__(self):
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
441
|
+
self.unload()
|
|
442
|
+
return False
|
|
443
|
+
|
|
444
|
+
system_one = predict
|
|
445
|
+
|
|
446
|
+
def __repr__(self):
|
|
447
|
+
return "Router(loaded=%s, max_loaded=%d, default=%r)" % (self.loaded, self.max_loaded, self.default)
|