Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd1d484e74 | |||
| d266380e6d | |||
| 1d200fc3f6 | |||
| 65faf99e11 |
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Prompt editor field for AssistantPrompt.content, AssistantSkill.content,
|
||||
* UserProfile.content. Inherits from the standard text field — adds:
|
||||
* - monospace font (mixed Hebrew + English + JSON snippets read better)
|
||||
* - dir="auto" so RTL/LTR works without a manual toggle
|
||||
* - taller textarea (min 500px) — these prompts run to thousands of chars
|
||||
*/
|
||||
define('smart-assistant:views/fields/prompt-editor', ['views/fields/text'], function (Dep) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
rowsDefault: 20,
|
||||
|
||||
afterRender: function () {
|
||||
Dep.prototype.afterRender.call(this);
|
||||
|
||||
if (this.isEditMode() || this.isDetailMode()) {
|
||||
var $area = this.$element;
|
||||
if ($area && $area.length) {
|
||||
$area.attr('dir', 'auto');
|
||||
$area.css({
|
||||
'font-family': "'Menlo','Consolas','DejaVu Sans Mono',monospace",
|
||||
'font-size': '13px',
|
||||
'line-height': '1.45',
|
||||
'min-height': '500px',
|
||||
'tab-size': '4',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Controllers;
|
||||
|
||||
/**
|
||||
* Standard CRUD over /api/v1/AssistantPrompt. Pattern matches AssistantRule.
|
||||
*/
|
||||
class AssistantPrompt extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Controllers;
|
||||
|
||||
/**
|
||||
* Standard CRUD over /api/v1/AssistantSkill. Pattern matches AssistantRule.
|
||||
*/
|
||||
class AssistantSkill extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Controllers;
|
||||
|
||||
/**
|
||||
* Standard CRUD over /api/v1/CaseMemory.
|
||||
*
|
||||
* Until SmartAssistant 2.10.0 the CaseMemory entity was created via
|
||||
* CaseMemoryService::createMemory and listed via Case relation panels —
|
||||
* never via the bare REST endpoint. Flipping `tab: true` exposes the
|
||||
* navbar list view, which uses GET /api/v1/CaseMemory and needs this
|
||||
* Controller class to exist (otherwise EspoCRM returns 404 "Controller
|
||||
* does not exist"). Same pattern as the AssistantRule fix shipped in 2.9.2.
|
||||
*/
|
||||
class CaseMemory extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Controllers;
|
||||
|
||||
/**
|
||||
* Standard CRUD over /api/v1/UserProfile.
|
||||
*
|
||||
* Pattern matches AssistantRule.php — without this class EspoCRM returns
|
||||
* 404 "Controller does not exist" even when the entity is fully defined.
|
||||
*/
|
||||
class UserProfile extends \Espo\Core\Controllers\Record
|
||||
{
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create AssistantPrompt": "צור פרומפט חדש"
|
||||
},
|
||||
"fields": {
|
||||
"name": "שם תיאורי",
|
||||
"key": "מפתח (key)",
|
||||
"description": "תיאור — מה הסעיף הזה עושה",
|
||||
"mode": "מצב",
|
||||
"displayOrder": "סדר הצגה",
|
||||
"content": "תוכן הפרומפט",
|
||||
"isActive": "פעיל",
|
||||
"createdAt": "נוצר",
|
||||
"modifiedAt": "עודכן"
|
||||
},
|
||||
"options": {
|
||||
"mode": {
|
||||
"both": "תיק + משרד",
|
||||
"case": "תיק בלבד",
|
||||
"office": "משרד בלבד"
|
||||
}
|
||||
},
|
||||
"tooltips": {
|
||||
"key": "המפתח שדרכו shira-hermes שולפת את הסעיף הזה. שמות מוכרים: tool_rules, legal_assistance, personality_case, personality_office, writing_style, other_rules_case, other_rules_office. שינוי שם המפתח של סעיף קיים יגרום לכך ש-fallback ל-default יחזור.",
|
||||
"mode": "אם 'both' — מופיע בכל סשן. 'case' — רק כשנפתח תיק. 'office' — רק במסך המשרד הכללי.",
|
||||
"displayOrder": "מספר נמוך = מופיע מוקדם יותר ב-prompt. ברירת מחדל 100.",
|
||||
"content": "הטקסט שיוזרק ל-system prompt. תמיכה ב-RTL אוטומטית. שורה ריקה מסמנת default — קוד ה-Python יחזור לערך המקורי.",
|
||||
"isActive": "כיבוי = הסעיף מתעלם, fallback ל-default מהקוד."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create AssistantSkill": "צור מיומנות חדשה"
|
||||
},
|
||||
"fields": {
|
||||
"name": "שם",
|
||||
"description": "תיאור קצר",
|
||||
"triggers": "מילות הפעלה",
|
||||
"content": "תוכן המיומנות (Markdown)",
|
||||
"version": "גרסה",
|
||||
"isActive": "פעיל",
|
||||
"createdAt": "נוצר",
|
||||
"modifiedAt": "עודכן"
|
||||
},
|
||||
"tooltips": {
|
||||
"name": "שם ייחודי (snake-case או kebab-case). למשל direct-access-report-flow.",
|
||||
"description": "משפט קצר: מתי כדאי לשירה להפעיל את המיומנות.",
|
||||
"triggers": "מילים/ביטויים שבהם המיומנות צריכה להופיע — מופרדים בפסיק.",
|
||||
"content": "ה-Markdown המלא של המיומנות. שירה תקרא אותו דרך read_skill כשהיא צריכה אותו."
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@
|
||||
"scopeNames": {
|
||||
"CaseMemory": "זיכרון תיק",
|
||||
"AssistantRule": "כלל התנהגות",
|
||||
"UserProfile": "פרופיל משתמש",
|
||||
"AssistantSkill": "מיומנות שירה",
|
||||
"AssistantPrompt": "פרומפט שירה",
|
||||
"SmartAssistant": "עוזר חכם"
|
||||
},
|
||||
"scopeNamesPlural": {
|
||||
"CaseMemory": "זיכרונות תיקים",
|
||||
"AssistantRule": "כללי התנהגות",
|
||||
"UserProfile": "פרופילי משתמשים",
|
||||
"AssistantSkill": "מיומנויות שירה",
|
||||
"AssistantPrompt": "פרומפטים של שירה",
|
||||
"SmartAssistant": "עוזר חכם"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"labels": {
|
||||
"Create UserProfile": "צור פרופיל משתמש"
|
||||
},
|
||||
"fields": {
|
||||
"name": "שם",
|
||||
"user": "משתמש",
|
||||
"content": "תוכן הפרופיל",
|
||||
"isActive": "פעיל",
|
||||
"createdAt": "נוצר",
|
||||
"modifiedAt": "עודכן",
|
||||
"createdBy": "נוצר על ידי",
|
||||
"modifiedBy": "עודכן על ידי"
|
||||
},
|
||||
"tooltips": {
|
||||
"content": "טקסט חופשי שמוזרק ל-prompt של שירה תחת '=== ABOUT THIS USER ===' לכל שיחה של המשתמש הזה. שמור עובדות מפתח: תפקיד, תחומי התמחות, העדפות תקשורת, נושאי טיק שמטופלים."
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"label": "Overview",
|
||||
"rows": [
|
||||
[{"name": "name"}, {"name": "key"}],
|
||||
[{"name": "mode"}, {"name": "displayOrder"}],
|
||||
[{"name": "isActive"}, false],
|
||||
[{"name": "description", "fullWidth": true}]
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Prompt",
|
||||
"rows": [
|
||||
[{"name": "content", "fullWidth": true}]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"name": "name", "link": true},
|
||||
{"name": "key"},
|
||||
{"name": "mode"},
|
||||
{"name": "displayOrder"},
|
||||
{"name": "isActive"},
|
||||
{"name": "modifiedAt"}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{
|
||||
"label": "Overview",
|
||||
"rows": [
|
||||
[{"name": "name"}, {"name": "version"}],
|
||||
[{"name": "description", "fullWidth": true}],
|
||||
[{"name": "triggers", "fullWidth": true}],
|
||||
[{"name": "isActive"}, false]
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Skill",
|
||||
"rows": [
|
||||
[{"name": "content", "fullWidth": true}]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{"name": "name", "link": true},
|
||||
{"name": "description"},
|
||||
{"name": "isActive"},
|
||||
{"name": "version"},
|
||||
{"name": "modifiedAt"}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"label": "Overview",
|
||||
"rows": [
|
||||
[{"name": "name"}, {"name": "user"}],
|
||||
[{"name": "isActive"}, false]
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Profile",
|
||||
"rows": [
|
||||
[{"name": "content", "fullWidth": true}]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{"name": "name", "link": true},
|
||||
{"name": "user", "link": true},
|
||||
{"name": "isActive"},
|
||||
{"name": "modifiedAt"}
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"shiraManagement": {
|
||||
"label": "ניהול שירה",
|
||||
"itemList": [
|
||||
{
|
||||
"url": "#AssistantPrompt",
|
||||
"label": "פרומפטים",
|
||||
"iconClass": "fas fa-file-code",
|
||||
"description": "שינוי הסעיפים שמוזרקים ל-system prompt של שירה. עריכה כאן מחליפה את ברירות-המחדל בקוד."
|
||||
},
|
||||
{
|
||||
"url": "#AssistantRule",
|
||||
"label": "כללי התנהגות",
|
||||
"iconClass": "fas fa-list-check",
|
||||
"description": "כללים גלובליים/לפי מצב שיופיעו תחת '=== BEHAVIORAL RULES (MUST FOLLOW) ===' של ה-system prompt."
|
||||
},
|
||||
{
|
||||
"url": "#AssistantSkill",
|
||||
"label": "מיומנויות (Skills)",
|
||||
"iconClass": "fas fa-magic",
|
||||
"description": "תהליכי עבודה מוכנים שהמודל יכול להפעיל. כל מיומנות פעילה מופיעה ב-context."
|
||||
},
|
||||
{
|
||||
"url": "#UserProfile",
|
||||
"label": "פרופילי משתמשים",
|
||||
"iconClass": "fas fa-user-cog",
|
||||
"description": "פרופיל אישי לכל עורך/ת דין. מופיע ב-system prompt תחת '=== ABOUT THIS USER ==='."
|
||||
},
|
||||
{
|
||||
"url": "#CaseMemory",
|
||||
"label": "זיכרון תיקים",
|
||||
"iconClass": "fas fa-brain",
|
||||
"description": "כל רשומות הזיכרון מכל התיקים. גישה ישירה לחיפוש/עריכה — לצד כניסה רגילה מתוך כל תיק."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"controller": "controllers/record",
|
||||
"color": "#d99848",
|
||||
"iconClass": "fas fa-file-code"
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"controller": "controllers/record",
|
||||
"color": "#aa72b3",
|
||||
"iconClass": "fas fa-magic"
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"controller": "controllers/record",
|
||||
"boolFilterList": ["onlyMy"],
|
||||
"filterList": [{"name": "active"}, {"name": "inactive"}],
|
||||
"color": "#7c8caf",
|
||||
"iconClass": "fas fa-user-cog"
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"maxLength": 200,
|
||||
"required": true,
|
||||
"trim": true
|
||||
},
|
||||
"key": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100,
|
||||
"required": true,
|
||||
"trim": true,
|
||||
"lowerCase": true
|
||||
},
|
||||
"description": {
|
||||
"type": "varchar",
|
||||
"maxLength": 500
|
||||
},
|
||||
"mode": {
|
||||
"type": "enum",
|
||||
"options": ["both", "case", "office"],
|
||||
"default": "both",
|
||||
"required": true,
|
||||
"style": {
|
||||
"both": "primary",
|
||||
"case": "info",
|
||||
"office": "warning"
|
||||
}
|
||||
},
|
||||
"displayOrder": {
|
||||
"type": "int",
|
||||
"default": 100,
|
||||
"min": 0
|
||||
},
|
||||
"content": {
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"view": "smart-assistant:views/fields/prompt-editor",
|
||||
"lengthOfMaxDisplay": 600
|
||||
},
|
||||
"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"}
|
||||
},
|
||||
"collection": {
|
||||
"orderBy": "displayOrder",
|
||||
"order": "asc",
|
||||
"textFilterFields": ["name", "key", "description", "content"]
|
||||
},
|
||||
"indexes": {
|
||||
"key": {"columns": ["key", "deleted"], "unique": true},
|
||||
"modeActive": {"columns": ["mode", "isActive", "deleted"]}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"maxLength": 100,
|
||||
"required": true,
|
||||
"trim": true
|
||||
},
|
||||
"description": {
|
||||
"type": "varchar",
|
||||
"maxLength": 500
|
||||
},
|
||||
"triggers": {
|
||||
"type": "text",
|
||||
"lengthOfMaxDisplay": 200
|
||||
},
|
||||
"content": {
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"view": "smart-assistant:views/fields/prompt-editor",
|
||||
"lengthOfMaxDisplay": 400
|
||||
},
|
||||
"version": {
|
||||
"type": "varchar",
|
||||
"maxLength": 20,
|
||||
"default": "1.0"
|
||||
},
|
||||
"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"}
|
||||
},
|
||||
"collection": {
|
||||
"orderBy": "name",
|
||||
"order": "asc",
|
||||
"textFilterFields": ["name", "description", "triggers", "content"]
|
||||
},
|
||||
"indexes": {
|
||||
"name": {"columns": ["name", "deleted"], "unique": true},
|
||||
"isActive": {"columns": ["isActive", "deleted"]}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"maxLength": 255,
|
||||
"required": true
|
||||
},
|
||||
"user": {
|
||||
"type": "link",
|
||||
"required": true
|
||||
},
|
||||
"content": {
|
||||
"type": "text",
|
||||
"view": "smart-assistant:views/fields/prompt-editor",
|
||||
"lengthOfMaxDisplay": 400
|
||||
},
|
||||
"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": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
},
|
||||
"modifiedBy": {
|
||||
"type": "belongsTo",
|
||||
"entity": "User"
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"orderBy": "modifiedAt",
|
||||
"order": "desc",
|
||||
"textFilterFields": ["name", "content"]
|
||||
},
|
||||
"indexes": {
|
||||
"userId": {
|
||||
"columns": ["userId", "deleted"],
|
||||
"unique": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"entity": true,
|
||||
"object": true,
|
||||
"module": "SmartAssistant",
|
||||
"acl": true,
|
||||
"aclActionList": ["create", "read", "edit", "delete"],
|
||||
"aclLevelList": ["all", "no"],
|
||||
"tab": true,
|
||||
"stream": false,
|
||||
"disabled": false,
|
||||
"importable": false,
|
||||
"customizable": true
|
||||
}
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"aclActionList": ["create", "read", "edit", "delete"],
|
||||
"aclLevelList": ["all", "team", "own", "no"],
|
||||
"stream": false,
|
||||
"tab": false,
|
||||
"tab": true,
|
||||
"disabled": false
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"entity": true,
|
||||
"object": true,
|
||||
"module": "SmartAssistant",
|
||||
"acl": true,
|
||||
"aclActionList": ["create", "read", "edit", "delete"],
|
||||
"aclLevelList": ["all", "no"],
|
||||
"tab": true,
|
||||
"stream": false,
|
||||
"disabled": false,
|
||||
"importable": false,
|
||||
"customizable": true
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"acl": true,
|
||||
"aclActionList": ["create", "read", "edit", "delete"],
|
||||
"aclLevelList": ["all", "team", "own", "no"],
|
||||
"tab": false,
|
||||
"tab": true,
|
||||
"stream": false,
|
||||
"disabled": false,
|
||||
"importable": false,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"entity": true,
|
||||
"object": true,
|
||||
"module": "SmartAssistant",
|
||||
"acl": true,
|
||||
"aclActionList": ["create", "read", "edit", "delete"],
|
||||
"aclLevelList": ["all", "own", "no"],
|
||||
"tab": true,
|
||||
"stream": false,
|
||||
"disabled": false,
|
||||
"importable": false,
|
||||
"customizable": true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Services;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
/**
|
||||
* Loads active prompt sections keyed by their `key` field, filtered by mode.
|
||||
*
|
||||
* Returned shape: `["tool_rules" => "...", "writing_style" => "...", ...]`.
|
||||
* The Python prompt_builder treats a missing key (or empty string) as
|
||||
* "use the in-code default" — so this provider is purely additive.
|
||||
*/
|
||||
class AssistantPromptContextProvider
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Log $log;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Log $log)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> key => content
|
||||
*/
|
||||
public function getPromptsForMode(string $mode): array
|
||||
{
|
||||
$entries = $this->entityManager->getRDBRepository('AssistantPrompt')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'deleted' => false,
|
||||
'mode' => ['both', $mode],
|
||||
])
|
||||
->order('displayOrder', 'ASC')
|
||||
->find();
|
||||
|
||||
$out = [];
|
||||
foreach ($entries as $entry) {
|
||||
$key = (string) $entry->get('key');
|
||||
$content = (string) ($entry->get('content') ?? '');
|
||||
if ($key === '' || $content === '') {
|
||||
continue;
|
||||
}
|
||||
$out[$key] = $content;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Services;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
/**
|
||||
* Lists active skills (metadata only — name/description/triggers) for the
|
||||
* system prompt's "=== SKILLS (learned workflows) ===" hint. The full body
|
||||
* is fetched on demand by shira-hermes' read_skill tool via REST.
|
||||
*/
|
||||
class AssistantSkillContextProvider
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Log $log;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Log $log)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, description: string, triggers: string}>
|
||||
*/
|
||||
public function listSkills(): array
|
||||
{
|
||||
$entries = $this->entityManager->getRDBRepository('AssistantSkill')
|
||||
->where(['isActive' => true, 'deleted' => false])
|
||||
->order('name', 'ASC')
|
||||
->find();
|
||||
|
||||
$skills = [];
|
||||
foreach ($entries as $entry) {
|
||||
$skills[] = [
|
||||
'name' => (string) $entry->get('name'),
|
||||
'description' => (string) ($entry->get('description') ?? ''),
|
||||
'triggers' => (string) ($entry->get('triggers') ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $skills;
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,18 @@ class SmartAssistantService
|
||||
$ruleProvider = $this->injectableFactory->create(AssistantRuleContextProvider::class);
|
||||
$context['assistantRules'] = $ruleProvider->getRulesForMode($mode === 'case' ? 'case' : 'office');
|
||||
|
||||
// Load editable prompt sections (DB overrides for the Python defaults).
|
||||
$promptProvider = $this->injectableFactory->create(AssistantPromptContextProvider::class);
|
||||
$context['assistantPrompts'] = $promptProvider->getPromptsForMode($mode === 'case' ? 'case' : 'office');
|
||||
|
||||
// Load per-user profile (replaces /opt/data/profiles/{uid}.md).
|
||||
$profileProvider = $this->injectableFactory->create(UserProfileContextProvider::class);
|
||||
$context['userProfile'] = $profileProvider->getProfileForUser($userId);
|
||||
|
||||
// Load active skills (metadata only; full body fetched via REST when needed).
|
||||
$skillProvider = $this->injectableFactory->create(AssistantSkillContextProvider::class);
|
||||
$context['availableSkills'] = $skillProvider->listSkills();
|
||||
|
||||
// Generate conversation ID
|
||||
if (!$conversationId) {
|
||||
$prefix = $mode === 'case' ? 'conv_' : 'oconv_';
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Services;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Log;
|
||||
|
||||
/**
|
||||
* Loads the active UserProfile row for a given user into Shira's context.
|
||||
*
|
||||
* Returns the markdown body that becomes the "=== ABOUT THIS USER ===" block
|
||||
* in the system prompt. Returns "" when no profile exists; the Python prompt
|
||||
* builder treats empty string as "skip this section".
|
||||
*/
|
||||
class UserProfileContextProvider
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Log $log;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Log $log)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->log = $log;
|
||||
}
|
||||
|
||||
public function getProfileForUser(string $userId): string
|
||||
{
|
||||
if (!$userId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getRDBRepository('UserProfile')
|
||||
->where([
|
||||
'userId' => $userId,
|
||||
'isActive' => true,
|
||||
'deleted' => false,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$entity) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (string) ($entity->get('content') ?? '');
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"module": "SmartAssistant",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||
"author": "klear",
|
||||
"version": "2.9.2",
|
||||
"version": "2.10.2",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* AfterInstall script for SmartAssistant extension 2.10.x.
|
||||
*
|
||||
* Idempotently seeds the AssistantPrompt entity with 7 default prompt
|
||||
* sections (tool_rules, writing_style, legal_assistance, personality_case,
|
||||
* personality_office, other_rules_case, other_rules_office). If a row with
|
||||
* the same `key` already exists it is left alone — admin edits win.
|
||||
*
|
||||
* Class must be named `AfterInstall` with NO namespace — that's what the
|
||||
* EspoCRM extension installer looks for in scripts/AfterInstall.php.
|
||||
* See application/Espo/Core/Upgrades/ExtensionManager.php scriptNames map.
|
||||
*/
|
||||
|
||||
use Espo\Core\Container;
|
||||
|
||||
class AfterInstall
|
||||
{
|
||||
public function run(Container $container, $params = null): void
|
||||
{
|
||||
$entityManager = $container->get('entityManager');
|
||||
$log = $container->get('log');
|
||||
|
||||
// The JSON file lives inside the installed module resources at runtime.
|
||||
$resourceFile = 'custom/Espo/Modules/SmartAssistant/Resources/data/default-prompts.json';
|
||||
|
||||
if (!is_file($resourceFile)) {
|
||||
$log->warning("[SmartAssistant] AfterInstall: $resourceFile not found, skipping seed");
|
||||
return;
|
||||
}
|
||||
|
||||
$json = file_get_contents($resourceFile);
|
||||
$defaults = json_decode($json, true);
|
||||
|
||||
if (!is_array($defaults)) {
|
||||
$log->warning("[SmartAssistant] AfterInstall: invalid JSON in $resourceFile");
|
||||
return;
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($defaults as $row) {
|
||||
$key = $row['key'] ?? null;
|
||||
if (!$key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = $entityManager->getRDBRepository('AssistantPrompt')
|
||||
->where(['key' => $key, 'deleted' => false])
|
||||
->findOne();
|
||||
|
||||
if ($existing) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$entity = $entityManager->getNewEntity('AssistantPrompt');
|
||||
$entity->set([
|
||||
'key' => $key,
|
||||
'name' => $row['name'] ?? $key,
|
||||
'description' => $row['description'] ?? null,
|
||||
'mode' => $row['mode'] ?? 'both',
|
||||
'displayOrder' => $row['displayOrder'] ?? 100,
|
||||
'content' => $row['content'] ?? '',
|
||||
'isActive' => true,
|
||||
]);
|
||||
// skipAll bypasses beforeSave hooks that require a logged-in user
|
||||
// service (installer runs without an HTTP session) and ACL checks
|
||||
// (we're seeding firm-wide defaults; no user is implicated).
|
||||
$entityManager->saveEntity($entity, ['skipAll' => true]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
$log->info("[SmartAssistant] AfterInstall: seeded $created AssistantPrompt rows, $skipped already existed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user