feat: SmartAssistant v2.4.0 — create_call tool + behavioral rules system

- Add create_call tool: records phone calls as Call entity (not Meeting)
- Add AssistantRule entity: persistent behavioral rules the AI follows
- Add save_rule tool: AI can save new rules when user requests them
- Add systemInstructions to webhook: call vs meeting guidance, open question tracking
- Add recentCalls to case context for AI awareness
- Add AssistantRuleContextProvider: injects active rules per mode into context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 06:20:23 +00:00
parent ca04dd72f8
commit 0dc26f3b15
14 changed files with 391 additions and 2 deletions
+120
View File
@@ -0,0 +1,120 @@
# SmartAssistant — פרומפט פריסה לסביבת פיתוח
> **העתק את הפרומפט הבא כהודעה פותחת בשיחה חדשה של Claude Code, מתוך תיקיית הפרויקט.**
---
```
## הקשר
אני עובד על הרחבת SmartAssistant ל-EspoCRM — עוזר AI צף למשרד עורכי דין.
הקוד מוכן (v1.0.3), וצריך להשלים את הפריסה בסביבת הפיתוח.
קרא את DEPLOYMENT_REVIEW.md לסיכום מלא של מה בוצע, מה נבדק, ומה נשאר.
## סביבת הפיתוח
- EspoCRM Dev: https://espocrm.dev.marcus-law.co.il (Coolify UUID: c591uqmu49qa5kh7t2z7a2mp)
- n8n: https://n8n.dev.marcus-law.co.il
- Gitea: https://gitea.dev.marcus-law.co.il (repo: espocrm-extensions/SmartAssistant)
- קרדנשיאלים ב-Infisical (תיקיות /espocrm, /n8n)
## המשימות — שלב 1: הכנה לפריסה
בצע את המשימות הבאות בסדר. לפני כל שלב — עדכן אותי מה אתה הולך לעשות וחכה לאישור.
### 1. בדיקת תאימות סביבה
- בדוק גרסת EspoCRM בסביבת dev (צריך >= 8.0.0)
- בדוק גרסת PHP (צריך >= 8.1)
- בדוק שאין שאריות של מודולים ישנים (CrmAssistant / OfficeAssistant הוסרו מהפרויקט)
### 2. בדיקת אנטיטי AssistantConversation
- המודול משתמש באנטיטי AssistantConversation לשמירת שיחות (ראה ConversationRepository.php)
- בדוק אם האנטיטי הזה קיים ב-EspoCRM Dev
- אם לא קיים — צריך ליצור אותו. בדוק את הקוד ב-ConversationRepository.php כדי להבין אילו שדות נדרשים, והכן entityDefs JSON
### 3. בניית n8n Workflow לטיפול בצ'אט
זה הרכיב הקריטי החסר. המודול שולח webhook ל-n8n ומצפה לתשובה.
**Webhook Payload (נשלח מ-EspoCRM):**
```json
{
"message": "הודעת המשתמש",
"context": { ... הקשר תיק/משרד מלא ... },
"conversationId": "conv_xxx",
"conversationHistory": [{ "role": "user/assistant", "content": "..." }],
"mode": "office|case"
}
```
**Response נדרש (חזרה ל-EspoCRM):**
```json
{
"text": "תשובת העוזר",
"actions": [
{
"tool": "create_task|add_note|change_status|create_meeting|schedule_hearing|list_documents|analyze_document|save_memory|rename_document",
"params": { ... },
"actionId": "action_xxx",
"displayText": "תיאור הפעולה למשתמש"
}
]
}
```
**הפעולות שהעוזר יכול לבקש:**
| כלי | פרמטרים | דורש אישור |
|------|----------|------------|
| create_task | name, dateEnd, priority, description | כן |
| add_note | text | כן |
| change_status | status | כן |
| create_meeting | name, dateStart, dateEnd, description | כן |
| create_call | name, dateStart, dateEnd, description, direction, status | כן |
| schedule_hearing | date, description | כן |
| list_documents | — | לא (מיידי) |
| analyze_document | documentId | כן |
| save_memory | name, content, category, importance | לא (מיידי) |
| save_rule | name, rule, scope (global/case/office) | לא (מיידי) |
| rename_document | documentId, newName | כן |
**דרישות ל-Workflow:**
- Webhook trigger שמקבל POST
- שליחת ההודעה + הקשר ל-LLM (Claude/GPT)
- System prompt שמכיר את הכלים הזמינים ואת פורמט התשובה
- החזרת JSON תקין בפורמט למעלה
- שמירת היסטוריית שיחה (conversationId)
- טיפול בשני המצבים (office/case) עם system prompts מתאימים
### 4. הגדרת Integration ב-EspoCRM
אחרי שה-workflow מוכן:
- Admin > Integrations > Smart Assistant
- webhookUrl = כתובת ה-webhook מ-n8n
- apiKey = מפתח אימות (אם הגדרנו)
- שאר הערכים: ברירת מחדל
### 5. בדיקות ידניות ראשונות
אחרי הפריסה:
- [ ] האם ה-FAB (כפתור צ'אט) מופיע?
- [ ] האם ניתן לשלוח הודעה במצב משרד ולקבל תשובה?
- [ ] האם ניתן לנווט לתיק ולשלוח הודעה במצב תיק?
- [ ] האם פעולות מוצגות לאישור?
- [ ] האם ההתראות מופיעות ב-navbar?
- [ ] האם הדשלט מציג נתונים?
## הנחיות
- השתמש ב-MCP tools (Coolify, Gitea, n8n, Infisical) לכל פעולה
- אל תבצע פעולות הרסניות בלי אישור שלי
- אם נתקלת בבעיה — תאר מה קרה ומה ניסית, ותשאל מה לעשות
- עדכן את DEPLOYMENT_REVIEW.md עם כל ממצא חדש
```
---
## הערות שימוש
- **היכן להריץ:** בתיקיית `/home/chaim/espocrm-extensions/SmartAssistant`
- **MCP נדרשים:** coolify-dev, gitea-dev, n8n-dev, infisical
- **משך משוער:** השלב הקריטי הוא בניית ה-n8n workflow (#3) — זה ידרוש עיצוב system prompts ובדיקות
- **מה הפרומפט לא כולל:** הסרת מודולים ישנים (תלוי בממצאי שלב 1), הוספת תרגום he_IL (שיפור עתידי)
@@ -0,0 +1,22 @@
{
"labels": {
"Create AssistantRule": "Create Rule"
},
"fields": {
"name": "Name",
"rule": "Rule",
"scope": "Scope",
"isActive": "Active",
"createdAt": "Created At",
"modifiedAt": "Modified At",
"createdBy": "Created By",
"modifiedBy": "Modified By"
},
"options": {
"scope": {
"global": "Global",
"case": "Case Mode",
"office": "Office Mode"
}
}
}
@@ -1,10 +1,12 @@
{
"scopeNames": {
"CaseMemory": "Case Memory",
"AssistantRule": "Assistant Rule",
"SmartAssistant": "Smart Assistant"
},
"scopeNamesPlural": {
"CaseMemory": "Case Memories",
"AssistantRule": "Assistant Rules",
"SmartAssistant": "Smart Assistant"
}
}
@@ -0,0 +1,22 @@
{
"labels": {
"Create AssistantRule": "הוסף כלל"
},
"fields": {
"name": "שם",
"rule": "כלל",
"scope": "היקף",
"isActive": "פעיל",
"createdAt": "נוצר בתאריך",
"modifiedAt": "עודכן בתאריך",
"createdBy": "נוצר על ידי",
"modifiedBy": "עודכן על ידי"
},
"options": {
"scope": {
"global": "גלובלי",
"case": "מצב תיק",
"office": "מצב משרד"
}
}
}
@@ -1,10 +1,12 @@
{
"scopeNames": {
"CaseMemory": "זיכרון תיק",
"AssistantRule": "כלל התנהגות",
"SmartAssistant": "עוזר חכם"
},
"scopeNamesPlural": {
"CaseMemory": "זיכרונות תיקים",
"AssistantRule": "כללי התנהגות",
"SmartAssistant": "עוזר חכם"
}
}
@@ -0,0 +1,56 @@
{
"fields": {
"name": {
"type": "varchar",
"maxLength": 255,
"required": true
},
"rule": {
"type": "text",
"required": true
},
"scope": {
"type": "enum",
"options": ["global", "case", "office"],
"default": "global"
},
"isActive": {
"type": "bool",
"default": true
},
"createdAt": {
"type": "datetime",
"readOnly": true
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
},
"createdBy": {
"type": "link",
"readOnly": true
},
"modifiedBy": {
"type": "link",
"readOnly": true
}
},
"links": {
"createdBy": {
"type": "belongsTo",
"entity": "User"
},
"modifiedBy": {
"type": "belongsTo",
"entity": "User"
}
},
"indexes": {
"isActive": {
"columns": ["isActive", "deleted"]
},
"scope": {
"columns": ["scope", "isActive", "deleted"]
}
}
}
@@ -0,0 +1,11 @@
[
{
"label": "Overview",
"rows": [
[{"name": "name"}, {"name": "scope"}],
[{"name": "isActive"}, false],
[{"name": "rule", "fullWidth": true}],
[{"name": "createdAt"}, {"name": "createdBy"}]
]
}
]
@@ -0,0 +1,7 @@
[
{"name": "name", "width": 35, "link": true},
{"name": "rule", "width": 30},
{"name": "scope", "width": 10},
{"name": "isActive", "width": 10},
{"name": "createdAt", "width": 15}
]
@@ -0,0 +1,11 @@
{
"entity": true,
"object": true,
"module": "SmartAssistant",
"acl": true,
"aclActionList": ["create", "read", "edit", "delete"],
"aclLevelList": ["all", "team", "own", "no"],
"stream": false,
"tab": false,
"disabled": false
}
@@ -41,6 +41,7 @@ class ActionExecutor
'add_note' => $this->addNote($params, $caseId, $userId),
'change_status' => $this->changeStatus($params, $caseId),
'create_meeting' => $this->createMeeting($params, $caseId, $userId),
'create_call' => $this->createCall($params, $caseId, $userId),
'schedule_hearing' => $this->scheduleHearing($params, $caseId, $userId),
'list_documents' => $this->listDocuments($caseId),
'analyze_document' => $this->analyzeDocument($params, $caseId),
@@ -49,6 +50,7 @@ class ActionExecutor
'generate_document' => $this->generateDocument($params, $caseId),
'merge_documents' => $this->mergeDocuments($params, $caseId),
'save_memory' => $this->saveMemory($params, $caseId, $userId),
'save_rule' => $this->saveRule($params, $userId),
default => $this->executePluginTool($tool, $params, $caseId, $userId),
};
}
@@ -80,6 +82,32 @@ class ActionExecutor
];
}
private function saveRule(array $params, string $userId): array
{
$name = $params['name'] ?? '';
$rule = $params['rule'] ?? '';
if (!$name || !$rule) {
throw new Error('save_rule requires name and rule parameters.');
}
$entity = $this->entityManager->getNewEntity('AssistantRule');
$entity->set([
'name' => $name,
'rule' => $rule,
'scope' => $params['scope'] ?? 'global',
'isActive' => true,
'createdById' => $userId,
]);
$this->entityManager->saveEntity($entity);
return [
'success' => true,
'message' => "✅ כלל חדש נשמר: \"{$name}\"",
'entityType' => 'AssistantRule',
'entityId' => $entity->get('id'),
];
}
private function createTask(array $params, ?string $caseId, string $userId): array
{
$taskData = [
@@ -166,6 +194,29 @@ class ActionExecutor
return ['success' => true, 'message' => "✅ פגישה \"{$params['name']}\" נקבעה בהצלחה", 'entityType' => 'Meeting', 'entityId' => $meeting->get('id')];
}
private function createCall(array $params, ?string $caseId, string $userId): array
{
$callData = [
'name' => $params['name'] ?? 'שיחה חדשה',
'status' => $params['status'] ?? 'Held',
'direction' => $params['direction'] ?? 'Outbound',
'dateStart' => $this->normalizeDateTime($params['dateStart'] ?? null),
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
'description' => $params['description'] ?? null,
'assignedUserId' => $userId,
];
if ($caseId) {
$callData['parentType'] = 'Case';
$callData['parentId'] = $caseId;
}
$call = $this->entityManager->getNewEntity('Call');
$call->set($callData);
$this->entityManager->saveEntity($call);
return ['success' => true, 'message' => "✅ שיחה \"{$params['name']}\" תועדה בהצלחה", 'entityType' => 'Call', 'entityId' => $call->get('id')];
}
private function scheduleHearing(array $params, ?string $caseId, string $userId): array
{
$case = $this->entityManager->getEntityById('Case', $caseId);
@@ -0,0 +1,41 @@
<?php
namespace Espo\Modules\SmartAssistant\Services;
use Espo\ORM\EntityManager;
use Espo\Core\Utils\Log;
class AssistantRuleContextProvider
{
private EntityManager $entityManager;
private Log $log;
public function __construct(EntityManager $entityManager, Log $log)
{
$this->entityManager = $entityManager;
$this->log = $log;
}
public function getRulesForMode(string $mode): array
{
$entries = $this->entityManager->getRDBRepository('AssistantRule')
->where([
'isActive' => true,
'deleted' => false,
'scope' => ['global', $mode],
])
->order('createdAt', 'ASC')
->find();
$rules = [];
foreach ($entries as $entry) {
$rules[] = [
'name' => $entry->get('name'),
'rule' => $entry->get('rule'),
'scope' => $entry->get('scope'),
];
}
return $rules;
}
}
@@ -29,6 +29,7 @@ class CaseContextBuilder
'contacts' => $this->getCaseContacts($case),
'openTasks' => $this->getOpenTasks($caseId),
'upcomingMeetings' => $this->getUpcomingMeetings($caseId),
'recentCalls' => $this->getRecentCalls($caseId),
'recentNotes' => $this->getRecentNotes($caseId),
'availableTemplates' => $this->getAvailableTemplates(),
'documents' => $this->getDocumentListing($caseId),
@@ -113,6 +114,24 @@ class CaseContextBuilder
return $meetings;
}
private function getRecentCalls(string $caseId): array
{
$calls = [];
$collection = $this->entityManager->getRDBRepository('Call')
->where(['parentType' => 'Case', 'parentId' => $caseId])
->order('dateStart', 'DESC')->limit(0, 10)->find();
foreach ($collection as $c) {
$calls[] = [
'id' => $c->get('id'), 'name' => $c->get('name'),
'status' => $c->get('status'), 'direction' => $c->get('direction'),
'dateStart' => $c->get('dateStart'), 'dateEnd' => $c->get('dateEnd'),
'description' => $c->get('description'),
];
}
return $calls;
}
private function getRecentNotes(string $caseId): array
{
$notes = [];
@@ -71,6 +71,10 @@ class SmartAssistantService
}
}
// Load behavioral rules
$ruleProvider = $this->injectableFactory->create(AssistantRuleContextProvider::class);
$context['assistantRules'] = $ruleProvider->getRulesForMode($mode === 'case' ? 'case' : 'office');
// Generate conversation ID
if (!$conversationId) {
$prefix = $mode === 'case' ? 'conv_' : 'oconv_';
@@ -382,6 +386,7 @@ class SmartAssistantService
'conversationId' => $conversationId,
'conversationHistory' => $history,
'mode' => $mode,
'systemInstructions' => $this->getSystemInstructions(),
'espocrm' => [
'url' => rtrim($siteUrl, '/'),
'apiKey' => $this->getEspocrmApiKey(),
@@ -477,6 +482,26 @@ class SmartAssistantService
return null;
}
private function getSystemInstructions(): array
{
return [
'call_vs_meeting' => implode(' ', [
'כשהמשתמש מדווח ששוחח, דיבר, התקשר, או שוחחה עם לקוח/ה — השתמש בכלי create_call (לא create_meeting).',
'כשהמשתמש מדווח שנפגש, קבע פגישה, או תיאם מפגש — השתמש בכלי create_meeting.',
'שיחת טלפון = create_call. פגישה פיזית/זום = create_meeting.',
]),
'open_questions' => implode(' ', [
'אם שאלת את המשתמש שאלה והוא לא ענה עליה בתגובה הבאה — חזור על השאלה בתחילת התשובה הבאה.',
'עקוב אחרי שאלות פתוחות ואל תוותר עליהן עד שהמשתמש עונה או אומר לך לוותר.',
]),
'behavioral_rules' => implode(' ', [
'אם context.assistantRules מכיל כללים — יש ליישם אותם בכל שיחה.',
'כללים גלובליים תקפים תמיד. כללי "case" תקפים רק במצב תיק. כללי "office" תקפים רק במצב משרד.',
'אם המשתמש מבקש להגדיר כלל חדש (למשל "תמיד תעשה X כש-Y") — השתמש בכלי save_rule כדי לשמור אותו.',
]),
];
}
private function getThresholds(): array
{
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
+2 -2
View File
@@ -3,11 +3,11 @@
"module": "SmartAssistant",
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
"author": "klear",
"version": "2.3.0",
"version": "2.4.0",
"acceptableVersions": [
">=8.0.0"
],
"releaseDate": "2026-04-05",
"releaseDate": "2026-04-06",
"php": [
">=8.1"
]