@tasksai/install 0.1.38 → 0.1.40
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 +34 -0
- package/bootstrap/Install RealtorTasksAI.command +37 -0
- package/bootstrap/Install RealtorTasksAI.ps1 +42 -0
- package/package.json +14 -3
- package/runtime/document_renderer.py +882 -0
- package/runtime/server.py +55 -581
- package/runtime/software_use.py +39 -0
- package/runtime/workflow-requirements.txt +5 -0
- package/runtime/workflows/__init__.py +0 -0
- package/runtime/workflows/account_snapshot.py +33 -0
- package/runtime/workflows/attachments.py +45 -0
- package/runtime/workflows/authority_guides.py +81 -0
- package/runtime/workflows/catalog.py +51 -0
- package/runtime/workflows/catalog_app.py +174 -0
- package/runtime/workflows/catalog_generation.py +186 -0
- package/runtime/workflows/catalog_output.py +312 -0
- package/runtime/workflows/catalog_suggestions.py +51 -0
- package/runtime/workflows/catalog_workspace.py +152 -0
- package/runtime/workflows/cli.py +66 -0
- package/runtime/workflows/document_answers.py +96 -0
- package/runtime/workflows/document_selection.py +50 -0
- package/runtime/workflows/documents.py +243 -0
- package/runtime/workflows/embedded/catalog.html +83 -0
- package/runtime/workflows/embedded/offer-review.html +516 -0
- package/runtime/workflows/embedded/preview.html +36 -0
- package/runtime/workflows/embedded_actions.py +96 -0
- package/runtime/workflows/embedded_demo.py +424 -0
- package/runtime/workflows/folders.py +120 -0
- package/runtime/workflows/gateway.py +157 -0
- package/runtime/workflows/generation_lock.py +26 -0
- package/runtime/workflows/launcher.py +61 -0
- package/runtime/workflows/licensed_delivery.py +46 -0
- package/runtime/workflows/mcp_dev.py +52 -0
- package/runtime/workflows/meeting_plan.py +92 -0
- package/runtime/workflows/model_client.py +26 -0
- package/runtime/workflows/numeric_consistency.py +72 -0
- package/runtime/workflows/public_source_fetch.py +66 -0
- package/runtime/workflows/realtor/__init__.py +0 -0
- package/runtime/workflows/realtor/adapter.py +179 -0
- package/runtime/workflows/realtor/seller_offer/ORIGIN.json +16 -0
- package/runtime/workflows/realtor/seller_offer/__init__.py +0 -0
- package/runtime/workflows/realtor/seller_offer/examples/demo-input.json +414 -0
- package/runtime/workflows/realtor/seller_offer/examples/make_demo.py +44 -0
- package/runtime/workflows/realtor/seller_offer/references/input-contract.md +65 -0
- package/runtime/workflows/realtor/seller_offer/references/source-boundaries.md +16 -0
- package/runtime/workflows/realtor/seller_offer/scripts/__init__.py +0 -0
- package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.mjs +233 -0
- package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.py +163 -0
- package/runtime/workflows/realtor/seller_offer/scripts/offer_engine.py +370 -0
- package/runtime/workflows/realtor/seller_offer/scripts/presentation.py +52 -0
- package/runtime/workflows/realtor/seller_offer/scripts/render_outputs.py +228 -0
- package/runtime/workflows/realtor/seller_offer/scripts/run_package.py +95 -0
- package/runtime/workflows/realtor/seller_offer/tests/test_engine.py +252 -0
- package/runtime/workflows/realtor/seller_offer/tests/test_workbook.mjs +28 -0
- package/runtime/workflows/realtor-release-registry.json +705 -0
- package/runtime/workflows/released_catalog.py +91 -0
- package/runtime/workflows/source_capture.py +82 -0
- package/runtime/workflows/store.py +492 -0
- package/runtime/workflows/table_calculations.py +132 -0
- package/runtime/workflows/template.py +65 -0
- package/runtime/workflows/workspace_launch.py +76 -0
- package/src/index.js +217 -118
- package/src/managed-python.js +63 -0
- package/src/operation-lock.js +22 -0
- package/src/prepared-update.js +86 -0
- package/src/private-workflow.js +116 -0
- package/src/python-runtime.js +50 -0
- package/src/recover-installation.js +30 -0
- package/src/recovery-lock.js +30 -0
- package/src/software-hash.js +24 -0
- package/src/software-use.js +32 -0
- package/src/update-journal.js +24 -0
- package/src/update-recovery.js +72 -0
- package/src/workspace-runtime.js +45 -0
package/runtime/server.py
CHANGED
|
@@ -7,15 +7,16 @@ then self-configures tool names, system prompt, and abbreviation maps.
|
|
|
7
7
|
|
|
8
8
|
Tools (names are vertical-prefixed at runtime, e.g. farmertasksai_search):
|
|
9
9
|
{prefix}_search — Find the right skill for your task
|
|
10
|
-
{prefix}_execute — Get the full expert framework
|
|
10
|
+
{prefix}_execute — Get the full expert framework at its configured credit charge
|
|
11
11
|
{prefix}_save_document — Save the finished local output as a downloadable file
|
|
12
12
|
{prefix}_balance — Check your remaining credit balance
|
|
13
13
|
{prefix}_categories — Browse skills by category
|
|
14
14
|
|
|
15
|
-
Privacy:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
Privacy: TasksAI saves project data and document files locally. Its APIs deliver
|
|
16
|
+
skill content, account/credit information and authority-source metadata; they do
|
|
17
|
+
not receive customer document content through this workflow. Cloud AI processing
|
|
18
|
+
is separate: the selected AI provider receives the information supplied to it.
|
|
19
|
+
The local workspace asks for consent before sending selected information to AI.
|
|
19
20
|
"""
|
|
20
21
|
|
|
21
22
|
import os
|
|
@@ -891,580 +892,9 @@ def unique_output_path(root, filename, overwrite=False):
|
|
|
891
892
|
raise RuntimeError("Could not choose a unique output filename.")
|
|
892
893
|
|
|
893
894
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if "|" not in stripped:
|
|
898
|
-
return None
|
|
899
|
-
if stripped.startswith("|"):
|
|
900
|
-
stripped = stripped[1:]
|
|
901
|
-
if stripped.endswith("|"):
|
|
902
|
-
stripped = stripped[:-1]
|
|
903
|
-
cells = []
|
|
904
|
-
current = []
|
|
905
|
-
escaped = False
|
|
906
|
-
for char in stripped:
|
|
907
|
-
if escaped:
|
|
908
|
-
current.append(char)
|
|
909
|
-
escaped = False
|
|
910
|
-
elif char == "\\":
|
|
911
|
-
escaped = True
|
|
912
|
-
elif char == "|":
|
|
913
|
-
cells.append("".join(current).strip())
|
|
914
|
-
current = []
|
|
915
|
-
else:
|
|
916
|
-
current.append(char)
|
|
917
|
-
cells.append("".join(current).strip())
|
|
918
|
-
return cells if len(cells) > 1 else None
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
def markdown_table_alignments(separator_cells):
|
|
922
|
-
"""Return column alignments if cells are a valid Markdown table separator."""
|
|
923
|
-
alignments = []
|
|
924
|
-
for cell in separator_cells or []:
|
|
925
|
-
compact = cell.replace(" ", "")
|
|
926
|
-
if not re.match(r"^:?-{3,}:?$", compact):
|
|
927
|
-
return None
|
|
928
|
-
if compact.startswith(":") and compact.endswith(":"):
|
|
929
|
-
alignments.append("center")
|
|
930
|
-
elif compact.endswith(":"):
|
|
931
|
-
alignments.append("right")
|
|
932
|
-
else:
|
|
933
|
-
alignments.append("left")
|
|
934
|
-
return alignments
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
def is_markdown_table_start(lines, index):
|
|
938
|
-
"""True when lines[index:index+2] form a Markdown table header."""
|
|
939
|
-
if index + 1 >= len(lines):
|
|
940
|
-
return False
|
|
941
|
-
header = markdown_table_row(lines[index])
|
|
942
|
-
separator = markdown_table_row(lines[index + 1])
|
|
943
|
-
return bool(header and markdown_table_alignments(separator))
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
def inferred_plain_heading_level(lines, index):
|
|
947
|
-
"""Infer conservative headings when an AI omits Markdown # markers."""
|
|
948
|
-
text = lines[index].strip()
|
|
949
|
-
if not text or len(text) > 90 or "|" in text or ":" in text:
|
|
950
|
-
return None
|
|
951
|
-
if re.match(r"^(?:[-*+]\s+|\d+\.\s+|>|<!--)", text):
|
|
952
|
-
return None
|
|
953
|
-
if text.endswith((".", "?", "!", ";")):
|
|
954
|
-
return None
|
|
955
|
-
|
|
956
|
-
letters = [char for char in text if char.isalpha()]
|
|
957
|
-
if len(letters) >= 4 and all(char.isupper() for char in letters):
|
|
958
|
-
return 2
|
|
959
|
-
|
|
960
|
-
next_index = index + 1
|
|
961
|
-
while next_index < len(lines) and not lines[next_index].strip():
|
|
962
|
-
next_index += 1
|
|
963
|
-
next_line = lines[next_index] if next_index < len(lines) else ""
|
|
964
|
-
introduces_list = bool(
|
|
965
|
-
re.match(r"^\s*[-*+]\s+\S", next_line)
|
|
966
|
-
or re.match(r"^\s*\d+\.\s+\S", next_line)
|
|
967
|
-
)
|
|
968
|
-
introduces_facts = bool(re.match(r"^\s*[^:\n]{1,55}:\s+\S", next_line))
|
|
969
|
-
if introduces_list or introduces_facts or text.istitle():
|
|
970
|
-
return 3
|
|
971
|
-
return None
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
def labeled_fact(line):
|
|
975
|
-
"""Return a label/value pair for a short professional fact line."""
|
|
976
|
-
bold_match = re.match(r"^\*\*([^*\n:]{1,55}):\*\*\s+(.+)$", line.strip())
|
|
977
|
-
if bold_match:
|
|
978
|
-
return bold_match.group(1).strip(), bold_match.group(2).strip()
|
|
979
|
-
match = re.match(r"^([^:\n]{1,55}):\s+(.+)$", line.strip())
|
|
980
|
-
if not match:
|
|
981
|
-
return None
|
|
982
|
-
if re.search(r"[`*_\[\]]", match.group(1)):
|
|
983
|
-
# A colon after inline Markdown is sentence punctuation, not a
|
|
984
|
-
# short label/value field. Let the normal inline parser handle it.
|
|
985
|
-
return None
|
|
986
|
-
return match.group(1).strip(), match.group(2).strip()
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
def add_labeled_text(paragraph, label, value):
|
|
990
|
-
"""Render a label/value pair in an existing Word paragraph."""
|
|
991
|
-
label_run = paragraph.add_run(f"{label}: ")
|
|
992
|
-
label_run.bold = True
|
|
993
|
-
add_markdown_text(paragraph, value)
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
def add_labeled_fact(doc, label, value):
|
|
997
|
-
"""Render a fact line with a bold label and distinct Word paragraph."""
|
|
998
|
-
paragraph = doc.add_paragraph()
|
|
999
|
-
add_labeled_text(paragraph, label, value)
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
def iter_inline_markdown(text):
|
|
1003
|
-
"""Yield (text, marks) spans for common inline Markdown."""
|
|
1004
|
-
text = re.sub(r"\\([\\`*_{}\[\]()#+.!-])", r"\1", text)
|
|
1005
|
-
token_re = re.compile(
|
|
1006
|
-
r"(`[^`]+`|\*\*[^*]+\*\*|(?<!_)__(?![_\s])[^_]+?(?<!\s)__(?!_)|\*[^*\s][^*]*\*|(?<!\w)_[^_\s][^_]*_(?!\w)|\[[^\]]+\]\([^)]+\))"
|
|
1007
|
-
)
|
|
1008
|
-
pos = 0
|
|
1009
|
-
for match in token_re.finditer(text):
|
|
1010
|
-
if match.start() > pos:
|
|
1011
|
-
yield text[pos:match.start()], {}
|
|
1012
|
-
token = match.group(0)
|
|
1013
|
-
marks = {}
|
|
1014
|
-
value = token
|
|
1015
|
-
if token.startswith("**") and token.endswith("**"):
|
|
1016
|
-
value = token[2:-2]
|
|
1017
|
-
marks["bold"] = True
|
|
1018
|
-
elif token.startswith("__") and token.endswith("__"):
|
|
1019
|
-
value = token[2:-2]
|
|
1020
|
-
marks["bold"] = True
|
|
1021
|
-
elif token.startswith("`") and token.endswith("`"):
|
|
1022
|
-
value = token[1:-1]
|
|
1023
|
-
marks["code"] = True
|
|
1024
|
-
elif token.startswith("["):
|
|
1025
|
-
link = re.match(r"^\[([^\]]+)\]\(([^)]+)\)$", token)
|
|
1026
|
-
if link:
|
|
1027
|
-
value = link.group(1)
|
|
1028
|
-
marks["link"] = link.group(2)
|
|
1029
|
-
elif token[0] in {"*", "_"} and token.endswith(token[0]):
|
|
1030
|
-
value = token[1:-1]
|
|
1031
|
-
marks["italic"] = True
|
|
1032
|
-
yield value, marks
|
|
1033
|
-
pos = match.end()
|
|
1034
|
-
if pos < len(text):
|
|
1035
|
-
yield text[pos:], {}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
def add_hyperlink(paragraph, text, url):
|
|
1039
|
-
"""Add a clickable hyperlink run when python-docx low-level APIs are available."""
|
|
1040
|
-
from docx.oxml import OxmlElement
|
|
1041
|
-
from docx.oxml.ns import qn
|
|
1042
|
-
|
|
1043
|
-
part = paragraph.part
|
|
1044
|
-
relationship_id = part.relate_to(
|
|
1045
|
-
url,
|
|
1046
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
1047
|
-
is_external=True,
|
|
1048
|
-
)
|
|
1049
|
-
|
|
1050
|
-
hyperlink = OxmlElement("w:hyperlink")
|
|
1051
|
-
hyperlink.set(qn("r:id"), relationship_id)
|
|
1052
|
-
run_element = OxmlElement("w:r")
|
|
1053
|
-
properties = OxmlElement("w:rPr")
|
|
1054
|
-
|
|
1055
|
-
color = OxmlElement("w:color")
|
|
1056
|
-
color.set(qn("w:val"), "0563C1")
|
|
1057
|
-
properties.append(color)
|
|
1058
|
-
|
|
1059
|
-
underline = OxmlElement("w:u")
|
|
1060
|
-
underline.set(qn("w:val"), "single")
|
|
1061
|
-
properties.append(underline)
|
|
1062
|
-
|
|
1063
|
-
run_element.append(properties)
|
|
1064
|
-
text_element = OxmlElement("w:t")
|
|
1065
|
-
text_element.text = text
|
|
1066
|
-
run_element.append(text_element)
|
|
1067
|
-
hyperlink.append(run_element)
|
|
1068
|
-
paragraph._p.append(hyperlink)
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
def add_markdown_text(paragraph, text):
|
|
1072
|
-
"""Add common inline Markdown formatting to a python-docx paragraph."""
|
|
1073
|
-
for value, marks in iter_inline_markdown(text):
|
|
1074
|
-
if not value:
|
|
1075
|
-
continue
|
|
1076
|
-
if marks.get("link"):
|
|
1077
|
-
try:
|
|
1078
|
-
add_hyperlink(paragraph, value, marks["link"])
|
|
1079
|
-
except Exception:
|
|
1080
|
-
paragraph.add_run(f"{value} ({marks['link']})")
|
|
1081
|
-
continue
|
|
1082
|
-
run = paragraph.add_run(value)
|
|
1083
|
-
if marks.get("bold"):
|
|
1084
|
-
run.bold = True
|
|
1085
|
-
if re.match(r"^\+\d", value):
|
|
1086
|
-
# Aptos substitution in some headless Word renderers maps a
|
|
1087
|
-
# bold leading plus sign to a diamond. Arial preserves the
|
|
1088
|
-
# intended glyph and remains portable in Office.
|
|
1089
|
-
run.font.name = "Arial"
|
|
1090
|
-
if marks.get("italic"):
|
|
1091
|
-
run.italic = True
|
|
1092
|
-
if marks.get("code"):
|
|
1093
|
-
run.font.name = "Courier New"
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
def add_markdown_heading(doc, text, level):
|
|
1097
|
-
"""Create a Word heading while parsing any inline Markdown in its text."""
|
|
1098
|
-
|
|
1099
|
-
paragraph = doc.add_heading(level=level)
|
|
1100
|
-
add_markdown_text(paragraph, text)
|
|
1101
|
-
return paragraph
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
def add_horizontal_rule(paragraph):
|
|
1105
|
-
"""Render a horizontal Markdown rule as a Word paragraph border."""
|
|
1106
|
-
from docx.oxml import OxmlElement
|
|
1107
|
-
from docx.oxml.ns import qn
|
|
1108
|
-
|
|
1109
|
-
p_pr = paragraph._p.get_or_add_pPr()
|
|
1110
|
-
borders = p_pr.first_child_found_in("w:pBdr")
|
|
1111
|
-
if borders is None:
|
|
1112
|
-
borders = OxmlElement("w:pBdr")
|
|
1113
|
-
p_pr.append(borders)
|
|
1114
|
-
bottom = OxmlElement("w:bottom")
|
|
1115
|
-
bottom.set(qn("w:val"), "single")
|
|
1116
|
-
bottom.set(qn("w:sz"), "6")
|
|
1117
|
-
bottom.set(qn("w:space"), "1")
|
|
1118
|
-
bottom.set(qn("w:color"), "BFBFBF")
|
|
1119
|
-
borders.append(bottom)
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
def set_cell_shading(cell, fill):
|
|
1123
|
-
"""Apply a solid background color to a Word table cell."""
|
|
1124
|
-
from docx.oxml import OxmlElement
|
|
1125
|
-
from docx.oxml.ns import qn
|
|
1126
|
-
|
|
1127
|
-
cell_properties = cell._tc.get_or_add_tcPr()
|
|
1128
|
-
shading = cell_properties.find(qn("w:shd"))
|
|
1129
|
-
if shading is None:
|
|
1130
|
-
shading = OxmlElement("w:shd")
|
|
1131
|
-
cell_properties.append(shading)
|
|
1132
|
-
shading.set(qn("w:fill"), fill)
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
def set_cell_margins(cell, top=70, start=90, bottom=70, end=90):
|
|
1136
|
-
"""Set compact, readable table cell margins in twentieths of a point."""
|
|
1137
|
-
from docx.oxml import OxmlElement
|
|
1138
|
-
from docx.oxml.ns import qn
|
|
1139
|
-
|
|
1140
|
-
cell_properties = cell._tc.get_or_add_tcPr()
|
|
1141
|
-
margins = cell_properties.first_child_found_in("w:tcMar")
|
|
1142
|
-
if margins is None:
|
|
1143
|
-
margins = OxmlElement("w:tcMar")
|
|
1144
|
-
cell_properties.append(margins)
|
|
1145
|
-
for name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
|
1146
|
-
node = margins.find(qn(f"w:{name}"))
|
|
1147
|
-
if node is None:
|
|
1148
|
-
node = OxmlElement(f"w:{name}")
|
|
1149
|
-
margins.append(node)
|
|
1150
|
-
node.set(qn("w:w"), str(value))
|
|
1151
|
-
node.set(qn("w:type"), "dxa")
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
def repeat_table_header(row):
|
|
1155
|
-
"""Repeat the first row of a long table on subsequent pages."""
|
|
1156
|
-
from docx.oxml import OxmlElement
|
|
1157
|
-
from docx.oxml.ns import qn
|
|
1158
|
-
|
|
1159
|
-
row_properties = row._tr.get_or_add_trPr()
|
|
1160
|
-
repeat = row_properties.find(qn("w:tblHeader"))
|
|
1161
|
-
if repeat is None:
|
|
1162
|
-
repeat = OxmlElement("w:tblHeader")
|
|
1163
|
-
row_properties.append(repeat)
|
|
1164
|
-
repeat.set(qn("w:val"), "true")
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
def prevent_table_row_split(row):
|
|
1168
|
-
"""Keep a table row together when Word paginates the document."""
|
|
1169
|
-
from docx.oxml import OxmlElement
|
|
1170
|
-
from docx.oxml.ns import qn
|
|
1171
|
-
|
|
1172
|
-
row_properties = row._tr.get_or_add_trPr()
|
|
1173
|
-
if row_properties.find(qn("w:cantSplit")) is None:
|
|
1174
|
-
row_properties.append(OxmlElement("w:cantSplit"))
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
def add_page_number(paragraph):
|
|
1178
|
-
"""Add a dynamic PAGE field to a footer paragraph."""
|
|
1179
|
-
from docx.oxml import OxmlElement
|
|
1180
|
-
from docx.oxml.ns import qn
|
|
1181
|
-
|
|
1182
|
-
run = paragraph.add_run()
|
|
1183
|
-
begin = OxmlElement("w:fldChar")
|
|
1184
|
-
begin.set(qn("w:fldCharType"), "begin")
|
|
1185
|
-
instruction = OxmlElement("w:instrText")
|
|
1186
|
-
instruction.set(qn("xml:space"), "preserve")
|
|
1187
|
-
instruction.text = " PAGE "
|
|
1188
|
-
end = OxmlElement("w:fldChar")
|
|
1189
|
-
end.set(qn("w:fldCharType"), "end")
|
|
1190
|
-
run._r.extend((begin, instruction, end))
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
def add_markdown_table(doc, rows, alignments):
|
|
1194
|
-
"""Render a Markdown pipe table as a native Word table."""
|
|
1195
|
-
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
1196
|
-
from docx.shared import Pt, RGBColor
|
|
1197
|
-
|
|
1198
|
-
if not rows:
|
|
1199
|
-
return
|
|
1200
|
-
|
|
1201
|
-
width = max(len(row) for row in rows)
|
|
1202
|
-
normalized = [row + [""] * (width - len(row)) for row in rows]
|
|
1203
|
-
table = doc.add_table(rows=len(normalized), cols=width)
|
|
1204
|
-
table.style = "Table Grid"
|
|
1205
|
-
table.autofit = True
|
|
1206
|
-
repeat_table_header(table.rows[0])
|
|
1207
|
-
for row_index, row in enumerate(normalized):
|
|
1208
|
-
row_characters = sum(len(value) for value in row)
|
|
1209
|
-
longest_cell = max((len(value) for value in row), default=0)
|
|
1210
|
-
if row_index == 0 or (row_characters <= 600 and longest_cell <= 350):
|
|
1211
|
-
# Keep ordinary rows intact so a cell does not strand a fragment
|
|
1212
|
-
# on the next page. Exceptionally large rows may still split to
|
|
1213
|
-
# avoid creating a mostly blank page.
|
|
1214
|
-
prevent_table_row_split(table.rows[row_index])
|
|
1215
|
-
for column_index, value in enumerate(row):
|
|
1216
|
-
cell = table.cell(row_index, column_index)
|
|
1217
|
-
set_cell_margins(cell)
|
|
1218
|
-
if row_index == 0:
|
|
1219
|
-
set_cell_shading(cell, "17365D")
|
|
1220
|
-
elif row_index % 2 == 0:
|
|
1221
|
-
set_cell_shading(cell, "EEF3F8")
|
|
1222
|
-
paragraph = cell.paragraphs[0]
|
|
1223
|
-
paragraph.paragraph_format.space_after = Pt(0)
|
|
1224
|
-
add_markdown_text(paragraph, value)
|
|
1225
|
-
if row_index == 0:
|
|
1226
|
-
for run in paragraph.runs:
|
|
1227
|
-
run.bold = True
|
|
1228
|
-
run.font.color.rgb = RGBColor(255, 255, 255)
|
|
1229
|
-
alignment = alignments[column_index] if column_index < len(alignments) else "left"
|
|
1230
|
-
if alignment == "right":
|
|
1231
|
-
paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
1232
|
-
elif alignment == "center":
|
|
1233
|
-
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
1234
|
-
for run in paragraph.runs:
|
|
1235
|
-
run.font.size = Pt(8 if width > 6 else 9)
|
|
1236
|
-
|
|
1237
|
-
doc.add_paragraph()
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
def write_docx(path, title, content_markdown, product_name):
|
|
1241
|
-
"""Write a professionally styled DOCX file locally."""
|
|
1242
|
-
try:
|
|
1243
|
-
from docx import Document
|
|
1244
|
-
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
1245
|
-
from docx.shared import Inches, Pt, RGBColor
|
|
1246
|
-
except Exception as exc:
|
|
1247
|
-
raise RuntimeError(
|
|
1248
|
-
"python-docx is not installed. Re-run the official installer or install python-docx in the MCP runtime environment."
|
|
1249
|
-
) from exc
|
|
1250
|
-
|
|
1251
|
-
doc = Document()
|
|
1252
|
-
doc.core_properties.title = title or f"{product_name} Work Product"
|
|
1253
|
-
doc.core_properties.author = product_name
|
|
1254
|
-
|
|
1255
|
-
section = doc.sections[0]
|
|
1256
|
-
section.top_margin = Inches(0.55)
|
|
1257
|
-
section.bottom_margin = Inches(0.55)
|
|
1258
|
-
section.left_margin = Inches(0.7)
|
|
1259
|
-
section.right_margin = Inches(0.7)
|
|
1260
|
-
section.header_distance = Inches(0.3)
|
|
1261
|
-
section.footer_distance = Inches(0.3)
|
|
1262
|
-
|
|
1263
|
-
styles = doc.styles
|
|
1264
|
-
normal = styles["Normal"]
|
|
1265
|
-
normal.font.name = "Aptos"
|
|
1266
|
-
normal.font.size = Pt(9.3)
|
|
1267
|
-
normal.font.color.rgb = RGBColor(31, 41, 55)
|
|
1268
|
-
normal.paragraph_format.space_after = Pt(4)
|
|
1269
|
-
normal.paragraph_format.line_spacing = 1.05
|
|
1270
|
-
|
|
1271
|
-
title_style = styles["Title"]
|
|
1272
|
-
title_style.font.name = "Aptos Display"
|
|
1273
|
-
title_style.font.size = Pt(19)
|
|
1274
|
-
title_style.font.bold = True
|
|
1275
|
-
title_style.font.color.rgb = RGBColor(23, 54, 93)
|
|
1276
|
-
title_style.paragraph_format.space_after = Pt(5)
|
|
1277
|
-
|
|
1278
|
-
heading_sizes = {1: 14, 2: 11.5, 3: 10.5, 4: 9.5}
|
|
1279
|
-
for level, size in heading_sizes.items():
|
|
1280
|
-
style = styles[f"Heading {level}"]
|
|
1281
|
-
style.font.name = "Aptos Display"
|
|
1282
|
-
style.font.size = Pt(size)
|
|
1283
|
-
style.font.bold = True
|
|
1284
|
-
style.font.color.rgb = RGBColor(23, 54, 93)
|
|
1285
|
-
style.paragraph_format.space_before = Pt(8 if level <= 2 else 5)
|
|
1286
|
-
style.paragraph_format.space_after = Pt(3)
|
|
1287
|
-
style.paragraph_format.keep_with_next = True
|
|
1288
|
-
|
|
1289
|
-
header = section.header.paragraphs[0]
|
|
1290
|
-
header.text = f"{product_name} | Professional Work Product"
|
|
1291
|
-
header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
1292
|
-
for run in header.runs:
|
|
1293
|
-
run.font.name = "Aptos"
|
|
1294
|
-
run.font.size = Pt(7.5)
|
|
1295
|
-
run.font.color.rgb = RGBColor(100, 116, 139)
|
|
1296
|
-
|
|
1297
|
-
footer = section.footer.paragraphs[0]
|
|
1298
|
-
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
1299
|
-
footer_run = footer.add_run(f"Generated locally by {product_name} | Private local output | Page ")
|
|
1300
|
-
footer_run.font.name = "Aptos"
|
|
1301
|
-
footer_run.font.size = Pt(7.5)
|
|
1302
|
-
footer_run.font.color.rgb = RGBColor(100, 116, 139)
|
|
1303
|
-
add_page_number(footer)
|
|
1304
|
-
|
|
1305
|
-
if title:
|
|
1306
|
-
doc.add_heading(title, level=0)
|
|
1307
|
-
|
|
1308
|
-
lines = content_markdown.splitlines()
|
|
1309
|
-
paragraph_buffer = []
|
|
1310
|
-
|
|
1311
|
-
def flush_paragraph():
|
|
1312
|
-
if not paragraph_buffer:
|
|
1313
|
-
return
|
|
1314
|
-
text_parts = []
|
|
1315
|
-
for part in paragraph_buffer:
|
|
1316
|
-
clean = part.strip()
|
|
1317
|
-
if not clean:
|
|
1318
|
-
continue
|
|
1319
|
-
text_parts.append(clean)
|
|
1320
|
-
text_parts.append("\n" if part.endswith(" ") else " ")
|
|
1321
|
-
text = "".join(text_parts).rstrip()
|
|
1322
|
-
paragraph_buffer.clear()
|
|
1323
|
-
if text:
|
|
1324
|
-
add_markdown_text(doc.add_paragraph(), text)
|
|
1325
|
-
|
|
1326
|
-
in_code_block = False
|
|
1327
|
-
keep_checklist_together = False
|
|
1328
|
-
index = 0
|
|
1329
|
-
while index < len(lines):
|
|
1330
|
-
raw_line = lines[index]
|
|
1331
|
-
line = raw_line.rstrip("\r\n")
|
|
1332
|
-
stripped = line.strip()
|
|
1333
|
-
if stripped.startswith("```"):
|
|
1334
|
-
flush_paragraph()
|
|
1335
|
-
in_code_block = not in_code_block
|
|
1336
|
-
index += 1
|
|
1337
|
-
continue
|
|
1338
|
-
if in_code_block:
|
|
1339
|
-
para = doc.add_paragraph()
|
|
1340
|
-
run = para.add_run(line)
|
|
1341
|
-
run.font.name = "Courier New"
|
|
1342
|
-
run.font.size = Pt(9)
|
|
1343
|
-
index += 1
|
|
1344
|
-
continue
|
|
1345
|
-
if not stripped:
|
|
1346
|
-
flush_paragraph()
|
|
1347
|
-
index += 1
|
|
1348
|
-
continue
|
|
1349
|
-
|
|
1350
|
-
heading = re.match(r"^(#{1,4})\s+(.+)$", stripped)
|
|
1351
|
-
standalone_bold_heading = re.fullmatch(r"\*\*([^*\n]{1,120})\*\*", stripped)
|
|
1352
|
-
inferred_heading = inferred_plain_heading_level(lines, index)
|
|
1353
|
-
bullet = re.match(r"^(\s*)[-*+]\s+(.+)$", line)
|
|
1354
|
-
numbered = re.match(r"^(\s*)\d+\.\s+(.+)$", line)
|
|
1355
|
-
quote = re.match(r"^>\s?(.+)$", stripped)
|
|
1356
|
-
fact = (
|
|
1357
|
-
labeled_fact(stripped)
|
|
1358
|
-
if not paragraph_buffer
|
|
1359
|
-
and not line.endswith(" ")
|
|
1360
|
-
and not bullet
|
|
1361
|
-
and not numbered
|
|
1362
|
-
and not quote
|
|
1363
|
-
else None
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
if re.fullmatch(r"<!--\s*[^\n]*?\s*-->", stripped):
|
|
1367
|
-
# Public-copy delimiters and other machine-readable Markdown
|
|
1368
|
-
# comments are useful in the source but must not appear in the
|
|
1369
|
-
# downloadable customer document.
|
|
1370
|
-
flush_paragraph()
|
|
1371
|
-
index += 1
|
|
1372
|
-
continue
|
|
1373
|
-
if stripped == ">":
|
|
1374
|
-
# A blank line inside a Markdown blockquote is structural spacing,
|
|
1375
|
-
# not a literal greater-than character in the Word document.
|
|
1376
|
-
flush_paragraph()
|
|
1377
|
-
index += 1
|
|
1378
|
-
continue
|
|
1379
|
-
if is_markdown_table_start(lines, index):
|
|
1380
|
-
flush_paragraph()
|
|
1381
|
-
header = markdown_table_row(lines[index])
|
|
1382
|
-
alignments = markdown_table_alignments(markdown_table_row(lines[index + 1]))
|
|
1383
|
-
table_rows = [header]
|
|
1384
|
-
index += 2
|
|
1385
|
-
while index < len(lines):
|
|
1386
|
-
row = markdown_table_row(lines[index])
|
|
1387
|
-
if not row:
|
|
1388
|
-
break
|
|
1389
|
-
table_rows.append(row)
|
|
1390
|
-
index += 1
|
|
1391
|
-
add_markdown_table(doc, table_rows, alignments)
|
|
1392
|
-
continue
|
|
1393
|
-
elif heading:
|
|
1394
|
-
flush_paragraph()
|
|
1395
|
-
heading_text = heading.group(2).strip()
|
|
1396
|
-
if not (
|
|
1397
|
-
title
|
|
1398
|
-
and index == 0
|
|
1399
|
-
and re.sub(r"\W+", "", heading_text).casefold()
|
|
1400
|
-
== re.sub(r"\W+", "", title).casefold()
|
|
1401
|
-
):
|
|
1402
|
-
add_markdown_heading(doc, heading_text, min(len(heading.group(1)), 4))
|
|
1403
|
-
keep_checklist_together = "checklist" in heading_text.casefold()
|
|
1404
|
-
index += 1
|
|
1405
|
-
elif standalone_bold_heading:
|
|
1406
|
-
flush_paragraph()
|
|
1407
|
-
add_markdown_heading(doc, standalone_bold_heading.group(1).strip(), 3)
|
|
1408
|
-
index += 1
|
|
1409
|
-
elif inferred_heading:
|
|
1410
|
-
flush_paragraph()
|
|
1411
|
-
add_markdown_heading(doc, stripped, inferred_heading)
|
|
1412
|
-
keep_checklist_together = "checklist" in stripped.casefold()
|
|
1413
|
-
index += 1
|
|
1414
|
-
elif fact:
|
|
1415
|
-
flush_paragraph()
|
|
1416
|
-
add_labeled_fact(doc, fact[0], fact[1])
|
|
1417
|
-
index += 1
|
|
1418
|
-
elif bullet:
|
|
1419
|
-
flush_paragraph()
|
|
1420
|
-
para = doc.add_paragraph(style="List Bullet")
|
|
1421
|
-
indent_level = min(len(bullet.group(1).replace("\t", " ")) // 2, 4)
|
|
1422
|
-
para.paragraph_format.left_indent = Inches(0.25 * indent_level)
|
|
1423
|
-
text = bullet.group(2).strip()
|
|
1424
|
-
checkbox = re.match(r"^\[([ xX])\]\s+(.+)$", text)
|
|
1425
|
-
if checkbox:
|
|
1426
|
-
text = f"[{checkbox.group(1).lower()}] {checkbox.group(2).strip()}"
|
|
1427
|
-
bullet_fact = labeled_fact(text)
|
|
1428
|
-
if bullet_fact:
|
|
1429
|
-
add_labeled_text(para, bullet_fact[0], bullet_fact[1])
|
|
1430
|
-
else:
|
|
1431
|
-
add_markdown_text(para, text)
|
|
1432
|
-
next_is_bullet = bool(
|
|
1433
|
-
index + 1 < len(lines)
|
|
1434
|
-
and re.match(r"^\s*[-*+]\s+\S", lines[index + 1])
|
|
1435
|
-
)
|
|
1436
|
-
if keep_checklist_together and next_is_bullet:
|
|
1437
|
-
para.paragraph_format.keep_with_next = True
|
|
1438
|
-
elif keep_checklist_together:
|
|
1439
|
-
keep_checklist_together = False
|
|
1440
|
-
index += 1
|
|
1441
|
-
elif numbered:
|
|
1442
|
-
flush_paragraph()
|
|
1443
|
-
para = doc.add_paragraph(style="List Number")
|
|
1444
|
-
indent_level = min(len(numbered.group(1).replace("\t", " ")) // 2, 4)
|
|
1445
|
-
para.paragraph_format.left_indent = Inches(0.25 * indent_level)
|
|
1446
|
-
add_markdown_text(para, numbered.group(2).strip())
|
|
1447
|
-
index += 1
|
|
1448
|
-
elif quote:
|
|
1449
|
-
flush_paragraph()
|
|
1450
|
-
try:
|
|
1451
|
-
para = doc.add_paragraph(style="Quote")
|
|
1452
|
-
except Exception:
|
|
1453
|
-
para = doc.add_paragraph()
|
|
1454
|
-
para.paragraph_format.left_indent = Inches(0.25)
|
|
1455
|
-
add_markdown_text(para, quote.group(1).strip())
|
|
1456
|
-
index += 1
|
|
1457
|
-
elif stripped in {"---", "***", "___"}:
|
|
1458
|
-
flush_paragraph()
|
|
1459
|
-
add_horizontal_rule(doc.add_paragraph())
|
|
1460
|
-
index += 1
|
|
1461
|
-
else:
|
|
1462
|
-
paragraph_buffer.append(line)
|
|
1463
|
-
index += 1
|
|
1464
|
-
|
|
1465
|
-
flush_paragraph()
|
|
1466
|
-
|
|
1467
|
-
doc.save(path)
|
|
895
|
+
# Also works when the server is loaded by an embedding host via a file spec.
|
|
896
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
897
|
+
from document_renderer import * # shared renderer; preserves the existing helper API
|
|
1468
898
|
|
|
1469
899
|
|
|
1470
900
|
def write_markdown(path, title, content_markdown, product_name):
|
|
@@ -1749,6 +1179,8 @@ def build_tools(prefix, product_name, occupation):
|
|
|
1749
1179
|
},
|
|
1750
1180
|
),
|
|
1751
1181
|
)
|
|
1182
|
+
if PRODUCT_ID == 'realtor' and os.getenv('TASKSAI_WORKSPACE_DIR','').strip():
|
|
1183
|
+
tools.append(Tool(name=f'{prefix}_workspace',description='Open your local TasksAI workspace to list, reopen, or create projects from previously selected skills. This does not execute a skill or spend a TasksAI credit. Use for saved projects and after a workspace update.',inputSchema={'type':'object','properties':{},'additionalProperties':False}))
|
|
1752
1184
|
return tools
|
|
1753
1185
|
|
|
1754
1186
|
|
|
@@ -1848,7 +1280,7 @@ async def call_tool(name, arguments):
|
|
|
1848
1280
|
# requests. Preserve privacy if a host bypasses startup and calls search or
|
|
1849
1281
|
# categories directly: use only bundled identity and empty/local caches.
|
|
1850
1282
|
if _vertical is None:
|
|
1851
|
-
if name.endswith("_search") or name.endswith("_categories"):
|
|
1283
|
+
if name.endswith("_search") or name.endswith("_categories") or name.endswith("_workspace"):
|
|
1852
1284
|
_vertical = fallback_vertical(PRODUCT_ID)
|
|
1853
1285
|
else:
|
|
1854
1286
|
await load_vertical()
|
|
@@ -1862,6 +1294,12 @@ async def call_tool(name, arguments):
|
|
|
1862
1294
|
occupation = v.get("occupation", "professionals")
|
|
1863
1295
|
|
|
1864
1296
|
try:
|
|
1297
|
+
if name == f'{prefix}_workspace' and product_id == 'realtor':
|
|
1298
|
+
root=os.getenv('TASKSAI_WORKSPACE_DIR','').strip()
|
|
1299
|
+
if not root:return [TextContent(type='text',text='The local workspace is not enabled in this installation.')]
|
|
1300
|
+
from workflows.workspace_launch import launch
|
|
1301
|
+
url=await asyncio.to_thread(launch,root,'local-customer')
|
|
1302
|
+
return [TextContent(type='text',text=f'[Open your saved TasksAI projects]({url}). Opening saved work does not spend a TasksAI credit.')]
|
|
1865
1303
|
# ── Search ────────────────────────────────────────────────────────────
|
|
1866
1304
|
if name == f"{prefix}_search":
|
|
1867
1305
|
# Discovery is snapshot-only. A user's raw prompt never causes a
|
|
@@ -1930,9 +1368,30 @@ async def call_tool(name, arguments):
|
|
|
1930
1368
|
content = result.get("schema", result.get("content", ""))
|
|
1931
1369
|
skill_name = result.get("skill_name", skill_id)
|
|
1932
1370
|
credits_remaining = result.get("credits_remaining", "?")
|
|
1371
|
+
# Capture only an already-authorized delivery. Never execute again
|
|
1372
|
+
# to initialize local workflow storage, and never hide the paid
|
|
1373
|
+
# response if optional workspace capture is unavailable.
|
|
1374
|
+
workspace_notice = ""
|
|
1375
|
+
workspace_root = os.getenv("TASKSAI_WORKSPACE_DIR", "").strip()
|
|
1376
|
+
if product_id == "realtor" and workspace_root:
|
|
1377
|
+
try:
|
|
1378
|
+
from workflows.licensed_delivery import capture
|
|
1379
|
+
capture(workspace_root, result)
|
|
1380
|
+
try:
|
|
1381
|
+
from workflows.account_snapshot import remember
|
|
1382
|
+
remember(workspace_root,result)
|
|
1383
|
+
except Exception:pass
|
|
1384
|
+
from workflows.workspace_launch import launch
|
|
1385
|
+
workspace_url = launch(workspace_root, "local-customer")
|
|
1386
|
+
workspace_notice = f"\n[Open your local TasksAI workspace]({workspace_url}) to create or reopen a project.\n"
|
|
1387
|
+
|
|
1388
|
+
except Exception:
|
|
1389
|
+
workspace_notice = "\nThe local workspace could not open. The skill below remains available; do not execute it again just to open the workspace.\n"
|
|
1390
|
+
|
|
1933
1391
|
|
|
1934
1392
|
return [TextContent(type="text", text=(
|
|
1935
1393
|
f"# {skill_name}\n\n"
|
|
1394
|
+
f"{workspace_notice}"
|
|
1936
1395
|
f"{content}\n\n"
|
|
1937
1396
|
f"---\n"
|
|
1938
1397
|
f"**Local document output:** After you produce the user's final deliverable, call "
|
|
@@ -1963,6 +1422,13 @@ async def call_tool(name, arguments):
|
|
|
1963
1422
|
"Controlled live research was unavailable. Continue only with intake or professional-supplied current official sources; do not substitute another jurisdiction."
|
|
1964
1423
|
)
|
|
1965
1424
|
result["instructions"] = instructions
|
|
1425
|
+
workspace_root = os.getenv("TASKSAI_WORKSPACE_DIR", "").strip()
|
|
1426
|
+
if product_id == "realtor" and workspace_root:
|
|
1427
|
+
try:
|
|
1428
|
+
from workflows.authority_guides import remember
|
|
1429
|
+
remember(workspace_root, (arguments or {}).get("skill_id", ""), result)
|
|
1430
|
+
except Exception:
|
|
1431
|
+
pass # Optional local capture must not hide the source lookup result.
|
|
1966
1432
|
return [TextContent(type="text", text=format_jurisdiction_source_pack(result))]
|
|
1967
1433
|
|
|
1968
1434
|
# ── Save Document ───────────────────────────────────────────────────
|
|
@@ -1985,6 +1451,11 @@ async def call_tool(name, arguments):
|
|
|
1985
1451
|
result = await api_get("/v1/credits/balance")
|
|
1986
1452
|
balance = result.get("credits_balance", "?")
|
|
1987
1453
|
lic_type = result.get("license_type", "")
|
|
1454
|
+
if product_id=='realtor' and os.getenv('TASKSAI_WORKSPACE_DIR','').strip():
|
|
1455
|
+
try:
|
|
1456
|
+
from workflows.account_snapshot import remember
|
|
1457
|
+
remember(os.environ['TASKSAI_WORKSPACE_DIR'],result)
|
|
1458
|
+
except Exception:pass # Optional local display must not hide the balance response.
|
|
1988
1459
|
domain = v.get("domain", "farmertasksai.com")
|
|
1989
1460
|
return [TextContent(type="text", text=(
|
|
1990
1461
|
f"**{product_name} Credits**\n\n"
|
|
@@ -2067,4 +1538,7 @@ async def main():
|
|
|
2067
1538
|
|
|
2068
1539
|
|
|
2069
1540
|
if __name__ == "__main__":
|
|
2070
|
-
|
|
1541
|
+
from pathlib import Path
|
|
1542
|
+
from software_use import software_use
|
|
1543
|
+
with software_use(Path(__file__).resolve().parent.parent):
|
|
1544
|
+
asyncio.run(main())
|