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

Feat: make taskprocessing task types toggleable #49727

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion apps/settings/lib/Controller/AISettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function __construct(
*/
#[AuthorizedAdminSetting(settings: ArtificialIntelligence::class)]
public function update($settings) {
$keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.taskprocessing_provider_preferences', 'ai.translation_provider_preferences', 'ai.text2image_provider'];
$keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.taskprocessing_provider_preferences','ai.taskprocessing_type_preferences', 'ai.translation_provider_preferences', 'ai.text2image_provider'];
foreach ($keys as $key) {
if (!isset($settings[$key])) {
continue;
Expand Down
8 changes: 6 additions & 2 deletions apps/settings/lib/Settings/Admin/ArtificialIntelligence.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@ public function getForm() {
}
}
$taskProcessingTaskTypes = [];
foreach ($this->taskProcessingManager->getAvailableTaskTypes() as $taskTypeId => $taskTypeDefinition) {
$taskProcessingTypeSettings = [];
foreach ($this->taskProcessingManager->getAvailableTaskTypes(true) as $taskTypeId => $taskTypeDefinition) {
$taskProcessingTaskTypes[] = [
'id' => $taskTypeId,
'name' => $taskTypeDefinition['name'],
'description' => $taskTypeDefinition['description'],
];
$taskProcessingTypeSettings[$taskTypeId] = true;
}

$this->initialState->provideInitialState('ai-stt-providers', $sttProviders);
Expand All @@ -135,14 +137,16 @@ public function getForm() {
'ai.textprocessing_provider_preferences' => $textProcessingSettings,
'ai.text2image_provider' => count($text2imageProviders) > 0 ? $text2imageProviders[0]['id'] : null,
'ai.taskprocessing_provider_preferences' => $taskProcessingSettings,
'ai.taskprocessing_type_preferences' => $taskProcessingTypeSettings,
];
foreach ($settings as $key => $defaultValue) {
$value = $defaultValue;
$json = $this->config->getAppValue('core', $key, '');
if ($json !== '') {
$value = json_decode($json, true);
$value = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
switch ($key) {
case 'ai.taskprocessing_provider_preferences':
case 'ai.taskprocessing_type_preferences':
case 'ai.textprocessing_provider_preferences':
// fill $value with $defaultValue values
$value = array_merge($defaultValue, $value);
Expand Down
7 changes: 6 additions & 1 deletion apps/settings/src/components/AdminAI.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
<div :key="type">
<h3>{{ t('settings', 'Task:') }} {{ type.name }}</h3>
<p>{{ type.description }}</p>
<p>&nbsp;</p>
<NcCheckboxRadioSwitch v-model="settings['ai.taskprocessing_type_preferences'][type.id]"
type="switch"
@update:modelValue="saveChanges">
{{ t('settings', 'Enable') }}
</NcCheckboxRadioSwitch>
<NcSelect v-model="settings['ai.taskprocessing_provider_preferences'][type.id]"
class="provider-select"
:clearable="false"
:disabled="!settings['ai.taskprocessing_type_preferences'][type.id]"
:options="taskProcessingProviders.filter(p => p.taskType === type.id).map(p => p.id)"
@input="saveChanges">
<template #option="{label}">
Expand Down
56 changes: 56 additions & 0 deletions core/Command/TaskProcessing/EnabledCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Command\TaskProcessing;

use OC\Core\Command\Base;
use OCP\IConfig;
use OCP\TaskProcessing\IManager;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class EnabledCommand extends Base {
public function __construct(
protected IManager $taskProcessingManager,
private IConfig $config,
) {
parent::__construct();
}

protected function configure() {
$this
->setName('taskprocessing:task-type:set-enabled')
->setDescription('Enable or disable a task type')
->addArgument(
'task-type-id',
InputArgument::REQUIRED,
'ID of the task type to configure'
)
->addArgument(
'enabled',
InputArgument::REQUIRED,
'status of the task type availability. Set 1 to enable and 0 to disable.'
);
parent::configure();
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$enabled = (bool)$input->getArgument('enabled');
$taskType = $input->getArgument('task-type-id');
$json = $this->config->getAppValue('core', 'ai.taskprocessing_type_preferences');
if ($json === '') {
$taskTypeSettings = [];
} else {
$taskTypeSettings = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
}

$taskTypeSettings[$taskType] = $enabled;

$this->config->setAppValue('core', 'ai.taskprocessing_type_preferences', json_encode($taskTypeSettings));
$this->writeArrayInOutputFormat($input, $output, $taskTypeSettings);
return 0;
}
}
1 change: 1 addition & 0 deletions core/register_command.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
$application->add(Server::get(Command\FilesMetadata\Get::class));

$application->add(Server::get(Command\TaskProcessing\GetCommand::class));
$application->add(Server::get(Command\TaskProcessing\EnabledCommand::class));
$application->add(Server::get(Command\TaskProcessing\ListCommand::class));
$application->add(Server::get(Command\TaskProcessing\Statistics::class));

Expand Down
4 changes: 2 additions & 2 deletions dist/settings-vue-settings-admin-ai.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/settings-vue-settings-admin-ai.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,7 @@
'OC\\Core\\Command\\SystemTag\\Delete' => $baseDir . '/core/Command/SystemTag/Delete.php',
'OC\\Core\\Command\\SystemTag\\Edit' => $baseDir . '/core/Command/SystemTag/Edit.php',
'OC\\Core\\Command\\SystemTag\\ListCommand' => $baseDir . '/core/Command/SystemTag/ListCommand.php',
'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => $baseDir . '/core/Command/TaskProcessing/EnabledCommand.php',
'OC\\Core\\Command\\TaskProcessing\\GetCommand' => $baseDir . '/core/Command/TaskProcessing/GetCommand.php',
'OC\\Core\\Command\\TaskProcessing\\ListCommand' => $baseDir . '/core/Command/TaskProcessing/ListCommand.php',
'OC\\Core\\Command\\TaskProcessing\\Statistics' => $baseDir . '/core/Command/TaskProcessing/Statistics.php',
Expand Down
1 change: 1 addition & 0 deletions lib/composer/composer/autoload_psr4.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
'OC\\' => array($baseDir . '/lib/private'),
'OCP\\' => array($baseDir . '/lib/public'),
'NCU\\' => array($baseDir . '/lib/unstable'),
'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'),
'' => array($baseDir . '/lib/private/legacy'),
);
9 changes: 9 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
'NCU\\' => 4,
),
'B' =>
array (
'Bamarni\\Composer\\Bin\\' => 21,
),
);

public static $prefixDirsPsr4 = array (
Expand All @@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
'Bamarni\\Composer\\Bin\\' =>
array (
0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
),
);

public static $fallbackDirsPsr4 = array (
Expand Down Expand Up @@ -1304,6 +1312,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\SystemTag\\Delete' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Delete.php',
'OC\\Core\\Command\\SystemTag\\Edit' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Edit.php',
'OC\\Core\\Command\\SystemTag\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/SystemTag/ListCommand.php',
'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/EnabledCommand.php',
'OC\\Core\\Command\\TaskProcessing\\GetCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/GetCommand.php',
'OC\\Core\\Command\\TaskProcessing\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/ListCommand.php',
'OC\\Core\\Command\\TaskProcessing\\Statistics' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/Statistics.php',
Expand Down
26 changes: 24 additions & 2 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,17 @@ private function _getTaskTypes(): array {
return $taskTypes;
}

/**
* @return array
*/
private function _getTaskTypeSettings(): array {
$json = $this->config->getAppValue('core', 'ai.taskprocessing_type_preferences', '');
if ($json === '') {
return [];
}
return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that this wasn't clear, we should also handle the JSON exceptions somewhere, so they don't break the whole server and cause a HTTP 500.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think should happen if the value is not parsable, return all task types or return none? I would say none so we do not enable something that should be disabled?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense. Perhaps, we can also reset the value in the config to a valid json value when this happens?

}

/**
* @param ShapeDescriptor[] $spec
* @param array<array-key, string|numeric> $defaults
Expand Down Expand Up @@ -721,12 +732,17 @@ public function getPreferredProvider(string $taskTypeId) {
throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
}

public function getAvailableTaskTypes(): array {
if ($this->availableTaskTypes === null) {
public function getAvailableTaskTypes(bool $showDisabled = false): array {
// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
if ($this->availableTaskTypes === null || $showDisabled) {
$taskTypes = $this->_getTaskTypes();
$taskTypeSettings = $this->_getTaskTypeSettings();

$availableTaskTypes = [];
foreach ($taskTypes as $taskType) {
if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
continue;
}
try {
$provider = $this->getPreferredProvider($taskType->getId());
} catch (\OCP\TaskProcessing\Exception\Exception $e) {
Expand All @@ -752,9 +768,15 @@ public function getAvailableTaskTypes(): array {
}
}

if ($showDisabled) {
// Do not cache showDisabled, ever.
return $availableTaskTypes;
}

$this->availableTaskTypes = $availableTaskTypes;
}


return $this->availableTaskTypes;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/public/TaskProcessing/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ public function getProviders(): array;
public function getPreferredProvider(string $taskTypeId);

/**
* @param bool $showDisabled if false, disabled task types will be filtered
marcelklehr marked this conversation as resolved.
Show resolved Hide resolved
* @return array<string, array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
* @since 30.0.0
* @since 31.0.0 Added the `showDisabled` argument.
*/
public function getAvailableTaskTypes(): array;
public function getAvailableTaskTypes(bool $showDisabled = false): array;

/**
* @param Task $task The task to run
Expand Down
66 changes: 59 additions & 7 deletions tests/lib/TaskProcessing/TaskProcessingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ public function getOptionalOutputShapeEnumValues(): array {
}
}



class FailingSyncProvider implements IProvider, ISynchronousProvider {
public const ERROR_MESSAGE = 'Failure';
public function getId(): string {
Expand Down Expand Up @@ -387,6 +389,7 @@ public function getExpectedRuntime(): int {
*/
class TaskProcessingTest extends \Test\TestCase {
private IManager $manager;
private IManager $disabledTypeManager;
private Coordinator $coordinator;
private array $providers;
private IServerContainer $serverContainer;
Expand All @@ -396,6 +399,7 @@ class TaskProcessingTest extends \Test\TestCase {
private IJobList $jobList;
private IUserMountCache $userMountCache;
private IRootFolder $rootFolder;
private IConfig $config;

public const TEST_USER = 'testuser';

Expand Down Expand Up @@ -442,11 +446,6 @@ protected function setUp(): void {
$this->jobList->expects($this->any())->method('add')->willReturnCallback(function () {
});

$config = $this->createMock(IConfig::class);
$config->method('getAppValue')
->with('core', 'ai.textprocessing_provider_preferences', '')
->willReturn('');

marcelklehr marked this conversation as resolved.
Show resolved Hide resolved
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

$text2imageManager = new \OC\TextToImage\Manager(
Expand All @@ -460,9 +459,9 @@ protected function setUp(): void {
);

$this->userMountCache = $this->createMock(IUserMountCache::class);

$this->config = \OC::$server->get(IConfig::class);
$this->manager = new Manager(
\OC::$server->get(IConfig::class),
$this->config,
$this->coordinator,
$this->serverContainer,
\OC::$server->get(LoggerInterface::class),
Expand Down Expand Up @@ -492,7 +491,24 @@ public function testShouldNotHaveAnyProviders(): void {
$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
}

public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void {
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSyncProvider::class)
]);
$taskProcessingTypeSettings = [
TextToText::ID => false,
];
$this->config->setAppValue('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings));
self::assertCount(0, $this->manager->getAvailableTaskTypes());
self::assertCount(1, $this->manager->getAvailableTaskTypes(true));
self::assertTrue($this->manager->hasProviders());
self::expectException(\OCP\TaskProcessing\Exception\PreConditionNotMetException::class);
$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
marcelklehr marked this conversation as resolved.
Show resolved Hide resolved
}


public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
$this->config->setAppValue('core', 'ai.taskprocessing_type_preferences', '');
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
new ServiceRegistration('test', BrokenSyncProvider::class)
]);
Expand Down Expand Up @@ -630,6 +646,42 @@ public function testProviderShouldBeRegisteredAndRun(): void {
self::assertEquals(1, $task->getProgress());
}

public function testTaskTypeExplicitlyEnabled(): void {
$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSyncProvider::class)
]);

$taskProcessingTypeSettings = [
TextToText::ID => true,
];
$this->config->setAppValue('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings));

self::assertCount(1, $this->manager->getAvailableTaskTypes());

self::assertTrue($this->manager->hasProviders());
$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
self::assertNull($task->getId());
self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
$this->manager->scheduleTask($task);
self::assertNotNull($task->getId());
self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());

$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));

$backgroundJob = new \OC\TaskProcessing\SynchronousBackgroundJob(
\OCP\Server::get(ITimeFactory::class),
$this->manager,
$this->jobList,
\OCP\Server::get(LoggerInterface::class),
);
$backgroundJob->start($this->jobList);

$task = $this->manager->getTask($task->getId());
self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
self::assertEquals(['output' => 'Hello'], $task->getOutput());
self::assertEquals(1, $task->getProgress());
}

public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void {
$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
new ServiceRegistration('test', AudioToImage::class)
Expand Down
Loading