Merge pull request #49727 from nextcloud/feat/make-tasks-types-toggleable

Feat: make taskprocessing task types toggleable
This commit is contained in:
Andy Scherzinger 2024-12-18 20:08:28 +01:00 committed by GitHub
commit a60f71bd93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 201 additions and 17 deletions

View file

@ -38,7 +38,7 @@ class AISettingsController extends Controller {
*/
#[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;

View file

@ -24,6 +24,7 @@ use OCP\Translation\ITranslationProviderWithId;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class ArtificialIntelligence implements IDelegatedSettings {
public function __construct(
@ -36,6 +37,7 @@ class ArtificialIntelligence implements IDelegatedSettings {
private ContainerInterface $container,
private \OCP\TextToImage\IManager $text2imageManager,
private \OCP\TaskProcessing\IManager $taskProcessingManager,
private LoggerInterface $logger,
) {
}
@ -113,12 +115,14 @@ class ArtificialIntelligence implements IDelegatedSettings {
}
}
$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);
@ -135,14 +139,29 @@ class ArtificialIntelligence implements IDelegatedSettings {
'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);

View file

@ -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}">

View file

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

View file

@ -147,6 +147,7 @@ if ($config->getSystemValueBool('installed', false)) {
$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));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1267,6 +1267,7 @@ return array(
'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',

View file

@ -10,5 +10,6 @@ return array(
'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'),
);

View file

@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
'NCU\\' => 4,
),
'B' =>
array (
'Bamarni\\Composer\\Bin\\' => 21,
),
);
public static $prefixDirsPsr4 = array (
@ -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 (
@ -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',

View file

@ -564,6 +564,29 @@ class Manager implements IManager {
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
@ -721,12 +744,17 @@ class Manager implements IManager {
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) {
@ -752,9 +780,15 @@ class Manager implements IManager {
}
}
if ($showDisabled) {
// Do not cache showDisabled, ever.
return $availableTaskTypes;
}
$this->availableTaskTypes = $availableTaskTypes;
}
return $this->availableTaskTypes;
}

View file

@ -46,10 +46,12 @@ interface IManager {
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

View file

@ -187,6 +187,8 @@ class SuccessfulSyncProvider implements IProvider, ISynchronousProvider {
}
}
class FailingSyncProvider implements IProvider, ISynchronousProvider {
public const ERROR_MESSAGE = 'Failure';
public function getId(): string {
@ -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';
@ -442,11 +445,6 @@ class TaskProcessingTest extends \Test\TestCase {
$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(
@ -460,9 +458,9 @@ class TaskProcessingTest extends \Test\TestCase {
);
$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),
@ -492,7 +490,24 @@ class TaskProcessingTest extends \Test\TestCase {
$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)
]);
@ -630,6 +645,42 @@ class TaskProcessingTest extends \Test\TestCase {
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)