@tasksai/install 0.1.30 → 0.1.32
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 +183 -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,48 @@ 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
|
+
REALTOR_DISCOVERY_WORDS = {
|
|
66
|
+
"browse", "display", "find", "get", "give", "look", "pull",
|
|
67
|
+
"search", "searching", "see", "show", "up", "view",
|
|
68
|
+
}
|
|
69
|
+
DEFAULT_TOKEN_ALIASES = {
|
|
70
|
+
"realtor": {
|
|
71
|
+
"assemble": "create",
|
|
72
|
+
"build": "create",
|
|
73
|
+
"develop": "create",
|
|
74
|
+
"draft": "create",
|
|
75
|
+
"dwelling": "property",
|
|
76
|
+
"generate": "create",
|
|
77
|
+
"home": "property",
|
|
78
|
+
"house": "property",
|
|
79
|
+
"listed": "listing",
|
|
80
|
+
"listings": "listing",
|
|
81
|
+
"list": "listing",
|
|
82
|
+
"make": "create",
|
|
83
|
+
"prepare": "create",
|
|
84
|
+
"prepared": "create",
|
|
85
|
+
"preparing": "create",
|
|
86
|
+
"preparation": "create",
|
|
87
|
+
"produce": "create",
|
|
88
|
+
"residence": "property",
|
|
89
|
+
"residential": "property",
|
|
90
|
+
"write": "create",
|
|
91
|
+
"writing": "create",
|
|
92
|
+
},
|
|
93
|
+
}
|
|
60
94
|
TRIGGER_EVIDENCE_RANK = {
|
|
61
|
-
"trigger_exact":
|
|
95
|
+
"trigger_exact": 5,
|
|
96
|
+
"trigger_equivalent": 4,
|
|
62
97
|
"trigger_complete": 3,
|
|
63
98
|
"trigger_query_complete": 2,
|
|
64
|
-
"trigger_partial":
|
|
99
|
+
"trigger_partial": 2,
|
|
100
|
+
"trigger_broad": 1,
|
|
101
|
+
"trigger_context": 1,
|
|
65
102
|
}
|
|
66
103
|
|
|
67
104
|
|
|
@@ -149,9 +186,95 @@ def _normalize_scoring_tokens(
|
|
|
149
186
|
value: object,
|
|
150
187
|
stop_words: set[str],
|
|
151
188
|
abbreviations: Mapping[str, object],
|
|
189
|
+
token_aliases: Mapping[str, str],
|
|
152
190
|
) -> tuple[str, ...]:
|
|
153
191
|
expanded = _expand_abbreviations(_tokens(value), abbreviations)
|
|
154
|
-
|
|
192
|
+
canonical = tuple(token_aliases.get(token, token) for token in expanded)
|
|
193
|
+
return tuple(token for token in canonical if token not in stop_words)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _same_token_bag(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
|
|
197
|
+
return bool(left) and len(left) == len(right) and sorted(left) == sorted(right)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _within_one_edit(left: str, right: str) -> bool:
|
|
201
|
+
"""Return True for one insertion, deletion, substitution, or transposition."""
|
|
202
|
+
if left == right or abs(len(left) - len(right)) > 1:
|
|
203
|
+
return False
|
|
204
|
+
if len(left) == len(right):
|
|
205
|
+
differences = [index for index, pair in enumerate(zip(left, right)) if pair[0] != pair[1]]
|
|
206
|
+
if len(differences) == 1:
|
|
207
|
+
return True
|
|
208
|
+
return (
|
|
209
|
+
len(differences) == 2
|
|
210
|
+
and differences[1] == differences[0] + 1
|
|
211
|
+
and left[differences[0]] == right[differences[1]]
|
|
212
|
+
and left[differences[1]] == right[differences[0]]
|
|
213
|
+
)
|
|
214
|
+
shorter, longer = (left, right) if len(left) < len(right) else (right, left)
|
|
215
|
+
short_index = long_index = differences = 0
|
|
216
|
+
while short_index < len(shorter) and long_index < len(longer):
|
|
217
|
+
if shorter[short_index] == longer[long_index]:
|
|
218
|
+
short_index += 1
|
|
219
|
+
long_index += 1
|
|
220
|
+
continue
|
|
221
|
+
differences += 1
|
|
222
|
+
long_index += 1
|
|
223
|
+
if differences > 1:
|
|
224
|
+
return False
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _build_token_index(
|
|
229
|
+
skills: Sequence[Mapping[str, object]],
|
|
230
|
+
triggers: Mapping[str, object],
|
|
231
|
+
product_id: str,
|
|
232
|
+
policy: Mapping[str, object],
|
|
233
|
+
) -> tuple[dict[tuple[int, str], set[str]], set[str]]:
|
|
234
|
+
"""Index public discovery tokens by length and first character for bounded typo repair."""
|
|
235
|
+
stop_words = policy["stop_words"]
|
|
236
|
+
abbreviations = policy["abbreviations"]
|
|
237
|
+
token_aliases = policy["token_aliases"]
|
|
238
|
+
vocabulary = set(token_aliases) | set(token_aliases.values())
|
|
239
|
+
for skill in skills:
|
|
240
|
+
for value in (skill.get("name", ""), _category_text(skill)):
|
|
241
|
+
vocabulary.update(
|
|
242
|
+
_normalize_scoring_tokens(value, stop_words, abbreviations, token_aliases)
|
|
243
|
+
)
|
|
244
|
+
skill_id = str(skill.get("id", ""))
|
|
245
|
+
for trigger in _trigger_values(triggers, skill_id, product_id):
|
|
246
|
+
vocabulary.update(
|
|
247
|
+
_normalize_scoring_tokens(trigger, stop_words, abbreviations, token_aliases)
|
|
248
|
+
)
|
|
249
|
+
index: dict[tuple[int, str], set[str]] = {}
|
|
250
|
+
for token in vocabulary:
|
|
251
|
+
if len(token) < 5:
|
|
252
|
+
continue
|
|
253
|
+
index.setdefault((len(token), token[0]), set()).add(token)
|
|
254
|
+
return index, vocabulary
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _repair_query_tokens(
|
|
258
|
+
tokens: tuple[str, ...],
|
|
259
|
+
token_index: Mapping[tuple[int, str], set[str]],
|
|
260
|
+
vocabulary: set[str],
|
|
261
|
+
) -> tuple[tuple[str, ...], list[str]]:
|
|
262
|
+
repaired: list[str] = []
|
|
263
|
+
diagnostics: list[str] = []
|
|
264
|
+
for token in tokens:
|
|
265
|
+
if len(token) < 5 or token in vocabulary:
|
|
266
|
+
repaired.append(token)
|
|
267
|
+
continue
|
|
268
|
+
candidates: set[str] = set()
|
|
269
|
+
for length in range(len(token) - 1, len(token) + 2):
|
|
270
|
+
candidates.update(token_index.get((length, token[0]), set()))
|
|
271
|
+
matches = sorted(candidate for candidate in candidates if _within_one_edit(token, candidate))
|
|
272
|
+
if len(matches) == 1:
|
|
273
|
+
repaired.append(matches[0])
|
|
274
|
+
diagnostics.append(f"fuzzy_corrected:{token}->{matches[0]}")
|
|
275
|
+
else:
|
|
276
|
+
repaired.append(token)
|
|
277
|
+
return tuple(repaired), diagnostics
|
|
155
278
|
|
|
156
279
|
|
|
157
280
|
def _contains_tokens(container: tuple[str, ...], candidate: tuple[str, ...]) -> bool:
|
|
@@ -222,6 +345,9 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
222
345
|
for value in supplied_stop_words
|
|
223
346
|
for token in _tokens(value)
|
|
224
347
|
}
|
|
348
|
+
if _canonical_product_id(product_id) == "realtor":
|
|
349
|
+
stop_words.difference_update(REALTOR_ACTION_WORDS)
|
|
350
|
+
stop_words.update(REALTOR_DISCOVERY_WORDS)
|
|
225
351
|
context_default = DEFAULT_CONTEXT_WORDS.get(_canonical_product_id(product_id), set())
|
|
226
352
|
supplied_context_words = source.get("context_words", context_default)
|
|
227
353
|
if not isinstance(supplied_context_words, Sequence) or isinstance(supplied_context_words, (str, bytes)):
|
|
@@ -242,10 +368,13 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
242
368
|
diagnostics.append("bundled_thresholds:invalid_order")
|
|
243
369
|
thresholds = dict(DEFAULT_THRESHOLDS)
|
|
244
370
|
|
|
245
|
-
|
|
371
|
+
product_default_count = DEFAULT_RESULT_COUNTS.get(
|
|
372
|
+
_canonical_product_id(product_id), DEFAULT_RESULT_COUNT
|
|
373
|
+
)
|
|
374
|
+
default_count = source.get("default_result_count", product_default_count)
|
|
246
375
|
maximum_count = source.get("maximum_result_count", MAXIMUM_RESULT_COUNT)
|
|
247
376
|
if not isinstance(default_count, int) or isinstance(default_count, bool) or default_count < 1:
|
|
248
|
-
default_count =
|
|
377
|
+
default_count = product_default_count
|
|
249
378
|
if not isinstance(maximum_count, int) or isinstance(maximum_count, bool) or maximum_count < 1:
|
|
250
379
|
maximum_count = MAXIMUM_RESULT_COUNT
|
|
251
380
|
maximum_count = min(maximum_count, MAXIMUM_RESULT_COUNT)
|
|
@@ -262,6 +391,8 @@ def _resolve_config(product_id: str, config: Mapping[str, object] | None) -> tup
|
|
|
262
391
|
"default_result_count": default_count,
|
|
263
392
|
"maximum_result_count": maximum_count,
|
|
264
393
|
"abbreviations": configured_abbreviations,
|
|
394
|
+
"token_aliases": DEFAULT_TOKEN_ALIASES.get(_canonical_product_id(product_id), {}),
|
|
395
|
+
"broad_matching": _canonical_product_id(product_id) == "realtor",
|
|
265
396
|
}, diagnostics
|
|
266
397
|
|
|
267
398
|
|
|
@@ -271,8 +402,12 @@ def _score_trigger(
|
|
|
271
402
|
stop_words: set[str],
|
|
272
403
|
context_words: set[str],
|
|
273
404
|
abbreviations: Mapping[str, object],
|
|
405
|
+
token_aliases: Mapping[str, str],
|
|
406
|
+
broad_matching: bool,
|
|
274
407
|
) -> tuple[int, int, set[str], str]:
|
|
275
|
-
trigger_tokens = _normalize_scoring_tokens(
|
|
408
|
+
trigger_tokens = _normalize_scoring_tokens(
|
|
409
|
+
trigger, stop_words, abbreviations, token_aliases
|
|
410
|
+
)
|
|
276
411
|
if not trigger_tokens:
|
|
277
412
|
return 0, 0, set(), ""
|
|
278
413
|
|
|
@@ -284,10 +419,14 @@ def _score_trigger(
|
|
|
284
419
|
|
|
285
420
|
if query_tokens == trigger_tokens:
|
|
286
421
|
return 120, specificity, matched, "trigger_exact"
|
|
422
|
+
if broad_matching and _same_token_bag(query_tokens, trigger_tokens):
|
|
423
|
+
return 110, specificity, matched, "trigger_equivalent"
|
|
287
424
|
if _contains_tokens(query_tokens, trigger_tokens):
|
|
288
425
|
bonus = min(15, max(0, specificity - 1) * 3)
|
|
289
426
|
return 90 + bonus, specificity, matched, "trigger_complete"
|
|
290
427
|
query_specific = query_set - context_words
|
|
428
|
+
if broad_matching and not query_specific and query_set and query_set <= trigger_set:
|
|
429
|
+
return 50, specificity, matched, "trigger_context"
|
|
291
430
|
if len(query_specific) >= 2 and _contains_tokens(trigger_tokens, query_tokens):
|
|
292
431
|
return 70, specificity, matched, "trigger_query_complete"
|
|
293
432
|
|
|
@@ -296,6 +435,10 @@ def _score_trigger(
|
|
|
296
435
|
coverage = len(covered) / len(coverage_tokens)
|
|
297
436
|
if len(covered) >= 2 and coverage >= 0.67:
|
|
298
437
|
return round(40 * coverage), specificity, matched, "trigger_partial"
|
|
438
|
+
matched_specific_query = matched & (query_set - context_words)
|
|
439
|
+
matched_context = matched & context_words
|
|
440
|
+
if broad_matching and matched_specific_query and matched_context:
|
|
441
|
+
return 35, specificity, matched, "trigger_broad"
|
|
299
442
|
return 0, specificity, set(), ""
|
|
300
443
|
|
|
301
444
|
|
|
@@ -309,6 +452,8 @@ def _score_skill(
|
|
|
309
452
|
stop_words = policy["stop_words"]
|
|
310
453
|
context_words = policy["context_words"]
|
|
311
454
|
abbreviations = policy["abbreviations"]
|
|
455
|
+
token_aliases = policy["token_aliases"]
|
|
456
|
+
broad_matching = policy["broad_matching"]
|
|
312
457
|
query_set = set(query_tokens)
|
|
313
458
|
specific_query = query_set - context_words
|
|
314
459
|
context_query = query_set & context_words
|
|
@@ -317,7 +462,8 @@ def _score_skill(
|
|
|
317
462
|
strongest_trigger = (0, 0, set(), "", "")
|
|
318
463
|
for trigger in _trigger_values(triggers, skill_id, product_id):
|
|
319
464
|
score, specificity, matched, evidence = _score_trigger(
|
|
320
|
-
query_tokens, trigger, stop_words, context_words, abbreviations
|
|
465
|
+
query_tokens, trigger, stop_words, context_words, abbreviations,
|
|
466
|
+
token_aliases, broad_matching
|
|
321
467
|
)
|
|
322
468
|
candidate = (score, specificity, matched, evidence, normalize_text(trigger))
|
|
323
469
|
candidate_key = (score, specificity, len(matched), normalize_text(trigger))
|
|
@@ -335,7 +481,7 @@ def _score_skill(
|
|
|
335
481
|
matched_tokens = set(trigger_matched)
|
|
336
482
|
|
|
337
483
|
name_tokens = _normalize_scoring_tokens(
|
|
338
|
-
skill.get("name", ""), stop_words, abbreviations
|
|
484
|
+
skill.get("name", ""), stop_words, abbreviations, token_aliases
|
|
339
485
|
)
|
|
340
486
|
name_set = set(name_tokens)
|
|
341
487
|
identity_score = 0
|
|
@@ -352,7 +498,9 @@ def _score_skill(
|
|
|
352
498
|
evidence.append(f"name_context:{token}")
|
|
353
499
|
|
|
354
500
|
category_set = set(
|
|
355
|
-
_normalize_scoring_tokens(
|
|
501
|
+
_normalize_scoring_tokens(
|
|
502
|
+
_category_text(skill), stop_words, abbreviations, token_aliases
|
|
503
|
+
)
|
|
356
504
|
)
|
|
357
505
|
category_score = 0
|
|
358
506
|
for token in sorted(specific_query & category_set):
|
|
@@ -365,7 +513,9 @@ def _score_skill(
|
|
|
365
513
|
evidence.append(f"category_context:{token}")
|
|
366
514
|
|
|
367
515
|
description_set = set(
|
|
368
|
-
_normalize_scoring_tokens(
|
|
516
|
+
_normalize_scoring_tokens(
|
|
517
|
+
skill.get("description", ""), stop_words, abbreviations, token_aliases
|
|
518
|
+
)
|
|
369
519
|
)
|
|
370
520
|
description_matches = sorted(specific_query & description_set)
|
|
371
521
|
description_score = len(description_matches)
|
|
@@ -457,7 +607,28 @@ def match_skills(
|
|
|
457
607
|
|
|
458
608
|
raw_tokens = _tokens(query)
|
|
459
609
|
expanded_tokens = _expand_abbreviations(raw_tokens, configured_abbreviations)
|
|
460
|
-
|
|
610
|
+
aliased_tokens = tuple(
|
|
611
|
+
policy["token_aliases"].get(token, token) for token in expanded_tokens
|
|
612
|
+
)
|
|
613
|
+
initial_query_tokens = tuple(
|
|
614
|
+
token for token in aliased_tokens if token not in policy["stop_words"]
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
|
|
618
|
+
if policy["broad_matching"]:
|
|
619
|
+
token_index, vocabulary = _build_token_index(
|
|
620
|
+
scoped_skills, triggers, product_id, policy
|
|
621
|
+
)
|
|
622
|
+
repaired_tokens, fuzzy_diagnostics = _repair_query_tokens(
|
|
623
|
+
initial_query_tokens, token_index, vocabulary
|
|
624
|
+
)
|
|
625
|
+
else:
|
|
626
|
+
repaired_tokens, fuzzy_diagnostics = initial_query_tokens, []
|
|
627
|
+
query_tokens = tuple(
|
|
628
|
+
policy["token_aliases"].get(token, token) for token in repaired_tokens
|
|
629
|
+
if policy["token_aliases"].get(token, token) not in policy["stop_words"]
|
|
630
|
+
)
|
|
631
|
+
diagnostics.extend(fuzzy_diagnostics)
|
|
461
632
|
query_normalized = " ".join(query_tokens)
|
|
462
633
|
|
|
463
634
|
requested_count = policy["default_result_count"] if result_count is None else result_count
|
|
@@ -465,7 +636,6 @@ def match_skills(
|
|
|
465
636
|
requested_count = policy["default_result_count"]
|
|
466
637
|
requested_count = max(1, min(requested_count, policy["maximum_result_count"]))
|
|
467
638
|
|
|
468
|
-
scoped_skills = [skill for skill in skills if _belongs_to_product(skill, product_id)]
|
|
469
639
|
if len(scoped_skills) != len(skills):
|
|
470
640
|
diagnostics.append("product_mismatches_filtered")
|
|
471
641
|
|