@weniatall/dsh-task-models 0.2.2 → 0.3.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/README.md +32 -17
- package/cordis.patch.yml +1 -0
- package/lib/client.js +463 -99
- package/lib/core.js +133 -25
- package/lib/index.js +326 -85
- package/package.json +2 -2
package/lib/core.js
CHANGED
|
@@ -19,6 +19,7 @@ export const DEFAULT_ROUTING = Object.freeze({
|
|
|
19
19
|
enabled: false,
|
|
20
20
|
fallback: 'taskRole',
|
|
21
21
|
useJev: true,
|
|
22
|
+
decisionTimeoutMs: 5_000,
|
|
22
23
|
respectExplicitChildModel: true,
|
|
23
24
|
classifier: Object.freeze({ provider: '', model: '', reasoningEffort: '' }),
|
|
24
25
|
})
|
|
@@ -44,10 +45,13 @@ function readString(value, fallback, label) {
|
|
|
44
45
|
return value.trim()
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function readNumber(value, fallback, label, min, max) {
|
|
48
|
+
function readNumber(value, fallback, label, min, max, requireInteger = false) {
|
|
48
49
|
if (value === undefined || value === null || value === '') return fallback
|
|
49
50
|
const number = typeof value === 'number' ? value : Number(value)
|
|
50
51
|
if (!Number.isFinite(number)) throw new Error(`task-models: ${label} must be a finite number`)
|
|
52
|
+
if (requireInteger && !Number.isInteger(number)) {
|
|
53
|
+
throw new Error(`task-models: ${label} must be an integer`)
|
|
54
|
+
}
|
|
51
55
|
if (number < min || number > max) {
|
|
52
56
|
throw new Error(`task-models: ${label} must be between ${String(min)} and ${String(max)}`)
|
|
53
57
|
}
|
|
@@ -112,6 +116,14 @@ export function resolveConfig(raw = {}) {
|
|
|
112
116
|
enabled: readBoolean(routingRaw.enabled, DEFAULT_ROUTING.enabled, 'routing.enabled'),
|
|
113
117
|
fallback,
|
|
114
118
|
useJev: readBoolean(routingRaw.useJev, DEFAULT_ROUTING.useJev, 'routing.useJev'),
|
|
119
|
+
decisionTimeoutMs: readNumber(
|
|
120
|
+
routingRaw.decisionTimeoutMs,
|
|
121
|
+
DEFAULT_ROUTING.decisionTimeoutMs,
|
|
122
|
+
'routing.decisionTimeoutMs',
|
|
123
|
+
100,
|
|
124
|
+
60_000,
|
|
125
|
+
true,
|
|
126
|
+
),
|
|
115
127
|
respectExplicitChildModel: readBoolean(
|
|
116
128
|
routingRaw.respectExplicitChildModel,
|
|
117
129
|
DEFAULT_ROUTING.respectExplicitChildModel,
|
|
@@ -182,40 +194,132 @@ export function resolveConfig(raw = {}) {
|
|
|
182
194
|
return Object.freeze({ routing, profiles, jev })
|
|
183
195
|
}
|
|
184
196
|
|
|
197
|
+
const ASCII_IDENTIFIER_PATTERN = /^[a-z0-9_]+$/i
|
|
198
|
+
|
|
199
|
+
function matchKeyword(target, keyword) {
|
|
200
|
+
if (typeof target !== 'string' || typeof keyword !== 'string') return false
|
|
201
|
+
const t = target.toLowerCase()
|
|
202
|
+
const k = keyword.toLowerCase()
|
|
203
|
+
if (t === '' || k === '') return false
|
|
204
|
+
if (ASCII_IDENTIFIER_PATTERN.test(k)) {
|
|
205
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${k}(?![\\p{L}\\p{N}_])`, 'u')
|
|
206
|
+
return pattern.test(t)
|
|
207
|
+
}
|
|
208
|
+
return t.includes(k)
|
|
209
|
+
}
|
|
210
|
+
|
|
185
211
|
/**
|
|
186
|
-
*
|
|
187
|
-
* no decision; configuration order must never choose the model.
|
|
212
|
+
* Explainable rule analysis over candidate profiles against a subtask label and prompt text.
|
|
188
213
|
*/
|
|
189
|
-
export function
|
|
214
|
+
export function analyzeRules(profiles, label, text) {
|
|
190
215
|
const labelText = typeof label === 'string' ? label.trim().toLowerCase() : ''
|
|
191
216
|
const taskText = typeof text === 'string' ? text.trim().toLowerCase() : ''
|
|
192
217
|
const values = profiles instanceof Map ? profiles.values() : profiles ?? []
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
218
|
+
|
|
219
|
+
const matches = []
|
|
220
|
+
|
|
196
221
|
for (const profile of values) {
|
|
197
222
|
if (!profile?.enabled) continue
|
|
223
|
+
const id = String(profile.id ?? '').trim().toLowerCase()
|
|
224
|
+
if (id === '') continue
|
|
225
|
+
|
|
226
|
+
const explicitLabel = labelText === id
|
|
227
|
+
|| labelText.startsWith(`[${id}]`)
|
|
228
|
+
|| labelText.startsWith(`${id}:`)
|
|
229
|
+
|
|
198
230
|
let score = 0
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
231
|
+
const matchedKeywords = []
|
|
232
|
+
const seenKeywords = new Set()
|
|
233
|
+
|
|
203
234
|
for (const rawKeyword of profile.keywords ?? []) {
|
|
204
|
-
const keyword = String(rawKeyword).trim().toLowerCase()
|
|
205
|
-
if (keyword === '') continue
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
const keyword = String(rawKeyword ?? '').trim().toLowerCase()
|
|
236
|
+
if (keyword === '' || seenKeywords.has(keyword)) continue
|
|
237
|
+
seenKeywords.add(keyword)
|
|
238
|
+
|
|
239
|
+
if (matchKeyword(labelText, keyword)) {
|
|
240
|
+
score += 3
|
|
241
|
+
matchedKeywords.push({ keyword, in: 'label' })
|
|
242
|
+
} else if (matchKeyword(taskText, keyword)) {
|
|
243
|
+
score += 1
|
|
244
|
+
matchedKeywords.push({ keyword, in: 'text' })
|
|
245
|
+
}
|
|
208
246
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
|
|
248
|
+
if (explicitLabel || matchedKeywords.length > 0) {
|
|
249
|
+
matches.push({
|
|
250
|
+
id: profile.id,
|
|
251
|
+
score,
|
|
252
|
+
explicitLabel,
|
|
253
|
+
matchedKeywords,
|
|
254
|
+
})
|
|
216
255
|
}
|
|
217
256
|
}
|
|
218
|
-
|
|
257
|
+
|
|
258
|
+
matches.sort((a, b) => {
|
|
259
|
+
if (a.explicitLabel !== b.explicitLabel) return a.explicitLabel ? -1 : 1
|
|
260
|
+
if (b.score !== a.score) return b.score - a.score
|
|
261
|
+
return a.id.localeCompare(b.id)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const explicitMatches = matches.filter((m) => m.explicitLabel)
|
|
265
|
+
if (explicitMatches.length === 1) {
|
|
266
|
+
return {
|
|
267
|
+
profileId: explicitMatches[0].id,
|
|
268
|
+
source: 'explicit-label',
|
|
269
|
+
matches,
|
|
270
|
+
tiedIds: [],
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (explicitMatches.length > 1) {
|
|
274
|
+
return {
|
|
275
|
+
source: 'tie',
|
|
276
|
+
matches,
|
|
277
|
+
tiedIds: explicitMatches.map((m) => m.id).sort(),
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let maxScore = 0
|
|
282
|
+
for (const match of matches) {
|
|
283
|
+
if (match.score > maxScore) maxScore = match.score
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (maxScore === 0) {
|
|
287
|
+
return {
|
|
288
|
+
source: 'none',
|
|
289
|
+
matches,
|
|
290
|
+
tiedIds: [],
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const topMatches = matches.filter((m) => m.score === maxScore)
|
|
295
|
+
if (topMatches.length === 1) {
|
|
296
|
+
return {
|
|
297
|
+
profileId: topMatches[0].id,
|
|
298
|
+
source: 'keywords',
|
|
299
|
+
matches,
|
|
300
|
+
tiedIds: [],
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
source: 'tie',
|
|
306
|
+
matches,
|
|
307
|
+
tiedIds: topMatches.map((m) => m.id).sort(),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Pick one enabled profile from a label and task text. A tie is deliberately
|
|
313
|
+
* no decision; configuration order must never choose the model.
|
|
314
|
+
*/
|
|
315
|
+
export function selectProfile(profiles, label, text) {
|
|
316
|
+
const analysis = analyzeRules(profiles, label, text)
|
|
317
|
+
if (!analysis.profileId) return undefined
|
|
318
|
+
if (profiles instanceof Map) {
|
|
319
|
+
return profiles.get(analysis.profileId)
|
|
320
|
+
?? [...profiles.values()].find((p) => String(p?.id ?? '').toLowerCase() === analysis.profileId.toLowerCase())
|
|
321
|
+
}
|
|
322
|
+
return (profiles ?? []).find((p) => String(p?.id ?? '').toLowerCase() === analysis.profileId.toLowerCase())
|
|
219
323
|
}
|
|
220
324
|
|
|
221
325
|
/** Accept one exact configured profile id from a classifier reply. */
|
|
@@ -303,8 +407,12 @@ export function parseJevChoice(payload, allowedIds, minConfidence = 0, key = 'ta
|
|
|
303
407
|
if (answer === null || answer === undefined || typeof answer !== 'object' || Array.isArray(answer)) return undefined
|
|
304
408
|
const choice = typeof answer.choice === 'string' ? answer.choice.trim().toLowerCase() : ''
|
|
305
409
|
if (choice === '' || !allowed.has(choice)) return undefined
|
|
306
|
-
|
|
307
|
-
|
|
410
|
+
if (typeof answer.confidence !== 'number' || !Number.isFinite(answer.confidence)) {
|
|
411
|
+
return undefined
|
|
412
|
+
}
|
|
413
|
+
if (answer.confidence < 0 || answer.confidence > 1 || answer.confidence < minConfidence) {
|
|
414
|
+
return undefined
|
|
415
|
+
}
|
|
308
416
|
return choice
|
|
309
417
|
}
|
|
310
418
|
|