@tasksai/install 0.1.30 → 0.1.31
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/package.json +1 -1
- package/runtime/skill_matcher.py +178 -13
package/package.json
CHANGED
package/runtime/skill_matcher.py
CHANGED
|
@@ -11,7 +11,7 @@ import unicodedata
|
|
|
11
11
|
from collections.abc import Iterable, Mapping, Sequence
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
POLICY_VERSION = "tasksai-trigger-matcher/2.
|
|
14
|
+
POLICY_VERSION = "tasksai-trigger-matcher/2.1"
|
|
15
15
|
|
|
16
16
|
DEFAULT_STOP_WORDS = {
|
|
17
17
|
"a", "an", "and", "are", "as", "at", "be", "by", "can", "create",
|
|
@@ -57,11 +57,44 @@ DEFAULT_CONTEXT_WORDS = {
|
|
|
57
57
|
DEFAULT_THRESHOLDS = {"high": 90, "medium": 45}
|
|
58
58
|
DEFAULT_RESULT_COUNT = 3
|
|
59
59
|
MAXIMUM_RESULT_COUNT = 10
|
|
60
|
+
DEFAULT_RESULT_COUNTS = {"realtor": 5}
|
|
61
|
+
REALTOR_ACTION_WORDS = {
|
|
62
|
+
"assemble", "build", "create", "develop", "document", "draft",
|
|
63
|
+
"generate", "make", "prepare", "produce", "write", "writing",
|
|
64
|
+
}
|
|
65
|
+
DEFAULT_TOKEN_ALIASES = {
|
|
66
|
+
"realtor": {
|
|
67
|
+
"assemble": "create",
|
|
68
|
+
"build": "create",
|
|
69
|
+
"develop": "create",
|
|
70
|
+
"draft": "create",
|
|
71
|
+
"dwelling": "property",
|
|
72
|
+
"generate": "create",
|
|
73
|
+
"home": "property",
|
|
74
|
+
"house": "property",
|
|
75
|
+
"listed": "listing",
|
|
76
|
+
"listings": "listing",
|
|
77
|
+
"list": "listing",
|
|
78
|
+
"make": "create",
|
|
79
|
+
"prepare": "create",
|
|
80
|
+
"prepared": "create",
|
|
81
|
+
"preparing": "create",
|
|
82
|
+
"preparation": "create",
|
|
83
|
+
"produce": "create",
|
|
84
|
+
"residence": "property",
|
|
85
|
+
"residential": "property",
|
|
86
|
+
"write": "create",
|
|
87
|
+
"writing": "create",
|
|
88
|
+
},
|
|
89
|
+
}
|
|
60
90
|
TRIGGER_EVIDENCE_RANK = {
|
|
61
|
-
"trigger_exact":
|
|
91
|
+
"trigger_exact": 5,
|
|
92
|
+
"trigger_equivalent": 4,
|
|
62
93
|
"trigger_complete": 3,
|
|
63
94
|
"trigger_query_complete": 2,
|
|
64
|
-
"trigger_partial":
|
|
95
|
+
"trigger_partial": 2,
|
|
96
|
+
"trigger_broad": 1,
|
|
97
|
+
"trigger_context": 1,
|
|
65
98
|
}
|
|
66
99
|
|
|
67
100
|
|
|
@@ -149,9 +182,95 @@ def _normalize_scoring_tokens(
|
|
|
149
182
|
value: object,
|
|
150
183
|
stop_words: set[str],
|
|
151
184
|
abbreviations: Mapping[str, object],
|
|
185
|
+
token_aliases: Mapping[str, str],
|
|
152
186
|
) -> tuple[str, ...]:
|
|
153
187
|
expanded = _expand_abbreviations(_tokens(value), abbreviations)
|
|
154
|
-
|
|
188
|
+
canonical = tuple(token_aliases.get(token, token) for token in expanded)
|
|
189
|
+
return tuple(token for token in canonical if token not in stop_words)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _same_token_bag(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
|
|
193
|
+
return bool(left) and len(left) == len(right) and sorted(left) == sorted(right)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _within_one_edit(left: str, right: str) -> bool:
|
|
197
|
+
"""Return True for one insertion, deletion, substitution, or transposition."""
|
|
198
|
+
if left == right or abs(len(left) - len(right)) > 1:
|
|
199
|
+
return False
|
|
200
|
+
if len(left) == len(right):
|
|
201
|
+
differences = [index for index, pair in enumerate(zip(left, right)) if pair[0] != pair[1]]
|
|
202
|
+
if len(differences) == 1:
|
|
203
|
+
return True
|
|
204
|
+
return (
|
|
205
|
+
len(differences) == 2
|
|
206
|
+
and differences[1] == differences[0] + 1
|
|
207
|
+
and left[differences[0]] == right[differences[1]]
|
|
208
|
+
and left[differences[1]] == right[differences[0]]
|
|
209
|
+
)
|
|
210
|
+
shorter, longer = (left, right) if len(left) < len(right) else (right, left)
|
|
211
|
+
short_index = long_index = differences = 0
|
|
212
|
+
while short_index < len(shorter) and long_index < len(longer):
|
|
213
|
+
if shorter[short_index] == longer[long_index]:
|
|
214
|
+
short_index += 1
|
|
215
|
+
long_index += 1
|
|
216
|
+
continue
|
|
217
|
+
differences += 1
|
|
218
|
+
long_index += 1
|
|
219
|
+
if differences > 1:
|
|
220
|
+
return False
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _build_token_index(
|
|
225
|
+
skills: Sequence[Mapping[str, object]],
|
|
226
|
+
triggers: Mapping[str, object],
|
|
227
|
+
product_id: str,
|
|
228
|
+
policy: Mapping[str, object],
|
|
229
|
+
) -> tuple[dict[tuple[int, str], set[str]], set[str]]:
|
|
230
|
+
"""Index public discovery tokens by length and first character for bounded typo repair."""
|
|
231
|
+
stop_words = policy["stop_words"]
|
|
232
|
+
abbreviations = policy["abbreviations"]
|
|
233
|
+
token_aliases = policy["token_aliases"]
|
|
234
|
+
vocabulary = set(token_aliases) | set(token_aliases.values())
|
|
235
|
+
for skill in skills:
|
|
236
|
+
for value in (skill.get("name", ""), _category_text(skill)):
|
|
237
|
+
vocabulary.update(
|
|
238
|
+
_normalize_scoring_tokens(value, stop_words, abbreviations, token_aliases)
|
|
239
|
+
)
|
|
240
|
+
skill_id = str(skill.get("id", ""))
|
|
241
|
+
for trigger in _trigger_values(triggers, skill_id, product_id):
|
|
242
|
+
vocabulary.update(
|
|
243
|
+
_normalize_scoring_tokens(trigger, stop_words, abbreviations, token_aliases)
|
|
244
|
+
)
|
|
245
|
+
index: dict[tuple[int, str], set[str]] = {}
|
|
246
|
+
for token in vocabulary:
|
|
247
|
+
if len(token) < 5:
|
|
248
|
+
continue
|
|
249
|
+
index.setdefault((len(token), token[0]), set()).add(token)
|
|
250
|
+
return index, vocabulary
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _repair_query_tokens(
|
|
254
|
+
tokens: tuple[str, ...],
|
|
255
|
+
token_index: Mapping[tuple[int, str], set[str]],
|
|
256
|
+
vocabulary: set[str],
|
|
257
|
+
) -> tuple[tuple[str, ...], list[str]]:
|
|
258
|
+
repaired: list[str] = []
|
|
259
|
+
diagnostics: list[str] = []
|
|
260
|
+
for token in tokens:
|
|
261
|
+
if len(token) < 5 or token in vocabulary:
|
|
262
|
+
repaired.append(token)
|
|
263
|
+
continue
|
|
264
|
+
candidates: set[str] = set()
|
|
265
|
+
for length in range(len(token) - 1, len(token) + 2):
|
|
266
|
+
candidates.update(token_index.get((length, token[0]), set()))
|
|
267
|
+
matches = sorted(candidate for candidate in candidates if _within_one_edit(token, candidate))
|
|
268
|
+
if len(matches) == 1:
|
|
269
|
+
repaired.append(matches[0])
|
|
270
|
+
diagnostics.append(f"fuzzy_corrected:{token}->{matches[0]}")
|
|
271
|
+
else:
|
|
272
|
+
repaired.append(token)
|
|
273
|
+
return tuple(repaired), diagnostics
|
|
155
274
|
|
|
156
275
|
|
|
157
276
|
def _contains_tokens(container: tuple[str, ...], candidate: tuple[str, ...]) -> bool:
|
|
@@ -222,6 +341,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
222
341
|
for value in supplied_stop_words
|
|
223
342
|
for token in _tokens(value)
|
|
224
343
|
}
|
|
344
|
+
if _canonical_product_id(product_id) == "realtor":
|
|
345
|
+
stop_words.difference_update(REALTOR_ACTION_WORDS)
|
|
225
346
|
context_default = DEFAULT_CONTEXT_WORDS.get(_canonical_product_id(product_id), set())
|
|
226
347
|
supplied_context_words = source.get("context_words", context_default)
|
|
227
348
|
if not isinstance(supplied_context_words, Sequence) or isinstance(supplied_context_words, (str, bytes)):
|
|
@@ -242,10 +363,13 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
242
363
|
diagnostics.append("bundled_thresholds:invalid_order")
|
|
243
364
|
thresholds = dict(DEFAULT_THRESHOLDS)
|
|
244
365
|
|
|
245
|
-
|
|
366
|
+
product_default_count = DEFAULT_RESULT_COUNTS.get(
|
|
367
|
+
_canonical_product_id(product_id), DEFAULT_RESULT_COUNT
|
|
368
|
+
)
|
|
369
|
+
default_count = source.get("default_result_count", product_default_count)
|
|
246
370
|
maximum_count = source.get("maximum_result_count", MAXIMUM_RESULT_COUNT)
|
|
247
371
|
if not isinstance(default_count, int) or isinstance(default_count, bool) or default_count < 1:
|
|
248
|
-
default_count =
|
|
372
|
+
default_count = product_default_count
|
|
249
373
|
if not isinstance(maximum_count, int) or isinstance(maximum_count, bool) or maximum_count < 1:
|
|
250
374
|
maximum_count = MAXIMUM_RESULT_COUNT
|
|
251
375
|
maximum_count = min(maximum_count, MAXIMUM_RESULT_COUNT)
|
|
@@ -262,6 +386,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
262
386
|
"default_result_count": default_count,
|
|
263
387
|
"maximum_result_count": maximum_count,
|
|
264
388
|
"abbreviations": configured_abbreviations,
|
|
389
|
+
"token_aliases": DEFAULT_TOKEN_ALIASES.get(_canonical_product_id(product_id), {}),
|
|
390
|
+
"broad_matching": _canonical_product_id(product_id) == "realtor",
|
|
265
391
|
}, diagnostics
|
|
266
392
|
|
|
267
393
|
|
|
@@ -271,8 +397,12 @@ def _score_trigger(
|
|
|
271
397
|
stop_words: set[str],
|
|
272
398
|
context_words: set[str],
|
|
273
399
|
abbreviations: Mapping[str, object],
|
|
400
|
+
token_aliases: Mapping[str, str],
|
|
401
|
+
broad_matching: bool,
|
|
274
402
|
) -> tuple[int, int, set[str], str]:
|
|
275
|
-
trigger_tokens = _normalize_scoring_tokens(
|
|
403
|
+
trigger_tokens = _normalize_scoring_tokens(
|
|
404
|
+
trigger, stop_words, abbreviations, token_aliases
|
|
405
|
+
)
|
|
276
406
|
if not trigger_tokens:
|
|
277
407
|
return 0, 0, set(), ""
|
|
278
408
|
|
|
@@ -284,10 +414,14 @@ def _score_trigger(
|
|
|
284
414
|
|
|
285
415
|
if query_tokens == trigger_tokens:
|
|
286
416
|
return 120, specificity, matched, "trigger_exact"
|
|
417
|
+
if broad_matching and _same_token_bag(query_tokens, trigger_tokens):
|
|
418
|
+
return 110, specificity, matched, "trigger_equivalent"
|
|
287
419
|
if _contains_tokens(query_tokens, trigger_tokens):
|
|
288
420
|
bonus = min(15, max(0, specificity - 1) * 3)
|
|
289
421
|
return 90 + bonus, specificity, matched, "trigger_complete"
|
|
290
422
|
query_specific = query_set - context_words
|
|
423
|
+
if broad_matching and not query_specific and query_set and query_set <= trigger_set:
|
|
424
|
+
return 50, specificity, matched, "trigger_context"
|
|
291
425
|
if len(query_specific) >= 2 and _contains_tokens(trigger_tokens, query_tokens):
|
|
292
426
|
return 70, specificity, matched, "trigger_query_complete"
|
|
293
427
|
|
|
@@ -296,6 +430,10 @@ def _score_trigger(
|
|
|
296
430
|
coverage = len(covered) / len(coverage_tokens)
|
|
297
431
|
if len(covered) >= 2 and coverage >= 0.67:
|
|
298
432
|
return round(40 * coverage), specificity, matched, "trigger_partial"
|
|
433
|
+
matched_specific_query = matched & (query_set - context_words)
|
|
434
|
+
matched_context = matched & context_words
|
|
435
|
+
if broad_matching and matched_specific_query and matched_context:
|
|
436
|
+
return 35, specificity, matched, "trigger_broad"
|
|
299
437
|
return 0, specificity, set(), ""
|
|
300
438
|
|
|
301
439
|
|
|
@@ -309,6 +447,8 @@ def _score_skill(
|
|
|
309
447
|
stop_words = policy["stop_words"]
|
|
310
448
|
context_words = policy["context_words"]
|
|
311
449
|
abbreviations = policy["abbreviations"]
|
|
450
|
+
token_aliases = policy["token_aliases"]
|
|
451
|
+
broad_matching = policy["broad_matching"]
|
|
312
452
|
query_set = set(query_tokens)
|
|
313
453
|
specific_query = query_set - context_words
|
|
314
454
|
context_query = query_set & context_words
|
|
@@ -317,7 +457,8 @@ def _score_skill(
|
|
|
317
457
|
strongest_trigger = (0, 0, set(), "", "")
|
|
318
458
|
for trigger in _trigger_values(triggers, skill_id, product_id):
|
|
319
459
|
score, specificity, matched, evidence = _score_trigger(
|
|
320
|
-
query_tokens, trigger, stop_words, context_words, abbreviations
|
|
460
|
+
query_tokens, trigger, stop_words, context_words, abbreviations,
|
|
461
|
+
token_aliases, broad_matching
|
|
321
462
|
)
|
|
322
463
|
candidate = (score, specificity, matched, evidence, normalize_text(trigger))
|
|
323
464
|
candidate_key = (score, specificity, len(matched), normalize_text(trigger))
|
|
@@ -335,7 +476,7 @@ def _score_skill(
|
|
|
335
476
|
matched_tokens = set(trigger_matched)
|
|
336
477
|
|
|
337
478
|
name_tokens = _normalize_scoring_tokens(
|
|
338
|
-
skill.get("name", ""), stop_words, abbreviations
|
|
479
|
+
skill.get("name", ""), stop_words, abbreviations, token_aliases
|
|
339
480
|
)
|
|
340
481
|
name_set = set(name_tokens)
|
|
341
482
|
identity_score = 0
|
|
@@ -352,7 +493,9 @@ def _score_skill(
|
|
|
352
493
|
evidence.append(f"name_context:{token}")
|
|
353
494
|
|
|
354
495
|
category_set = set(
|
|
355
|
-
_normalize_scoring_tokens(
|
|
496
|
+
_normalize_scoring_tokens(
|
|
497
|
+
_category_text(skill), stop_words, abbreviations, token_aliases
|
|
498
|
+
)
|
|
356
499
|
)
|
|
357
500
|
category_score = 0
|
|
358
501
|
for token in sorted(specific_query & category_set):
|
|
@@ -365,7 +508,9 @@ def _score_skill(
|
|
|
365
508
|
evidence.append(f"category_context:{token}")
|
|
366
509
|
|
|
367
510
|
description_set = set(
|
|
368
|
-
_normalize_scoring_tokens(
|
|
511
|
+
_normalize_scoring_tokens(
|
|
512
|
+
skill.get("description", ""), stop_words, abbreviations, token_aliases
|
|
513
|
+
)
|
|
369
514
|
)
|
|
370
515
|
description_matches = sorted(specific_query & description_set)
|
|
371
516
|
description_score = len(description_matches)
|
|
@@ -457,7 +602,28 @@ def match_skills(
|
|
|
457
602
|
|
|
458
603
|
raw_tokens = _tokens(query)
|
|
459
604
|
expanded_tokens = _expand_abbreviations(raw_tokens, configured_abbreviations)
|
|
460
|
-
|
|
605
|
+
aliased_tokens = tuple(
|
|
606
|
+
policy["token_aliases"].get(token, token) for token in expanded_tokens
|
|
607
|
+
)
|
|
608
|
+
initial_query_tokens = tuple(
|
|
609
|
+
token for token in aliased_tokens if token not in policy["stop_words"]
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
|
|
613
|
+
if policy["broad_matching"]:
|
|
614
|
+
token_index, vocabulary = _build_token_index(
|
|
615
|
+
scoped_skills, triggers, product_id, policy
|
|
616
|
+
)
|
|
617
|
+
repaired_tokens, fuzzy_diagnostics = _repair_query_tokens(
|
|
618
|
+
initial_query_tokens, token_index, vocabulary
|
|
619
|
+
)
|
|
620
|
+
else:
|
|
621
|
+
repaired_tokens, fuzzy_diagnostics = initial_query_tokens, []
|
|
622
|
+
query_tokens = tuple(
|
|
623
|
+
policy["token_aliases"].get(token, token) for token in repaired_tokens
|
|
624
|
+
if policy["token_aliases"].get(token, token) not in policy["stop_words"]
|
|
625
|
+
)
|
|
626
|
+
diagnostics.extend(fuzzy_diagnostics)
|
|
461
627
|
query_normalized = " ".join(query_tokens)
|
|
462
628
|
|
|
463
629
|
requested_count = policy["default_result_count"] if result_count is None else result_count
|
|
@@ -465,7 +631,6 @@ def match_skills(
|
|
|
465
631
|
requested_count = policy["default_result_count"]
|
|
466
632
|
requested_count = max(1, min(requested_count, policy["maximum_result_count"]))
|
|
467
633
|
|
|
468
|
-
scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
|
|
469
634
|
if len(scoped_skills) != len(skills):
|
|
470
635
|
diagnostics.append("product_mismatches_filtered")
|
|
471
636
|
|