Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f84b2f1cef | |||
| 2a8046b737 | |||
| f319f981f9 | |||
| bf98495198 |
@@ -9,17 +9,22 @@ use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Modules\SmartAssistant\Services\SmartAssistantService;
|
||||
use Espo\Modules\SmartAssistant\Services\ActionExecutor;
|
||||
use Espo\Modules\SmartAssistant\Services\CaseMemoryService;
|
||||
use Espo\Modules\SmartAssistant\Services\DocumentAnalyzer;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class SmartAssistant
|
||||
{
|
||||
private InjectableFactory $injectableFactory;
|
||||
private Acl $acl;
|
||||
private User $user;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory, Acl $acl)
|
||||
public function __construct(InjectableFactory $injectableFactory, Acl $acl, User $user)
|
||||
{
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->acl = $acl;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
private function getService(): SmartAssistantService
|
||||
@@ -169,4 +174,107 @@ class SmartAssistant
|
||||
'id' => $entry->get('id'),
|
||||
];
|
||||
}
|
||||
|
||||
public function postActionExecuteTool(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->tool)) {
|
||||
throw new BadRequest('tool is required.');
|
||||
}
|
||||
|
||||
$tool = $data->tool;
|
||||
$params = (array) ($data->params ?? []);
|
||||
$caseId = $data->caseId ?? null;
|
||||
|
||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||
|
||||
return $executor->execute($tool, $params, $caseId, $this->user->getId());
|
||||
}
|
||||
|
||||
public function postActionReadDocument(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$filePath = $data->filePath ?? null;
|
||||
if (empty($filePath)) {
|
||||
throw new BadRequest('filePath is required.');
|
||||
}
|
||||
|
||||
$maxLength = isset($data->maxLength) ? (int) $data->maxLength : null;
|
||||
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$text = $analyzer->extractFullText($filePath, $maxLength);
|
||||
|
||||
return [
|
||||
'success' => $text !== null,
|
||||
'text' => $text,
|
||||
'fileName' => basename($filePath),
|
||||
'charCount' => $text !== null ? mb_strlen($text) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function postActionReadMultipleDocuments(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$filePaths = $data->filePaths ?? [];
|
||||
if (empty($filePaths)) {
|
||||
throw new BadRequest('filePaths is required (array of file paths).');
|
||||
}
|
||||
|
||||
$maxPerFile = isset($data->maxCharsPerFile) ? (int) $data->maxCharsPerFile : 50000;
|
||||
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$results = $analyzer->extractMultipleDocuments((array) $filePaths, $maxPerFile);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'documents' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
public function postActionBatchRename(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$renames = $data->renames ?? [];
|
||||
if (empty($renames)) {
|
||||
throw new BadRequest('renames is required (array of {sourcePath, newName}).');
|
||||
}
|
||||
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$results = [];
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($renames as $rename) {
|
||||
$rename = (array) $rename;
|
||||
$sourcePath = $rename['sourcePath'] ?? null;
|
||||
$newName = $rename['newName'] ?? null;
|
||||
|
||||
if (!$sourcePath || !$newName) {
|
||||
$results[] = ['success' => false, 'error' => 'Missing sourcePath or newName'];
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $analyzer->renameDocument($sourcePath, $newName);
|
||||
$results[] = ['success' => true, 'oldName' => $result['oldName'], 'newName' => $result['newName']];
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$results[] = ['success' => false, 'oldName' => basename($sourcePath), 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'totalCount' => count($renames),
|
||||
'successCount' => $successCount,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -27,6 +27,18 @@
|
||||
"upcomingHearingDays": {
|
||||
"type": "int",
|
||||
"default": 7
|
||||
},
|
||||
"maxDocumentChars": {
|
||||
"type": "int",
|
||||
"default": 100000
|
||||
},
|
||||
"agenticLoopEnabled": {
|
||||
"type": "bool",
|
||||
"default": true
|
||||
},
|
||||
"maxAgenticLoops": {
|
||||
"type": "int",
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"allowUserAccess": false,
|
||||
|
||||
@@ -86,5 +86,13 @@
|
||||
"controller": "SmartAssistant",
|
||||
"action": "saveMemory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "/SmartAssistant/action/executeTool",
|
||||
"method": "post",
|
||||
"params": {
|
||||
"controller": "SmartAssistant",
|
||||
"action": "executeTool"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,7 +49,10 @@ class ActionExecutor
|
||||
'schedule_hearing' => $this->scheduleHearing($params, $caseId, $userId),
|
||||
'list_documents' => $this->listDocuments($caseId),
|
||||
'analyze_document' => $this->analyzeDocument($params, $caseId),
|
||||
'read_document' => $this->readDocument($params, $caseId),
|
||||
'read_multiple_documents' => $this->readMultipleDocuments($params, $caseId),
|
||||
'rename_document' => $this->renameDocument($params),
|
||||
'batch_rename_documents' => $this->batchRenameDocuments($params),
|
||||
'upload_document' => $this->uploadDocument($params, $caseId),
|
||||
'generate_document' => $this->generateDocument($params, $caseId),
|
||||
'merge_documents' => $this->mergeDocuments($params, $caseId),
|
||||
@@ -383,6 +386,65 @@ class ActionExecutor
|
||||
return ['success' => true, 'message' => implode("\n", $lines), 'data' => ['filePath' => $filePath, 'hasTextContent' => $text !== null]];
|
||||
}
|
||||
|
||||
private function readDocument(array $params, ?string $caseId): array
|
||||
{
|
||||
$filePath = $params['filePath'] ?? null;
|
||||
if (!$filePath) throw new Error('filePath parameter is required.');
|
||||
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$text = $analyzer->extractFullText($filePath);
|
||||
$fileName = basename($filePath);
|
||||
|
||||
if ($text === null) {
|
||||
return ['success' => false, 'message' => "לא ניתן לחלץ טקסט מ-\"{$fileName}\""];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $text,
|
||||
'data' => [
|
||||
'filePath' => $filePath,
|
||||
'fileName' => $fileName,
|
||||
'charCount' => mb_strlen($text),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function readMultipleDocuments(array $params, ?string $caseId): array
|
||||
{
|
||||
$filePaths = $params['filePaths'] ?? [];
|
||||
if (empty($filePaths)) throw new Error('filePaths parameter is required (array of file paths).');
|
||||
|
||||
$maxPerFile = $params['maxCharsPerFile'] ?? 50000;
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$results = $analyzer->extractMultipleDocuments($filePaths, $maxPerFile);
|
||||
|
||||
$lines = [];
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($results as $r) {
|
||||
$lines[] = "=== {$r['fileName']} ===";
|
||||
if (!empty($r['error'])) {
|
||||
$lines[] = "שגיאה: {$r['error']}";
|
||||
} elseif ($r['text'] === null) {
|
||||
$lines[] = "לא ניתן לחלץ טקסט";
|
||||
} else {
|
||||
$lines[] = $r['text'];
|
||||
$successCount++;
|
||||
}
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => implode("\n", $lines),
|
||||
'data' => [
|
||||
'totalFiles' => count($filePaths),
|
||||
'successCount' => $successCount,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function renameDocument(array $params): array
|
||||
{
|
||||
$sourcePath = $params['sourcePath'] ?? null;
|
||||
@@ -395,6 +457,37 @@ class ActionExecutor
|
||||
return ['success' => true, 'message' => "המסמך שונה מ-\"{$result['oldName']}\" ל-\"{$result['newName']}\"", 'data' => $result];
|
||||
}
|
||||
|
||||
private function batchRenameDocuments(array $params): array
|
||||
{
|
||||
$renames = $params['renames'] ?? [];
|
||||
if (empty($renames)) throw new Error('renames parameter is required (array of {sourcePath, newName}).');
|
||||
|
||||
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
||||
$results = [];
|
||||
$successCount = 0;
|
||||
|
||||
foreach ($renames as $rename) {
|
||||
$sourcePath = $rename['sourcePath'] ?? null;
|
||||
$newName = $rename['newName'] ?? null;
|
||||
|
||||
if (!$sourcePath || !$newName) {
|
||||
$results[] = "שגיאה: חסר sourcePath או newName";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $analyzer->renameDocument($sourcePath, $newName);
|
||||
$results[] = "\"{$result['oldName']}\" → \"{$result['newName']}\"";
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$results[] = "שגיאה בשינוי שם \"" . basename($sourcePath) . "\": " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$msg = "שינוי שמות: {$successCount}/" . count($renames) . " הצליחו\n" . implode("\n", $results);
|
||||
return ['success' => true, 'message' => $msg];
|
||||
}
|
||||
|
||||
private function uploadDocument(array $params, ?string $caseId): array
|
||||
{
|
||||
if (!$caseId) {
|
||||
|
||||
@@ -27,7 +27,6 @@ class CaseContextBuilder
|
||||
return [
|
||||
'case' => $this->getCaseData($case),
|
||||
'contacts' => $this->getCaseContacts($case),
|
||||
'legalAid' => $this->getLegalAidData($caseId),
|
||||
'openTasks' => $this->getOpenTasks($caseId),
|
||||
'upcomingMeetings' => $this->getUpcomingMeetings($caseId),
|
||||
'recentCalls' => $this->getRecentCalls($caseId),
|
||||
@@ -81,38 +80,6 @@ class CaseContextBuilder
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
private function getLegalAidData(string $caseId): ?array
|
||||
{
|
||||
$legalAid = $this->entityManager
|
||||
->getRDBRepository('LegalAid')
|
||||
->where(['caseId' => $caseId])
|
||||
->findOne();
|
||||
|
||||
if (!$legalAid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $legalAid->get('id'),
|
||||
'status' => $legalAid->get('status'),
|
||||
'legalAidNumber' => $legalAid->get('legalAidNumber'),
|
||||
'contactName' => $legalAid->get('contactName'),
|
||||
'aidType' => $legalAid->get('aidType'),
|
||||
'urgencyLevel' => $legalAid->get('urgencyLevel'),
|
||||
'appointmentDate' => $legalAid->get('appointmentDate'),
|
||||
'filingDeadline' => $legalAid->get('filingDeadline'),
|
||||
'firstContactDate' => $legalAid->get('firstContactDate'),
|
||||
'meetingDate' => $legalAid->get('meetingDate'),
|
||||
'contactStatus' => $legalAid->get('contactStatus'),
|
||||
'proceedingNumber' => $legalAid->get('proceedingNumber'),
|
||||
'courtName' => $legalAid->get('courtName'),
|
||||
'proceedingSubject' => $legalAid->get('proceedingSubject'),
|
||||
'recommendationType' => $legalAid->get('recommendationType'),
|
||||
'additionalAidNeeded' => $legalAid->get('additionalAidNeeded'),
|
||||
'notes' => $legalAid->get('notes'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getOpenTasks(string $caseId): array
|
||||
{
|
||||
$tasks = [];
|
||||
|
||||
@@ -11,13 +11,14 @@ use Espo\Modules\NetworkStorageIntegration\Classes\StorageClientInterface;
|
||||
|
||||
class DocumentAnalyzer
|
||||
{
|
||||
private const MAX_TEXT_LENGTH = 2000;
|
||||
private const DEFAULT_MAX_TEXT_LENGTH = 100000;
|
||||
private const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
private const TMP_DIR = 'data/tmp/assistant-docs';
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private InjectableFactory $injectableFactory;
|
||||
private Log $log;
|
||||
private ?int $maxTextLength = null;
|
||||
|
||||
private const CATEGORY_PATTERNS = [
|
||||
'כתבי_טענות' => ['כתב_תביעה', 'כתב תביעה', 'כתב_הגנה', 'כתב הגנה', 'סיכומים', 'תביעה', 'הגנה', 'כתב_ערעור', 'כתב ערעור', 'בקשה', 'תגובה', 'תשובה'],
|
||||
@@ -73,6 +74,13 @@ class DocumentAnalyzer
|
||||
}
|
||||
|
||||
public function extractTextContent(string $filePath): ?string
|
||||
{
|
||||
$maxLen = $this->getMaxTextLength();
|
||||
$text = $this->extractFullText($filePath, $maxLen);
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function extractFullText(string $filePath, ?int $maxLength = null): ?string
|
||||
{
|
||||
$client = $this->getStorageClient();
|
||||
$filename = basename($filePath);
|
||||
@@ -98,8 +106,41 @@ class DocumentAnalyzer
|
||||
|
||||
if (file_exists($tmpFile)) unlink($tmpFile);
|
||||
if ($text === null) return null;
|
||||
if (mb_strlen($text) > self::MAX_TEXT_LENGTH) $text = mb_substr($text, 0, self::MAX_TEXT_LENGTH) . '...';
|
||||
return trim($text);
|
||||
|
||||
$text = trim($text);
|
||||
if ($maxLength !== null && mb_strlen($text) > $maxLength) {
|
||||
$text = mb_substr($text, 0, $maxLength) . '...';
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function extractMultipleDocuments(array $filePaths, int $maxPerFile = 50000): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($filePaths as $filePath) {
|
||||
$fileName = basename($filePath);
|
||||
try {
|
||||
$text = $this->extractFullText($filePath, $maxPerFile);
|
||||
$results[] = [
|
||||
'path' => $filePath,
|
||||
'fileName' => $fileName,
|
||||
'text' => $text,
|
||||
'charCount' => $text !== null ? mb_strlen($text) : 0,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results[] = [
|
||||
'path' => $filePath,
|
||||
'fileName' => $fileName,
|
||||
'text' => null,
|
||||
'charCount' => 0,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function renameDocument(string $sourcePath, string $newName): array
|
||||
@@ -169,6 +210,27 @@ class DocumentAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private function getMaxTextLength(): int
|
||||
{
|
||||
if ($this->maxTextLength !== null) {
|
||||
return $this->maxTextLength;
|
||||
}
|
||||
|
||||
try {
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = (array) ($integration->get('data') ?? []);
|
||||
$this->maxTextLength = (int) ($data['maxDocumentChars'] ?? self::DEFAULT_MAX_TEXT_LENGTH);
|
||||
return $this->maxTextLength;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
$this->maxTextLength = self::DEFAULT_MAX_TEXT_LENGTH;
|
||||
return $this->maxTextLength;
|
||||
}
|
||||
|
||||
private function getStorageClient(): StorageClientInterface
|
||||
{
|
||||
return $this->getNetworkDocumentService()->getClient();
|
||||
|
||||
@@ -18,7 +18,17 @@ class SmartAssistantService
|
||||
public const NOTE_TYPE_ACTION = 'SmartAction';
|
||||
|
||||
// All tools execute inline — Shira has full permissions
|
||||
private const TOOLS_REQUIRING_CASE = ['change_status', 'schedule_hearing', 'list_documents', 'analyze_document', 'upload_document', 'generate_document', 'merge_documents', 'save_memory'];
|
||||
private const TOOLS_REQUIRING_CASE = [
|
||||
'change_status', 'schedule_hearing', 'list_documents', 'analyze_document',
|
||||
'read_document', 'read_multiple_documents', 'upload_document',
|
||||
'generate_document', 'merge_documents', 'save_memory',
|
||||
'batch_rename_documents',
|
||||
];
|
||||
|
||||
// Tools whose output is fed back to the AI for a follow-up webhook call
|
||||
private const AGENTIC_TOOLS = ['analyze_document', 'read_document', 'read_multiple_documents', 'list_documents'];
|
||||
|
||||
private const DEFAULT_MAX_AGENTIC_LOOPS = 3;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private InjectableFactory $injectableFactory;
|
||||
@@ -105,68 +115,20 @@ class SmartAssistantService
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Call webhook
|
||||
$response = $this->callWebhook($message, $context, $conversationId, $conversationHistory, $mode);
|
||||
|
||||
// Process actions — execute all inline (Shira has full permissions)
|
||||
$actions = $response['actions'] ?? [];
|
||||
$responseText = $response['text'] ?? '';
|
||||
$inlineResults = [];
|
||||
$executedActions = [];
|
||||
|
||||
// Resolve effective caseId: explicit case mode, or drill-down detection in office mode
|
||||
$effectiveCaseId = $caseId;
|
||||
if (!$effectiveCaseId && !empty($context['drillDown']['case']['id'])) {
|
||||
$effectiveCaseId = $context['drillDown']['case']['id'];
|
||||
}
|
||||
|
||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||
// Execute tools with agentic loop — read tools feed results back to AI
|
||||
$loopResult = $this->executeActionsWithLoop(
|
||||
$message, $context, $conversationId, $conversationHistory,
|
||||
$mode, $effectiveCaseId, $userId
|
||||
);
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$tool = $action['tool'] ?? '';
|
||||
$actionCaseId = $effectiveCaseId;
|
||||
|
||||
// Resolve case from action params if not available
|
||||
if (!$actionCaseId && !empty($action['params']['caseId'])) {
|
||||
$actionCaseId = $action['params']['caseId'];
|
||||
}
|
||||
|
||||
// Skip tools that require a case if no case context available
|
||||
if (!$actionCaseId && in_array($tool, self::TOOLS_REQUIRING_CASE, true)) {
|
||||
$inlineResults[] = "⚠️ לא ניתן לבצע {$tool} — לא זוהה תיק. נסה מתוך מסך התיק.";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $executor->execute($tool, $action['params'] ?? [], $actionCaseId, $userId);
|
||||
if (!empty($result['message'])) {
|
||||
$inlineResults[] = $result['message'];
|
||||
}
|
||||
$executedActions[] = [
|
||||
'tool' => $tool,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
];
|
||||
|
||||
// Create stream note for executed action in case context
|
||||
if ($actionCaseId) {
|
||||
$this->createStreamNote(self::NOTE_TYPE_ACTION, $result['message'] ?? 'פעולה בוצעה', $actionCaseId, [
|
||||
'conversationId' => $conversationId,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning("SmartAssistant: Execution of {$tool} failed: " . $e->getMessage());
|
||||
$inlineResults[] = "שגיאה בביצוע {$tool}: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($inlineResults)) {
|
||||
$responseText .= "\n\n" . implode("\n\n", $inlineResults);
|
||||
}
|
||||
$responseText = $loopResult['responseText'];
|
||||
$executedActions = $loopResult['executedActions'];
|
||||
|
||||
// Persist response
|
||||
try {
|
||||
@@ -191,6 +153,148 @@ class SmartAssistantService
|
||||
];
|
||||
}
|
||||
|
||||
private function executeActionsWithLoop(
|
||||
string $message,
|
||||
array $context,
|
||||
string $conversationId,
|
||||
array $conversationHistory,
|
||||
string $mode,
|
||||
?string $effectiveCaseId,
|
||||
string $userId
|
||||
): array {
|
||||
$agenticEnabled = $this->getIntegrationSetting('agenticLoopEnabled', true);
|
||||
$maxLoops = (int) $this->getIntegrationSetting('maxAgenticLoops', self::DEFAULT_MAX_AGENTIC_LOOPS);
|
||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||
|
||||
$loopCount = 0;
|
||||
$allExecutedActions = [];
|
||||
$currentHistory = $conversationHistory;
|
||||
$finalResponseText = '';
|
||||
|
||||
while ($loopCount <= $maxLoops) {
|
||||
// Call webhook (first call uses original message, subsequent calls use empty message)
|
||||
$currentMessage = $loopCount === 0 ? $message : '';
|
||||
$response = $this->callWebhook($currentMessage, $context, $conversationId, $currentHistory, $mode);
|
||||
|
||||
$actions = $response['actions'] ?? [];
|
||||
$responseText = $response['text'] ?? '';
|
||||
|
||||
// If no actions, we're done
|
||||
if (empty($actions)) {
|
||||
$finalResponseText = $responseText;
|
||||
break;
|
||||
}
|
||||
|
||||
$agenticResults = [];
|
||||
$inlineResults = [];
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$tool = $action['tool'] ?? '';
|
||||
$actionCaseId = $effectiveCaseId;
|
||||
|
||||
if (!$actionCaseId && !empty($action['params']['caseId'])) {
|
||||
$actionCaseId = $action['params']['caseId'];
|
||||
}
|
||||
|
||||
if (!$actionCaseId && in_array($tool, self::TOOLS_REQUIRING_CASE, true)) {
|
||||
$inlineResults[] = "⚠️ לא ניתן לבצע {$tool} — לא זוהה תיק. נסה מתוך מסך התיק.";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $executor->execute($tool, $action['params'] ?? [], $actionCaseId, $userId);
|
||||
|
||||
$allExecutedActions[] = [
|
||||
'tool' => $tool,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
];
|
||||
|
||||
if ($actionCaseId) {
|
||||
$this->createStreamNote(self::NOTE_TYPE_ACTION, $result['message'] ?? 'פעולה בוצעה', $actionCaseId, [
|
||||
'conversationId' => $conversationId,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
// Separate agentic tools (need re-invocation) from inline tools
|
||||
if ($agenticEnabled && in_array($tool, self::AGENTIC_TOOLS, true)) {
|
||||
$agenticResults[] = [
|
||||
'tool' => $tool,
|
||||
'params' => $action['params'] ?? [],
|
||||
'result' => $result['message'] ?? '',
|
||||
];
|
||||
} else {
|
||||
if (!empty($result['message'])) {
|
||||
$inlineResults[] = $result['message'];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning("SmartAssistant: Execution of {$tool} failed: " . $e->getMessage());
|
||||
$inlineResults[] = "שגיאה בביצוע {$tool}: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// If no agentic results, we're done — return response with inline results
|
||||
if (empty($agenticResults)) {
|
||||
$finalResponseText = $responseText;
|
||||
if (!empty($inlineResults)) {
|
||||
$finalResponseText .= "\n\n" . implode("\n\n", $inlineResults);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Feed agentic results back to AI for next iteration
|
||||
$this->log->debug("SmartAssistant: Agentic loop iteration " . ($loopCount + 1) . " — " . count($agenticResults) . " tool results to feed back");
|
||||
|
||||
// Add the AI response to history
|
||||
$currentHistory[] = ['role' => 'assistant', 'content' => $responseText];
|
||||
|
||||
// Add each tool result to history
|
||||
foreach ($agenticResults as $ar) {
|
||||
$currentHistory[] = [
|
||||
'role' => 'tool_result',
|
||||
'toolName' => $ar['tool'],
|
||||
'content' => $ar['result'],
|
||||
];
|
||||
}
|
||||
|
||||
// Also append any inline results
|
||||
if (!empty($inlineResults)) {
|
||||
$currentHistory[] = [
|
||||
'role' => 'tool_result',
|
||||
'toolName' => '_inline_actions',
|
||||
'content' => implode("\n\n", $inlineResults),
|
||||
];
|
||||
}
|
||||
|
||||
$loopCount++;
|
||||
}
|
||||
|
||||
// Safety: if we exhausted the loop, use the last response
|
||||
if ($loopCount > $maxLoops && empty($finalResponseText)) {
|
||||
$finalResponseText = $response['text'] ?? 'הגעתי למגבלת הסיבובים. אנא נסה שוב.';
|
||||
}
|
||||
|
||||
return [
|
||||
'responseText' => $finalResponseText,
|
||||
'executedActions' => $allExecutedActions,
|
||||
];
|
||||
}
|
||||
|
||||
private function getIntegrationSetting(string $key, $default = null)
|
||||
{
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'SmartAssistant');
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = (array) ($integration->get('data') ?? []);
|
||||
return $data[$key] ?? $default;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function executeAction(string $conversationId, string $actionId, bool $approved): array
|
||||
{
|
||||
$conversation = $this->loadConversation($conversationId);
|
||||
|
||||
+2
-2
@@ -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.5.3",
|
||||
"version": "2.6.1",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"releaseDate": "2026-04-06",
|
||||
"releaseDate": "2026-04-09",
|
||||
"php": [
|
||||
">=8.1"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user