mirror of
https://github.com/nextcloud/server.git
synced 2026-06-10 17:23:59 -04:00
Merge pull request #49727 from nextcloud/feat/make-tasks-types-toggleable
Feat: make taskprocessing task types toggleable
This commit is contained in:
commit
a60f71bd93
13 changed files with 201 additions and 17 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@
|
|||
<div :key="type">
|
||||
<h3>{{ t('settings', 'Task:') }} {{ type.name }}</h3>
|
||||
<p>{{ type.description }}</p>
|
||||
<p> </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}">
|
||||
|
|
|
|||
61
core/Command/TaskProcessing/EnabledCommand.php
Normal file
61
core/Command/TaskProcessing/EnabledCommand.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
4
dist/settings-vue-settings-admin-ai.js
vendored
4
dist/settings-vue-settings-admin-ai.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-vue-settings-admin-ai.js.map
vendored
2
dist/settings-vue-settings-admin-ai.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue