feat: initial SmartAssistant module — unified AI assistant with case memory
Merges CrmAssistant + OfficeAssistant into a single module with: - Floating chat (FAB) with auto office/case mode detection - CaseMemory entity for persistent per-case knowledge - save_memory AI tool for proactive memory saving - Auto-extraction hooks (status changes, hearings) - AlertCalculator for office-level alerts - Full conversation persistence via ConversationRepository - RTL/Hebrew support throughout (fa_IR) 47 files, 0 dependencies on old modules.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Espo\Modules\SmartAssistant\Controllers;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Modules\SmartAssistant\Services\SmartAssistantService;
|
||||
use Espo\Modules\SmartAssistant\Services\CaseMemoryService;
|
||||
|
||||
class SmartAssistant
|
||||
{
|
||||
private InjectableFactory $injectableFactory;
|
||||
private Acl $acl;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory, Acl $acl)
|
||||
{
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
$this->acl = $acl;
|
||||
}
|
||||
|
||||
private function getService(): SmartAssistantService
|
||||
{
|
||||
return $this->injectableFactory->create(SmartAssistantService::class);
|
||||
}
|
||||
|
||||
private function checkAccess(): void
|
||||
{
|
||||
if (!$this->acl->checkScope('Case', 'read')) {
|
||||
throw new Forbidden('No access to Case.');
|
||||
}
|
||||
}
|
||||
|
||||
public function postActionChat(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->message)) {
|
||||
throw new BadRequest('message is required.');
|
||||
}
|
||||
|
||||
$mode = $data->mode ?? 'office';
|
||||
$caseId = $data->caseId ?? null;
|
||||
|
||||
if ($mode === 'case' && empty($caseId)) {
|
||||
throw new BadRequest('caseId is required for case mode.');
|
||||
}
|
||||
|
||||
return $this->getService()->chat(
|
||||
trim($data->message),
|
||||
$mode,
|
||||
$caseId,
|
||||
$data->conversationId ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public function postActionExecute(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->conversationId) || empty($data->actionId)) {
|
||||
throw new BadRequest('conversationId and actionId are required.');
|
||||
}
|
||||
|
||||
return $this->getService()->executeAction(
|
||||
$data->conversationId,
|
||||
$data->actionId,
|
||||
(bool) ($data->approved ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
public function getActionSummary(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
return $this->getService()->getSummary();
|
||||
}
|
||||
|
||||
public function getActionAlerts(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
return $this->getService()->getAlerts();
|
||||
}
|
||||
|
||||
public function getActionHistory(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$caseId = $request->getQueryParam('caseId');
|
||||
return $this->getService()->getHistory($caseId);
|
||||
}
|
||||
|
||||
public function getActionConversations(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$type = $request->getQueryParam('type');
|
||||
return $this->getService()->getConversations($type);
|
||||
}
|
||||
|
||||
public function getActionConversationMessages(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$key = $request->getQueryParam('conversationKey');
|
||||
|
||||
if (empty($key)) {
|
||||
throw new BadRequest('conversationKey is required.');
|
||||
}
|
||||
|
||||
return $this->getService()->getConversationMessages($key);
|
||||
}
|
||||
|
||||
public function getActionSearchHistory(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$query = $request->getQueryParam('query');
|
||||
|
||||
if (empty($query)) {
|
||||
throw new BadRequest('query is required.');
|
||||
}
|
||||
|
||||
return $this->getService()->searchHistory($query);
|
||||
}
|
||||
|
||||
public function getActionCaseMemory(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$caseId = $request->getQueryParam('caseId');
|
||||
|
||||
if (empty($caseId)) {
|
||||
throw new BadRequest('caseId is required.');
|
||||
}
|
||||
|
||||
$category = $request->getQueryParam('category');
|
||||
$includeArchived = (bool) $request->getQueryParam('includeArchived');
|
||||
|
||||
$service = $this->injectableFactory->create(CaseMemoryService::class);
|
||||
return $service->getMemoriesByCase($caseId, $category, $includeArchived);
|
||||
}
|
||||
|
||||
public function postActionSaveMemory(Request $request, Response $response): array
|
||||
{
|
||||
$this->checkAccess();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->caseId) || empty($data->content)) {
|
||||
throw new BadRequest('caseId and content are required.');
|
||||
}
|
||||
|
||||
$service = $this->injectableFactory->create(CaseMemoryService::class);
|
||||
$entry = $service->createMemory(
|
||||
$data->caseId,
|
||||
$data->category ?? 'key_facts',
|
||||
$data->name ?? mb_substr($data->content, 0, 100),
|
||||
$data->content,
|
||||
$data->importance ?? 'normal',
|
||||
'manual'
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'id' => $entry->get('id'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user