Skip to content

Commit

Permalink
Merge pull request #49727 from nextcloud/feat/make-tasks-types-toggle…
Browse files Browse the repository at this point in the history
…able

Feat: make taskprocessing task types toggleable
  • Loading branch information
AndyScherzinger authored Dec 18, 2024
2 parents d2923aa + 75e64e2 commit a60f71b
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 17 deletions.
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
23 changes: 21 additions & 2 deletions apps/settings/lib/Settings/Admin/ArtificialIntelligence.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;

class ArtificialIntelligence implements IDelegatedSettings {
public function __construct(
Expand All @@ -36,6 +37,7 @@ public function __construct(
private ContainerInterface $container,
private \OCP\TextToImage\IManager $text2imageManager,
private \OCP\TaskProcessing\IManager $taskProcessingManager,
private LoggerInterface $logger,
) {
}

Expand Down Expand Up @@ -113,12 +115,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 +139,29 @@ 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);
try {
$value = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->logger->error('Failed to get settings. JSON Error in ' . $key, ['exception' => $e]);
if ($key === 'ai.taskprocessing_type_preferences') {
$value = [];
foreach ($taskProcessingTypeSettings as $taskTypeId => $taskTypeValue) {
$value[$taskTypeId] = false;
}
$settings[$key] = $value;
}
continue;
}

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
61 changes: 61 additions & 0 deletions core/Command/TaskProcessing/EnabledCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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');
try {
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;
} catch (\JsonException $e) {
throw new \JsonException('Error in TaskType DB entry');
}

}
}
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 @@ -1267,6 +1267,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 @@ -1308,6 +1316,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
38 changes: 36 additions & 2 deletions lib/private/TaskProcessing/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,29 @@ private function _getTaskTypes(): array {
return $taskTypes;
}

/**
* @return array
*/
private function _getTaskTypeSettings(): array {
try {
$json = $this->config->getAppValue('core', 'ai.taskprocessing_type_preferences', '');
if ($json === '') {
return [];
}
return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
$taskTypeSettings = [];
$taskTypes = $this->_getTaskTypes();
foreach ($taskTypes as $taskType) {
$taskTypeSettings[$taskType->getId()] = false;
};

return $taskTypeSettings;
}

}

/**
* @param ShapeDescriptor[] $spec
* @param array<array-key, string|numeric> $defaults
Expand Down Expand Up @@ -721,12 +744,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 +780,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
* @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
65 changes: 58 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 @@ -396,6 +398,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 +445,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('');

$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

$text2imageManager = new \OC\TextToImage\Manager(
Expand All @@ -460,9 +458,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 +490,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));
}


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 +645,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

0 comments on commit a60f71b

Please sign in to comment.