Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3212e71fe | |||
| f84b2f1cef |
@@ -9,18 +9,23 @@ 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\Modules\SmartAssistant\Services\GenericTemplateGenerator;
|
||||
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
|
||||
@@ -171,6 +176,24 @@ class SmartAssistant
|
||||
];
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -255,4 +278,27 @@ class SmartAssistant
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
public function postActionGenerateFromTemplate(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$templateId = $data->templateId ?? null;
|
||||
$caseId = $data->caseId ?? null;
|
||||
|
||||
if (empty($templateId)) {
|
||||
throw new BadRequest('templateId is required.');
|
||||
}
|
||||
if (empty($caseId)) {
|
||||
throw new BadRequest('caseId is required.');
|
||||
}
|
||||
|
||||
$documentSubject = $data->documentSubject ?? null;
|
||||
$customPlaceholders = (array) ($data->customPlaceholders ?? []);
|
||||
|
||||
$generator = $this->injectableFactory->create(GenericTemplateGenerator::class);
|
||||
|
||||
return $generator->generate($templateId, $caseId, $documentSubject, $customPlaceholders);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,5 +86,13 @@
|
||||
"controller": "SmartAssistant",
|
||||
"action": "saveMemory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"route": "/SmartAssistant/action/executeTool",
|
||||
"method": "post",
|
||||
"params": {
|
||||
"controller": "SmartAssistant",
|
||||
"action": "executeTool"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Services;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\NetworkStorageIntegration\Services\DocumentTemplateService;
|
||||
use Espo\Modules\NetworkStorageIntegration\Services\NetworkDocumentService;
|
||||
use PhpOffice\PhpWord\TemplateProcessor;
|
||||
|
||||
class GenericTemplateGenerator
|
||||
{
|
||||
private const TEMP_DIR = 'data/tmp/';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Log $log,
|
||||
private User $user
|
||||
) {}
|
||||
|
||||
public function generate(string $templateId, string $caseId, ?string $documentSubject = null, array $customPlaceholders = []): array
|
||||
{
|
||||
$template = $this->entityManager->getEntityById('DocumentTemplate', $templateId);
|
||||
if (!$template) {
|
||||
throw new NotFound("Template {$templateId} not found.");
|
||||
}
|
||||
|
||||
if (!$template->get('isActive')) {
|
||||
throw new Error("Template is not active.");
|
||||
}
|
||||
|
||||
$templatePath = $template->get('templatePath');
|
||||
if (!$templatePath) {
|
||||
throw new Error("Template path is not configured.");
|
||||
}
|
||||
|
||||
$case = $this->entityManager->getEntityById('Case', $caseId);
|
||||
if (!$case) {
|
||||
throw new NotFound("Case {$caseId} not found.");
|
||||
}
|
||||
|
||||
$templateName = $template->get('name') ?? 'document';
|
||||
|
||||
// Step 1: Download template from WebDAV
|
||||
$localTemplatePath = $this->downloadTemplate($templatePath);
|
||||
|
||||
try {
|
||||
// Step 2: Build placeholder data from entity fields
|
||||
$templateService = $this->injectableFactory->create(DocumentTemplateService::class);
|
||||
$placeholderData = $templateService->buildPlaceholderData($case);
|
||||
|
||||
// Step 3: Merge custom placeholders (override entity data)
|
||||
foreach ($customPlaceholders as $key => $value) {
|
||||
$placeholderData[$key] = $value;
|
||||
}
|
||||
|
||||
// Step 4: Process template
|
||||
$outputPath = self::TEMP_DIR . uniqid('gen_') . '.docx';
|
||||
if (!is_dir(self::TEMP_DIR)) {
|
||||
mkdir(self::TEMP_DIR, 0775, true);
|
||||
}
|
||||
|
||||
$this->processTemplate($localTemplatePath, $outputPath, $placeholderData);
|
||||
|
||||
// Step 5: Read generated content
|
||||
$content = file_get_contents($outputPath);
|
||||
@unlink($outputPath);
|
||||
@unlink($localTemplatePath);
|
||||
|
||||
// Step 6: Build document name
|
||||
$contactName = $this->getContactName($case);
|
||||
$docName = $documentSubject ?? "{$templateName} — {$contactName}";
|
||||
|
||||
// Step 7: Create Attachment + Document
|
||||
$attachment = $this->entityManager->getNewEntity('Attachment');
|
||||
$attachment->set([
|
||||
'name' => $docName . '.docx',
|
||||
'type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'role' => 'Attachment',
|
||||
'size' => strlen($content),
|
||||
'relatedType' => 'Document',
|
||||
]);
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
file_put_contents('data/upload/' . $attachment->getId(), $content);
|
||||
|
||||
$document = $this->entityManager->getNewEntity('Document');
|
||||
$document->set([
|
||||
'name' => $docName,
|
||||
'fileId' => $attachment->getId(),
|
||||
'fileName' => $docName . '.docx',
|
||||
'assignedUserId' => $this->user->getId(),
|
||||
]);
|
||||
$this->entityManager->saveEntity($document);
|
||||
|
||||
// Link to Case
|
||||
$this->entityManager->getRDBRepository('Case')
|
||||
->getRelation($case, 'documents')
|
||||
->relate($document);
|
||||
|
||||
$this->log->info("GenericTemplateGenerator: Created '{$docName}' from template '{$templateName}' for Case {$caseId}");
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "✅ מסמך \"{$docName}\" נוצר בהצלחה מתבנית \"{$templateName}\"\n📄 המסמך צורף לתיק.",
|
||||
'entityType' => 'Document',
|
||||
'entityId' => $document->getId(),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
if (isset($localTemplatePath) && file_exists($localTemplatePath)) {
|
||||
@unlink($localTemplatePath);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadTemplate(string $templatePath): string
|
||||
{
|
||||
// Try local paths first
|
||||
if (file_exists($templatePath)) {
|
||||
return $templatePath;
|
||||
}
|
||||
|
||||
$localPath = 'data/document-templates/' . ltrim($templatePath, '/');
|
||||
if (file_exists($localPath)) {
|
||||
return $localPath;
|
||||
}
|
||||
|
||||
// Download from NetworkStorage (WebDAV)
|
||||
try {
|
||||
$nds = $this->injectableFactory->create(NetworkDocumentService::class);
|
||||
$client = $nds->getClient();
|
||||
|
||||
$integration = $this->entityManager->getEntityById('Integration', 'NetworkStorage');
|
||||
$templatesFolderPath = 'Templates';
|
||||
if ($integration && $integration->get('enabled')) {
|
||||
$data = (array) ($integration->get('data') ?? []);
|
||||
$templatesFolderPath = $data['templatesFolderPath'] ?? 'Templates';
|
||||
}
|
||||
|
||||
$storagePath = $templatesFolderPath . '/' . $templatePath;
|
||||
|
||||
if (!$client->exists($storagePath)) {
|
||||
throw new Error("Template file not found on storage: {$storagePath}");
|
||||
}
|
||||
|
||||
$content = $client->downloadFile($storagePath);
|
||||
|
||||
if (!is_dir(self::TEMP_DIR)) {
|
||||
mkdir(self::TEMP_DIR, 0775, true);
|
||||
}
|
||||
$tmpPath = self::TEMP_DIR . 'tpl_' . uniqid() . '.docx';
|
||||
file_put_contents($tmpPath, $content);
|
||||
|
||||
return $tmpPath;
|
||||
} catch (Error $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
throw new Error("Failed to download template: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function processTemplate(string $inputPath, string $outputPath, array $data): void
|
||||
{
|
||||
if (!class_exists(TemplateProcessor::class, false)) {
|
||||
$autoloader = 'vendor/phpoffice/phpword/src/PhpWord/Autoloader.php';
|
||||
if (file_exists($autoloader)) {
|
||||
require_once $autoloader;
|
||||
\PhpOffice\PhpWord\Autoloader::register();
|
||||
}
|
||||
}
|
||||
|
||||
$processor = new TemplateProcessor($inputPath);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value === null) {
|
||||
$value = '';
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$value = implode(', ', $value);
|
||||
} elseif (is_bool($value)) {
|
||||
$value = $value ? 'כן' : 'לא';
|
||||
}
|
||||
$processor->setValue($key, (string) $value);
|
||||
}
|
||||
|
||||
$processor->saveAs($outputPath);
|
||||
}
|
||||
|
||||
private function getContactName($case): string
|
||||
{
|
||||
$contactId = $case->get('contactId');
|
||||
if (!$contactId) {
|
||||
$ids = $case->getLinkMultipleIdList('contacts');
|
||||
if (!empty($ids)) {
|
||||
$contactId = $ids[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ($contactId) {
|
||||
$contact = $this->entityManager->getEntityById('Contact', $contactId);
|
||||
if ($contact) {
|
||||
return trim($contact->get('firstName') . ' ' . $contact->get('lastName'));
|
||||
}
|
||||
}
|
||||
|
||||
return $case->get('name') ?? '';
|
||||
}
|
||||
}
|
||||
+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.6.0",
|
||||
"version": "2.7.0",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user