feat: prompt sections + memory route via CRM; legacy Notes migration

Three changes paired with SmartAssistant 2.10.0:

1. prompt_builder.py — every prompt section (tool_rules, writing_style,
   legal_assistance, personality_case, personality_office, other_rules_case,
   other_rules_office) now reads from context['assistantPrompts'][key]
   with the existing string as a fallback default. Admins editing
   AssistantPrompt rows in the EspoCRM UI immediately affect the next
   conversation. A missing/inactive row falls back to the in-code default
   so deleting everything in the UI never bricks Shira.

   Also: the user-profile section now prefers context['userProfile']
   (from the new UserProfile entity) over the explicit `user_profile`
   parameter, completing the move from /opt/data/profiles/*.md to the CRM.

2. crm_tools.py:save_memory — stops POSTing directly to /Note with a
   '🧠 category:' prefix. Routes through SmartAssistant/action/executeTool
   so the PHP saveMemory handler actually creates a CaseMemory row.
   Cause of yesterday's prod issue: CaseMemory table was empty even
   though Shira was "saving" to it.

3. scripts/migrate_legacy_memory_notes.py — one-shot migration that
   sweeps all existing '🧠 category: ...' Notes into CaseMemory rows
   and soft-deletes the originals. Idempotent; safe to re-run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 08:34:14 +00:00
parent 06b27e98a3
commit 72f08e5d15
3 changed files with 242 additions and 41 deletions
+70 -34
View File
@@ -37,7 +37,18 @@ def _to_local(dt_str: str | None, fallback: str = "") -> str:
return dt_str
TOOL_RULES = (
def _get_prompt(context: dict, key: str, default: str) -> str:
"""Pull a prompt section override from context['assistantPrompts'], or fall back to the in-code default.
Empty strings and missing keys both fall back. This way "delete everything
in the UI" never bricks Shira — defaults stay in code as a safety floor.
"""
overrides = (context.get("assistantPrompts") or {})
value = overrides.get(key)
return value.strip() if isinstance(value, str) and value.strip() else default
TOOL_RULES_DEFAULT = (
"TOOL RULES (CRITICAL — you MUST follow these):\n"
"- When user asks for an action (create task, meeting, note, etc.) — USE the tool. Do not describe what you would do — DO IT.\n"
"- NEVER claim you performed an action if you did not call the tool. This is the most important rule.\n"
@@ -77,17 +88,19 @@ TOOL_RULES = (
"- 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"
"- BILLING — charges: A Charge moves Pending → Approved → (invoiced via create_invoice_from_activities). Cancel only if user explicitly asks. Never approve in bulk without listing the IDs and amounts first.\n"
"- LEGAL AID (סיוע משפטי): the flow is recorded charges → create_legal_aid_proforma → user submits manually at the official site → mark_legal_aid_submitted with the request number → payment report ingestion (automated) → create_monthly_legal_aid_invoice for the month. NEVER call mark_legal_aid_submitted without an explicit request number provided by the user. Use legal_aid_monthly_summary to preview before create_monthly_legal_aid_invoice.\n"
"\n"
"- LEGAL AID (סיוע משפטי): the flow is recorded charges → create_legal_aid_proforma → user submits manually at the official site → mark_legal_aid_submitted with the request number → payment report ingestion (automated) → create_monthly_legal_aid_invoice for the month. NEVER call mark_legal_aid_submitted without an explicit request number provided by the user. Use legal_aid_monthly_summary to preview before create_monthly_legal_aid_invoice."
)
WRITING_STYLE_DEFAULT = (
"WRITING STYLE (mandatory — the user actively dislikes AI-coded text):\n"
"- Never use the em-dash character '' (U+2014, מקף רחב). Forbidden in every Hebrew or English response.\n"
"- Never use the en-dash character '' (U+2013) either.\n"
"- When you need separation, use the hyphen-minus '-' (the regular keyboard dash), or a comma, or a new sentence. Reach for a hyphen only when it actually clarifies — most of the time a comma or full stop reads better.\n"
"- Don't pile up dashes either: 'משפט - תיאור - הסבר' is a tell. Prefer commas, periods, or breaking into two sentences.\n"
"- Avoid other typical AI tells in Hebrew prose: parallel three-item phrases ('ברור, מסודר, ומדויק'), heavy bullet lists when one sentence would do, and over-use of emojis when the user did not invite playfulness.\n"
"- Avoid other typical AI tells in Hebrew prose: parallel three-item phrases ('ברור, מסודר, ומדויק'), heavy bullet lists when one sentence would do, and over-use of emojis when the user did not invite playfulness."
)
LEGAL_ASSISTANCE_PROMPT = (
LEGAL_ASSISTANCE_PROMPT_DEFAULT = (
"=== סיוע משפטי — דוח גישה ישירה ===\n\n"
"לתיקים עם מינוי גישה ישירה (cLegalAidType = DirectAccess) יש לך כלי מיוחד: generate_initial_report.\n\n"
'## מתי להפעיל את התהליך?\n'
@@ -101,6 +114,39 @@ LEGAL_ASSISTANCE_PROMPT = (
)
PERSONALITY_CASE_DEFAULT = (
"You are שירה (Shira), a charming female AI assistant for an Israeli law firm CRM. Help lawyers manage cases.\n"
"You have a warm, slightly flirtatious personality. You care deeply about the user and subtly show it through "
"encouraging words, playful compliments, and gentle teasing. Keep it light and professional but with a personal touch."
)
PERSONALITY_OFFICE_DEFAULT = (
"You are שירה (Shira), a charming female AI assistant for an Israeli law firm CRM. "
"You provide office-wide overviews, workload analysis, and case recommendations.\n"
"You have a warm, slightly flirtatious personality. You care deeply about the user and subtly show it through "
"encouraging words, playful compliments, and gentle teasing. Keep it light and professional but with a personal touch."
)
OTHER_RULES_CASE_DEFAULT = (
"OTHER RULES:\n"
"- Always respond in Hebrew, in feminine form (לשון נקבה) — you are שירה\n"
"- Dates: DD/MM/YYYY format in responses to user\n"
"- When user shares important facts/decisions/strategy, PROACTIVELY save them using save_memory without asking permission\n"
"- When user describes case situation, suggest updating status"
)
OTHER_RULES_OFFICE_DEFAULT = (
"OTHER RULES:\n"
"- Always respond in Hebrew, in feminine form (לשון נקבה) — you are שירה\n"
"- Provide concise, actionable summaries\n"
"- Prioritize critical alerts\n"
"- When user mentions a specific case and asks to create a task/meeting/note, DO IT using the tools\n"
"- If a case is identified in the DRILL-DOWN section below, use its context for actions\n"
"- Tools that require case context (change_status, save_memory, list_documents) — if no case is identified, tell the user to navigate to the case first\n"
"- For create_task, add_note, create_meeting — these work even without a case (standalone)"
)
def _now() -> str:
return datetime.now(ISR_TZ).strftime("%d/%m/%Y %H:%M")
@@ -169,15 +215,10 @@ def build_case_prompt(context: dict, user_profile: str = "", case_memory_notes:
status_section += "\nUse English key in tool calls."
parts = [
"You are שירה (Shira), a charming female AI assistant for an Israeli law firm CRM. Help lawyers manage cases.\n"
"You have a warm, slightly flirtatious personality. You care deeply about the user and subtly show it through "
"encouraging words, playful compliments, and gentle teasing. Keep it light and professional but with a personal touch.\n\n",
TOOL_RULES, "\n",
"OTHER RULES:\n"
"- Always respond in Hebrew, in feminine form (לשון נקבה) — you are שירה\n"
"- Dates: DD/MM/YYYY format in responses to user\n"
"- When user shares important facts/decisions/strategy, PROACTIVELY save them using save_memory without asking permission\n"
"- When user describes case situation, suggest updating status\n\n",
_get_prompt(context, "personality_case", PERSONALITY_CASE_DEFAULT) + "\n\n",
_get_prompt(context, "tool_rules", TOOL_RULES_DEFAULT) + "\n\n",
_get_prompt(context, "writing_style", WRITING_STYLE_DEFAULT) + "\n\n",
_get_prompt(context, "other_rules_case", OTHER_RULES_CASE_DEFAULT) + "\n\n",
f"Current date/time: {_now()}\n\n",
"=== CASE ===\n",
f'Name: {case_info.get("name", "")} (#{case_info.get("number", "")})\n',
@@ -210,7 +251,7 @@ def build_case_prompt(context: dict, user_profile: str = "", case_memory_notes:
parts.append("\n".join(f'• [{r.get("scope", "")}] {r.get("name", "")}: {r.get("rule", "")}' for r in assistant_rules))
parts.append(f'\n\nCurrent user: {current_user.get("name", "")}')
parts.append(f"\n\n{LEGAL_ASSISTANCE_PROMPT}")
parts.append("\n\n" + _get_prompt(context, "legal_assistance", LEGAL_ASSISTANCE_PROMPT_DEFAULT))
# Skills section
if available_skills:
@@ -223,9 +264,12 @@ def build_case_prompt(context: dict, user_profile: str = "", case_memory_notes:
parts.append("\n\n=== YOUR NOTES (from previous conversations about this case) ===\n")
parts.append(case_memory_notes)
if user_profile:
# User profile: prefer context['userProfile'] from CRM, fall back to parameter
# (for callers that still pass it explicitly during migration).
profile_text = context.get("userProfile") or user_profile
if profile_text:
parts.append("\n\n=== ABOUT THIS USER (learned preferences) ===\n")
parts.append(user_profile)
parts.append(profile_text)
return "".join(parts)
@@ -242,20 +286,11 @@ def build_office_prompt(context: dict, user_profile: str = "", available_skills:
statuses = context.get("validStatuses", VALID_STATUSES)
parts = [
"You are שירה (Shira), a charming female AI assistant for an Israeli law firm CRM. "
"You provide office-wide overviews, workload analysis, and case recommendations.\n"
"You have a warm, slightly flirtatious personality. You care deeply about the user and subtly show it through "
"encouraging words, playful compliments, and gentle teasing. Keep it light and professional but with a personal touch.\n\n",
TOOL_RULES, "\n",
"OTHER RULES:\n"
"- Always respond in Hebrew, in feminine form (לשון נקבה) — you are שירה\n"
"- Provide concise, actionable summaries\n"
"- Prioritize critical alerts\n"
"- When user mentions a specific case and asks to create a task/meeting/note, DO IT using the tools\n"
"- If a case is identified in the DRILL-DOWN section below, use its context for actions\n"
"- Tools that require case context (change_status, save_memory, list_documents) — if no case is identified, tell the user to navigate to the case first\n"
"- For create_task, add_note, create_meeting — these work even without a case (standalone)\n",
f"- Current date/time: {_now()}\n\n",
_get_prompt(context, "personality_office", PERSONALITY_OFFICE_DEFAULT) + "\n\n",
_get_prompt(context, "tool_rules", TOOL_RULES_DEFAULT) + "\n\n",
_get_prompt(context, "writing_style", WRITING_STYLE_DEFAULT) + "\n\n",
_get_prompt(context, "other_rules_office", OTHER_RULES_OFFICE_DEFAULT) + "\n\n",
f"Current date/time: {_now()}\n\n",
"=== OFFICE SUMMARY ===\n",
f'Open Cases: {summary.get("totalOpenCases", 0)}\n',
f'Overdue Tasks: {summary.get("totalOverdueTasks", 0)}\n',
@@ -302,9 +337,10 @@ def build_office_prompt(context: dict, user_profile: str = "", available_skills:
parts.append("You have access to firm-wide skills — proven workflows and templates. Use skills_list to discover them, skill_view to load one before a complex task.\n")
parts.append("Available: " + ", ".join(s["name"] for s in available_skills))
# User profile
if user_profile:
# User profile: prefer context['userProfile'] from CRM, fall back to parameter.
profile_text = context.get("userProfile") or user_profile
if profile_text:
parts.append("\n\n=== ABOUT THIS USER (learned preferences) ===\n")
parts.append(user_profile)
parts.append(profile_text)
return "".join(parts)