]> source.dussan.org Git - nextcloud-server.git/commitdiff
Massive refactoring: Turn LanguageModel OCP API into TextProcessing API
authorMarcel Klehr <mklehr@gmx.net>
Fri, 14 Jul 2023 13:59:50 +0000 (15:59 +0200)
committerMarcel Klehr <mklehr@gmx.net>
Wed, 9 Aug 2023 08:05:05 +0000 (10:05 +0200)
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
(cherry picked from commit ffe27ce14ca74b509c8721c9fba7c759498fa471)

45 files changed:
core/Controller/LanguageModelApiController.php [deleted file]
core/Controller/TextProcessingApiController.php [new file with mode: 0644]
core/routes.php
lib/private/AppFramework/Bootstrap/RegistrationContext.php
lib/private/LanguageModel/Db/Task.php [deleted file]
lib/private/LanguageModel/Db/TaskMapper.php [deleted file]
lib/private/LanguageModel/LanguageModelManager.php [deleted file]
lib/private/LanguageModel/RemoveOldTasksBackgroundJob.php [deleted file]
lib/private/LanguageModel/TaskBackgroundJob.php [deleted file]
lib/private/Repair/AddRemoveOldTasksBackgroundJob.php
lib/private/Server.php
lib/private/Setup.php
lib/private/TextProcessing/Db/Task.php [new file with mode: 0644]
lib/private/TextProcessing/Db/TaskMapper.php [new file with mode: 0644]
lib/private/TextProcessing/Manager.php [new file with mode: 0644]
lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php [new file with mode: 0644]
lib/private/TextProcessing/TaskBackgroundJob.php [new file with mode: 0644]
lib/public/AppFramework/Bootstrap/IRegistrationContext.php
lib/public/LanguageModel/AbstractLanguageModelTask.php [deleted file]
lib/public/LanguageModel/Events/AbstractLanguageModelEvent.php [deleted file]
lib/public/LanguageModel/Events/TaskFailedEvent.php [deleted file]
lib/public/LanguageModel/Events/TaskSuccessfulEvent.php [deleted file]
lib/public/LanguageModel/FreePromptTask.php [deleted file]
lib/public/LanguageModel/HeadlineTask.php [deleted file]
lib/public/LanguageModel/IHeadlineProvider.php [deleted file]
lib/public/LanguageModel/ILanguageModelManager.php [deleted file]
lib/public/LanguageModel/ILanguageModelProvider.php [deleted file]
lib/public/LanguageModel/ILanguageModelTask.php [deleted file]
lib/public/LanguageModel/ISummaryProvider.php [deleted file]
lib/public/LanguageModel/ITopicsProvider.php [deleted file]
lib/public/LanguageModel/SummaryTask.php [deleted file]
lib/public/LanguageModel/TopicsTask.php [deleted file]
lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php [new file with mode: 0644]
lib/public/TextProcessing/Events/TaskFailedEvent.php [new file with mode: 0644]
lib/public/TextProcessing/Events/TaskSuccessfulEvent.php [new file with mode: 0644]
lib/public/TextProcessing/FreePromptTaskType.php [new file with mode: 0644]
lib/public/TextProcessing/HeadlineTaskType.php [new file with mode: 0644]
lib/public/TextProcessing/IManager.php [new file with mode: 0644]
lib/public/TextProcessing/IProvider.php [new file with mode: 0644]
lib/public/TextProcessing/ITaskType.php [new file with mode: 0644]
lib/public/TextProcessing/SummaryTaskType.php [new file with mode: 0644]
lib/public/TextProcessing/Task.php [new file with mode: 0644]
lib/public/TextProcessing/TopicsTaskType.php [new file with mode: 0644]
tests/lib/LanguageModel/LanguageModelManagerTest.php [deleted file]
tests/lib/TextProcessing/TextProcessingTest.php [new file with mode: 0644]

