@tasksai/install 0.1.29 → 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/README.md +12 -0
- package/package.json +1 -1
- package/runtime/server.py +73 -8
- package/runtime/skill_matcher.py +178 -13
package/README.md
CHANGED
|
@@ -75,3 +75,15 @@ folder. The save tool supports `.docx` and Markdown output, sanitizes filenames,
|
|
|
75
75
|
and avoids overwriting existing files unless explicitly told to overwrite.
|
|
76
76
|
Generated document content is handled by the customer's AI framework and local
|
|
77
77
|
MCP runtime; it is not sent to TasksAI websites or APIs.
|
|
78
|
+
|
|
79
|
+
## Realtor jurisdiction sources
|
|
80
|
+
|
|
81
|
+
For jurisdiction-sensitive RealtorTasksAI workflows, the local runtime sends
|
|
82
|
+
only the selected skill ID and general state, county, city, public-authority,
|
|
83
|
+
and MLS labels to the TasksAI API. It never sends the user's prompt, property
|
|
84
|
+
address, client facts, uploaded records, or generated document.
|
|
85
|
+
|
|
86
|
+
The API returns a reviewed source pack when one exists. If it does not, the
|
|
87
|
+
licensed runtime can request a controlled official-web lookup. Live results are
|
|
88
|
+
cached, source-linked, and visibly labeled `provisional`; they never substitute
|
|
89
|
+
for private MLS rules, brokerage policy, approved forms, or professional review.
|
package/package.json
CHANGED
package/runtime/server.py
CHANGED
|
@@ -125,7 +125,7 @@ if not LICENSE_KEY:
|
|
|
125
125
|
print("Find your key in your purchase confirmation email.", file=sys.stderr, flush=True)
|
|
126
126
|
sys.exit(1)
|
|
127
127
|
|
|
128
|
-
SERVER_VERSION = "2.
|
|
128
|
+
SERVER_VERSION = "2.5.0"
|
|
129
129
|
|
|
130
130
|
AUTH_HEADERS = {
|
|
131
131
|
"Authorization": f"Bearer {LICENSE_KEY}",
|
|
@@ -509,8 +509,8 @@ async def api_get(path):
|
|
|
509
509
|
return resp.json()
|
|
510
510
|
|
|
511
511
|
|
|
512
|
-
async def api_post(path, payload):
|
|
513
|
-
async with httpx.AsyncClient(timeout=
|
|
512
|
+
async def api_post(path, payload, *, timeout=10.0):
|
|
513
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
514
514
|
resp = await client.post(
|
|
515
515
|
f"{API_BASE}{path}",
|
|
516
516
|
json=payload,
|
|
@@ -523,20 +523,58 @@ async def api_post(path, payload):
|
|
|
523
523
|
def jurisdiction_sources_path(arguments):
|
|
524
524
|
"""Build a metadata-only source lookup path without customer or property data."""
|
|
525
525
|
skill_id = (arguments.get("skill_id") or "").strip()
|
|
526
|
-
state = (arguments.get("state")
|
|
526
|
+
state = general_authority_label(arguments.get("state"), "state")
|
|
527
527
|
if not skill_id:
|
|
528
528
|
raise ValueError("skill_id is required.")
|
|
529
529
|
|
|
530
530
|
params = {"skill_id": skill_id}
|
|
531
531
|
if state:
|
|
532
532
|
params["state"] = state
|
|
533
|
-
for field in ("county", "locality", "district"):
|
|
534
|
-
value = (arguments.get(field)
|
|
533
|
+
for field in ("county", "locality", "district", "mls"):
|
|
534
|
+
value = general_authority_label(arguments.get(field), field)
|
|
535
535
|
if value:
|
|
536
536
|
params[field] = value
|
|
537
537
|
return f"/v1/skills/jurisdiction-sources?{urlencode(params)}"
|
|
538
538
|
|
|
539
539
|
|
|
540
|
+
_GENERAL_AUTHORITY_LABEL = re.compile(r"^[A-Za-z0-9 .,'&()/-]{1,240}$")
|
|
541
|
+
_LIKELY_STREET_ADDRESS = re.compile(
|
|
542
|
+
r"^\d+\s+.+\b(?:street|st|avenue|ave|road|rd|drive|dr|lane|ln|boulevard|blvd|court|ct|way)\b",
|
|
543
|
+
re.IGNORECASE,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def general_authority_label(value, field):
|
|
548
|
+
"""Normalize a public authority label and reject likely customer data."""
|
|
549
|
+
normalized = " ".join(str(value or "").strip().split())
|
|
550
|
+
if not normalized:
|
|
551
|
+
return None
|
|
552
|
+
if (
|
|
553
|
+
not _GENERAL_AUTHORITY_LABEL.fullmatch(normalized)
|
|
554
|
+
or _LIKELY_STREET_ADDRESS.search(normalized)
|
|
555
|
+
):
|
|
556
|
+
raise ValueError(
|
|
557
|
+
f"{field} must be a general authority label, not an address or customer record."
|
|
558
|
+
)
|
|
559
|
+
return normalized
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def jurisdiction_research_payload(arguments):
|
|
563
|
+
"""Build the privacy-safe research request sent only after reviewed lookup misses."""
|
|
564
|
+
payload = {}
|
|
565
|
+
for field in ("skill_id", "state", "county", "locality", "district", "mls"):
|
|
566
|
+
value = (
|
|
567
|
+
(arguments.get(field) or "").strip()
|
|
568
|
+
if field == "skill_id"
|
|
569
|
+
else general_authority_label(arguments.get(field), field)
|
|
570
|
+
)
|
|
571
|
+
if value:
|
|
572
|
+
payload[field] = value
|
|
573
|
+
if not payload.get("skill_id") or not payload.get("state"):
|
|
574
|
+
raise ValueError("skill_id and state are required for live jurisdiction research.")
|
|
575
|
+
return payload
|
|
576
|
+
|
|
577
|
+
|
|
540
578
|
def format_jurisdiction_source_pack(pack):
|
|
541
579
|
"""Render the public source pack as compact instructions for the local AI."""
|
|
542
580
|
status = pack.get("status", "unsupported")
|
|
@@ -548,6 +586,7 @@ def format_jurisdiction_source_pack(pack):
|
|
|
548
586
|
f"- Skill: `{pack.get('skill_id', '')}`",
|
|
549
587
|
f"- Registry reviewed: {pack.get('registry_reviewed_on', 'not supplied')}",
|
|
550
588
|
f"- Registry refresh due: {pack.get('registry_refresh_due_on', 'not supplied')}",
|
|
589
|
+
f"- Resolution: **{pack.get('resolution_source', 'packaged')}**",
|
|
551
590
|
]
|
|
552
591
|
if pack.get("state_name"):
|
|
553
592
|
lines.append(f"- State: {pack['state_name']} ({pack.get('state_code', '')})")
|
|
@@ -566,6 +605,8 @@ def format_jurisdiction_source_pack(pack):
|
|
|
566
605
|
lines.extend(["", "## Sources"])
|
|
567
606
|
for source in sources:
|
|
568
607
|
effective = source.get("effective_date") or "not specified"
|
|
608
|
+
verification = source.get("verification_status", "reviewed")
|
|
609
|
+
date_label = "Machine researched" if verification == "machine_researched" else "Reviewed"
|
|
569
610
|
lines.extend(
|
|
570
611
|
[
|
|
571
612
|
"",
|
|
@@ -573,7 +614,8 @@ def format_jurisdiction_source_pack(pack):
|
|
|
573
614
|
f"- Authority: {source.get('authority', '')}",
|
|
574
615
|
f"- Jurisdiction: {source.get('jurisdiction', '')} ({source.get('scope_type', 'scope not supplied')})",
|
|
575
616
|
f"- Effective date: {effective}",
|
|
576
|
-
f"-
|
|
617
|
+
f"- Verification: {verification}",
|
|
618
|
+
f"- {date_label}: {source.get('reviewed_on', '')}",
|
|
577
619
|
f"- Refresh due: {source.get('refresh_due_on', '')}",
|
|
578
620
|
f"- URL: {source.get('url', '')}",
|
|
579
621
|
f"- Use: {source.get('purpose', '')}",
|
|
@@ -1672,8 +1714,9 @@ def build_tools(prefix, product_name, occupation):
|
|
|
1672
1714
|
Tool(
|
|
1673
1715
|
name=f"{prefix}_jurisdiction_sources",
|
|
1674
1716
|
description=(
|
|
1675
|
-
f"Retrieve the
|
|
1717
|
+
f"Retrieve the current jurisdiction-and-authority sources for a jurisdiction-sensitive {product_name} workflow. "
|
|
1676
1718
|
"Use after the applicable state is known and before applying state, local, district, regulator, form, or policy requirements. "
|
|
1719
|
+
"For RealtorTasksAI, a missing reviewed pack can trigger a licensed, privacy-safe official-web lookup whose results are explicitly provisional. "
|
|
1677
1720
|
"Send only the skill ID and general authority labels; never send an address, person name, student or client facts, documents, or generated content."
|
|
1678
1721
|
),
|
|
1679
1722
|
inputSchema={
|
|
@@ -1699,6 +1742,10 @@ def build_tools(prefix, product_name, occupation):
|
|
|
1699
1742
|
"type": "string",
|
|
1700
1743
|
"description": "Optional public authority or school-district name only; do not include a person, student, or record identifier.",
|
|
1701
1744
|
},
|
|
1745
|
+
"mls": {
|
|
1746
|
+
"type": "string",
|
|
1747
|
+
"description": "Optional MLS or Realtor-association name only; never send credentials, listing data, or member-only content.",
|
|
1748
|
+
},
|
|
1702
1749
|
},
|
|
1703
1750
|
"required": ["skill_id"],
|
|
1704
1751
|
},
|
|
@@ -1905,6 +1952,24 @@ async def call_tool(name, arguments):
|
|
|
1905
1952
|
}:
|
|
1906
1953
|
path = jurisdiction_sources_path(arguments or {})
|
|
1907
1954
|
result = await api_get(path)
|
|
1955
|
+
if (
|
|
1956
|
+
prefix == "realtortasksai"
|
|
1957
|
+
and (arguments or {}).get("state")
|
|
1958
|
+
and result.get("status") not in {"ready", "provisional"}
|
|
1959
|
+
):
|
|
1960
|
+
try:
|
|
1961
|
+
result = await api_post(
|
|
1962
|
+
"/v1/skills/jurisdiction-research",
|
|
1963
|
+
jurisdiction_research_payload(arguments or {}),
|
|
1964
|
+
timeout=90.0,
|
|
1965
|
+
)
|
|
1966
|
+
except Exception:
|
|
1967
|
+
result = dict(result)
|
|
1968
|
+
instructions = list(result.get("instructions") or [])
|
|
1969
|
+
instructions.append(
|
|
1970
|
+
"Controlled live research was unavailable. Continue only with intake or professional-supplied current official sources; do not substitute another jurisdiction."
|
|
1971
|
+
)
|
|
1972
|
+
result["instructions"] = instructions
|
|
1908
1973
|
return [TextContent(type="text", text=format_jurisdiction_source_pack(result))]
|
|
1909
1974
|
|
|
1910
1975
|
# ── Save Document ───────────────────────────────────────────────────
|
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
|
|