diff --git a/DEPLOYMENT_PROMPT.md b/DEPLOYMENT_PROMPT.md new file mode 100644 index 0000000..f3dc6a8 --- /dev/null +++ b/DEPLOYMENT_PROMPT.md @@ -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 (שיפור עתידי) diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/AssistantRule.json b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/AssistantRule.json new file mode 100644 index 0000000..5a6ea24 --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/AssistantRule.json @@ -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" + } + } +} diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/Global.json b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/Global.json index 448d8ec..4aee85f 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/Global.json +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/en_US/Global.json @@ -1,10 +1,12 @@ { "scopeNames": { "CaseMemory": "Case Memory", + "AssistantRule": "Assistant Rule", "SmartAssistant": "Smart Assistant" }, "scopeNamesPlural": { "CaseMemory": "Case Memories", + "AssistantRule": "Assistant Rules", "SmartAssistant": "Smart Assistant" } } diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/AssistantRule.json b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/AssistantRule.json new file mode 100644 index 0000000..dd0251b --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/AssistantRule.json @@ -0,0 +1,22 @@ +{ + "labels": { + "Create AssistantRule": "הוסף כלל" + }, + "fields": { + "name": "שם", + "rule": "כלל", + "scope": "היקף", + "isActive": "פעיל", + "createdAt": "נוצר בתאריך", + "modifiedAt": "עודכן בתאריך", + "createdBy": "נוצר על ידי", + "modifiedBy": "עודכן על ידי" + }, + "options": { + "scope": { + "global": "גלובלי", + "case": "מצב תיק", + "office": "מצב משרד" + } + } +} diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/Global.json b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/Global.json index 18a3fbf..470d2c0 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/Global.json +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/i18n/fa_IR/Global.json @@ -1,10 +1,12 @@ { "scopeNames": { "CaseMemory": "זיכרון תיק", + "AssistantRule": "כלל התנהגות", "SmartAssistant": "עוזר חכם" }, "scopeNamesPlural": { "CaseMemory": "זיכרונות תיקים", + "AssistantRule": "כללי התנהגות", "SmartAssistant": "עוזר חכם" } } diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/entityDefs/AssistantRule.json b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/entityDefs/AssistantRule.json new file mode 100644 index 0000000..664ec06 --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/entityDefs/AssistantRule.json @@ -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"] + } + } +} diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/detail.json b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/detail.json new file mode 100644 index 0000000..de9c79f --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/detail.json @@ -0,0 +1,11 @@ +[ + { + "label": "Overview", + "rows": [ + [{"name": "name"}, {"name": "scope"}], + [{"name": "isActive"}, false], + [{"name": "rule", "fullWidth": true}], + [{"name": "createdAt"}, {"name": "createdBy"}] + ] + } +] diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/list.json b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/list.json new file mode 100644 index 0000000..d9ab118 --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/layouts/AssistantRule/list.json @@ -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} +] diff --git a/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/scopes/AssistantRule.json b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/scopes/AssistantRule.json new file mode 100644 index 0000000..f1d929f --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Resources/metadata/scopes/AssistantRule.json @@ -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 +} diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/ActionExecutor.php b/files/custom/Espo/Modules/SmartAssistant/Services/ActionExecutor.php index 4f440ff..f80a0a8 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/ActionExecutor.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/ActionExecutor.php @@ -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); diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/AssistantRuleContextProvider.php b/files/custom/Espo/Modules/SmartAssistant/Services/AssistantRuleContextProvider.php new file mode 100644 index 0000000..364dffe --- /dev/null +++ b/files/custom/Espo/Modules/SmartAssistant/Services/AssistantRuleContextProvider.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php b/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php index 9a3e142..efbd23b 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/CaseContextBuilder.php @@ -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 = []; diff --git a/files/custom/Espo/Modules/SmartAssistant/Services/SmartAssistantService.php b/files/custom/Espo/Modules/SmartAssistant/Services/SmartAssistantService.php index c8af48b..23b416a 100644 --- a/files/custom/Espo/Modules/SmartAssistant/Services/SmartAssistantService.php +++ b/files/custom/Espo/Modules/SmartAssistant/Services/SmartAssistantService.php @@ -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'); diff --git a/manifest.json b/manifest.json index 24e9972..cb087b0 100644 --- a/manifest.json +++ b/manifest.json @@ -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" ]