feat: add plugin tool handler support via metadata lookup

ActionExecutor now checks app.smartAssistant.tools.{name}.handler
metadata for unknown tools before throwing. This allows extensions
to register their own tool handlers without modifying SmartAssistant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 05:08:39 +00:00
parent 87bc4b6770
commit ca04dd72f8
@@ -7,18 +7,21 @@ use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory; use Espo\Core\InjectableFactory;
use Espo\ORM\EntityManager; use Espo\ORM\EntityManager;
use Espo\Core\Utils\Log; use Espo\Core\Utils\Log;
use Espo\Core\Utils\Metadata;
class ActionExecutor class ActionExecutor
{ {
private EntityManager $entityManager; private EntityManager $entityManager;
private InjectableFactory $injectableFactory; private InjectableFactory $injectableFactory;
private Log $log; private Log $log;
private Metadata $metadata;
public function __construct(EntityManager $entityManager, InjectableFactory $injectableFactory, Log $log) public function __construct(EntityManager $entityManager, InjectableFactory $injectableFactory, Log $log, Metadata $metadata)
{ {
$this->entityManager = $entityManager; $this->entityManager = $entityManager;
$this->injectableFactory = $injectableFactory; $this->injectableFactory = $injectableFactory;
$this->log = $log; $this->log = $log;
$this->metadata = $metadata;
} }
public const VALID_STATUSES = [ public const VALID_STATUSES = [
@@ -46,7 +49,7 @@ class ActionExecutor
'generate_document' => $this->generateDocument($params, $caseId), 'generate_document' => $this->generateDocument($params, $caseId),
'merge_documents' => $this->mergeDocuments($params, $caseId), 'merge_documents' => $this->mergeDocuments($params, $caseId),
'save_memory' => $this->saveMemory($params, $caseId, $userId), 'save_memory' => $this->saveMemory($params, $caseId, $userId),
default => throw new Error("Unknown tool: {$tool}"), default => $this->executePluginTool($tool, $params, $caseId, $userId),
}; };
} }
@@ -353,4 +356,23 @@ class ActionExecutor
if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB'; if ($bytes >= 1024) return round($bytes / 1024, 1) . ' KB';
return $bytes . ' B'; 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);
}
} }