2a8046b737
Enable the AI assistant to read full documents from network storage (PDF/DOCX/TXT), summarize multiple documents, and batch-rename files. Key changes: - Raise text extraction limit from 2,000 to 100,000 chars (configurable) - Add agentic loop in SmartAssistantService for multi-turn tool execution - New tools: read_document, read_multiple_documents, batch_rename_documents - New API endpoints: readDocument, readMultipleDocuments, batchRename - New integration settings: maxDocumentChars, agenticLoopEnabled, maxAgenticLoops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
618 lines
24 KiB
PHP
618 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Espo\Modules\SmartAssistant\Services;
|
|
|
|
use Espo\Core\Exceptions\Error;
|
|
use Espo\Core\Exceptions\NotFound;
|
|
use Espo\Core\InjectableFactory;
|
|
use Espo\ORM\EntityManager;
|
|
use Espo\Core\Utils\Log;
|
|
use Espo\Core\Utils\Metadata;
|
|
|
|
class ActionExecutor
|
|
{
|
|
private EntityManager $entityManager;
|
|
private InjectableFactory $injectableFactory;
|
|
private Log $log;
|
|
private Metadata $metadata;
|
|
|
|
public function __construct(EntityManager $entityManager, InjectableFactory $injectableFactory, Log $log, Metadata $metadata)
|
|
{
|
|
$this->entityManager = $entityManager;
|
|
$this->injectableFactory = $injectableFactory;
|
|
$this->log = $log;
|
|
$this->metadata = $metadata;
|
|
}
|
|
|
|
public const VALID_STATUSES = [
|
|
'New' => 'חדש', 'Assigned' => 'בטיפול', 'Pending' => 'ממתין',
|
|
'PendingHearing' => 'ממתין לדיון', 'PendingDecision' => 'ממתין להחלטה',
|
|
'PendingResponse' => 'ממתין לתשובה', 'Negotiation' => 'משא ומתן',
|
|
'Suspended' => 'מושהה', 'ClosedSettlement' => 'סגור - הסכם',
|
|
'ClosedJudgment' => 'סגור - פס״ד', 'Closed' => 'סגור', 'Rejected' => 'נדחה',
|
|
];
|
|
|
|
public function execute(string $tool, array $params, ?string $caseId, string $userId): array
|
|
{
|
|
$this->log->debug("SmartAssistant: Executing tool={$tool} for case={$caseId}");
|
|
|
|
return match ($tool) {
|
|
'create_task' => $this->createTask($params, $caseId, $userId),
|
|
'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),
|
|
'delete_meeting' => $this->deleteMeeting($params),
|
|
'delete_call' => $this->deleteCall($params),
|
|
'delete_task' => $this->deleteTask($params),
|
|
'delete_note' => $this->deleteNote($params),
|
|
'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),
|
|
'save_memory' => $this->saveMemory($params, $caseId, $userId),
|
|
'save_rule' => $this->saveRule($params, $userId),
|
|
default => $this->executePluginTool($tool, $params, $caseId, $userId),
|
|
};
|
|
}
|
|
|
|
private function saveMemory(array $params, ?string $caseId, string $userId): array
|
|
{
|
|
if (!$caseId) {
|
|
throw new Error('save_memory requires a case context.');
|
|
}
|
|
|
|
$service = $this->injectableFactory->create(CaseMemoryService::class);
|
|
$content = $params['content'] ?? '';
|
|
$name = $params['name'] ?? mb_substr($content, 0, 100);
|
|
|
|
$entry = $service->createMemory(
|
|
$caseId,
|
|
$params['category'] ?? 'key_facts',
|
|
$name,
|
|
$content,
|
|
$params['importance'] ?? 'normal',
|
|
'assistant'
|
|
);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "נשמר בזיכרון התיק: \"{$name}\"",
|
|
'entityType' => 'CaseMemory',
|
|
'entityId' => $entry->get('id'),
|
|
];
|
|
}
|
|
|
|
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 = [
|
|
'name' => $params['name'] ?? 'משימה חדשה',
|
|
'status' => 'Not Started',
|
|
'priority' => $params['priority'] ?? 'Normal',
|
|
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
|
'description' => $params['description'] ?? null,
|
|
'assignedUserId' => $params['assignedUserId'] ?? $userId,
|
|
];
|
|
|
|
if ($caseId) {
|
|
$taskData['parentType'] = 'Case';
|
|
$taskData['parentId'] = $caseId;
|
|
}
|
|
|
|
$task = $this->entityManager->getNewEntity('Task');
|
|
$task->set($taskData);
|
|
$this->entityManager->saveEntity($task);
|
|
|
|
$msg = "✅ משימה \"{$params['name']}\" נוצרה בהצלחה";
|
|
if ($caseId) {
|
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
|
if ($case) {
|
|
$msg .= " בתיק " . $case->get('name');
|
|
}
|
|
}
|
|
|
|
return ['success' => true, 'message' => $msg, 'entityType' => 'Task', 'entityId' => $task->get('id')];
|
|
}
|
|
|
|
private function addNote(array $params, ?string $caseId, string $userId): array
|
|
{
|
|
$noteData = ['type' => 'Post', 'post' => $params['post'] ?? '', 'createdById' => $userId];
|
|
|
|
if ($caseId) {
|
|
$noteData['parentType'] = 'Case';
|
|
$noteData['parentId'] = $caseId;
|
|
}
|
|
|
|
$note = $this->entityManager->getNewEntity('Note');
|
|
$note->set($noteData);
|
|
$this->entityManager->saveEntity($note);
|
|
return ['success' => true, 'message' => '✅ הערה נוספה' . ($caseId ? ' לתיק' : ''), 'entityType' => 'Note', 'entityId' => $note->get('id')];
|
|
}
|
|
|
|
private function changeStatus(array $params, ?string $caseId): array
|
|
{
|
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
|
if (!$case) throw new NotFound("Case {$caseId} not found.");
|
|
|
|
$oldStatus = $case->get('status');
|
|
$newStatus = $params['status'] ?? null;
|
|
if (!$newStatus) throw new Error('status parameter is required.');
|
|
if (!isset(self::VALID_STATUSES[$newStatus])) {
|
|
throw new Error("Invalid status '{$newStatus}'. Valid: " . implode(', ', array_keys(self::VALID_STATUSES)));
|
|
}
|
|
|
|
$case->set('status', $newStatus);
|
|
$case->set('cLegalStatusManual', true);
|
|
$this->entityManager->saveEntity($case);
|
|
|
|
return ['success' => true, 'message' => "סטטוס תיק שונה מ-\"" . (self::VALID_STATUSES[$oldStatus] ?? $oldStatus) . "\" ל-\"" . self::VALID_STATUSES[$newStatus] . "\"", 'entityType' => 'Case', 'entityId' => $caseId];
|
|
}
|
|
|
|
private function createMeeting(array $params, ?string $caseId, string $userId): array
|
|
{
|
|
$meetingData = [
|
|
'name' => $params['name'] ?? 'פגישה חדשה', 'status' => 'Planned',
|
|
'dateStart' => $this->normalizeDateTime($params['dateStart'] ?? null),
|
|
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
|
'description' => $params['description'] ?? null,
|
|
'assignedUserId' => $userId,
|
|
];
|
|
|
|
if ($caseId) {
|
|
$meetingData['parentType'] = 'Case';
|
|
$meetingData['parentId'] = $caseId;
|
|
}
|
|
|
|
$meeting = $this->entityManager->getNewEntity('Meeting');
|
|
$meeting->set($meetingData);
|
|
$this->entityManager->saveEntity($meeting);
|
|
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 deleteTask(array $params): array
|
|
{
|
|
$entityId = $params['entityId'] ?? null;
|
|
if (!$entityId) {
|
|
throw new Error('entityId parameter is required.');
|
|
}
|
|
|
|
$entity = $this->entityManager->getEntityById('Task', $entityId);
|
|
if (!$entity) {
|
|
throw new NotFound("Task {$entityId} not found.");
|
|
}
|
|
|
|
$name = $entity->get('name') ?? '';
|
|
$this->entityManager->removeEntity($entity);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "✅ משימה \"{$name}\" נמחקה בהצלחה",
|
|
'entityType' => 'Task',
|
|
'entityId' => $entityId,
|
|
];
|
|
}
|
|
|
|
private function scheduleHearing(array $params, ?string $caseId, string $userId): array
|
|
{
|
|
$case = $this->entityManager->getEntityById('Case', $caseId);
|
|
if (!$case) throw new NotFound("Case {$caseId} not found.");
|
|
|
|
$dateTime = $this->normalizeDateTime($params['dateTime'] ?? null);
|
|
if ($dateTime) {
|
|
$case->set('cNextHearing', $dateTime);
|
|
$this->entityManager->saveEntity($case);
|
|
}
|
|
|
|
$meetingName = $params['description'] ?? 'דיון בבית המשפט';
|
|
if (!empty($params['court'])) $meetingName .= ' - ' . $params['court'];
|
|
$dateEnd = $dateTime ? date('Y-m-d H:i:s', strtotime($dateTime) + 3600) : null;
|
|
|
|
$meeting = $this->entityManager->getNewEntity('Meeting');
|
|
$meeting->set([
|
|
'name' => $meetingName, 'status' => 'Planned',
|
|
'dateStart' => $dateTime, 'dateEnd' => $dateEnd,
|
|
'description' => $params['description'] ?? null,
|
|
'parentType' => 'Case', 'parentId' => $caseId, 'assignedUserId' => $userId,
|
|
]);
|
|
$this->entityManager->saveEntity($meeting);
|
|
return ['success' => true, 'message' => "דיון נקבע ל-{$dateTime}", 'entityType' => 'Meeting', 'entityId' => $meeting->get('id')];
|
|
}
|
|
|
|
private function listDocuments(?string $caseId): array
|
|
{
|
|
if (!$caseId) return ['success' => true, 'message' => 'לא ניתן להציג מסמכים ללא תיק'];
|
|
|
|
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
|
$result = $analyzer->listDocumentsRecursive($caseId);
|
|
|
|
if ($result['totalFiles'] === 0) return ['success' => true, 'message' => 'לא נמצאו מסמכים בתיק'];
|
|
|
|
$lines = ["נמצאו {$result['totalFiles']} מסמכים בתיק:", ''];
|
|
foreach ($result['folders'] as $folder) {
|
|
$lines[] = "📁 {$folder['name']} (" . count($folder['files']) . " קבצים)";
|
|
foreach ($folder['files'] as $file) {
|
|
$lines[] = " - {$file['name']} (" . $this->formatFileSize($file['size']) . ")";
|
|
}
|
|
}
|
|
if (!empty($result['rootFiles'])) {
|
|
$lines[] = '';
|
|
$lines[] = "📄 קבצים בתיקייה הראשית:";
|
|
foreach ($result['rootFiles'] as $file) {
|
|
$lines[] = " - {$file['name']} (" . $this->formatFileSize($file['size']) . ")";
|
|
}
|
|
}
|
|
|
|
return ['success' => true, 'message' => implode("\n", $lines), 'data' => $result];
|
|
}
|
|
|
|
private function analyzeDocument(array $params, ?string $caseId): array
|
|
{
|
|
$filePath = $params['filePath'] ?? null;
|
|
if (!$filePath) throw new Error('filePath parameter is required.');
|
|
|
|
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
|
$lines = ["ניתוח מסמך: " . basename($filePath)];
|
|
$text = $analyzer->extractTextContent($filePath);
|
|
|
|
if ($text) {
|
|
$lines = array_merge($lines, ['', 'תוכן מתוך המסמך:', '---', $text, '---', '', 'ניתן להשתמש בכלי rename_document כדי לשנות את שם הקובץ.']);
|
|
} else {
|
|
$lines[] = 'לא ניתן לחלץ טקסט מהמסמך';
|
|
}
|
|
|
|
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;
|
|
$newName = $params['newName'] ?? null;
|
|
if (!$sourcePath) throw new Error('sourcePath parameter is required.');
|
|
if (!$newName) throw new Error('newName parameter is required.');
|
|
|
|
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
|
$result = $analyzer->renameDocument($sourcePath, $newName);
|
|
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) {
|
|
throw new Error('upload_document requires a case context.');
|
|
}
|
|
|
|
$fileName = $params['fileName'] ?? null;
|
|
$content = $params['content'] ?? null;
|
|
if (!$fileName) throw new Error('fileName parameter is required.');
|
|
if (!$content) throw new Error('content parameter is required (base64-encoded file data).');
|
|
|
|
$decoded = base64_decode($content, true);
|
|
if ($decoded === false) {
|
|
throw new Error('content is not valid base64.');
|
|
}
|
|
|
|
$analyzer = $this->injectableFactory->create(DocumentAnalyzer::class);
|
|
$result = $analyzer->uploadDocument($caseId, $fileName, $decoded, $params['folder'] ?? null);
|
|
|
|
$msg = "✅ מסמך \"{$fileName}\" הועלה בהצלחה";
|
|
if (!empty($params['folder'])) {
|
|
$msg .= " לתיקייה \"{$params['folder']}\"";
|
|
}
|
|
|
|
return ['success' => true, 'message' => $msg, 'data' => $result];
|
|
}
|
|
|
|
private function generateDocument(array $params, ?string $caseId): array
|
|
{
|
|
if (!$caseId) {
|
|
throw new Error('generate_document requires a case context.');
|
|
}
|
|
|
|
$templateId = $params['templateId'] ?? null;
|
|
if (!$templateId) throw new Error('templateId parameter is required.');
|
|
|
|
$template = $this->entityManager->getEntityById('DocumentTemplate', $templateId);
|
|
if (!$template) throw new NotFound("Template {$templateId} not found.");
|
|
|
|
$documentSubject = $params['documentSubject'] ?? null;
|
|
|
|
$templateService = $this->injectableFactory->create(
|
|
\Espo\Modules\NetworkStorageIntegration\Services\DocumentTemplateService::class
|
|
);
|
|
|
|
$result = $templateService->createFromTemplate($templateId, 'Case', $caseId, $documentSubject);
|
|
|
|
if (empty($result['success'])) {
|
|
throw new Error('Failed to generate document from template.');
|
|
}
|
|
|
|
$templateName = $template->get('name') ?? 'document';
|
|
$fileName = $result['fileName'] ?? "{$templateName}.docx";
|
|
|
|
$msg = "✅ מסמך \"{$fileName}\" נוצר בהצלחה מתבנית \"{$templateName}\"";
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => $msg,
|
|
'data' => ['fileName' => $fileName],
|
|
];
|
|
}
|
|
|
|
private function mergeDocuments(array $params, ?string $caseId): array
|
|
{
|
|
if (!$caseId) {
|
|
throw new Error('merge_documents requires a case context.');
|
|
}
|
|
|
|
$documentIds = $params['documentIds'] ?? [];
|
|
if (empty($documentIds)) throw new Error('documentIds parameter is required (array of document IDs).');
|
|
|
|
$documents = [];
|
|
foreach ($documentIds as $docId) {
|
|
$doc = $this->entityManager->getEntityById('Document', $docId);
|
|
if (!$doc) throw new NotFound("Document {$docId} not found.");
|
|
$documents[] = $doc;
|
|
}
|
|
|
|
$mergerService = $this->injectableFactory->create(
|
|
\Espo\Modules\NetworkStorageIntegration\Services\DocumentMergerService::class
|
|
);
|
|
|
|
$options = $params['options'] ?? [];
|
|
$mergedPath = $mergerService->mergeWithLabels($documents, $options);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => "✅ " . count($documents) . " מסמכים מוזגו בהצלחה",
|
|
'data' => ['path' => $mergedPath, 'documentCount' => count($documents)],
|
|
];
|
|
}
|
|
|
|
private function normalizeDateTime(?string $value): ?string
|
|
{
|
|
if ($value === null || $value === '') return null;
|
|
$ts = strtotime($value);
|
|
return $ts === false ? null : date('Y-m-d H:i:s', $ts);
|
|
}
|
|
|
|
private function formatFileSize(?int $bytes): string
|
|
{
|
|
if ($bytes === null) return '?';
|
|
if ($bytes >= 1048576) return round($bytes / 1048576, 1) . ' MB';
|
|
if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB';
|
|
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);
|
|
}
|
|
}
|