@tasksai/install 0.1.28 → 0.1.30
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 +147 -8
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', '')}",
|
|
@@ -1436,11 +1478,85 @@ def write_markdown(path, title, content_markdown, product_name):
|
|
|
1436
1478
|
path.write_text("\n\n".join(part for part in parts if part), encoding="utf-8")
|
|
1437
1479
|
|
|
1438
1480
|
|
|
1481
|
+
def reconcile_public_copy_counts(content_markdown):
|
|
1482
|
+
"""Replace stale public-copy counts with deterministic local counts.
|
|
1483
|
+
|
|
1484
|
+
AI hosts sometimes leave ``NOT VERIFIED`` or an estimated count beside a
|
|
1485
|
+
marked public-copy body. The local runtime can resolve that mechanical
|
|
1486
|
+
value without transmitting the document. It changes only count lines tied
|
|
1487
|
+
to balanced PUBLIC COPY markers and a detectable numeric maximum.
|
|
1488
|
+
"""
|
|
1489
|
+
start_marker = "<!-- PUBLIC COPY START -->"
|
|
1490
|
+
end_marker = "<!-- PUBLIC COPY END -->"
|
|
1491
|
+
if content_markdown.count(start_marker) != content_markdown.count(end_marker):
|
|
1492
|
+
return content_markdown
|
|
1493
|
+
|
|
1494
|
+
blocks = list(
|
|
1495
|
+
re.finditer(
|
|
1496
|
+
re.escape(start_marker) + r"\n?(.*?)\n?" + re.escape(end_marker),
|
|
1497
|
+
content_markdown,
|
|
1498
|
+
re.DOTALL,
|
|
1499
|
+
)
|
|
1500
|
+
)
|
|
1501
|
+
if not blocks:
|
|
1502
|
+
return content_markdown
|
|
1503
|
+
|
|
1504
|
+
count_pattern = re.compile(
|
|
1505
|
+
r"(?im)^(?P<prefix>\s*(?:\*\*)?Deterministic count:(?:\*\*)?\s*)"
|
|
1506
|
+
r"(?P<actual>NOT VERIFIED|[\d,]+)"
|
|
1507
|
+
r"(?:\s*/\s*(?P<target>[\d,]+)\s*(?P<unit>words|characters))?\s*$"
|
|
1508
|
+
)
|
|
1509
|
+
limit_patterns = (
|
|
1510
|
+
re.compile(r"(?i)\bmaximum(?:\s+public[- ]copy\s+length)?\s+(?:is\s+)?([\d,]+)\s*(words|characters)\b"),
|
|
1511
|
+
re.compile(r"(?i)\bmax(?:imum)?\s*[:=]?\s*([\d,]+)\s*(words|characters)\b"),
|
|
1512
|
+
re.compile(r"(?i)\b([\d,]+)[ -](word|character)\s+maximum\b"),
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
replacements = []
|
|
1516
|
+
count_matches = list(count_pattern.finditer(content_markdown))
|
|
1517
|
+
for block in blocks:
|
|
1518
|
+
following = [match for match in count_matches if match.start() >= block.end()]
|
|
1519
|
+
if not following:
|
|
1520
|
+
continue
|
|
1521
|
+
count_match = min(following, key=lambda match: match.start() - block.end())
|
|
1522
|
+
if count_match.start() - block.end() > 2000:
|
|
1523
|
+
continue
|
|
1524
|
+
|
|
1525
|
+
target_text = count_match.group("target")
|
|
1526
|
+
unit = (count_match.group("unit") or "").lower()
|
|
1527
|
+
if not target_text or not unit:
|
|
1528
|
+
limit_match = None
|
|
1529
|
+
for pattern in limit_patterns:
|
|
1530
|
+
limit_match = pattern.search(content_markdown)
|
|
1531
|
+
if limit_match:
|
|
1532
|
+
break
|
|
1533
|
+
if not limit_match:
|
|
1534
|
+
continue
|
|
1535
|
+
target_text = limit_match.group(1)
|
|
1536
|
+
unit = limit_match.group(2).lower()
|
|
1537
|
+
if unit == "word":
|
|
1538
|
+
unit = "words"
|
|
1539
|
+
elif unit == "character":
|
|
1540
|
+
unit = "characters"
|
|
1541
|
+
|
|
1542
|
+
target = int(target_text.replace(",", ""))
|
|
1543
|
+
body = block.group(1)
|
|
1544
|
+
actual = len(body) if unit == "characters" else len(re.findall(r"\S+", body))
|
|
1545
|
+
replacement = f"{count_match.group('prefix')}{actual} / {target} {unit}"
|
|
1546
|
+
replacements.append((count_match.start(), count_match.end(), replacement))
|
|
1547
|
+
|
|
1548
|
+
corrected = content_markdown
|
|
1549
|
+
for start, end, replacement in reversed(replacements):
|
|
1550
|
+
corrected = corrected[:start] + replacement + corrected[end:]
|
|
1551
|
+
return corrected
|
|
1552
|
+
|
|
1553
|
+
|
|
1439
1554
|
def save_document(arguments, product_name):
|
|
1440
1555
|
"""Save AI-produced final content to a local downloadable file."""
|
|
1441
1556
|
content_markdown = (arguments.get("content_markdown") or arguments.get("content") or "").strip()
|
|
1442
1557
|
if not content_markdown:
|
|
1443
1558
|
raise ValueError("content_markdown is required.")
|
|
1559
|
+
content_markdown = reconcile_public_copy_counts(content_markdown)
|
|
1444
1560
|
|
|
1445
1561
|
title = (arguments.get("title") or "").strip()
|
|
1446
1562
|
skill_id = (arguments.get("skill_id") or "").strip()
|
|
@@ -1598,8 +1714,9 @@ def build_tools(prefix, product_name, occupation):
|
|
|
1598
1714
|
Tool(
|
|
1599
1715
|
name=f"{prefix}_jurisdiction_sources",
|
|
1600
1716
|
description=(
|
|
1601
|
-
f"Retrieve the
|
|
1717
|
+
f"Retrieve the current jurisdiction-and-authority sources for a jurisdiction-sensitive {product_name} workflow. "
|
|
1602
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. "
|
|
1603
1720
|
"Send only the skill ID and general authority labels; never send an address, person name, student or client facts, documents, or generated content."
|
|
1604
1721
|
),
|
|
1605
1722
|
inputSchema={
|
|
@@ -1625,6 +1742,10 @@ def build_tools(prefix, product_name, occupation):
|
|
|
1625
1742
|
"type": "string",
|
|
1626
1743
|
"description": "Optional public authority or school-district name only; do not include a person, student, or record identifier.",
|
|
1627
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
|
+
},
|
|
1628
1749
|
},
|
|
1629
1750
|
"required": ["skill_id"],
|
|
1630
1751
|
},
|
|
@@ -1831,6 +1952,24 @@ async def call_tool(name, arguments):
|
|
|
1831
1952
|
}:
|
|
1832
1953
|
path = jurisdiction_sources_path(arguments or {})
|
|
1833
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
|
|
1834
1973
|
return [TextContent(type="text", text=format_jurisdiction_source_pack(result))]
|
|
1835
1974
|
|
|
1836
1975
|
# ── Save Document ───────────────────────────────────────────────────
|