feat: 9 new tools — document creation, folder management, CRM updates
Adds the Python side of the SmartAssistant 2.9 release. Each tool maps to either a new EspoCRM action or a generic CRM endpoint. Document creation: - create_document: free-form RTL DOCX with markdown body; calls SmartAssistant/action/createDocument. Folder management (all sandboxed to the current case's root): - write_document_to_folder: raw upload to a path inside the case folder. - create_subfolder: mkdir with recursive parent creation. - rename_item / move_item: rename/move files and folders. - mark_document_for_deletion: soft-delete — moves the file to "מסמכים למחיקה/" with a "למחיקה - " prefix. Shira is explicitly told she has NO physical-delete permission. CRM gaps: - update_case_fields: allow-listed PUT on Case (judge, court, caseType, practiceArea, opposingParty, status, priority, etc.). - search_entities: generic GET with EspoCRM where[] params across Case/Contact/Account/Document/Task. Wildcard "*" maps to LIKE. - draft_email: POST Email with status="Draft", linked to the current case. prompt_builder additions: - FREE DOCUMENT CREATION: don't say "I can't" — call create_document. - FOLDER MANAGEMENT: full read/write inside the case folder only. - FILE DELETION: always mark_document_for_deletion; never claim a physical delete. Pairs with SmartAssistant extension release that adds FreeDocumentGenerator, CaseFolderManager, and 6 controller actions. Refs Task Master #4 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,12 +36,24 @@
|
||||
"priority": "high",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-25T20:39:11.905Z"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"title": "Add 9 tools: create_document, write/create/rename/move folder ops, mark_document_for_deletion, update_case_fields, search_entities, draft_email",
|
||||
"description": "Expose new EspoCRM SmartAssistant actions through Python tools and update prompt_builder with new behavioral rules.",
|
||||
"details": "Tools added in mcp_server/tools/document_tools.py: create_document, write_document_to_folder, create_subfolder, rename_item, move_item, mark_document_for_deletion. Tools added in mcp_server/tools/crm_tools.py: update_case_fields, search_entities, draft_email. Prompt updates in api/services/prompt_builder.py: FREE DOCUMENT CREATION rule, FOLDER MANAGEMENT rule, FILE DELETION rule (always mark_document_for_deletion). Pairs with SmartAssistant extension release that adds FreeDocumentGenerator + CaseFolderManager + 6 controller actions.",
|
||||
"testStrategy": "End-to-end test in EspoCRM dev: ask Shira to draft a letter, create/rename/move folders, mark for deletion, update case fields, search entities, draft email.",
|
||||
"priority": "medium",
|
||||
"status": "in-progress",
|
||||
"dependencies": [],
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-14T12:39:55.190Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-04-25T20:39:11.905Z",
|
||||
"taskCount": 3,
|
||||
"lastModified": "2026-05-14T12:39:55.190Z",
|
||||
"taskCount": 4,
|
||||
"completedCount": 1,
|
||||
"tags": [
|
||||
"master"
|
||||
|
||||
@@ -55,6 +55,9 @@ TOOL_RULES = (
|
||||
"- When user asks to summarize multiple documents or the entire case folder — use read_multiple_documents with all relevant file paths.\n"
|
||||
"- When user asks to rename or organize files — use batch_rename_documents with an array of rename operations.\n"
|
||||
"- TEMPLATE GENERATION: When user asks to generate/create a document from a template — use generate_from_template. The templateId comes from availableTemplates in your context.\n"
|
||||
"- FREE DOCUMENT CREATION: When the user asks for a document AND availableTemplates contains no template that matches the requested document type, use create_document(title, body, recipient?). DO NOT say 'I can't create documents' — that tool exists. Compose the full text yourself in Hebrew, organized into paragraphs. Use '#' for the section heading, '- ' for bullets, '**bold**' for emphasis. For letters to authorities (ביטוח לאומי, בתי משפט, רשויות) set documentType='letter' and put the authority name in recipient. The document will be created as a Hebrew RTL DOCX, attached to the case, and copied to the case network folder. Only fall back to a plain note (add_note) if the user explicitly asks NOT to create a real document.\n"
|
||||
"- FOLDER MANAGEMENT: You have full read/write access ONLY to the current case's network folder (paths are sandboxed). Use create_subfolder to create a new folder, rename_item to rename a file or folder, move_item to move a file between subfolders, write_document_to_folder for raw uploads. ALL paths are relative to the case root — never absolute, never with '..'.\n"
|
||||
"- FILE DELETION: You are NOT permitted to physically delete files. When the user asks you to delete a file, ALWAYS call mark_document_for_deletion. This moves the file to a 'מסמכים למחיקה' subfolder with a 'למחיקה - ' prefix; the lawyer reviews and deletes manually. Explain this softly to the user: 'סימנתי את הקובץ למחיקה, הוא בתיקיית מסמכים למחיקה ומחכה לאישור שלך למחיקה הפיזית.' Never claim a file was deleted when in fact it was only marked.\n"
|
||||
"- BILLING (חיוב/חשבונית): When user asks about billable activities, charges, or invoices — start with list_unbilled_activities for a case, or list_charges for charge status views. Show the user a summary BEFORE creating an invoice.\n"
|
||||
"- BILLING — invoice creation: create_invoice_from_activities only creates a Draft. It does NOT auto-send to Green Invoice. Always show the user the draft amount and ask if they want to send it before calling send_invoice_to_green_invoice.\n"
|
||||
"- BILLING — irreversible: send_invoice_to_green_invoice issues an official tax document and CANNOT be undone. Require an explicit instruction like 'תשלחי לחשבונית ירוקה', not a vague 'כן'. If unsure, ask again.\n"
|
||||
|
||||
@@ -191,6 +191,137 @@ def register_crm_tools(
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בשמירת זיכרון: {e}")
|
||||
|
||||
# Allowlist of Case fields that Shira may update via update_case_fields.
|
||||
_UPDATABLE_CASE_FIELDS = {
|
||||
"name", "description", "caseType", "practiceArea",
|
||||
"court", "judge", "opposingParty",
|
||||
"cNextHearing", "nextHearingAt", "status", "priority",
|
||||
"assignedUserId",
|
||||
}
|
||||
|
||||
# Allowlist of entity types Shira may search across.
|
||||
_SEARCHABLE_ENTITIES = {"Case", "Contact", "Account", "Document", "Task"}
|
||||
|
||||
async def update_case_fields(updates_json: str) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי לעדכן שדות")
|
||||
try:
|
||||
updates = updates_json if isinstance(updates_json, dict) else __import__("json").loads(updates_json)
|
||||
except Exception:
|
||||
return fail("updates חייב להיות JSON object")
|
||||
if not isinstance(updates, dict) or not updates:
|
||||
return fail("חסר dict של עדכונים")
|
||||
|
||||
rejected = [k for k in updates if k not in _UPDATABLE_CASE_FIELDS]
|
||||
if rejected:
|
||||
return fail(f"שדות לא מותרים לעדכון: {', '.join(rejected)}. שדות מותרים: {', '.join(sorted(_UPDATABLE_CASE_FIELDS))}")
|
||||
|
||||
safe_updates = {k: v for k, v in updates.items() if k in _UPDATABLE_CASE_FIELDS}
|
||||
if not safe_updates:
|
||||
return fail("אין שדות תקפים לעדכון")
|
||||
|
||||
try:
|
||||
await crm.put(f"Case/{case_id}", safe_updates)
|
||||
applied = ", ".join(f"{k}={v!r}" for k, v in safe_updates.items())
|
||||
return ok(f"✅ התיק עודכן: {applied}")
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בעדכון תיק: {e}")
|
||||
|
||||
async def search_entities(
|
||||
entity_type: str,
|
||||
filters_json: str = "",
|
||||
limit: int = 20,
|
||||
) -> str:
|
||||
if entity_type not in _SEARCHABLE_ENTITIES:
|
||||
return fail(f"סוג ישות לא מותר: {entity_type}. מותרים: {', '.join(sorted(_SEARCHABLE_ENTITIES))}")
|
||||
|
||||
import json as _json
|
||||
filters: dict = {}
|
||||
if filters_json:
|
||||
try:
|
||||
filters = filters_json if isinstance(filters_json, dict) else _json.loads(filters_json)
|
||||
except Exception:
|
||||
return fail("filters חייב להיות JSON object")
|
||||
if not isinstance(filters, dict):
|
||||
return fail("filters חייב להיות JSON object")
|
||||
|
||||
try:
|
||||
limit = max(1, min(int(limit), 50))
|
||||
except Exception:
|
||||
limit = 20
|
||||
|
||||
from urllib.parse import urlencode
|
||||
query_pairs: list[tuple[str, str]] = [
|
||||
("maxSize", str(limit)),
|
||||
("offset", "0"),
|
||||
]
|
||||
idx = 0
|
||||
for field, value in filters.items():
|
||||
if value is None or value == "":
|
||||
continue
|
||||
s_value = str(value)
|
||||
if "*" in s_value:
|
||||
comparison = "like"
|
||||
normalized = s_value.replace("*", "%")
|
||||
else:
|
||||
comparison = "equals"
|
||||
normalized = s_value
|
||||
query_pairs.append((f"where[{idx}][type]", comparison))
|
||||
query_pairs.append((f"where[{idx}][attribute]", field))
|
||||
query_pairs.append((f"where[{idx}][value]", normalized))
|
||||
idx += 1
|
||||
|
||||
endpoint = f"{entity_type}?{urlencode(query_pairs)}"
|
||||
|
||||
try:
|
||||
result = await crm.get(endpoint)
|
||||
rows = result.get("list", []) if isinstance(result, dict) else []
|
||||
total = result.get("total", len(rows)) if isinstance(result, dict) else len(rows)
|
||||
if not rows:
|
||||
return ok(f'לא נמצאו רשומות {entity_type} התואמות.')
|
||||
lines = [f"נמצאו {len(rows)} מתוך {total} רשומות {entity_type}:"]
|
||||
for r in rows[:limit]:
|
||||
bits = [f'id={r.get("id", "?")}']
|
||||
for key in ("name", "status", "phoneNumber", "emailAddress", "assignedUserName"):
|
||||
if r.get(key):
|
||||
bits.append(f"{key}={r[key]}")
|
||||
lines.append("• " + ", ".join(bits))
|
||||
return ok("\n".join(lines))
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בחיפוש: {e}")
|
||||
|
||||
async def draft_email(
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
cc: str = "",
|
||||
) -> str:
|
||||
if not to or not subject or not body:
|
||||
return fail("חסר to / subject / body")
|
||||
try:
|
||||
payload = {
|
||||
"status": "Draft",
|
||||
"to": to.strip(),
|
||||
"subject": subject.strip(),
|
||||
"body": body,
|
||||
"isHtml": False,
|
||||
"from": None,
|
||||
}
|
||||
if cc and cc.strip():
|
||||
payload["cc"] = cc.strip()
|
||||
if case_id:
|
||||
payload["parentType"] = "Case"
|
||||
payload["parentId"] = case_id
|
||||
if user_id:
|
||||
payload["assignedUserId"] = user_id
|
||||
result = await crm.post("Email", payload)
|
||||
return ok(
|
||||
f'✉️ טיוטת מייל נוצרה (לא נשלחה!): "{subject.strip()}" → {to.strip()}'
|
||||
f'\nID: {result.get("id", "?")} — ניתן לערוך ולשלוח דרך EspoCRM.'
|
||||
)
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה ביצירת טיוטה: {e}")
|
||||
|
||||
async def save_rule(name: str, rule: str, scope: str = "global") -> str:
|
||||
# Try CRM first, fall back to local user profile if entity doesn't exist
|
||||
try:
|
||||
@@ -361,3 +492,62 @@ def register_crm_tools(
|
||||
},
|
||||
"handler": save_rule,
|
||||
}
|
||||
|
||||
tools["update_case_fields"] = {
|
||||
"description": (
|
||||
"Update one or more allowed fields on the current case. Pass updates as a JSON "
|
||||
"object string, e.g. '{\"judge\": \"כב' השופט יוסי כהן\", \"court\": \"שלום ת\\\"א\"}'. "
|
||||
"Only these fields are accepted: " + ", ".join(sorted(_UPDATABLE_CASE_FIELDS)) + ". "
|
||||
"Use change_status for status updates that need validated mapping; this tool is "
|
||||
"for arbitrary case metadata."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updates_json": {
|
||||
"type": "string",
|
||||
"description": "JSON object of field→value updates. Example: '{\"judge\":\"...\",\"court\":\"...\"}'.",
|
||||
},
|
||||
},
|
||||
"required": ["updates_json"],
|
||||
},
|
||||
"handler": update_case_fields,
|
||||
}
|
||||
|
||||
tools["search_entities"] = {
|
||||
"description": (
|
||||
"Search any of: Case, Contact, Account, Document, Task by field filters. "
|
||||
"filters_json is a JSON object mapping field name to value. Values containing "
|
||||
"'*' use a LIKE match (e.g. {\"name\": \"כהן*\"}); otherwise exact match. "
|
||||
"Returns up to `limit` records with their key fields."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entity_type": {"type": "string", "enum": sorted(_SEARCHABLE_ENTITIES), "description": "Entity type to search."},
|
||||
"filters_json": {"type": "string", "description": "JSON object of field→value filters. Empty for no filters (returns first N)."},
|
||||
"limit": {"type": "integer", "description": "Max records to return (default 20, max 50)."},
|
||||
},
|
||||
"required": ["entity_type"],
|
||||
},
|
||||
"handler": search_entities,
|
||||
}
|
||||
|
||||
tools["draft_email"] = {
|
||||
"description": (
|
||||
"Create a Draft Email in EspoCRM linked to the current case (if in case mode). "
|
||||
"Does NOT send the email — the lawyer reviews and sends from CRM. Use when the "
|
||||
"user asks to 'send', 'write', or 'draft' an email."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": {"type": "string", "description": "Recipient email address."},
|
||||
"subject": {"type": "string", "description": "Email subject."},
|
||||
"body": {"type": "string", "description": "Email body (plain text)."},
|
||||
"cc": {"type": "string", "description": "Optional CC address."},
|
||||
},
|
||||
"required": ["to", "subject", "body"],
|
||||
},
|
||||
"handler": draft_email,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Document tools: list, read, read_multiple, batch_rename, generate_from_template."""
|
||||
"""Document tools: list, read, read_multiple, batch_rename, generate_from_template, create_document, folder management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -183,6 +184,127 @@ def register_document_tools(
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה ביצירת מסמך מתבנית: {e}")
|
||||
|
||||
async def write_document_to_folder(
|
||||
relPath: str,
|
||||
contentBase64: str,
|
||||
) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי לכתוב לתיקייה")
|
||||
if not relPath or not relPath.strip():
|
||||
return fail("חסר נתיב יחסי לתיק (relPath)")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/writeToFolder", {
|
||||
"caseId": case_id,
|
||||
"relPath": relPath.strip(),
|
||||
"contentBase64": contentBase64,
|
||||
})
|
||||
path = result.get("path")
|
||||
size = result.get("size", 0)
|
||||
return ok(f"✅ קובץ נכתב: {path} ({size} bytes).")
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בכתיבת קובץ: {e}")
|
||||
|
||||
async def create_subfolder(relPath: str) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי ליצור תיקייה")
|
||||
if not relPath or not relPath.strip():
|
||||
return fail("חסר נתיב לתיקייה החדשה")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/createFolder", {
|
||||
"caseId": case_id,
|
||||
"relPath": relPath.strip(),
|
||||
})
|
||||
path = result.get("path")
|
||||
existed = result.get("alreadyExisted", False)
|
||||
verb = "כבר קיימת" if existed else "נוצרה"
|
||||
return ok(f"✅ תיקייה {verb}: {path}")
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה ביצירת תיקייה: {e}")
|
||||
|
||||
async def rename_item(currentRelPath: str, newName: str) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי לשנות שם")
|
||||
if not currentRelPath or not newName:
|
||||
return fail("חסר currentRelPath או newName")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/renameItem", {
|
||||
"caseId": case_id,
|
||||
"currentRelPath": currentRelPath.strip(),
|
||||
"newName": newName.strip(),
|
||||
})
|
||||
return ok(f'✅ שם שונה: "{result.get("oldPath")}" → "{result.get("newPath")}"')
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בשינוי שם: {e}")
|
||||
|
||||
async def move_item(sourceRelPath: str, targetRelPath: str) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי להעביר")
|
||||
if not sourceRelPath or not targetRelPath:
|
||||
return fail("חסר sourceRelPath או targetRelPath")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/moveItem", {
|
||||
"caseId": case_id,
|
||||
"sourceRelPath": sourceRelPath.strip(),
|
||||
"targetRelPath": targetRelPath.strip(),
|
||||
})
|
||||
return ok(f'✅ הועבר: "{result.get("oldPath")}" → "{result.get("newPath")}"')
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בהעברה: {e}")
|
||||
|
||||
async def mark_document_for_deletion(fileRelPath: str) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי לסמן למחיקה")
|
||||
if not fileRelPath:
|
||||
return fail("חסר fileRelPath")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/markForDeletion", {
|
||||
"caseId": case_id,
|
||||
"fileRelPath": fileRelPath.strip(),
|
||||
})
|
||||
original = result.get("originalPath")
|
||||
new = result.get("newPath")
|
||||
return ok(
|
||||
f'📌 המסמך סומן למחיקה (לא נמחק פיזית).\n'
|
||||
f'הועבר ל: {new}\n'
|
||||
f'(במקור: {original}). העו"ד יוכל למחוק ידנית מתיקיית "מסמכים למחיקה".'
|
||||
)
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה בסימון למחיקה: {e}")
|
||||
|
||||
async def create_document(
|
||||
title: str,
|
||||
body: str,
|
||||
documentType: str = "letter",
|
||||
recipient: str = "",
|
||||
) -> str:
|
||||
if not case_id:
|
||||
return fail("צריך להיות בתוך תיק כדי ליצור מסמך")
|
||||
if not title or not title.strip():
|
||||
return fail("חסר שם למסמך (title)")
|
||||
if not body or not body.strip():
|
||||
return fail("חסר תוכן למסמך (body)")
|
||||
try:
|
||||
result = await crm.post("SmartAssistant/action/createDocument", {
|
||||
"caseId": case_id,
|
||||
"title": title.strip(),
|
||||
"body": body,
|
||||
"documentType": documentType or "letter",
|
||||
"recipient": recipient.strip() if recipient else None,
|
||||
})
|
||||
msg = result.get("message", "✅ מסמך נוצר בהצלחה")
|
||||
doc_id = result.get("documentId")
|
||||
net_path = result.get("networkPath")
|
||||
extra = []
|
||||
if doc_id:
|
||||
extra.append(f"documentId={doc_id}")
|
||||
if net_path:
|
||||
extra.append(f"path={net_path}")
|
||||
if extra:
|
||||
msg += "\n" + " | ".join(extra)
|
||||
return ok(msg)
|
||||
except Exception as e:
|
||||
return fail(f"שגיאה ביצירת מסמך: {e}")
|
||||
|
||||
# Register
|
||||
tools["list_documents"] = {
|
||||
"description": "List documents in the case folder. Shows folder structure with file names and paths. When totalFiles is 0, returns a diagnostic explaining why (missing path, folder doesn't exist, etc.). Pass refresh=true to re-fetch from EspoCRM after the folder path changes.",
|
||||
@@ -305,3 +427,112 @@ def register_document_tools(
|
||||
},
|
||||
"handler": generate_from_template,
|
||||
}
|
||||
|
||||
tools["write_document_to_folder"] = {
|
||||
"description": (
|
||||
"Write a new file to a path inside the current case's network folder. "
|
||||
"Operations are sandboxed to the case folder — relPath is relative to the "
|
||||
"case root (e.g. 'חוזים/2026/agreement.pdf'). The content must be base64-encoded. "
|
||||
"Use create_document for DOCX letters; this is for raw file uploads."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"relPath": {"type": "string", "description": "Path inside the case folder, including filename and extension. Subfolders are created if missing."},
|
||||
"contentBase64": {"type": "string", "description": "Base64-encoded file content."},
|
||||
},
|
||||
"required": ["relPath", "contentBase64"],
|
||||
},
|
||||
"handler": write_document_to_folder,
|
||||
}
|
||||
|
||||
tools["create_subfolder"] = {
|
||||
"description": (
|
||||
"Create a new subfolder inside the current case's network folder. "
|
||||
"relPath is relative to the case root. Multi-level paths are supported "
|
||||
"(e.g. 'חוזים/2026') and missing parents are created."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"relPath": {"type": "string", "description": "Folder path inside the case folder."},
|
||||
},
|
||||
"required": ["relPath"],
|
||||
},
|
||||
"handler": create_subfolder,
|
||||
}
|
||||
|
||||
tools["rename_item"] = {
|
||||
"description": (
|
||||
"Rename a file or folder inside the current case's network folder. "
|
||||
"Does NOT move it to another folder — use move_item for that. newName "
|
||||
"must be a plain name without slashes."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currentRelPath": {"type": "string", "description": "Current path of the file/folder, relative to case root."},
|
||||
"newName": {"type": "string", "description": "New name (without path, without slashes)."},
|
||||
},
|
||||
"required": ["currentRelPath", "newName"],
|
||||
},
|
||||
"handler": rename_item,
|
||||
}
|
||||
|
||||
tools["move_item"] = {
|
||||
"description": (
|
||||
"Move a file or folder to a different location inside the current case's "
|
||||
"network folder. Both source and target must stay inside the case root. "
|
||||
"Missing intermediate folders on the target side are created."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sourceRelPath": {"type": "string", "description": "Current path of the file/folder."},
|
||||
"targetRelPath": {"type": "string", "description": "Destination path (including the final name)."},
|
||||
},
|
||||
"required": ["sourceRelPath", "targetRelPath"],
|
||||
},
|
||||
"handler": move_item,
|
||||
}
|
||||
|
||||
tools["mark_document_for_deletion"] = {
|
||||
"description": (
|
||||
"Soft-delete a document: move it to a 'מסמכים למחיקה' subfolder inside the case "
|
||||
"with a 'למחיקה - ' filename prefix. Does NOT physically delete. You DO NOT have "
|
||||
"permission to delete files; always call this tool when the user asks you to "
|
||||
"delete a document, and explain to the user that you marked it for the lawyer to "
|
||||
"delete manually."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileRelPath": {"type": "string", "description": "Path to the file inside the case folder."},
|
||||
},
|
||||
"required": ["fileRelPath"],
|
||||
},
|
||||
"handler": mark_document_for_deletion,
|
||||
}
|
||||
|
||||
tools["create_document"] = {
|
||||
"description": (
|
||||
"Create a new free-form DOCX document attached to the current case. "
|
||||
"Use when no suitable template exists in availableTemplates. The body accepts "
|
||||
"lightweight markdown: '#'/'##'/'###' for headings, '- ' for bullets, "
|
||||
"'**bold**' and '*italic*' inline. The document is created as a Hebrew RTL DOCX "
|
||||
"with a David font, today's date, optional recipient line, the title centered, "
|
||||
"then the body. It is saved both as an EspoCRM Document attached to the case "
|
||||
"and as a file in the case's network folder."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Document title shown both in the CRM record and as the centered heading."},
|
||||
"body": {"type": "string", "description": "Document body. Lightweight markdown is supported. Plain paragraphs are fine."},
|
||||
"documentType": {"type": "string", "enum": ["letter", "motion", "request", "general"], "description": "Document category (default 'letter')."},
|
||||
"recipient": {"type": "string", "description": "Optional recipient line shown under the date (e.g. 'המוסד לביטוח לאומי')."},
|
||||
},
|
||||
"required": ["title", "body"],
|
||||
},
|
||||
"handler": create_document,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user