diff --git a/core/Controller/LanguageModelApiController.php b/core/Controller/LanguageModelApiController.php
deleted file mode 100644 (file)
index 74ed26e..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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<Http::STATUS_OK, array{types: string[]}, array{}>
-        *
-        * 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<Http::STATUS_OK, array{task: array{id: ?int, type: string, status: int, userId: ?string, appId: string, input: string, output: ?string, identifier: string}}, array{}>|DataResponse<Http::STATUS_PRECONDITION_FAILED|Http::STATUS_BAD_REQUEST, array{message: string}, array{}>
-        *
-        * 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<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message:string}, array{}>|DataResponse<Http::STATUS_OK, array{task: array{id: ?int, type: string, status: int, userId: ?string, appId: string, input: string, output: ?string, identifier: string}}, array{}>
-        *
-        * 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 (file)
index 0000000..7cc7199
--- /dev/null
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+
+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<Http::STATUS_OK, array{types: list<array{id: string, name: string, description: string}>}, array{}>
+        *
+        * 200: Task types returned
+        */
+       public function taskTypes(): DataResponse {
+               $typeClasses = $this->languageModelManager->getAvailableTaskTypes();
+               /** @var list<array{id: string, name: string, description: string}> $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<Http::STATUS_OK, array{task: array{id: ?int, type: string, status: int, userId: ?string, appId: string, input: string, output: ?string, identifier: string}}, array{}>|DataResponse<Http::STATUS_PRECONDITION_FAILED|Http::STATUS_BAD_REQUEST, array{message: string}, array{}>
+        *
+        * 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<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message:string}, array{}>|DataResponse<Http::STATUS_OK, array{task: array{id: ?int, type: string, status: int, userId: ?string, appId: string, input: string, output: ?string, identifier: string}}, array{}>
+        *
+        * 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);
+               }
+       }
+}
index 24af95cd7af3a2c9fcad9b2c12faafefa7f30521..4790f32af32306bfc02291259be2d00ec4fe04a0 100644 (file)
@@ -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'],
        ],
 ]);
 
index 67e8b390c158b9bc69bf0abfc7d82d901e77c60e..5aea2a7a744b0deab02ed1d3b8d7d86a06ebc438 100644 (file)
@@ -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<ISpeechToTextProvider>[] */
        private $speechToTextProviders = [];
 
-       /** @var ServiceRegistration<ILanguageModelProvider>[] */
-       private $languageModelProviders = [];
+       /** @var ServiceRegistration<ITextProcessingProvider>[] */
+       private $textProcessingProviders = [];
 
        /** @var ServiceRegistration<ICustomTemplateProvider>[] */
        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<ILanguageModelProvider>[]
+        * @return ServiceRegistration<ITextProcessingProvider>[]
         */
-       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 (file)
index 4e46f19..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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 (file)
index 9b93ea1..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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<Task>
- */
-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 (file)
index 970d968..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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 (file)
index fa3a716..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index 5ac37ba..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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);
-       }
-}
index 713192b06f957efeae06b077329bcd1897db413f..94ae39f2183e2919ad89037b21b05232306e4de3 100644 (file)
@@ -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;
index d1f18a1235f03c23471450074c6eaace61376860..516dc0b6e8b0c6bd16832078505fbcc5205fb0ef 100644 (file)
@@ -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;
@@ -229,7 +228,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;
@@ -1463,7 +1461,7 @@ class Server extends ServerContainer implements IServerContainer {
 
                $this->registerAlias(ISpeechToTextManager::class, SpeechToTextManager::class);
 
-               $this->registerAlias(ILanguageModelManager::class, LanguageModelManager::class);
+               $this->registerAlias(\OCP\TextProcessing\IManager::class, \OC\TextProcessing\Manager::class);
 
                $this->connectDispatcher();
        }
