Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New OCS endpoint to list text processing tasks #39680

Merged
merged 6 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 74 additions & 19 deletions core/Controller/TextProcessingApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
use InvalidArgumentException;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\Common\Exception\NotFoundException;
use OCP\IL10N;
Expand All @@ -47,26 +51,25 @@
*/
class TextProcessingApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private IManager $languageModelManager,
private IL10N $l,
private ?string $userId,
string $appName,
IRequest $request,
private IManager $textProcessingManager,
private IL10N $l,
private ?string $userId,
private ContainerInterface $container,
private LoggerInterface $logger,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}

/**
* This endpoint returns all available LanguageModel task types
*
* @PublicPage
*
* @return DataResponse<Http::STATUS_OK, array{types: array{id: string, name: string, description: string}[]}, array{}>
*/
#[PublicPage]
public function taskTypes(): DataResponse {
$typeClasses = $this->languageModelManager->getAvailableTaskTypes();
$typeClasses = $this->textProcessingManager->getAvailableTaskTypes();
$types = [];
/** @var string $typeClass */
foreach ($typeClasses as $typeClass) {
Expand All @@ -92,10 +95,6 @@ public function taskTypes(): DataResponse {
/**
* This endpoint allows scheduling a language model task
*
* @PublicPage
* @UserRateThrottle(limit=20, period=120)
* @AnonRateThrottle(limit=5, period=120)
*
* @param string $input Input text
* @param string $type Type of the task
* @param string $appId ID of the app that will execute the task
Expand All @@ -107,14 +106,17 @@ public function taskTypes(): DataResponse {
* 400: Scheduling task is not possible
* 412: Scheduling task is not possible
*/
#[PublicPage]
#[UserRateLimit(limit: 20, period: 120)]
#[AnonRateLimit(limit: 5, period: 120)]
public function schedule(string $input, string $type, string $appId, string $identifier = ''): DataResponse {
try {
$task = new Task($type, $input, $appId, $this->userId, $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);
$this->textProcessingManager->scheduleTask($task);

$json = $task->jsonSerialize();

Expand All @@ -130,21 +132,46 @@ public function schedule(string $input, string $type, string $appId, string $ide
* 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_OK, array{task: CoreTextProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[PublicPage]
public function getTask(int $id): DataResponse {
try {
$task = $this->languageModelManager->getTask($id);
$task = $this->textProcessingManager->getUserTask($id, $this->userId);

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);
}
}

/**
* This endpoint allows to delete a scheduled task for a user
*
* @param int $id The id of the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[NoAdminRequired]
public function deleteTask(int $id): DataResponse {
marcelklehr marked this conversation as resolved.
Show resolved Hide resolved
try {
$task = $this->textProcessingManager->getUserTask($id, $this->userId);

$this->textProcessingManager->deleteTask($task);

$json = $task->jsonSerialize();

Expand All @@ -157,4 +184,32 @@ public function getTask(int $id): DataResponse {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}


/**
* This endpoint returns a list of tasks of a user that are related
* with a specific appId and optionally with an identifier
*
* @param string $appId
* @param string|null $identifier
* @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextProcessingTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task list returned
*/
#[NoAdminRequired]
public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse {
try {
$tasks = $this->textProcessingManager->getUserTasksByApp($this->userId, $appId, $identifier);
/** @var CoreTextProcessingTask[] $json */
$json = array_map(static function (Task $task) {
return $task->jsonSerialize();
}, $tasks);

return new DataResponse([
'tasks' => $json,
]);
Fixed Show fixed Hide fixed
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
66 changes: 66 additions & 0 deletions core/Migrations/Version28000Date20230803221055.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Julien Veyssier <julien-nc@posteo.net>
*
* @author Julien Veyssier <julien-nc@posteo.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\Migrations;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Adjust textprocessing_tasks table
*/
class Version28000Date20230803221055 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changed = false;

if ($schema->hasTable('textprocessing_tasks')) {
$table = $schema->getTable('textprocessing_tasks');

$column = $table->getColumn('user_id');
$column->setNotnull(false);

$table->addIndex(['user_id', 'app_id', 'identifier'], 'tp_tasks_uid_appid_ident');

$changed = true;
}

if ($changed) {
return $schema;
}

return null;
}
}
2 changes: 2 additions & 0 deletions core/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
['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'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#deleteTask', 'url' => '/task/{id}', 'verb' => 'DELETE'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#listTasksByApp', 'url' => '/tasks/app/{appId}', 'verb' => 'GET'],
],
]);

Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@
'OC\\Core\\Migrations\\Version27000Date20230309104802' => $baseDir . '/core/Migrations/Version27000Date20230309104802.php',
'OC\\Core\\Migrations\\Version28000Date20230616104802' => $baseDir . '/core/Migrations/Version28000Date20230616104802.php',
'OC\\Core\\Migrations\\Version28000Date20230728104802' => $baseDir . '/core/Migrations/Version28000Date20230728104802.php',
'OC\\Core\\Migrations\\Version28000Date20230803221055' => $baseDir . '/core/Migrations/Version28000Date20230803221055.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version27000Date20230309104802' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20230309104802.php',
'OC\\Core\\Migrations\\Version28000Date20230616104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230616104802.php',
'OC\\Core\\Migrations\\Version28000Date20230728104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230728104802.php',
'OC\\Core\\Migrations\\Version28000Date20230803221055' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230803221055.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
Expand Down
6 changes: 3 additions & 3 deletions lib/private/TextProcessing/Db/Task.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@
* @method string getOutput()
* @method setStatus(int $type)
* @method int getStatus()
* @method setUserId(string $type)
* @method string getuserId()
* @method setUserId(?string $userId)
* @method string|null getUserId()
* @method setAppId(string $type)
* @method string getAppId()
* @method setIdentifier(string $type)
* @method setIdentifier(string $identifier)
* @method string getIdentifier()
*/
class Task extends Entity {
Expand Down
40 changes: 40 additions & 0 deletions lib/private/TextProcessing/Db/TaskMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,46 @@ public function find(int $id): Task {
return $this->findEntity($qb);
}

/**
* @param int $id
* @param string|null $userId
* @return Task
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function findByIdAndUser(int $id, ?string $userId): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
if ($userId === null) {
$qb->andWhere($qb->expr()->isNull('user_id'));
} else {
$qb->andWhere($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)));
}
return $this->findEntity($qb);
}

/**
* @param string $userId
* @param string $appId
* @param string|null $identifier
* @return array
* @throws Exception
*/
public function findUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)))
->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId)));
if ($identifier !== null) {
$qb->andWhere($qb->expr()->eq('identifier', $qb->createPositionalParameter($identifier)));
juliushaertl marked this conversation as resolved.
Show resolved Hide resolved
}
return $this->findEntities($qb);
}

