Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b810c9fcff | |||
| 0dc26f3b15 | |||
| ca04dd72f8 |
@@ -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 (שיפור עתידי)
|
||||||
@@ -24,6 +24,9 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
' <button class="btn btn-link sa-new-btn" type="button" title="{{translate \'New Conversation\' scope=\'SmartAssistant\'}}">' +
|
' <button class="btn btn-link sa-new-btn" type="button" title="{{translate \'New Conversation\' scope=\'SmartAssistant\'}}">' +
|
||||||
' <span class="fas fa-plus"></span>' +
|
' <span class="fas fa-plus"></span>' +
|
||||||
' </button>' +
|
' </button>' +
|
||||||
|
' <button class="btn btn-link sa-expand-btn" type="button" title="{{translate \'Expand\' scope=\'SmartAssistant\'}}">' +
|
||||||
|
' <span class="fas fa-expand-alt"></span>' +
|
||||||
|
' </button>' +
|
||||||
' <button class="btn btn-link sa-close-btn" type="button">' +
|
' <button class="btn btn-link sa-close-btn" type="button">' +
|
||||||
' <span class="fas fa-times"></span>' +
|
' <span class="fas fa-times"></span>' +
|
||||||
' </button>' +
|
' </button>' +
|
||||||
@@ -67,7 +70,10 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
'.sa-fab:hover { transform: scale(1.08); }' +
|
'.sa-fab:hover { transform: scale(1.08); }' +
|
||||||
'.sa-fab-avatar { width: 100%; height: 100%; object-fit: cover; }' +
|
'.sa-fab-avatar { width: 100%; height: 100%; object-fit: cover; }' +
|
||||||
'.sa-fab-badge { position: absolute; top: -4px; right: -4px; background: #c62828; color: #fff; border-radius: 10px; min-width: 20px; height: 20px; font-size: 11px; font-weight: bold; display: flex; align-items: center; justify-content: center; padding: 0 5px; z-index: 1; }' +
|
'.sa-fab-badge { position: absolute; top: -4px; right: -4px; background: #c62828; color: #fff; border-radius: 10px; min-width: 20px; height: 20px; font-size: 11px; font-weight: bold; display: flex; align-items: center; justify-content: center; padding: 0 5px; z-index: 1; }' +
|
||||||
'.sa-chat-panel { position: absolute; bottom: 68px; left: 0; width: 400px; max-height: 600px; background: #fff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); display: flex; flex-direction: column; overflow: hidden; animation: sa-slide-up 0.25s ease-out; }' +
|
'.sa-chat-panel { position: absolute; bottom: 68px; left: 0; width: 400px; max-height: 600px; background: #fff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); display: flex; flex-direction: column; overflow: hidden; animation: sa-slide-up 0.25s ease-out; transition: width 0.3s ease, max-height 0.3s ease; }' +
|
||||||
|
'.sa-chat-panel.sa-expanded { width: 700px; max-height: 85vh; }' +
|
||||||
|
'.sa-chat-panel.sa-expanded .sa-chat-messages { max-height: calc(85vh - 180px); }' +
|
||||||
|
'.sa-chat-panel.sa-expanded .sa-history-list, .sa-chat-panel.sa-expanded .sa-memory-list { max-height: calc(85vh - 200px); }' +
|
||||||
'@keyframes sa-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }' +
|
'@keyframes sa-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }' +
|
||||||
'.sa-chat-header { display: flex; align-items: center; padding: 10px 16px; background: #5c6bc0; color: #fff; gap: 8px; }' +
|
'.sa-chat-header { display: flex; align-items: center; padding: 10px 16px; background: #5c6bc0; color: #fff; gap: 8px; }' +
|
||||||
'.sa-chat-title { font-size: 15px; font-weight: 600; white-space: nowrap; }' +
|
'.sa-chat-title { font-size: 15px; font-weight: 600; white-space: nowrap; }' +
|
||||||
@@ -122,7 +128,7 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
'.sa-badge-source { background: #f3e5f5; color: #7b1fa2; }' +
|
'.sa-badge-source { background: #f3e5f5; color: #7b1fa2; }' +
|
||||||
'.sa-memory-entry-content { font-size: 12px; color: #555; line-height: 1.4; max-height: 60px; overflow: hidden; }' +
|
'.sa-memory-entry-content { font-size: 12px; color: #555; line-height: 1.4; max-height: 60px; overflow: hidden; }' +
|
||||||
'.sa-memory-entry-meta { font-size: 10px; color: #999; margin-top: 4px; }' +
|
'.sa-memory-entry-meta { font-size: 10px; color: #999; margin-top: 4px; }' +
|
||||||
'@media (max-width: 480px) { .sa-chat-panel { width: calc(100vw - 48px); left: 0; } .smart-assistant-fab-container { bottom: 16px; left: 16px; } }' +
|
'@media (max-width: 480px) { .sa-chat-panel { width: calc(100vw - 48px); left: 0; } .sa-chat-panel.sa-expanded { width: calc(100vw - 48px); } .smart-assistant-fab-container { bottom: 16px; left: 16px; } }' +
|
||||||
'</style>',
|
'</style>',
|
||||||
|
|
||||||
conversationId: null,
|
conversationId: null,
|
||||||
@@ -131,12 +137,14 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
currentCaseId: null,
|
currentCaseId: null,
|
||||||
currentCaseName: null,
|
currentCaseName: null,
|
||||||
currentView: 'chat',
|
currentView: 'chat',
|
||||||
|
isExpanded: false,
|
||||||
currentMemoryCategory: null,
|
currentMemoryCategory: null,
|
||||||
memoryData: null,
|
memoryData: null,
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
'click .sa-fab': function () { this.togglePanel(); },
|
'click .sa-fab': function () { this.togglePanel(); },
|
||||||
'click .sa-close-btn': function () { this.closePanel(); },
|
'click .sa-close-btn': function () { this.closePanel(); },
|
||||||
|
'click .sa-expand-btn': function () { this.toggleExpand(); },
|
||||||
'click .sa-chat-send': function () { this.sendMessage(); },
|
'click .sa-chat-send': function () { this.sendMessage(); },
|
||||||
'click .sa-history-btn': function () { this.showHistory(); },
|
'click .sa-history-btn': function () { this.showHistory(); },
|
||||||
'click .sa-new-btn': function () { this.startNewConversation(); },
|
'click .sa-new-btn': function () { this.startNewConversation(); },
|
||||||
@@ -260,7 +268,26 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
|
|
||||||
closePanel: function () {
|
closePanel: function () {
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.$el.find('.sa-chat-panel').css('display', 'none');
|
this.isExpanded = false;
|
||||||
|
var $panel = this.$el.find('.sa-chat-panel');
|
||||||
|
$panel.removeClass('sa-expanded').css('display', 'none');
|
||||||
|
this.$el.find('.sa-expand-btn .fas').removeClass('fa-compress-alt').addClass('fa-expand-alt');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleExpand: function () {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
var $panel = this.$el.find('.sa-chat-panel');
|
||||||
|
var $icon = this.$el.find('.sa-expand-btn .fas');
|
||||||
|
|
||||||
|
if (this.isExpanded) {
|
||||||
|
$panel.addClass('sa-expanded');
|
||||||
|
$icon.removeClass('fa-expand-alt').addClass('fa-compress-alt');
|
||||||
|
} else {
|
||||||
|
$panel.removeClass('sa-expanded');
|
||||||
|
$icon.removeClass('fa-compress-alt').addClass('fa-expand-alt');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollToBottom();
|
||||||
},
|
},
|
||||||
|
|
||||||
showChat: function () {
|
showChat: function () {
|
||||||
@@ -741,7 +768,7 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
recordView.model.trigger('update-all');
|
recordView.model.trigger('update-all');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entityType === 'Task' || entityType === 'Meeting') {
|
if (entityType === 'Task' || entityType === 'Meeting' || entityType === 'Call') {
|
||||||
var sideView = currentView.getView('side');
|
var sideView = currentView.getView('side');
|
||||||
|
|
||||||
if (sideView) {
|
if (sideView) {
|
||||||
|
|||||||
@@ -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": {
|
"scopeNames": {
|
||||||
"CaseMemory": "Case Memory",
|
"CaseMemory": "Case Memory",
|
||||||
|
"AssistantRule": "Assistant Rule",
|
||||||
"SmartAssistant": "Smart Assistant"
|
"SmartAssistant": "Smart Assistant"
|
||||||
},
|
},
|
||||||
"scopeNamesPlural": {
|
"scopeNamesPlural": {
|
||||||
"CaseMemory": "Case Memories",
|
"CaseMemory": "Case Memories",
|
||||||
|
"AssistantRule": "Assistant Rules",
|
||||||
"SmartAssistant": "Smart Assistant"
|
"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": {
|
"scopeNames": {
|
||||||
"CaseMemory": "זיכרון תיק",
|
"CaseMemory": "זיכרון תיק",
|
||||||
|
"AssistantRule": "כלל התנהגות",
|
||||||
"SmartAssistant": "עוזר חכם"
|
"SmartAssistant": "עוזר חכם"
|
||||||
},
|
},
|
||||||
"scopeNamesPlural": {
|
"scopeNamesPlural": {
|
||||||
"CaseMemory": "זיכרונות תיקים",
|
"CaseMemory": "זיכרונות תיקים",
|
||||||
|
"AssistantRule": "כללי התנהגות",
|
||||||
"SmartAssistant": "עוזר חכם"
|
"SmartAssistant": "עוזר חכם"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+56
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Overview",
|
||||||
|
"rows": [
|
||||||
|
[{"name": "name"}, {"name": "scope"}],
|
||||||
|
[{"name": "isActive"}, false],
|
||||||
|
[{"name": "rule", "fullWidth": true}],
|
||||||
|
[{"name": "createdAt"}, {"name": "createdBy"}]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
+7
@@ -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
|
||||||
|
}
|
||||||
@@ -7,18 +7,21 @@ use Espo\Core\Exceptions\NotFound;
|
|||||||
use Espo\Core\InjectableFactory;
|
use Espo\Core\InjectableFactory;
|
||||||
use Espo\ORM\EntityManager;
|
use Espo\ORM\EntityManager;
|
||||||
use Espo\Core\Utils\Log;
|
use Espo\Core\Utils\Log;
|
||||||
|
use Espo\Core\Utils\Metadata;
|
||||||
|
|
||||||
class ActionExecutor
|
class ActionExecutor
|
||||||
{
|
{
|
||||||
private EntityManager $entityManager;
|
private EntityManager $entityManager;
|
||||||
private InjectableFactory $injectableFactory;
|
private InjectableFactory $injectableFactory;
|
||||||
private Log $log;
|
private Log $log;
|
||||||
|
private Metadata $metadata;
|
||||||
|
|
||||||
public function __construct(EntityManager $entityManager, InjectableFactory $injectableFactory, Log $log)
|
public function __construct(EntityManager $entityManager, InjectableFactory $injectableFactory, Log $log, Metadata $metadata)
|
||||||
{
|
{
|
||||||
$this->entityManager = $entityManager;
|
$this->entityManager = $entityManager;
|
||||||
$this->injectableFactory = $injectableFactory;
|
$this->injectableFactory = $injectableFactory;
|
||||||
$this->log = $log;
|
$this->log = $log;
|
||||||
|
$this->metadata = $metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
public const VALID_STATUSES = [
|
public const VALID_STATUSES = [
|
||||||
@@ -38,6 +41,10 @@ class ActionExecutor
|
|||||||
'add_note' => $this->addNote($params, $caseId, $userId),
|
'add_note' => $this->addNote($params, $caseId, $userId),
|
||||||
'change_status' => $this->changeStatus($params, $caseId),
|
'change_status' => $this->changeStatus($params, $caseId),
|
||||||
'create_meeting' => $this->createMeeting($params, $caseId, $userId),
|
'create_meeting' => $this->createMeeting($params, $caseId, $userId),
|
||||||
|
'create_call' => $this->createCall($params, $caseId, $userId),
|
||||||
|
'delete_meeting' => $this->deleteMeeting($params),
|
||||||
|
'delete_call' => $this->deleteCall($params),
|
||||||
|
'delete_note' => $this->deleteNote($params),
|
||||||
'schedule_hearing' => $this->scheduleHearing($params, $caseId, $userId),
|
'schedule_hearing' => $this->scheduleHearing($params, $caseId, $userId),
|
||||||
'list_documents' => $this->listDocuments($caseId),
|
'list_documents' => $this->listDocuments($caseId),
|
||||||
'analyze_document' => $this->analyzeDocument($params, $caseId),
|
'analyze_document' => $this->analyzeDocument($params, $caseId),
|
||||||
@@ -46,7 +53,8 @@ class ActionExecutor
|
|||||||
'generate_document' => $this->generateDocument($params, $caseId),
|
'generate_document' => $this->generateDocument($params, $caseId),
|
||||||
'merge_documents' => $this->mergeDocuments($params, $caseId),
|
'merge_documents' => $this->mergeDocuments($params, $caseId),
|
||||||
'save_memory' => $this->saveMemory($params, $caseId, $userId),
|
'save_memory' => $this->saveMemory($params, $caseId, $userId),
|
||||||
default => throw new Error("Unknown tool: {$tool}"),
|
'save_rule' => $this->saveRule($params, $userId),
|
||||||
|
default => $this->executePluginTool($tool, $params, $caseId, $userId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +85,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
|
private function createTask(array $params, ?string $caseId, string $userId): array
|
||||||
{
|
{
|
||||||
$taskData = [
|
$taskData = [
|
||||||
@@ -163,6 +197,97 @@ class ActionExecutor
|
|||||||
return ['success' => true, 'message' => "✅ פגישה \"{$params['name']}\" נקבעה בהצלחה", 'entityType' => 'Meeting', 'entityId' => $meeting->get('id')];
|
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 deleteMeeting(array $params): array
|
||||||
|
{
|
||||||
|
$entityId = $params['entityId'] ?? null;
|
||||||
|
if (!$entityId) {
|
||||||
|
throw new Error('entityId parameter is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = $this->entityManager->getEntityById('Meeting', $entityId);
|
||||||
|
if (!$entity) {
|
||||||
|
throw new NotFound("Meeting {$entityId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $entity->get('name') ?? '';
|
||||||
|
$this->entityManager->removeEntity($entity);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => "✅ פגישה \"{$name}\" נמחקה בהצלחה",
|
||||||
|
'entityType' => 'Meeting',
|
||||||
|
'entityId' => $entityId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteCall(array $params): array
|
||||||
|
{
|
||||||
|
$entityId = $params['entityId'] ?? null;
|
||||||
|
if (!$entityId) {
|
||||||
|
throw new Error('entityId parameter is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = $this->entityManager->getEntityById('Call', $entityId);
|
||||||
|
if (!$entity) {
|
||||||
|
throw new NotFound("Call {$entityId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $entity->get('name') ?? '';
|
||||||
|
$this->entityManager->removeEntity($entity);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => "✅ שיחה \"{$name}\" נמחקה בהצלחה",
|
||||||
|
'entityType' => 'Call',
|
||||||
|
'entityId' => $entityId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteNote(array $params): array
|
||||||
|
{
|
||||||
|
$entityId = $params['entityId'] ?? null;
|
||||||
|
if (!$entityId) {
|
||||||
|
throw new Error('entityId parameter is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$entity = $this->entityManager->getEntityById('Note', $entityId);
|
||||||
|
if (!$entity) {
|
||||||
|
throw new NotFound("Note {$entityId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->removeEntity($entity);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => '✅ הערה נמחקה בהצלחה',
|
||||||
|
'entityType' => 'Note',
|
||||||
|
'entityId' => $entityId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function scheduleHearing(array $params, ?string $caseId, string $userId): array
|
private function scheduleHearing(array $params, ?string $caseId, string $userId): array
|
||||||
{
|
{
|
||||||
$case = $this->entityManager->getEntityById('Case', $caseId);
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
||||||
@@ -353,4 +478,23 @@ class ActionExecutor
|
|||||||
if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB';
|
if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB';
|
||||||
return $bytes . ' B';
|
return $bytes . ' B';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function executePluginTool(string $tool, array $params, ?string $caseId, string $userId): array
|
||||||
|
{
|
||||||
|
$handlerClass = $this->metadata->get(['app', 'smartAssistant', 'tools', $tool, 'handler']);
|
||||||
|
|
||||||
|
if (!$handlerClass) {
|
||||||
|
throw new Error("Unknown tool: {$tool}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log->debug("SmartAssistant: Executing plugin tool={$tool} handler={$handlerClass}");
|
||||||
|
|
||||||
|
$handler = $this->injectableFactory->create($handlerClass);
|
||||||
|
|
||||||
|
if (!method_exists($handler, 'execute')) {
|
||||||
|
throw new Error("Plugin tool handler {$handlerClass} must have an execute() method.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->execute($params, $caseId, $userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
'contacts' => $this->getCaseContacts($case),
|
||||||
'openTasks' => $this->getOpenTasks($caseId),
|
'openTasks' => $this->getOpenTasks($caseId),
|
||||||
'upcomingMeetings' => $this->getUpcomingMeetings($caseId),
|
'upcomingMeetings' => $this->getUpcomingMeetings($caseId),
|
||||||
|
'recentCalls' => $this->getRecentCalls($caseId),
|
||||||
'recentNotes' => $this->getRecentNotes($caseId),
|
'recentNotes' => $this->getRecentNotes($caseId),
|
||||||
'availableTemplates' => $this->getAvailableTemplates(),
|
'availableTemplates' => $this->getAvailableTemplates(),
|
||||||
'documents' => $this->getDocumentListing($caseId),
|
'documents' => $this->getDocumentListing($caseId),
|
||||||
@@ -113,6 +114,24 @@ class CaseContextBuilder
|
|||||||
return $meetings;
|
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
|
private function getRecentNotes(string $caseId): array
|
||||||
{
|
{
|
||||||
$notes = [];
|
$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
|
// Generate conversation ID
|
||||||
if (!$conversationId) {
|
if (!$conversationId) {
|
||||||
$prefix = $mode === 'case' ? 'conv_' : 'oconv_';
|
$prefix = $mode === 'case' ? 'conv_' : 'oconv_';
|
||||||
@@ -382,6 +386,7 @@ class SmartAssistantService
|
|||||||
'conversationId' => $conversationId,
|
'conversationId' => $conversationId,
|
||||||
'conversationHistory' => $history,
|
'conversationHistory' => $history,
|
||||||
'mode' => $mode,
|
'mode' => $mode,
|
||||||
|
'systemInstructions' => $this->getSystemInstructions(),
|
||||||
'espocrm' => [
|
'espocrm' => [
|
||||||
'url' => rtrim($siteUrl, '/'),
|
'url' => rtrim($siteUrl, '/'),
|
||||||
'apiKey' => $this->getEspocrmApiKey(),
|
'apiKey' => $this->getEspocrmApiKey(),
|
||||||
@@ -477,6 +482,26 @@ class SmartAssistantService
|
|||||||
return null;
|
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
|
private function getThresholds(): array
|
||||||
{
|
{
|
||||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||||
|
|||||||
+2
-2
@@ -3,11 +3,11 @@
|
|||||||
"module": "SmartAssistant",
|
"module": "SmartAssistant",
|
||||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||||
"author": "klear",
|
"author": "klear",
|
||||||
"version": "2.3.0",
|
"version": "2.5.0",
|
||||||
"acceptableVersions": [
|
"acceptableVersions": [
|
||||||
">=8.0.0"
|
">=8.0.0"
|
||||||
],
|
],
|
||||||
"releaseDate": "2026-04-05",
|
"releaseDate": "2026-04-06",
|
||||||
"php": [
|
"php": [
|
||||||
">=8.1"
|
">=8.1"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user