From: Marcel Klehr Date: Fri, 14 Jul 2023 13:59:50 +0000 (+0200) Subject: Massive refactoring: Turn LanguageModel OCP API into TextProcessing API X-Git-Tag: v28.0.0beta1~664^2~9 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=ffe27ce14ca74b509c8721c9fba7c759498fa471;p=nextcloud-server.git Massive refactoring: Turn LanguageModel OCP API into TextProcessing API Signed-off-by: Marcel Klehr --- diff --git a/core/Controller/LanguageModelApiController.php b/core/Controller/LanguageModelApiController.php deleted file mode 100644 index 74ed26e604a..00000000000 --- a/core/Controller/LanguageModelApiController.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OC\Core\Controller; - -use InvalidArgumentException; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataResponse; -use OCP\Common\Exception\NotFoundException; -use OCP\IL10N; -use OCP\IRequest; -use OCP\LanguageModel\AbstractLanguageModelTask; -use OCP\LanguageModel\ILanguageModelManager; -use OCP\PreConditionNotMetException; - -class LanguageModelApiController extends \OCP\AppFramework\OCSController { - public function __construct( - string $appName, - IRequest $request, - private ILanguageModelManager $languageModelManager, - private IL10N $l, - private ?string $userId, - ) { - parent::__construct($appName, $request); - } - - /** - * This endpoint returns all available LanguageModel task types - * - * @PublicPage - * @return DataResponse - * - * 200: Task types returned - */ - public function taskTypes(): DataResponse { - return new DataResponse([ - 'types' => $this->languageModelManager->getAvailableTaskTypes(), - ]); - } - - /** - * This endpoint allows scheduling a language model task - * - * @PublicPage - * @UserRateThrottle(limit=20, period=120) - * @AnonRateThrottle(limit=5, period=120) - * @param string $input The input for the language model task - * @param string $type The task type - * @param string $appId The originating app ID - * @param string $identifier An identifier to identify this task - * @return DataResponse|DataResponse - * - * 200: Task scheduled - * 400: Task type does not exist - * 412: Task type not available - */ - public function schedule(string $input, string $type, string $appId, string $identifier = ''): DataResponse { - try { - $task = AbstractLanguageModelTask::factory($type, $input, $this->userId, $appId, $identifier); - } catch (InvalidArgumentException $e) { - return new DataResponse(['message' => $this->l->t('Requested task type does not exist')], Http::STATUS_BAD_REQUEST); - } - try { - $this->languageModelManager->scheduleTask($task); - - /** @var array{id: int|null, type: string, status: int, userId: string|null, appId: string, input: string, output: string|null, identifier: string} $json */ - $json = $task->jsonSerialize(); - - return new DataResponse([ - 'task' => $json, - ]); - } catch (PreConditionNotMetException) { - return new DataResponse(['message' => $this->l->t('Necessary language model provider is not available')], Http::STATUS_PRECONDITION_FAILED); - } - } - - /** - * This endpoint allows checking the status and results of a task. - * Tasks are removed 1 week after receiving their last update. - * - * @PublicPage - * @param int $id The id of the task - * @return DataResponse|DataResponse - * - * 200: Task returned - * 404: Task not found - * 500: Internal error - */ - public function getTask(int $id): DataResponse { - try { - $task = $this->languageModelManager->getTask($id); - - if ($this->userId !== $task->getUserId()) { - return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); - } - - /** @var array{id: int|null, type: string, status: int, userId: string|null, appId: string, input: string, output: string|null, identifier: string} $json */ - $json = $task->jsonSerialize(); - - return new DataResponse([ - 'task' => $json, - ]); - } catch (NotFoundException $e) { - return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); - } catch (\RuntimeException $e) { - return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } -} diff --git a/core/Controller/TextProcessingApiController.php b/core/Controller/TextProcessingApiController.php new file mode 100644 index 00000000000..7cc7199dfbd --- /dev/null +++ b/core/Controller/TextProcessingApiController.php @@ -0,0 +1,155 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OC\Core\Controller; + +use InvalidArgumentException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\Common\Exception\NotFoundException; +use OCP\IL10N; +use OCP\IRequest; +use OCP\TextProcessing\ITaskType; +use OCP\TextProcessing\Task; +use OCP\TextProcessing\IManager; +use OCP\PreConditionNotMetException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; + +class TextProcessingApiController extends \OCP\AppFramework\OCSController { + public function __construct( + string $appName, + IRequest $request, + private IManager $languageModelManager, + private IL10N $l, + private ?string $userId, + private ContainerInterface $container, + private LoggerInterface $logger, + ) { + parent::__construct($appName, $request); + } + + /** + * This endpoint returns all available LanguageModel task types + * + * @PublicPage + * @return DataResponse}, array{}> + * + * 200: Task types returned + */ + public function taskTypes(): DataResponse { + $typeClasses = $this->languageModelManager->getAvailableTaskTypes(); + /** @var list $types */ + $types = []; + foreach ($typeClasses as $typeClass) { + /** @var ITaskType $object */ + try { + $object = $this->container->get($typeClass); + } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { + $this->logger->warning('Could not find ' . $typeClass, ['exception' => $e]); + continue; + } + $types[] = [ + 'id' => $typeClass, + 'name' => $object->getName(), + 'description' => $object->getDescription(), + ]; + } + + return new DataResponse([ + 'types' => $types, + ]); + } + + /** + * This endpoint allows scheduling a language model task + * + * @PublicPage + * @UserRateThrottle(limit=20, period=120) + * @AnonRateThrottle(limit=5, period=120) + * @param string $input The input for the language model task + * @param string $type The task type + * @param string $appId The originating app ID + * @param string $identifier An identifier to identify this task + * @return DataResponse|DataResponse + * + * 200: Task scheduled + * 400: Task type does not exist + * 412: Task type not available + */ + public function schedule(string $input, string $type, string $appId, string $identifier = ''): DataResponse { + try { + $task = Task::factory($type, $input, $this->userId, $appId, $identifier); + } catch (InvalidArgumentException) { + return new DataResponse(['message' => $this->l->t('Requested task type does not exist')], Http::STATUS_BAD_REQUEST); + } + try { + $this->languageModelManager->scheduleTask($task); + + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l->t('Necessary language model provider is not available')], Http::STATUS_PRECONDITION_FAILED); + } + } + + /** + * This endpoint allows checking the status and results of a task. + * Tasks are removed 1 week after receiving their last update. + * + * @PublicPage + * @param int $id The id of the task + * @return DataResponse|DataResponse + * + * 200: Task returned + * 404: Task not found + * 500: Internal error + */ + public function getTask(int $id): DataResponse { + try { + $task = $this->languageModelManager->getTask($id); + + if ($this->userId !== $task->getUserId()) { + return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + } + + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (NotFoundException $e) { + return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + } catch (\RuntimeException $e) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/core/routes.php b/core/routes.php index 24af95cd7af..4790f32af32 100644 --- a/core/routes.php +++ b/core/routes.php @@ -146,9 +146,9 @@ $application->registerRoutes($this, [ ['root' => '/translation', 'name' => 'TranslationApi#languages', 'url' => '/languages', 'verb' => 'GET'], ['root' => '/translation', 'name' => 'TranslationApi#translate', 'url' => '/translate', 'verb' => 'POST'], - ['root' => '/languagemodel', 'name' => 'LanguageModelApi#taskTypes', 'url' => '/tasktypes', 'verb' => 'GET'], - ['root' => '/languagemodel', 'name' => 'LanguageModelApi#schedule', 'url' => '/schedule', 'verb' => 'POST'], - ['root' => '/languagemodel', 'name' => 'LanguageModelApi#getTask', 'url' => '/task/{id}', 'verb' => 'GET'], + ['root' => '/textprocessing', 'name' => 'TextProcessingApi#taskTypes', 'url' => '/tasktypes', 'verb' => 'GET'], + ['root' => '/textprocessing', 'name' => 'TextProcessingApi#schedule', 'url' => '/schedule', 'verb' => 'POST'], + ['root' => '/textprocessing', 'name' => 'TextProcessingApi#getTask', 'url' => '/task/{id}', 'verb' => 'GET'], ], ]); diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 67e8b390c15..5aea2a7a744 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -33,7 +33,7 @@ use Closure; use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Room\IBackend as IRoomBackend; use OCP\Collaboration\Reference\IReferenceProvider; -use OCP\LanguageModel\ILanguageModelProvider; +use OCP\TextProcessing\IProvider as ITextProcessingProvider; use OCP\SpeechToText\ISpeechToTextProvider; use OCP\Talk\ITalkBackend; use OCP\Translation\ITranslationProvider; @@ -116,8 +116,8 @@ class RegistrationContext { /** @var ServiceRegistration[] */ private $speechToTextProviders = []; - /** @var ServiceRegistration[] */ - private $languageModelProviders = []; + /** @var ServiceRegistration[] */ + private $textProcessingProviders = []; /** @var ServiceRegistration[] */ private $templateProviders = []; @@ -266,8 +266,8 @@ class RegistrationContext { $providerClass ); } - public function registerLanguageModelProvider(string $providerClass): void { - $this->context->registerLanguageModelProvider( + public function registerTextProcessingProvider(string $providerClass): void { + $this->context->registerTextProcessingProvider( $this->appId, $providerClass ); @@ -439,8 +439,8 @@ class RegistrationContext { $this->speechToTextProviders[] = new ServiceRegistration($appId, $class); } - public function registerLanguageModelProvider(string $appId, string $class): void { - $this->languageModelProviders[] = new ServiceRegistration($appId, $class); + public function registerTextProcessingProvider(string $appId, string $class): void { + $this->textProcessingProviders[] = new ServiceRegistration($appId, $class); } public function registerTemplateProvider(string $appId, string $class): void { @@ -722,10 +722,10 @@ class RegistrationContext { } /** - * @return ServiceRegistration[] + * @return ServiceRegistration[] */ - public function getLanguageModelProviders(): array { - return $this->languageModelProviders; + public function getTextProcessingProviders(): array { + return $this->textProcessingProviders; } /** diff --git a/lib/private/LanguageModel/Db/Task.php b/lib/private/LanguageModel/Db/Task.php deleted file mode 100644 index 4e46f19e8a9..00000000000 --- a/lib/private/LanguageModel/Db/Task.php +++ /dev/null @@ -1,113 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OC\LanguageModel\Db; - -use OCP\AppFramework\Db\Entity; -use OCP\LanguageModel\AbstractLanguageModelTask; -use OCP\LanguageModel\ILanguageModelTask; - -/** - * @method setType(string $type) - * @method string getType() - * @method setLastUpdated(int $lastUpdated) - * @method int getLastUpdated() - * @method setInput(string $type) - * @method string getInput() - * @method setOutput(string $type) - * @method string getOutput() - * @method setStatus(int $type) - * @method int getStatus() - * @method setUserId(string $type) - * @method string getuserId() - * @method setAppId(string $type) - * @method string getAppId() - * @method setIdentifier(string $type) - * @method string getIdentifier() - */ -class Task extends Entity { - protected $lastUpdated; - protected $type; - protected $input; - protected $output; - protected $status; - protected $userId; - protected $appId; - protected $identifier; - - /** - * @var string[] - */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier']; - - /** - * @var string[] - */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier']; - - - public function __construct() { - // add types in constructor - $this->addType('id', 'integer'); - $this->addType('lastUpdated', 'integer'); - $this->addType('type', 'string'); - $this->addType('input', 'string'); - $this->addType('output', 'string'); - $this->addType('status', 'integer'); - $this->addType('userId', 'string'); - $this->addType('appId', 'string'); - $this->addType('identifier', 'string'); - } - - public function toRow(): array { - return array_combine(self::$columns, array_map(function ($field) { - return $this->{'get'.ucfirst($field)}(); - }, self::$fields)); - } - - public static function fromLanguageModelTask(ILanguageModelTask $task): Task { - /** @var Task $task */ - $task = Task::fromParams([ - 'id' => $task->getId(), - 'type' => $task->getType(), - 'lastUpdated' => time(), - 'status' => $task->getStatus(), - 'input' => $task->getInput(), - 'output' => $task->getOutput(), - 'userId' => $task->getUserId(), - 'appId' => $task->getAppId(), - 'identifier' => $task->getIdentifier(), - ]); - return $task; - } - - public function toLanguageModelTask(): ILanguageModelTask { - $task = AbstractLanguageModelTask::factory($this->getType(), $this->getInput(), $this->getuserId(), $this->getAppId(), $this->getIdentifier()); - $task->setId($this->getId()); - $task->setStatus($this->getStatus()); - $task->setOutput($this->getOutput()); - return $task; - } -} diff --git a/lib/private/LanguageModel/Db/TaskMapper.php b/lib/private/LanguageModel/Db/TaskMapper.php deleted file mode 100644 index 9b93ea1990f..00000000000 --- a/lib/private/LanguageModel/Db/TaskMapper.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OC\LanguageModel\Db; - -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\Entity; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\AppFramework\Db\QBMapper; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\DB\Exception; -use OCP\IDBConnection; - -/** - * @extends QBMapper - */ -class TaskMapper extends QBMapper { - public function __construct( - IDBConnection $db, - private ITimeFactory $timeFactory, - ) { - parent::__construct($db, 'llm_tasks', Task::class); - } - - /** - * @param int $id - * @return Task - * @throws Exception - * @throws DoesNotExistException - * @throws MultipleObjectsReturnedException - */ - public function find(int $id): Task { - $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) - ->from($this->tableName) - ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); - return $this->findEntity($qb); - } - - /** - * @param int $timeout - * @return int the number of deleted tasks - * @throws Exception - */ - public function deleteOlderThan(int $timeout): int { - $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) - ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter(time() - $timeout))); - return $qb->executeStatement(); - } - - public function update(Entity $entity): Entity { - $entity->setLastUpdated($this->timeFactory->now()->getTimestamp()); - return parent::update($entity); - } -} diff --git a/lib/private/LanguageModel/LanguageModelManager.php b/lib/private/LanguageModel/LanguageModelManager.php deleted file mode 100644 index 970d968c883..00000000000 --- a/lib/private/LanguageModel/LanguageModelManager.php +++ /dev/null @@ -1,210 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OC\LanguageModel; - -use OC\AppFramework\Bootstrap\Coordinator; -use OC\LanguageModel\Db\Task; -use OC\LanguageModel\Db\TaskMapper; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Db\MultipleObjectsReturnedException; -use OCP\BackgroundJob\IJobList; -use OCP\Common\Exception\NotFoundException; -use OCP\DB\Exception; -use OCP\IServerContainer; -use OCP\LanguageModel\FreePromptTask; -use OCP\LanguageModel\HeadlineTask; -use OCP\LanguageModel\IHeadlineProvider; -use OCP\LanguageModel\ILanguageModelManager; -use OCP\LanguageModel\ILanguageModelProvider; -use OCP\LanguageModel\ILanguageModelTask; -use OCP\LanguageModel\ISummaryProvider; -use OCP\LanguageModel\ITopicsProvider; -use OCP\LanguageModel\SummaryTask; -use OCP\LanguageModel\TopicsTask; -use OCP\PreConditionNotMetException; -use Psr\Log\LoggerInterface; -use RuntimeException; -use Throwable; - -class LanguageModelManager implements ILanguageModelManager { - /** @var ?ILanguageModelProvider[] */ - private ?array $providers = null; - - public function __construct( - private IServerContainer $serverContainer, - private Coordinator $coordinator, - private LoggerInterface $logger, - private IJobList $jobList, - private TaskMapper $taskMapper, - ) { - } - - public function getProviders(): array { - $context = $this->coordinator->getRegistrationContext(); - if ($context === null) { - return []; - } - - if ($this->providers !== null) { - return $this->providers; - } - - $this->providers = []; - - foreach ($context->getLanguageModelProviders() as $providerServiceRegistration) { - $class = $providerServiceRegistration->getService(); - try { - $this->providers[$class] = $this->serverContainer->get($class); - } catch (Throwable $e) { - $this->logger->error('Failed to load LanguageModel provider ' . $class, [ - 'exception' => $e, - ]); - } - } - - return $this->providers; - } - - public function hasProviders(): bool { - $context = $this->coordinator->getRegistrationContext(); - if ($context === null) { - return false; - } - return count($context->getLanguageModelProviders()) > 0; - } - - /** - * @inheritDoc - */ - public function getAvailableTaskClasses(): array { - $tasks = []; - foreach ($this->getProviders() as $provider) { - $tasks[FreePromptTask::class] = true; - if ($provider instanceof ISummaryProvider) { - $tasks[SummaryTask::class] = true; - } - if ($provider instanceof IHeadlineProvider) { - $tasks[HeadlineTask::class] = true; - } - if ($provider instanceof ITopicsProvider) { - $tasks[TopicsTask::class] = true; - } - } - return array_keys($tasks); - } - - /** - * @inheritDoc - */ - public function getAvailableTaskTypes(): array { - return array_map(fn ($taskClass) => $taskClass::TYPE, $this->getAvailableTaskClasses()); - } - - public function canHandleTask(ILanguageModelTask $task): bool { - foreach ($this->getAvailableTaskClasses() as $class) { - if ($task instanceof $class) { - return true; - } - } - return false; - } - - /** - * @inheritDoc - */ - public function runTask(ILanguageModelTask $task): string { - if (!$this->canHandleTask($task)) { - throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); - } - foreach ($this->getProviders() as $provider) { - if (!$task->canUseProvider($provider)) { - continue; - } - try { - $task->setStatus(ILanguageModelTask::STATUS_RUNNING); - if ($task->getId() === null) { - $taskEntity = $this->taskMapper->insert(Task::fromLanguageModelTask($task)); - $task->setId($taskEntity->getId()); - } else { - $this->taskMapper->update(Task::fromLanguageModelTask($task)); - } - $output = $task->visitProvider($provider); - $task->setOutput($output); - $task->setStatus(ILanguageModelTask::STATUS_SUCCESSFUL); - $this->taskMapper->update(Task::fromLanguageModelTask($task)); - return $output; - } catch (\RuntimeException $e) { - $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); - $task->setStatus(ILanguageModelTask::STATUS_FAILED); - $this->taskMapper->update(Task::fromLanguageModelTask($task)); - throw $e; - } catch (\Throwable $e) { - $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); - $task->setStatus(ILanguageModelTask::STATUS_FAILED); - $this->taskMapper->update(Task::fromLanguageModelTask($task)); - throw new RuntimeException('LanguageModel call using provider ' . $provider->getName() . ' failed: ' . $e->getMessage(), 0, $e); - } - } - - throw new RuntimeException('Could not run task'); - } - - /** - * @inheritDoc - * @throws Exception - */ - public function scheduleTask(ILanguageModelTask $task): void { - if (!$this->canHandleTask($task)) { - throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); - } - $task->setStatus(ILanguageModelTask::STATUS_SCHEDULED); - $taskEntity = Task::fromLanguageModelTask($task); - $this->taskMapper->insert($taskEntity); - $task->setId($taskEntity->getId()); - $this->jobList->add(TaskBackgroundJob::class, [ - 'taskId' => $task->getId() - ]); - } - - /** - * @param int $id The id of the task - * @return ILanguageModelTask - * @throws RuntimeException If the query failed - * @throws NotFoundException If the task could not be found - */ - public function getTask(int $id): ILanguageModelTask { - try { - $taskEntity = $this->taskMapper->find($id); - return $taskEntity->toLanguageModelTask(); - } catch (DoesNotExistException $e) { - throw new NotFoundException('Could not find task with the provided id'); - } catch (MultipleObjectsReturnedException $e) { - throw new RuntimeException('Could not uniquely identify task with given id', 0, $e); - } catch (Exception $e) { - throw new RuntimeException('Failure while trying to find task by id: '.$e->getMessage(), 0, $e); - } - } -} diff --git a/lib/private/LanguageModel/RemoveOldTasksBackgroundJob.php b/lib/private/LanguageModel/RemoveOldTasksBackgroundJob.php deleted file mode 100644 index fa3a716a2c6..00000000000 --- a/lib/private/LanguageModel/RemoveOldTasksBackgroundJob.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OC\LanguageModel; - -use OC\LanguageModel\Db\TaskMapper; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\TimedJob; -use OCP\DB\Exception; -use Psr\Log\LoggerInterface; - -class RemoveOldTasksBackgroundJob extends TimedJob { - public const MAX_TASK_AGE_SECONDS = 60 * 50 * 24 * 7; // 1 week - - public function __construct( - ITimeFactory $timeFactory, - private TaskMapper $taskMapper, - private LoggerInterface $logger, - - ) { - parent::__construct($timeFactory); - $this->setInterval(60 * 60 * 24); - } - - /** - * @param mixed $argument - * @inheritDoc - */ - protected function run($argument) { - try { - $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); - } catch (Exception $e) { - $this->logger->warning('Failed to delete stale language model tasks', ['exception' => $e]); - } - } -} diff --git a/lib/private/LanguageModel/TaskBackgroundJob.php b/lib/private/LanguageModel/TaskBackgroundJob.php deleted file mode 100644 index 5ac37baf332..00000000000 --- a/lib/private/LanguageModel/TaskBackgroundJob.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OC\LanguageModel; - -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\QueuedJob; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\LanguageModel\Events\TaskFailedEvent; -use OCP\LanguageModel\Events\TaskSuccessfulEvent; -use OCP\LanguageModel\ILanguageModelManager; - -class TaskBackgroundJob extends QueuedJob { - public function __construct( - ITimeFactory $timeFactory, - private ILanguageModelManager $languageModelManager, - private IEventDispatcher $eventDispatcher, - ) { - parent::__construct($timeFactory); - // We want to avoid overloading the machine with these jobs - // so we only allow running one job at a time - $this->setAllowParallelRuns(false); - } - - /** - * @param array{taskId: int} $argument - * @inheritDoc - */ - protected function run($argument) { - $taskId = $argument['taskId']; - $task = $this->languageModelManager->getTask($taskId); - try { - $this->languageModelManager->runTask($task); - $event = new TaskSuccessfulEvent($task); - } catch (\Throwable $e) { - $event = new TaskFailedEvent($task, $e->getMessage()); - } - $this->eventDispatcher->dispatchTyped($event); - } -} diff --git a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php index 713192b06f9..94ae39f2183 100644 --- a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php +++ b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php @@ -25,7 +25,7 @@ declare(strict_types=1); */ namespace OC\Repair; -use OC\LanguageModel\RemoveOldTasksBackgroundJob; +use OC\TextProcessing\RemoveOldTasksBackgroundJob; use OCP\BackgroundJob\IJobList; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; diff --git a/lib/private/Server.php b/lib/private/Server.php index 3a18779ac86..03c03e1b6ed 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -110,7 +110,6 @@ use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Helpers\AppLocator; use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; -use OC\LanguageModel\LanguageModelManager; use OC\LDAP\NullLDAPProviderFactory; use OC\KnownUser\KnownUserService; use OC\Lock\DBLockingProvider; @@ -230,7 +229,6 @@ use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; -use OCP\LanguageModel\ILanguageModelManager; use OCP\LDAP\ILDAPProvider; use OCP\LDAP\ILDAPProviderFactory; use OCP\Lock\ILockingProvider; @@ -1472,7 +1470,7 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(IEventSourceFactory::class, EventSourceFactory::class); - $this->registerAlias(ILanguageModelManager::class, LanguageModelManager::class); + $this->registerAlias(\OCP\TextProcessing\IManager::class, \OC\TextProcessing\Manager::class); $this->connectDispatcher(); } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 76bd5e6c615..0993fe54f47 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -53,7 +53,7 @@ use Exception; use InvalidArgumentException; use OC\Authentication\Token\PublicKeyTokenProvider; use OC\Authentication\Token\TokenCleanupJob; -use OC\LanguageModel\RemoveOldTasksBackgroundJob; +use OC\TextProcessing\RemoveOldTasksBackgroundJob; use OC\Log\Rotate; use OC\Preview\BackgroundCleanupJob; use OCP\AppFramework\Utility\ITimeFactory; diff --git a/lib/private/TextProcessing/Db/Task.php b/lib/private/TextProcessing/Db/Task.php new file mode 100644 index 00000000000..bc1bbdc13db --- /dev/null +++ b/lib/private/TextProcessing/Db/Task.php @@ -0,0 +1,112 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OC\TextProcessing\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\TextProcessing\Task as OCPTask; + +/** + * @method setType(string $type) + * @method string getType() + * @method setLastUpdated(int $lastUpdated) + * @method int getLastUpdated() + * @method setInput(string $type) + * @method string getInput() + * @method setOutput(string $type) + * @method string getOutput() + * @method setStatus(int $type) + * @method int getStatus() + * @method setUserId(string $type) + * @method string getuserId() + * @method setAppId(string $type) + * @method string getAppId() + * @method setIdentifier(string $type) + * @method string getIdentifier() + */ +class Task extends Entity { + protected $lastUpdated; + protected $type; + protected $input; + protected $output; + protected $status; + protected $userId; + protected $appId; + protected $identifier; + + /** + * @var string[] + */ + public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier']; + + /** + * @var string[] + */ + public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier']; + + + public function __construct() { + // add types in constructor + $this->addType('id', 'integer'); + $this->addType('lastUpdated', 'integer'); + $this->addType('type', 'string'); + $this->addType('input', 'string'); + $this->addType('output', 'string'); + $this->addType('status', 'integer'); + $this->addType('userId', 'string'); + $this->addType('appId', 'string'); + $this->addType('identifier', 'string'); + } + + public function toRow(): array { + return array_combine(self::$columns, array_map(function ($field) { + return $this->{'get'.ucfirst($field)}(); + }, self::$fields)); + } + + public static function fromPublicTask(OCPTask $task): Task { + /** @var Task $task */ + $task = Task::fromParams([ + 'id' => $task->getId(), + 'type' => $task->getType(), + 'lastUpdated' => time(), + 'status' => $task->getStatus(), + 'input' => $task->getInput(), + 'output' => $task->getOutput(), + 'userId' => $task->getUserId(), + 'appId' => $task->getAppId(), + 'identifier' => $task->getIdentifier(), + ]); + return $task; + } + + public function toPublicTask(): OCPTask { + $task = OCPTask::factory($this->getType(), $this->getInput(), $this->getuserId(), $this->getAppId(), $this->getIdentifier()); + $task->setId($this->getId()); + $task->setStatus($this->getStatus()); + $task->setOutput($this->getOutput()); + return $task; + } +} diff --git a/lib/private/TextProcessing/Db/TaskMapper.php b/lib/private/TextProcessing/Db/TaskMapper.php new file mode 100644 index 00000000000..508f3fdf3b8 --- /dev/null +++ b/lib/private/TextProcessing/Db/TaskMapper.php @@ -0,0 +1,78 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OC\TextProcessing\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\IDBConnection; + +/** + * @extends QBMapper + */ +class TaskMapper extends QBMapper { + public function __construct( + IDBConnection $db, + private ITimeFactory $timeFactory, + ) { + parent::__construct($db, 'llm_tasks', Task::class); + } + + /** + * @param int $id + * @return Task + * @throws Exception + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function find(int $id): Task { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); + return $this->findEntity($qb); + } + + /** + * @param int $timeout + * @return int the number of deleted tasks + * @throws Exception + */ + public function deleteOlderThan(int $timeout): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter(time() - $timeout))); + return $qb->executeStatement(); + } + + public function update(Entity $entity): Entity { + $entity->setLastUpdated($this->timeFactory->now()->getTimestamp()); + return parent::update($entity); + } +} diff --git a/lib/private/TextProcessing/Manager.php b/lib/private/TextProcessing/Manager.php new file mode 100644 index 00000000000..34e4b2bb4cc --- /dev/null +++ b/lib/private/TextProcessing/Manager.php @@ -0,0 +1,182 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OC\TextProcessing; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\TextProcessing\Db\Task as DbTask; +use \OCP\TextProcessing\Task as OCPTask; +use OC\TextProcessing\Db\TaskMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\BackgroundJob\IJobList; +use OCP\Common\Exception\NotFoundException; +use OCP\DB\Exception; +use OCP\IServerContainer; +use OCP\TextProcessing\IManager; +use OCP\TextProcessing\IProvider; +use OCP\PreConditionNotMetException; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class Manager implements IManager { + /** @var ?IProvider[] */ + private ?array $providers = null; + + public function __construct( + private IServerContainer $serverContainer, + private Coordinator $coordinator, + private LoggerInterface $logger, + private IJobList $jobList, + private TaskMapper $taskMapper, + ) { + } + + public function getProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return []; + } + + if ($this->providers !== null) { + return $this->providers; + } + + $this->providers = []; + + foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) { + $class = $providerServiceRegistration->getService(); + try { + $this->providers[$class] = $this->serverContainer->get($class); + } catch (Throwable $e) { + $this->logger->error('Failed to load Text processing provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return $this->providers; + } + + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return false; + } + return count($context->getTextProcessingProviders()) > 0; + } + + /** + * @inheritDoc + */ + public function getAvailableTaskTypes(): array { + $tasks = []; + foreach ($this->getProviders() as $provider) { + $tasks[$provider->getTaskType()] = true; + } + return array_keys($tasks); + } + + public function canHandleTask(OCPTask $task): bool { + return in_array($task->getType(), $this->getAvailableTaskTypes()); + } + + /** + * @inheritDoc + */ + public function runTask(OCPTask $task): string { + if (!$this->canHandleTask($task)) { + throw new PreConditionNotMetException('No text processing provider is installed that can handle this task'); + } + foreach ($this->getProviders() as $provider) { + if (!$task->canUseProvider($provider)) { + continue; + } + try { + $task->setStatus(OCPTask::STATUS_RUNNING); + if ($task->getId() === null) { + $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); + $task->setId($taskEntity->getId()); + } else { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } + $output = $task->visitProvider($provider); + $task->setOutput($output); + $task->setStatus(OCPTask::STATUS_SUCCESSFUL); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + return $output; + } catch (\RuntimeException $e) { + $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); + $task->setStatus(OCPTask::STATUS_FAILED); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + throw $e; + } catch (\Throwable $e) { + $this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]); + $task->setStatus(OCPTask::STATUS_FAILED); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + throw new RuntimeException('LanguageModel call using provider ' . $provider->getName() . ' failed: ' . $e->getMessage(), 0, $e); + } + } + + throw new RuntimeException('Could not run task'); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function scheduleTask(OCPTask $task): void { + if (!$this->canHandleTask($task)) { + throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); + } + $task->setStatus(OCPTask::STATUS_SCHEDULED); + $taskEntity = DbTask::fromPublicTask($task); + $this->taskMapper->insert($taskEntity); + $task->setId($taskEntity->getId()); + $this->jobList->add(TaskBackgroundJob::class, [ + 'taskId' => $task->getId() + ]); + } + + /** + * @param int $id The id of the task + * @return OCPTask + * @throws RuntimeException If the query failed + * @throws NotFoundException If the task could not be found + */ + public function getTask(int $id): OCPTask { + try { + $taskEntity = $this->taskMapper->find($id); + return $taskEntity->toPublicTask(); + } catch (DoesNotExistException $e) { + throw new NotFoundException('Could not find task with the provided id'); + } catch (MultipleObjectsReturnedException $e) { + throw new RuntimeException('Could not uniquely identify task with given id', 0, $e); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find task by id: '.$e->getMessage(), 0, $e); + } + } +} diff --git a/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php b/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php new file mode 100644 index 00000000000..89d329acfbb --- /dev/null +++ b/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php @@ -0,0 +1,59 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OC\TextProcessing; + +use OC\TextProcessing\Db\TaskMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\Exception; +use Psr\Log\LoggerInterface; + +class RemoveOldTasksBackgroundJob extends TimedJob { + public const MAX_TASK_AGE_SECONDS = 60 * 50 * 24 * 7; // 1 week + + public function __construct( + ITimeFactory $timeFactory, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + + ) { + parent::__construct($timeFactory); + $this->setInterval(60 * 60 * 24); + } + + /** + * @param mixed $argument + * @inheritDoc + */ + protected function run($argument) { + try { + $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); + } catch (Exception $e) { + $this->logger->warning('Failed to delete stale language model tasks', ['exception' => $e]); + } + } +} diff --git a/lib/private/TextProcessing/TaskBackgroundJob.php b/lib/private/TextProcessing/TaskBackgroundJob.php new file mode 100644 index 00000000000..4c24b3e531f --- /dev/null +++ b/lib/private/TextProcessing/TaskBackgroundJob.php @@ -0,0 +1,63 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OC\TextProcessing; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\TextProcessing\Events\TaskFailedEvent; +use OCP\TextProcessing\Events\TaskSuccessfulEvent; +use OCP\TextProcessing\IManager; + +class TaskBackgroundJob extends QueuedJob { + public function __construct( + ITimeFactory $timeFactory, + private IManager $textProcessingManager, + private IEventDispatcher $eventDispatcher, + ) { + parent::__construct($timeFactory); + // We want to avoid overloading the machine with these jobs + // so we only allow running one job at a time + $this->setAllowParallelRuns(false); + } + + /** + * @param array{taskId: int} $argument + * @inheritDoc + */ + protected function run($argument) { + $taskId = $argument['taskId']; + $task = $this->textProcessingManager->getTask($taskId); + try { + $this->textProcessingManager->runTask($task); + $event = new TaskSuccessfulEvent($task); + } catch (\Throwable $e) { + $event = new TaskFailedEvent($task, $e->getMessage()); + } + $this->eventDispatcher->dispatchTyped($event); + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 66435d45934..720803a78d1 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -37,7 +37,7 @@ use OCP\Collaboration\Reference\IReferenceProvider; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\IContainer; -use OCP\LanguageModel\ILanguageModelProvider; +use OCP\TextProcessing\IProvider as ITextProcessingProvider; use OCP\Notification\INotifier; use OCP\Preview\IProviderV2; use OCP\SpeechToText\ISpeechToTextProvider; @@ -221,14 +221,14 @@ interface IRegistrationContext { public function registerSpeechToTextProvider(string $providerClass): void; /** - * Register a custom LanguageModel provider class that provides a promptable language model - * through the OCP\LanguageModel APIs + * Register a custom text processing provider class that provides a promptable language model + * through the OCP\TextProcessing APIs * * @param string $providerClass - * @psalm-param class-string $providerClass + * @psalm-param class-string $providerClass * @since 27.1.0 */ - public function registerLanguageModelProvider(string $providerClass): void; + public function registerTextProcessingProvider(string $providerClass): void; /** * Register a custom template provider class that is able to inject custom templates diff --git a/lib/public/LanguageModel/AbstractLanguageModelTask.php b/lib/public/LanguageModel/AbstractLanguageModelTask.php deleted file mode 100644 index 91b81b9615b..00000000000 --- a/lib/public/LanguageModel/AbstractLanguageModelTask.php +++ /dev/null @@ -1,178 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * This is an abstract LanguageModel task that implements basic - * goodies for downstream tasks - * @since 28.0. - * @template T of ILanguageModelProvider - * @template-implements ILanguageModelTask - */ -abstract class AbstractLanguageModelTask implements ILanguageModelTask { - protected ?int $id = null; - protected ?string $output = null; - - /** - * @psalm-var ILanguageModelTask::STATUS_* - */ - protected int $status = ILanguageModelTask::STATUS_UNKNOWN; - - /** - * @param string $input - * @param string $appId - * @param string|null $userId - * @param string $identifier An arbitrary identifier for this task. max length: 255 chars - * @since 27.1.0 - */ - final public function __construct( - protected string $input, - protected string $appId, - protected ?string $userId, - protected string $identifier = '', - ) { - } - - /** - * @return string - * @since 27.1.0 - */ - abstract public function getType(): string; - - /** - * @return string|null - * @since 27.1.0 - */ - final public function getOutput(): ?string { - return $this->output; - } - - /** - * @param string|null $output - * @since 27.1.0 - */ - final public function setOutput(?string $output): void { - $this->output = $output; - } - - /** - * @psalm-return ILanguageModelTask::STATUS_* - * @since 27.1.0 - */ - final public function getStatus(): int { - return $this->status; - } - - /** - * @psalm-param ILanguageModelTask::STATUS_* $status - * @since 27.1.0 - */ - final public function setStatus(int $status): void { - $this->status = $status; - } - - /** - * @return int|null - * @since 27.1.0 - */ - final public function getId(): ?int { - return $this->id; - } - - /** - * @param int|null $id - * @since 27.1.0 - */ - final public function setId(?int $id): void { - $this->id = $id; - } - - /** - * @return string - * @since 27.1.0 - */ - final public function getInput(): string { - return $this->input; - } - - /** - * @return string - * @since 27.1.0 - */ - final public function getAppId(): string { - return $this->appId; - } - - /** - * @return string - * @since 27.1.0 - */ - final public function getIdentifier(): string { - return $this->identifier; - } - - /** - * @return string|null - * @since 27.1.0 - */ - final public function getUserId(): ?string { - return $this->userId; - } - - /** - * @return array - * @since 27.1.0 - */ - public function jsonSerialize() { - return [ - 'id' => $this->getId(), - 'type' => $this->getType(), - 'status' => $this->getStatus(), - 'userId' => $this->getUserId(), - 'appId' => $this->getAppId(), - 'input' => $this->getInput(), - 'output' => $this->getOutput(), - 'identifier' => $this->getIdentifier(), - ]; - } - - /** - * @param string $type - * @param string $input - * @param string|null $userId - * @param string $appId - * @param string $identifier - * @return ILanguageModelTask - * @throws \InvalidArgumentException - * @since 27.1.0 - */ - final public static function factory(string $type, string $input, ?string $userId, string $appId, string $identifier = ''): ILanguageModelTask { - if (!in_array($type, array_keys(self::TYPES))) { - throw new \InvalidArgumentException('Unknown task type'); - } - return new (ILanguageModelTask::TYPES[$type])($input, $appId, $userId, $identifier); - } -} diff --git a/lib/public/LanguageModel/Events/AbstractLanguageModelEvent.php b/lib/public/LanguageModel/Events/AbstractLanguageModelEvent.php deleted file mode 100644 index c8abc7373eb..00000000000 --- a/lib/public/LanguageModel/Events/AbstractLanguageModelEvent.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ -namespace OCP\LanguageModel\Events; - -use OCP\EventDispatcher\Event; -use OCP\LanguageModel\ILanguageModelTask; - -/** - * @since 27.1.0 - */ -abstract class AbstractLanguageModelEvent extends Event { - /** - * @since 27.1.0 - */ - public function __construct( - private ILanguageModelTask $task - ) { - parent::__construct(); - } - - /** - * @return ILanguageModelTask - * @since 27.1.0 - */ - public function getTask(): ILanguageModelTask { - return $this->task; - } -} diff --git a/lib/public/LanguageModel/Events/TaskFailedEvent.php b/lib/public/LanguageModel/Events/TaskFailedEvent.php deleted file mode 100644 index f42203a6e48..00000000000 --- a/lib/public/LanguageModel/Events/TaskFailedEvent.php +++ /dev/null @@ -1,30 +0,0 @@ -errorMessage; - } -} diff --git a/lib/public/LanguageModel/Events/TaskSuccessfulEvent.php b/lib/public/LanguageModel/Events/TaskSuccessfulEvent.php deleted file mode 100644 index 77a61ac5c6e..00000000000 --- a/lib/public/LanguageModel/Events/TaskSuccessfulEvent.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * @since 27.1.0 - * @template-extends AbstractLanguageModelTask - */ -final class FreePromptTask extends AbstractLanguageModelTask { - /** - * @since 27.1.0 - */ - public const TYPE = 'free_prompt'; - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function visitProvider(ILanguageModelProvider $provider): string { - return $provider->prompt($this->getInput()); - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function canUseProvider(ILanguageModelProvider $provider): bool { - return true; - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function getType(): string { - return self::TYPE; - } -} diff --git a/lib/public/LanguageModel/HeadlineTask.php b/lib/public/LanguageModel/HeadlineTask.php deleted file mode 100644 index 4c62b9722a9..00000000000 --- a/lib/public/LanguageModel/HeadlineTask.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * This LanguageModel Task represents headline generation - * which generates a headline for the passed text - * @since 27.1.0 - * @template-extends AbstractLanguageModelTask - */ -final class HeadlineTask extends AbstractLanguageModelTask { - /** - * @since 27.1.0 - */ - public const TYPE = 'headline'; - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function visitProvider(ILanguageModelProvider $provider): string { - if (!$this->canUseProvider($provider)) { - throw new \RuntimeException('HeadlineTask#visitProvider expects IHeadlineProvider'); - } - return $provider->findHeadline($this->getInput()); - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function canUseProvider(ILanguageModelProvider $provider): bool { - return $provider instanceof IHeadlineProvider; - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function getType(): string { - return self::TYPE; - } -} diff --git a/lib/public/LanguageModel/IHeadlineProvider.php b/lib/public/LanguageModel/IHeadlineProvider.php deleted file mode 100644 index 30185f4d4b3..00000000000 --- a/lib/public/LanguageModel/IHeadlineProvider.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OCP\LanguageModel; - -use RuntimeException; - -/** - * This LanguageModel Provider represents headline generation - * which generates a headline for the passed text - * @since 27.1.0 - */ -interface IHeadlineProvider extends ILanguageModelProvider { - /** - * @param string $text The text to find headline for - * @returns string the headline - * @since 27.1.0 - * @throws RuntimeException If the text could not be transcribed - */ - public function findHeadline(string $text): string; -} diff --git a/lib/public/LanguageModel/ILanguageModelManager.php b/lib/public/LanguageModel/ILanguageModelManager.php deleted file mode 100644 index 0afc99b91ab..00000000000 --- a/lib/public/LanguageModel/ILanguageModelManager.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OCP\LanguageModel; - -use OCP\Common\Exception\NotFoundException; -use OCP\PreConditionNotMetException; -use RuntimeException; - -/** - * API surface for apps interacting with and making use of LanguageModel providers - * without known which providers are installed - * @since 27.1.0 - */ -interface ILanguageModelManager { - /** - * @since 27.1.0 - */ - public function hasProviders(): bool; - - /** - * @return string[] - * @since 27.1.0 - */ - public function getAvailableTaskClasses(): array; - - /** - * @return string[] - * @since 27.1.0 - */ - public function getAvailableTaskTypes(): array; - - /** - * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called - * @throws RuntimeException If something else failed - * @since 27.1.0 - */ - public function runTask(ILanguageModelTask $task): string; - - /** - * Will schedule an LLM inference process in the background. The result will become available - * with the \OCP\LanguageModel\Events\TaskSuccessfulEvent - * If inference fails a \OCP\LanguageModel\Events\TaskFailedEvent will be dispatched instead - * - * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called - * @since 27.1.0 - */ - public function scheduleTask(ILanguageModelTask $task) : void; - - /** - * @param int $id The id of the task - * @return ILanguageModelTask - * @throws RuntimeException If the query failed - * @throws NotFoundException If the task could not be found - * @since 27.1.0 - */ - public function getTask(int $id): ILanguageModelTask; -} diff --git a/lib/public/LanguageModel/ILanguageModelProvider.php b/lib/public/LanguageModel/ILanguageModelProvider.php deleted file mode 100644 index 34e7eb6c4e5..00000000000 --- a/lib/public/LanguageModel/ILanguageModelProvider.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OCP\LanguageModel; - -use RuntimeException; - -/** - * This is the minimum interface that is implemented by apps that - * implement a LanguageModel provider - * @since 27.1.0 - */ -interface ILanguageModelProvider { - /** - * @since 27.1.0 - */ - public function getName(): string; - - /** - * @param string $prompt The prompt to call the model with - * @return string the output - * @since 27.1.0 - * @throws RuntimeException If the text could not be transcribed - */ - public function prompt(string $prompt): string; -} diff --git a/lib/public/LanguageModel/ILanguageModelTask.php b/lib/public/LanguageModel/ILanguageModelTask.php deleted file mode 100644 index 0f552c8de54..00000000000 --- a/lib/public/LanguageModel/ILanguageModelTask.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * @since 27.1.0 - * @template T of ILanguageModelProvider - */ -interface ILanguageModelTask extends \JsonSerializable { - /** - * @since 27.1.0 - */ - public const STATUS_FAILED = 4; - /** - * @since 27.1.0 - */ - public const STATUS_SUCCESSFUL = 3; - /** - * @since 27.1.0 - */ - public const STATUS_RUNNING = 2; - /** - * @since 27.1.0 - */ - public const STATUS_SCHEDULED = 1; - /** - * @since 27.1.0 - */ - public const STATUS_UNKNOWN = 0; - - /** - * @since 27.1.0 - */ - public const TYPES = [ - FreePromptTask::TYPE => FreePromptTask::class, - SummaryTask::TYPE => SummaryTask::class, - HeadlineTask::TYPE => HeadlineTask::class, - TopicsTask::TYPE => TopicsTask::class, - ]; - - /** - * @psalm-param T $provider - * @param ILanguageModelProvider $provider - * @return string - * @since 27.1.0 - */ - public function visitProvider(ILanguageModelProvider $provider): string; - - /** - * @psalm-param T $provider - * @param ILanguageModelProvider $provider - * @return bool - * @since 27.1.0 - */ - public function canUseProvider(ILanguageModelProvider $provider): bool; - - - /** - * @return string - * @since 27.1.0 - */ - public function getType(): string; - - /** - * @return ILanguageModelTask::STATUS_* - * @since 27.1.0 - */ - public function getStatus(): int; - - /** - * @param ILanguageModelTask::STATUS_* $status - * @since 27.1.0 - */ - public function setStatus(int $status): void; - - /** - * @param int|null $id - * @since 27.1.0 - */ - public function setId(?int $id): void; - - /** - * @return int|null - * @since 27.1.0 - */ - public function getId(): ?int; - - /** - * @return string - * @since 27.1.0 - */ - public function getInput(): string; - - /** - * @param string|null $output - * @since 27.1.0 - */ - public function setOutput(?string $output): void; - - /** - * @return null|string - * @since 27.1.0 - */ - public function getOutput(): ?string; - - /** - * @return string - * @since 27.1.0 - */ - public function getAppId(): string; - - /** - * @return string - * @since 27.1.0 - */ - public function getIdentifier(): string; - - /** - * @return string|null - * @since 27.1.0 - */ - public function getUserId(): ?string; -} diff --git a/lib/public/LanguageModel/ISummaryProvider.php b/lib/public/LanguageModel/ISummaryProvider.php deleted file mode 100644 index c286e74eb19..00000000000 --- a/lib/public/LanguageModel/ISummaryProvider.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OCP\LanguageModel; - -use RuntimeException; - -/** - * This LanguageModel Provider implements summarization - * which sums up the passed text. - * @since 27.1.0 - */ -interface ISummaryProvider extends ILanguageModelProvider { - /** - * @param string $text The text to summarize - * @returns string the summary - * @since 27.1.0 - * @throws RuntimeException If the text could not be transcribed - */ - public function summarize(string $text): string; -} diff --git a/lib/public/LanguageModel/ITopicsProvider.php b/lib/public/LanguageModel/ITopicsProvider.php deleted file mode 100644 index f061976a3ba..00000000000 --- a/lib/public/LanguageModel/ITopicsProvider.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - - -namespace OCP\LanguageModel; - -use RuntimeException; - -/** - * This LanguageModel Provider implements topics synthesis - * which outputs comma-separated topics for the passed text - * @since 27.1.0 - */ -interface ITopicsProvider extends ILanguageModelProvider { - /** - * @param string $text The text to find topics for - * @returns string the topics, comma separated - * @since 27.1.0 - * @throws RuntimeException If the text could not be transcribed - */ - public function findTopics(string $text): string; -} diff --git a/lib/public/LanguageModel/SummaryTask.php b/lib/public/LanguageModel/SummaryTask.php deleted file mode 100644 index 47864532ae3..00000000000 --- a/lib/public/LanguageModel/SummaryTask.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * This is an absctract LanguageModel Task represents summarization - * which sums up the passed text. - * @since 27.1.0 - * @template-extends AbstractLanguageModelTask - */ -final class SummaryTask extends AbstractLanguageModelTask { - /** - * @since 27.1.0 - */ - public const TYPE = 'summarize'; - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function visitProvider(ILanguageModelProvider $provider): string { - if (!$this->canUseProvider($provider)) { - throw new \RuntimeException('SummaryTask#visitProvider expects ISummaryProvider'); - } - return $provider->summarize($this->getInput()); - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function canUseProvider(ILanguageModelProvider $provider): bool { - return $provider instanceof ISummaryProvider; - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function getType(): string { - return self::TYPE; - } -} diff --git a/lib/public/LanguageModel/TopicsTask.php b/lib/public/LanguageModel/TopicsTask.php deleted file mode 100644 index ab2c5916061..00000000000 --- a/lib/public/LanguageModel/TopicsTask.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * @author Marcel Klehr - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -namespace OCP\LanguageModel; - -/** - * This LanguageModel Task represents topics synthesis - * which outputs comma-separated topics for the passed text - * @since 27.1.0 - * @template-extends AbstractLanguageModelTask - */ -final class TopicsTask extends AbstractLanguageModelTask { - /** - * @since 27.1.0 - */ - public const TYPE = 'topics'; - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function visitProvider(ILanguageModelProvider $provider): string { - if (!$this->canUseProvider($provider)) { - throw new \RuntimeException('TopicsTask#visitProvider expects ITopicsProvider'); - } - return $provider->findTopics($this->getInput()); - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function canUseProvider(ILanguageModelProvider $provider): bool { - return $provider instanceof ITopicsProvider; - } - - /** - * @inheritDoc - * @since 27.1.0 - */ - public function getType(): string { - return self::TYPE; - } -} diff --git a/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php b/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php new file mode 100644 index 00000000000..10c592fe031 --- /dev/null +++ b/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php @@ -0,0 +1,52 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCP\TextProcessing\Events; + +use OCP\EventDispatcher\Event; +use OCP\TextProcessing\ILanguageModelTask; +use OCP\TextProcessing\Task; + +/** + * @since 27.1.0 + */ +abstract class AbstractTextProcessingEvent extends Event { + /** + * @since 27.1.0 + */ + public function __construct( + private Task $task + ) { + parent::__construct(); + } + + /** + * @return Task + * @since 27.1.0 + */ + public function getTask(): Task { + return $this->task; + } +} diff --git a/lib/public/TextProcessing/Events/TaskFailedEvent.php b/lib/public/TextProcessing/Events/TaskFailedEvent.php new file mode 100644 index 00000000000..f9765e362dc --- /dev/null +++ b/lib/public/TextProcessing/Events/TaskFailedEvent.php @@ -0,0 +1,30 @@ +errorMessage; + } +} diff --git a/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php b/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php new file mode 100644 index 00000000000..73fbbb87f45 --- /dev/null +++ b/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php @@ -0,0 +1,18 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +use OCP\IL10N; + +/** + * This is the text processing task type for free prompting + * @since 27.1.0 + */ +class FreePromptTaskType implements ITaskType { + /** + * Constructor for FreePromptTaskType + * + * @param IL10N $l + * @since 27.1.0 + */ + public function __construct( + private IL10N $l, + ) { + } + + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l->t('Free prompt'); + } + + /** + * @inheritDoc + */ + public function getDescription(): string { + return $this->l->t('Runs an arbitrary prompt through the built-in language model.'); + } +} diff --git a/lib/public/TextProcessing/HeadlineTaskType.php b/lib/public/TextProcessing/HeadlineTaskType.php new file mode 100644 index 00000000000..4ced298fd4d --- /dev/null +++ b/lib/public/TextProcessing/HeadlineTaskType.php @@ -0,0 +1,60 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +use OCP\IL10N; + +/** + * This is the text processing task type for creating headline + * @since 27.1.0 + */ +class HeadlineTaskType implements ITaskType { + /** + * Constructor for HeadlineTaskType + * + * @param IL10N $l + * @since 27.1.0 + */ + public function __construct( + private IL10N $l, + ) { + } + + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l->t('Generate headline'); + } + + /** + * @inheritDoc + */ + public function getDescription(): string { + return $this->l->t('Generates a possible headline for a text'); + } +} diff --git a/lib/public/TextProcessing/IManager.php b/lib/public/TextProcessing/IManager.php new file mode 100644 index 00000000000..90e25894d4f --- /dev/null +++ b/lib/public/TextProcessing/IManager.php @@ -0,0 +1,77 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OCP\TextProcessing; + +use OCP\Common\Exception\NotFoundException; +use OCP\PreConditionNotMetException; +use RuntimeException; + +/** + * API surface for apps interacting with and making use of LanguageModel providers + * without known which providers are installed + * @since 27.1.0 + */ +interface IManager { + /** + * @since 27.1.0 + */ + public function hasProviders(): bool; + + /** + * @return class-string[] + * @since 27.1.0 + */ + public function getAvailableTaskTypes(): array; + + /** + * @param Task $task The task to run + * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called + * @throws RuntimeException If something else failed + * @since 27.1.0 + */ + public function runTask(Task $task): string; + + /** + * Will schedule an LLM inference process in the background. The result will become available + * with the \OCP\LanguageModel\Events\TaskSuccessfulEvent + * If inference fails a \OCP\LanguageModel\Events\TaskFailedEvent will be dispatched instead + * + * @param Task $task The task to schedule + * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called + * @since 27.1.0 + */ + public function scheduleTask(Task $task) : void; + + /** + * @param int $id The id of the task + * @return Task + * @throws RuntimeException If the query failed + * @throws NotFoundException If the task could not be found + * @since 27.1.0 + */ + public function getTask(int $id): Task; +} diff --git a/lib/public/TextProcessing/IProvider.php b/lib/public/TextProcessing/IProvider.php new file mode 100644 index 00000000000..3eb83aef8c3 --- /dev/null +++ b/lib/public/TextProcessing/IProvider.php @@ -0,0 +1,61 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OCP\TextProcessing; + +use RuntimeException; + +/** + * This is the interface that is implemented by apps that + * implement a text processing provider + * @template T of ITaskType + * @since 27.1.0 + */ +interface IProvider { + /** + * The localized name of this provider + * @since 27.1.0 + */ + public function getName(): string; + + /** + * Processes a text + * + * @param string $prompt The input text + * @return string the output text + * @since 27.1.0 + * @throws RuntimeException If the text could not be processed + */ + public function process(string $prompt): string; + + /** + * Returns the task type class string of the task type, that this + * provider handles + * + * @return class-string + */ + public function getTaskType(): string; +} diff --git a/lib/public/TextProcessing/ITaskType.php b/lib/public/TextProcessing/ITaskType.php new file mode 100644 index 00000000000..d08da3f7ac7 --- /dev/null +++ b/lib/public/TextProcessing/ITaskType.php @@ -0,0 +1,49 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +/** + * This is a task type interface that is implemented by text processing + * task types + * @since 27.1.0 + */ +interface ITaskType { + /** + * Returns the localized name of this task type + * + * @since 27.1.0 + * @return string + */ + public function getName(): string; + + /** + * Returns the localized description of this task type + * + * @since 27.1.0 + * @return string + */ + public function getDescription(): string; +} diff --git a/lib/public/TextProcessing/SummaryTaskType.php b/lib/public/TextProcessing/SummaryTaskType.php new file mode 100644 index 00000000000..7db695c18f7 --- /dev/null +++ b/lib/public/TextProcessing/SummaryTaskType.php @@ -0,0 +1,60 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +use OCP\IL10N; + +/** + * This is the text processing task type for summaries + * @since 27.1.0 + */ +class SummaryTaskType implements ITaskType { + /** + * Constructor for SummaryTaskType + * + * @param IL10N $l + * @since 27.1.0 + */ + public function __construct( + private IL10N $l, + ) { + } + + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l->t('Summarize'); + } + + /** + * @inheritDoc + */ + public function getDescription(): string { + return $this->l->t('Summarizes text by reducing its length without losing key information.'); + } +} diff --git a/lib/public/TextProcessing/Task.php b/lib/public/TextProcessing/Task.php new file mode 100644 index 00000000000..59cd38b720c --- /dev/null +++ b/lib/public/TextProcessing/Task.php @@ -0,0 +1,235 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +/** + * This is a text processing task + * @since 27.1.0 + * @template T of ITaskType + */ +final class Task implements \JsonSerializable { + protected ?int $id = null; + protected ?string $output = null; + + /** + * @since 27.1.0 + */ + public const TYPES = [ + FreePromptTaskType::class, + SummaryTaskType::class, + HeadlineTaskType::class, + TopicsTaskType::class, + ]; + + /** + * @since 27.1.0 + */ + public const STATUS_FAILED = 4; + /** + * @since 27.1.0 + */ + public const STATUS_SUCCESSFUL = 3; + /** + * @since 27.1.0 + */ + public const STATUS_RUNNING = 2; + /** + * @since 27.1.0 + */ + public const STATUS_SCHEDULED = 1; + /** + * @since 27.1.0 + */ + public const STATUS_UNKNOWN = 0; + + /** + * @psalm-var self::STATUS_* + */ + protected int $status = self::STATUS_UNKNOWN; + + /** + * @param class-string $type + * @param string $input + * @param string $appId + * @param string|null $userId + * @param string $identifier An arbitrary identifier for this task. max length: 255 chars + * @since 27.1.0 + */ + final public function __construct( + protected string $type, + protected string $input, + protected string $appId, + protected ?string $userId, + protected string $identifier = '', + ) { + } + + /** + * @psalm-param IProvider $provider + * @param IProvider $provider + * @return string + * @since 27.1.0 + */ + public function visitProvider(IProvider $provider): string { + if ($this->canUseProvider($provider)) { + return $provider->process($this->getInput()); + } else { + throw new \RuntimeException('Task of type ' . $this->getType() . ' cannot visit provider with task type ' . $provider->getTaskType()); + } + } + + /** + * @psalm-param IProvider $provider + * @param IProvider $provider + * @return bool + * @since 27.1.0 + */ + public function canUseProvider(IProvider $provider): bool { + return $provider->getTaskType() === $this->getType(); + } + + /** + * @return class-string + * @since 27.1.0 + */ + final public function getType(): string { + return $this->type; + } + + /** + * @return string|null + * @since 27.1.0 + */ + final public function getOutput(): ?string { + return $this->output; + } + + /** + * @param string|null $output + * @since 27.1.0 + */ + final public function setOutput(?string $output): void { + $this->output = $output; + } + + /** + * @psalm-return self::STATUS_* + * @since 27.1.0 + */ + final public function getStatus(): int { + return $this->status; + } + + /** + * @psalm-param self::STATUS_* $status + * @since 27.1.0 + */ + final public function setStatus(int $status): void { + $this->status = $status; + } + + /** + * @return int|null + * @since 27.1.0 + */ + final public function getId(): ?int { + return $this->id; + } + + /** + * @param int|null $id + * @since 27.1.0 + */ + final public function setId(?int $id): void { + $this->id = $id; + } + + /** + * @return string + * @since 27.1.0 + */ + final public function getInput(): string { + return $this->input; + } + + /** + * @return string + * @since 27.1.0 + */ + final public function getAppId(): string { + return $this->appId; + } + + /** + * @return string + * @since 27.1.0 + */ + final public function getIdentifier(): string { + return $this->identifier; + } + + /** + * @return string|null + * @since 27.1.0 + */ + final public function getUserId(): ?string { + return $this->userId; + } + + /** + * @return array{id: ?string, type: class-string, status: int, userId: ?string, appId: string, input: string, output: ?string, identifier: string} + * @since 27.1.0 + */ + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'type' => $this->getType(), + 'status' => $this->getStatus(), + 'userId' => $this->getUserId(), + 'appId' => $this->getAppId(), + 'input' => $this->getInput(), + 'output' => $this->getOutput(), + 'identifier' => $this->getIdentifier(), + ]; + } + + /** + * @param string $type + * @param string $input + * @param string|null $userId + * @param string $appId + * @param string $identifier + * @return Task + * @throws \InvalidArgumentException + * @since 27.1.0 + */ + final public static function factory(string $type, string $input, ?string $userId, string $appId, string $identifier = ''): Task { + if (!in_array($type, self::TYPES)) { + throw new \InvalidArgumentException('Unknown task type'); + } + return new Task($type, $input, $appId, $userId, $identifier); + } +} diff --git a/lib/public/TextProcessing/TopicsTaskType.php b/lib/public/TextProcessing/TopicsTaskType.php new file mode 100644 index 00000000000..8b41b3ee61a --- /dev/null +++ b/lib/public/TextProcessing/TopicsTaskType.php @@ -0,0 +1,60 @@ + + * + * @author Marcel Klehr + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCP\TextProcessing; + +use OCP\IL10N; + +/** + * This is the text processing task type for topics extraction + * @since 27.1.0 + */ +class TopicsTaskType implements ITaskType { + /** + * Constructor for TopicsTaskType + * + * @param IL10N $l + * @since 27.1.0 + */ + public function __construct( + private IL10N $l, + ) { + } + + + /** + * @inheritDoc + */ + public function getName(): string { + return $this->l->t('Extract topics'); + } + + /** + * @inheritDoc + */ + public function getDescription(): string { + return $this->l->t('Extracts topics from a text and outputs them separated by commas.'); + } +} diff --git a/tests/lib/LanguageModel/LanguageModelManagerTest.php b/tests/lib/LanguageModel/LanguageModelManagerTest.php deleted file mode 100644 index 6f8d6cd868d..00000000000 --- a/tests/lib/LanguageModel/LanguageModelManagerTest.php +++ /dev/null @@ -1,343 +0,0 @@ - - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -namespace Test\LanguageModel; - -use OC\AppFramework\Bootstrap\Coordinator; -use OC\AppFramework\Bootstrap\RegistrationContext; -use OC\AppFramework\Bootstrap\ServiceRegistration; -use OC\EventDispatcher\EventDispatcher; -use OC\LanguageModel\Db\Task; -use OC\LanguageModel\Db\TaskMapper; -use OC\LanguageModel\LanguageModelManager; -use OC\LanguageModel\RemoveOldTasksBackgroundJob; -use OC\LanguageModel\TaskBackgroundJob; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Common\Exception\NotFoundException; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\IServerContainer; -use OCP\LanguageModel\Events\TaskFailedEvent; -use OCP\LanguageModel\Events\TaskSuccessfulEvent; -use OCP\LanguageModel\FreePromptTask; -use OCP\LanguageModel\HeadlineTask; -use OCP\LanguageModel\IHeadlineProvider; -use OCP\LanguageModel\ILanguageModelManager; -use OCP\LanguageModel\ILanguageModelProvider; -use OCP\LanguageModel\ILanguageModelTask; -use OCP\LanguageModel\ISummaryProvider; -use OCP\LanguageModel\SummaryTask; -use OCP\LanguageModel\TopicsTask; -use OCP\PreConditionNotMetException; -use PHPUnit\Framework\Constraint\IsInstanceOf; -use Psr\Log\LoggerInterface; -use Test\BackgroundJob\DummyJobList; - -class TestVanillaLanguageModelProvider implements ILanguageModelProvider { - public bool $ran = false; - - public function getName(): string { - return 'TEST Vanilla LLM Provider'; - } - - public function prompt(string $prompt): string { - $this->ran = true; - return $prompt . ' Free Prompt'; - } -} - -class TestFailingLanguageModelProvider implements ILanguageModelProvider { - public bool $ran = false; - - public function getName(): string { - return 'TEST Vanilla LLM Provider'; - } - - public function prompt(string $prompt): string { - $this->ran = true; - throw new \Exception('ERROR'); - } -} - -class TestAdvancedLanguageModelProvider implements ILanguageModelProvider, ISummaryProvider, IHeadlineProvider { - public function getName(): string { - return 'TEST Full LLM Provider'; - } - - public function prompt(string $prompt): string { - return $prompt . ' Free Prompt'; - } - - public function findHeadline(string $text): string { - return $text . ' Headline'; - } - - public function summarize(string $text): string { - return $text. ' Summarize'; - } -} - -class LanguageModelManagerTest extends \Test\TestCase { - private ILanguageModelManager $languageModelManager; - private Coordinator $coordinator; - - protected function setUp(): void { - parent::setUp(); - - $this->providers = [ - TestVanillaLanguageModelProvider::class => new TestVanillaLanguageModelProvider(), - TestAdvancedLanguageModelProvider::class => new TestAdvancedLanguageModelProvider(), - TestFailingLanguageModelProvider::class => new TestFailingLanguageModelProvider(), - ]; - - $this->serverContainer = $this->createMock(IServerContainer::class); - $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) { - return $this->providers[$class]; - }); - - $this->eventDispatcher = new EventDispatcher( - new \Symfony\Component\EventDispatcher\EventDispatcher(), - $this->serverContainer, - \OC::$server->get(LoggerInterface::class), - ); - - $this->registrationContext = $this->createMock(RegistrationContext::class); - $this->coordinator = $this->createMock(Coordinator::class); - $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext); - - $this->currentTime = new \DateTimeImmutable('now'); - - $this->taskMapper = $this->createMock(TaskMapper::class); - $this->tasksDb = []; - $this->taskMapper - ->expects($this->any()) - ->method('insert') - ->willReturnCallback(function (Task $task) { - $task->setId(count($this->tasksDb) ? max(array_keys($this->tasksDb)) : 1); - $task->setLastUpdated($this->currentTime->getTimestamp()); - $this->tasksDb[$task->getId()] = $task->toRow(); - return $task; - }); - $this->taskMapper - ->expects($this->any()) - ->method('update') - ->willReturnCallback(function (Task $task) { - $task->setLastUpdated($this->currentTime->getTimestamp()); - $this->tasksDb[$task->getId()] = $task->toRow(); - return $task; - }); - $this->taskMapper - ->expects($this->any()) - ->method('find') - ->willReturnCallback(function (int $id) { - if (!isset($this->tasksDb[$id])) { - throw new DoesNotExistException('Could not find it'); - } - return Task::fromRow($this->tasksDb[$id]); - }); - $this->taskMapper - ->expects($this->any()) - ->method('deleteOlderThan') - ->willReturnCallback(function (int $timeout) { - $this->tasksDb = array_filter($this->tasksDb, function (array $task) use ($timeout) { - return $task['last_updated'] >= $this->currentTime->getTimestamp() - $timeout; - }); - }); - - $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']); - $this->jobList->expects($this->any())->method('add')->willReturnCallback(function () { - }); - - $this->languageModelManager = new LanguageModelManager( - $this->serverContainer, - $this->coordinator, - \OC::$server->get(LoggerInterface::class), - $this->jobList, - $this->taskMapper, - ); - } - - public function testShouldNotHaveAnyProviders() { - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([]); - $this->assertCount(0, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(0, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertFalse($this->languageModelManager->hasProviders()); - $this->expectException(PreConditionNotMetException::class); - $this->languageModelManager->runTask(new FreePromptTask('Hello', 'test', null)); - } - - public function testProviderShouldBeRegisteredAndRun() { - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([ - new ServiceRegistration('test', TestVanillaLanguageModelProvider::class) - ]); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertTrue($this->languageModelManager->hasProviders()); - $this->assertEquals('Hello Free Prompt', $this->languageModelManager->runTask(new FreePromptTask('Hello', 'test', null))); - - // Summaries are not implemented by the vanilla provider, only free prompt - $this->expectException(PreConditionNotMetException::class); - $this->languageModelManager->runTask(new SummaryTask('Hello', 'test', null)); - } - - public function testProviderShouldBeRegisteredAndScheduled() { - // register provider - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([ - new ServiceRegistration('test', TestVanillaLanguageModelProvider::class) - ]); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertTrue($this->languageModelManager->hasProviders()); - - // create task object - $task = new FreePromptTask('Hello', 'test', null); - $this->assertNull($task->getId()); - $this->assertNull($task->getOutput()); - - // schedule works - $this->assertEquals(ILanguageModelTask::STATUS_UNKNOWN, $task->getStatus()); - $this->languageModelManager->scheduleTask($task); - - // Task object is up-to-date - $this->assertNotNull($task->getId()); - $this->assertNull($task->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->languageModelManager->getTask($task->getId()); - $this->assertEquals($task->getId(), $task2->getId()); - $this->assertEquals('Hello', $task2->getInput()); - $this->assertNull($task2->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); - - // run background job - $bgJob = new TaskBackgroundJob( - \OC::$server->get(ITimeFactory::class), - $this->languageModelManager, - $this->eventDispatcher, - ); - $bgJob->setArgument(['taskId' => $task->getId()]); - $bgJob->start($this->jobList); - $provider = $this->providers[TestVanillaLanguageModelProvider::class]; - $this->assertTrue($provider->ran); - - // Task object retrieved from db is up-to-date - $task3 = $this->languageModelManager->getTask($task->getId()); - $this->assertEquals($task->getId(), $task3->getId()); - $this->assertEquals('Hello', $task3->getInput()); - $this->assertEquals('Hello Free Prompt', $task3->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_SUCCESSFUL, $task3->getStatus()); - } - - public function testMultipleProvidersShouldBeRegisteredAndRunCorrectly() { - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([ - new ServiceRegistration('test', TestVanillaLanguageModelProvider::class), - new ServiceRegistration('test', TestAdvancedLanguageModelProvider::class), - ]); - $this->assertCount(3, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(3, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertTrue($this->languageModelManager->hasProviders()); - - // Try free prompt again - $this->assertEquals('Hello Free Prompt', $this->languageModelManager->runTask(new FreePromptTask('Hello', 'test', null))); - - // Try headline task - $this->assertEquals('Hello Headline', $this->languageModelManager->runTask(new HeadlineTask('Hello', 'test', null))); - - // Try summary task - $this->assertEquals('Hello Summarize', $this->languageModelManager->runTask(new SummaryTask('Hello', 'test', null))); - - // Topics are not implemented by both the vanilla provider and the full provider - $this->expectException(PreConditionNotMetException::class); - $this->languageModelManager->runTask(new TopicsTask('Hello', 'test', null)); - } - - public function testNonexistentTask() { - $this->expectException(NotFoundException::class); - $this->languageModelManager->getTask(98765432456); - } - - public function testTaskFailure() { - // register provider - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([ - new ServiceRegistration('test', TestFailingLanguageModelProvider::class), - ]); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertTrue($this->languageModelManager->hasProviders()); - - // create task object - $task = new FreePromptTask('Hello', 'test', null); - $this->assertNull($task->getId()); - $this->assertNull($task->getOutput()); - - // schedule works - $this->assertEquals(ILanguageModelTask::STATUS_UNKNOWN, $task->getStatus()); - $this->languageModelManager->scheduleTask($task); - - // Task object is up-to-date - $this->assertNotNull($task->getId()); - $this->assertNull($task->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_SCHEDULED, $task->getStatus()); - - // Task object retrieved from db is up-to-date - $task2 = $this->languageModelManager->getTask($task->getId()); - $this->assertEquals($task->getId(), $task2->getId()); - $this->assertEquals('Hello', $task2->getInput()); - $this->assertNull($task2->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_SCHEDULED, $task2->getStatus()); - - $this->eventDispatcher = $this->createMock(IEventDispatcher::class); - $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); - - // run background job - $bgJob = new TaskBackgroundJob( - \OC::$server->get(ITimeFactory::class), - $this->languageModelManager, - $this->eventDispatcher, - ); - $bgJob->setArgument(['taskId' => $task->getId()]); - $bgJob->start($this->jobList); - $provider = $this->providers[TestFailingLanguageModelProvider::class]; - $this->assertTrue($provider->ran); - - // Task object retrieved from db is up-to-date - $task3 = $this->languageModelManager->getTask($task->getId()); - $this->assertEquals($task->getId(), $task3->getId()); - $this->assertEquals('Hello', $task3->getInput()); - $this->assertNull($task3->getOutput()); - $this->assertEquals(ILanguageModelTask::STATUS_FAILED, $task3->getStatus()); - } - - public function testOldTasksShouldBeCleanedUp() { - $this->registrationContext->expects($this->any())->method('getLanguageModelProviders')->willReturn([ - new ServiceRegistration('test', TestVanillaLanguageModelProvider::class) - ]); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskClasses()); - $this->assertCount(1, $this->languageModelManager->getAvailableTaskTypes()); - $this->assertTrue($this->languageModelManager->hasProviders()); - $task = new FreePromptTask('Hello', 'test', null); - $this->assertEquals('Hello Free Prompt', $this->languageModelManager->runTask($task)); - - $this->currentTime = $this->currentTime->add(new \DateInterval('P1Y')); - // run background job - $bgJob = new RemoveOldTasksBackgroundJob( - \OC::$server->get(ITimeFactory::class), - $this->taskMapper, - \OC::$server->get(LoggerInterface::class), - ); - $bgJob->setArgument([]); - $bgJob->start($this->jobList); - - $this->expectException(NotFoundException::class); - $this->languageModelManager->getTask($task->getId()); - } -} diff --git a/tests/lib/TextProcessing/TextProcessingTest.php b/tests/lib/TextProcessing/TextProcessingTest.php new file mode 100644 index 00000000000..797571019ce --- /dev/null +++ b/tests/lib/TextProcessing/TextProcessingTest.php @@ -0,0 +1,338 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\TextProcessing; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Bootstrap\RegistrationContext; +use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\EventDispatcher\EventDispatcher; +use OC\TextProcessing\Db\Task as DbTask; +use OC\TextProcessing\Db\TaskMapper; +use OC\TextProcessing\Manager; +use OC\TextProcessing\RemoveOldTasksBackgroundJob; +use OC\TextProcessing\TaskBackgroundJob; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Common\Exception\NotFoundException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IServerContainer; +use OCP\TextProcessing\Events\TaskFailedEvent; +use OCP\TextProcessing\Events\TaskSuccessfulEvent; +use OCP\TextProcessing\FreePromptTaskType; +use OCP\TextProcessing\IManager; +use OCP\TextProcessing\IProvider; +use OCP\TextProcessing\SummaryTaskType; +use OCP\PreConditionNotMetException; +use OCP\TextProcessing\Task; +use OCP\TextProcessing\TopicsTaskType; +use PHPUnit\Framework\Constraint\IsInstanceOf; +use Psr\Log\LoggerInterface; +use Test\BackgroundJob\DummyJobList; + +class SuccessfulSummaryProvider implements IProvider { + public bool $ran = false; + + public function getName(): string { + return 'TEST Vanilla LLM Provider'; + } + + public function process(string $prompt): string { + $this->ran = true; + return $prompt . ' Summarize'; + } + + public function getTaskType(): string { + return SummaryTaskType::class; + } +} + +class FailingSummaryProvider implements IProvider { + public bool $ran = false; + + public function getName(): string { + return 'TEST Vanilla LLM Provider'; + } + + public function process(string $prompt): string { + $this->ran = true; + throw new \Exception('ERROR'); + } + + public function getTaskType(): string { + return SummaryTaskType::class; + } +} + +class FreePromptProvider implements IProvider { + public bool $ran = false; + + public function getName(): string { + return 'TEST Free Prompt Provider'; + } + + public function process(string $prompt): string { + $this->ran = true; + return $prompt . ' Free Prompt'; + } + + public function getTaskType(): string { + return FreePromptTaskType::class; + } +} + +class TextProcessingTest extends \Test\TestCase { + private IManager $manager; + private Coordinator $coordinator; + + protected function setUp(): void { + parent::setUp(); + + $this->providers = [ + SuccessfulSummaryProvider::class => new SuccessfulSummaryProvider(), + FailingSummaryProvider::class => new FailingSummaryProvider(), + FreePromptProvider::class => new FreePromptProvider(), + ]; + + $this->serverContainer = $this->createMock(IServerContainer::class); + $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) { + return $this->providers[$class]; + }); + + $this->eventDispatcher = new EventDispatcher( + new \Symfony\Component\EventDispatcher\EventDispatcher(), + $this->serverContainer, + \OC::$server->get(LoggerInterface::class), + ); + + $this->registrationContext = $this->createMock(RegistrationContext::class); + $this->coordinator = $this->createMock(Coordinator::class); + $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext); + + $this->currentTime = new \DateTimeImmutable('now'); + + $this->taskMapper = $this->createMock(TaskMapper::class); + $this->tasksDb = []; + $this->taskMapper + ->expects($this->any()) + ->method('insert') + ->willReturnCallback(function (DbTask $task) { + $task->setId(count($this->tasksDb) ? max(array_keys($this->tasksDb)) : 1); + $task->setLastUpdated($this->currentTime->getTimestamp()); + $this->tasksDb[$task->getId()] = $task->toRow(); + return $task; + }); + $this->taskMapper + ->expects($this->any()) + ->method('update') + ->willReturnCallback(function (DbTask $task) { + $task->setLastUpdated($this->currentTime->getTimestamp()); + $this->tasksDb[$task->getId()] = $task->toRow(); + return $task; + }); + $this->taskMapper + ->expects($this->any()) + ->method('find') + ->willReturnCallback(function (int $id) { + if (!isset($this->tasksDb[$id])) { + throw new DoesNotExistException('Could not find it'); + } + return DbTask::fromRow($this->tasksDb[$id]); + }); + $this->taskMapper + ->expects($this->any()) + ->method('deleteOlderThan') + ->willReturnCallback(function (int $timeout) { + $this->tasksDb = array_filter($this->tasksDb, function (array $task) use ($timeout) { + return $task['last_updated'] >= $this->currentTime->getTimestamp() - $timeout; + }); + }); + + $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']); + $this->jobList->expects($this->any())->method('add')->willReturnCallback(function () { + }); + + $this->manager = new Manager( + $this->serverContainer, + $this->coordinator, + \OC::$server->get(LoggerInterface::class), + $this->jobList, + $this->taskMapper, + ); + } + + public function testShouldNotHaveAnyProviders() { + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]); + $this->assertCount(0, $this->manager->getAvailableTaskTypes()); + $this->assertFalse($this->manager->hasProviders()); + $this->expectException(PreConditionNotMetException::class); + $this->manager->runTask(new \OCP\TextProcessing\Task(FreePromptTaskType::class, 'Hello', 'test', null)); + } + + public function testProviderShouldBeRegisteredAndRun() { + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ + new ServiceRegistration('test', SuccessfulSummaryProvider::class) + ]); + $this->assertCount(1, $this->manager->getAvailableTaskTypes()); + $this->assertTrue($this->manager->hasProviders()); + $this->assertEquals('Hello Summarize', $this->manager->runTask(new Task(SummaryTaskType::class, 'Hello', 'test', null))); + + // Summaries are not implemented by the vanilla provider, only free prompt + $this->expectException(PreConditionNotMetException::class); + $this->manager->runTask(new Task(FreePromptTaskType::class, 'Hello', 'test', null)); + } + + public function testProviderShouldBeRegisteredAndScheduled() { + // register provider + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ + new ServiceRegistration('test', SuccessfulSummaryProvider::class) + ]); + $this->assertCount(1, $this->manager->getAvailableTaskTypes()); + $this->assertTrue($this->manager->hasProviders()); + + // create task object + $task = new Task(SummaryTaskType::class, 'Hello', 'test', null); + $this->assertNull($task->getId()); + $this->assertNull($task->getOutput()); + + // schedule works + $this->assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); + $this->manager->scheduleTask($task); + + // Task object is up-to-date + $this->assertNotNull($task->getId()); + $this->assertNull($task->getOutput()); + $this->assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); + + // Task object retrieved from db is up-to-date + $task2 = $this->manager->getTask($task->getId()); + $this->assertEquals($task->getId(), $task2->getId()); + $this->assertEquals('Hello', $task2->getInput()); + $this->assertNull($task2->getOutput()); + $this->assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class)); + + // run background job + $bgJob = new TaskBackgroundJob( + \OC::$server->get(ITimeFactory::class), + $this->manager, + $this->eventDispatcher, + ); + $bgJob->setArgument(['taskId' => $task->getId()]); + $bgJob->start($this->jobList); + $provider = $this->providers[SuccessfulSummaryProvider::class]; + $this->assertTrue($provider->ran); + + // Task object retrieved from db is up-to-date + $task3 = $this->manager->getTask($task->getId()); + $this->assertEquals($task->getId(), $task3->getId()); + $this->assertEquals('Hello', $task3->getInput()); + $this->assertEquals('Hello Summarize', $task3->getOutput()); + $this->assertEquals(Task::STATUS_SUCCESSFUL, $task3->getStatus()); + } + + public function testMultipleProvidersShouldBeRegisteredAndRunCorrectly() { + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ + new ServiceRegistration('test', SuccessfulSummaryProvider::class), + new ServiceRegistration('test', FreePromptProvider::class), + ]); + $this->assertCount(2, $this->manager->getAvailableTaskTypes()); + $this->assertTrue($this->manager->hasProviders()); + + // Try free prompt again + $this->assertEquals('Hello Free Prompt', $this->manager->runTask(new Task(FreePromptTaskType::class, 'Hello', 'test', null))); + + // Try summary task + $this->assertEquals('Hello Summarize', $this->manager->runTask(new Task(SummaryTaskType::class, 'Hello', 'test', null))); + + // Topics are not implemented by both the vanilla provider and the full provider + $this->expectException(PreConditionNotMetException::class); + $this->manager->runTask(new Task(TopicsTaskType::class, 'Hello', 'test', null)); + } + + public function testNonexistentTask() { + $this->expectException(NotFoundException::class); + $this->manager->getTask(98765432456); + } + + public function testTaskFailure() { + // register provider + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ + new ServiceRegistration('test', FailingSummaryProvider::class), + ]); + $this->assertCount(1, $this->manager->getAvailableTaskTypes()); + $this->assertTrue($this->manager->hasProviders()); + + // create task object + $task = new Task(SummaryTaskType::class, 'Hello', 'test', null); + $this->assertNull($task->getId()); + $this->assertNull($task->getOutput()); + + // schedule works + $this->assertEquals(Task::STATUS_UNKNOWN, $task->getStatus()); + $this->manager->scheduleTask($task); + + // Task object is up-to-date + $this->assertNotNull($task->getId()); + $this->assertNull($task->getOutput()); + $this->assertEquals(Task::STATUS_SCHEDULED, $task->getStatus()); + + // Task object retrieved from db is up-to-date + $task2 = $this->manager->getTask($task->getId()); + $this->assertEquals($task->getId(), $task2->getId()); + $this->assertEquals('Hello', $task2->getInput()); + $this->assertNull($task2->getOutput()); + $this->assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus()); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class)); + + // run background job + $bgJob = new TaskBackgroundJob( + \OC::$server->get(ITimeFactory::class), + $this->manager, + $this->eventDispatcher, + ); + $bgJob->setArgument(['taskId' => $task->getId()]); + $bgJob->start($this->jobList); + $provider = $this->providers[FailingSummaryProvider::class]; + $this->assertTrue($provider->ran); + + // Task object retrieved from db is up-to-date + $task3 = $this->manager->getTask($task->getId()); + $this->assertEquals($task->getId(), $task3->getId()); + $this->assertEquals('Hello', $task3->getInput()); + $this->assertNull($task3->getOutput()); + $this->assertEquals(Task::STATUS_FAILED, $task3->getStatus()); + } + + public function testOldTasksShouldBeCleanedUp() { + $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([ + new ServiceRegistration('test', SuccessfulSummaryProvider::class) + ]); + $this->assertCount(1, $this->manager->getAvailableTaskTypes()); + $this->assertTrue($this->manager->hasProviders()); + $task = new Task(SummaryTaskType::class, 'Hello', 'test', null); + $this->assertEquals('Hello Summarize', $this->manager->runTask($task)); + + $this->currentTime = $this->currentTime->add(new \DateInterval('P1Y')); + // run background job + $bgJob = new RemoveOldTasksBackgroundJob( + \OC::$server->get(ITimeFactory::class), + $this->taskMapper, + \OC::$server->get(LoggerInterface::class), + ); + $bgJob->setArgument([]); + $bgJob->start($this->jobList); + + $this->expectException(NotFoundException::class); + $this->manager->getTask($task->getId()); + } +}