/**
* @param int $timeout
* @return int the number of deleted tasks
Expand Down
60 changes: 59 additions & 1 deletion lib/private/TextProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OC\AppFramework\Bootstrap\Coordinator;
use OC\TextProcessing\Db\Task as DbTask;
use OCP\IConfig;
use OCP\TextProcessing\Task;
use OCP\TextProcessing\Task as OCPTask;
use OC\TextProcessing\Db\TaskMapper;
use OCP\AppFramework\Db\DoesNotExistException;
Expand Down Expand Up @@ -178,6 +179,19 @@ public function scheduleTask(OCPTask $task): void {
}

/**
* @inheritDoc
*/
public function deleteTask(Task $task): void {
$taskEntity = DbTask::fromPublicTask($task);
$this->taskMapper->delete($taskEntity);
$this->jobList->remove(TaskBackgroundJob::class, [
'taskId' => $task->getId()
]);
}

/**
* Get a task from its id
*
* @param int $id The id of the task
* @return OCPTask
* @throws RuntimeException If the query failed
Expand All @@ -192,7 +206,51 @@ public function getTask(int $id): OCPTask {
} 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);
throw new RuntimeException('Failure while trying to find task by id: ' . $e->getMessage(), 0, $e);
}
}

/**
* Get a task from its user id and task id
* If userId is null, this can only get a task that was scheduled anonymously
*
* @param int $id The id of the task
* @param string|null $userId The user id that scheduled the task
* @return OCPTask
* @throws RuntimeException If the query failed
* @throws NotFoundException If the task could not be found
*/
public function getUserTask(int $id, ?string $userId): OCPTask {
marcelklehr marked this conversation as resolved.
Show resolved Hide resolved
try {
$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
return $taskEntity->toPublicTask();
} catch (DoesNotExistException $e) {
throw new NotFoundException('Could not find task with the provided id and user id');
} catch (MultipleObjectsReturnedException $e) {
throw new RuntimeException('Could not uniquely identify task with given id and user id', 0, $e);
} catch (Exception $e) {
throw new RuntimeException('Failure while trying to find task by id and user id: ' . $e->getMessage(), 0, $e);
}
}

/**
* Get a list of tasks scheduled by a specific user for a specific app
* and optionally with a specific identifier.
* This cannot be used to get anonymously scheduled tasks
*
* @param string $userId
* @param string $appId
* @param string|null $identifier
* @return array
*/
public function getUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array {
try {
$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier);
return array_map(static function (DbTask $taskEntity) {
return $taskEntity->toPublicTask();
}, $taskEntities);
} catch (Exception $e) {
throw new RuntimeException('Failure while trying to find tasks by appId and identifier: ' . $e->getMessage(), 0, $e);
}
}
}
Loading
Loading