index a80afee6d86c22daf6845d6d27ceb9db7f9b1498..3c1ac559c87a051cbe5e5c8c15708a3b3c15a49f 100644 (file)
@@ -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 (file)
index 0000000..bc1bbdc
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..508f3fd
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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<Task>
+ */
+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 (file)
index 0000000..34e4b2b
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..89d329a
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+
+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 (file)
index 0000000..4c24b3e
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+
+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);
+       }
+}
index 66435d4593466d20d13d492d0bfcc9ff5a79dd0b..720803a78d170866377712fcbb9c7af726d4e1d3 100644 (file)
@@ -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<ILanguageModelProvider> $providerClass
+        * @psalm-param class-string<ITextProcessingProvider> $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 (file)
index 91b81b9..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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<T>
- */
-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 (file)
index c8abc73..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- *
- */
-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 (file)
index f42203a..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace OCP\LanguageModel\Events;
-
-use OCP\LanguageModel\ILanguageModelTask;
-
-/**
- * @since 27.1.0
- */
-class TaskFailedEvent extends AbstractLanguageModelEvent {
-       /**
-        * @param ILanguageModelTask $task
-        * @param string $errorMessage
-        * @since 27.1.0
-        */
-       public function __construct(
-               ILanguageModelTask $task,
-               private string $errorMessage,
-       ) {
-               parent::__construct($task);
-       }
-
-       /**
-        * @return string
-        * @since 27.1.0
-        */
-       public function getErrorMessage(): string {
-               return $this->errorMessage;
-       }
-}
diff --git a/lib/public/LanguageModel/Events/TaskSuccessfulEvent.php b/lib/public/LanguageModel/Events/TaskSuccessfulEvent.php
deleted file mode 100644 (file)
index 77a61ac..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-
-namespace OCP\LanguageModel\Events;
-
-use OCP\LanguageModel\ILanguageModelTask;
-
-/**
- * @since 27.1.0
- */
-class TaskSuccessfulEvent extends AbstractLanguageModelEvent {
-       /**
-        * @param ILanguageModelTask $task
-        * @since 27.1.0
-        */
-       public function __construct(ILanguageModelTask $task) {
-               parent::__construct($task);
-       }
-}
diff --git a/lib/public/LanguageModel/FreePromptTask.php b/lib/public/LanguageModel/FreePromptTask.php
deleted file mode 100644 (file)
index 560d6e7..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-namespace OCP\LanguageModel;
-
-/**
- * @since 27.1.0
- * @template-extends AbstractLanguageModelTask<ILanguageModelProvider>
- */
-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 (file)
index 4c62b97..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-namespace OCP\LanguageModel;
-
-/**
- * This LanguageModel Task represents headline generation
- * which generates a headline for the passed text
- * @since 27.1.0
- * @template-extends AbstractLanguageModelTask<IHeadlineProvider>
- */
-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 (file)
index 30185f4..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2022 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index 0afc99b..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index 34e7eb6..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index 0f552c8..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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 (file)
index c286e74..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2022 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index f061976..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-
-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 (file)
index 4786453..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-namespace OCP\LanguageModel;
-
-/**
- * This is an absctract LanguageModel Task represents summarization
- * which sums up the passed text.
- * @since 27.1.0
- * @template-extends AbstractLanguageModelTask<ISummaryProvider>
- */
-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 (file)
index ab2c591..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/**
- * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- *
- * @author Marcel Klehr <mklehr@gmx.net>
- *
- * @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 <http://www.gnu.org/licenses/>.
- */
-
-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<ITopicsProvider>
- */
-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 (file)
index 0000000..10c592f
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..f9765e3
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace OCP\TextProcessing\Events;
+
+use OCP\TextProcessing\Task;
+
+/**
+ * @since 27.1.0
+ */
+class TaskFailedEvent extends AbstractTextProcessingEvent {
+       /**
+        * @param Task $task
+        * @param string $errorMessage
+        * @since 27.1.0
+        */
+       public function __construct(
+               Task $task,
+               private string $errorMessage,
+       ) {
+               parent::__construct($task);
+       }
+
+       /**
+        * @return string
+        * @since 27.1.0
+        */
+       public function getErrorMessage(): string {
+               return $this->errorMessage;
+       }
+}
diff --git a/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php b/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php
new file mode 100644 (file)
index 0000000..73fbbb8
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+namespace OCP\TextProcessing\Events;
+
+use OCP\TextProcessing\Task;
+
+/**
+ * @since 27.1.0
+ */
+class TaskSuccessfulEvent extends AbstractTextProcessingEvent {
+       /**
+        * @param Task $task
+        * @since 27.1.0
+        */
+       public function __construct(Task $task) {
+               parent::__construct($task);
+       }
+}
diff --git a/lib/public/TextProcessing/FreePromptTaskType.php b/lib/public/TextProcessing/FreePromptTaskType.php
new file mode 100644 (file)
index 0000000..aa1d684
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..4ced298
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..90e2589
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+
+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<ITaskType>[]
+        * @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 (file)
index 0000000..3eb83ae
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+
+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<T>
+        */
+       public function getTaskType(): string;
+}
diff --git a/lib/public/TextProcessing/ITaskType.php b/lib/public/TextProcessing/ITaskType.php
new file mode 100644 (file)
index 0000000..d08da3f
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..7db695c
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..59cd38b
--- /dev/null
@@ -0,0 +1,235 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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<T> $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<T> $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<T> $provider
+        * @param IProvider $provider
+        * @return bool
+        * @since 27.1.0
+        */
+       public function canUseProvider(IProvider $provider): bool {
+               return $provider->getTaskType() === $this->getType();
+       }
+
+       /**
+        * @return class-string<T>
+        * @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<T>, 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 (file)
index 0000000..8b41b3e
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ *
+ * @author Marcel Klehr <mklehr@gmx.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 6f8d6cd..0000000
+++ /dev/null
@@ -1,343 +0,0 @@
-<?php
-/**
- * Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
- * 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 (file)
index 0000000..7975710
--- /dev/null
@@ -0,0 +1,338 @@
+<?php
+/**
+ * Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
+ * 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());
+       }
+}