diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php index 6e2a039606f..e0c8eb90464 100644 --- a/core/Controller/TaskProcessingApiController.php +++ b/core/Controller/TaskProcessingApiController.php @@ -36,7 +36,7 @@ use OCP\TaskProcessing\Exception\PreConditionNotMetException; use OCP\TaskProcessing\Exception\UnauthorizedException; use OCP\TaskProcessing\Exception\ValidationException; use OCP\TaskProcessing\IManager; -use OCP\TaskProcessing\ShapeDescriptor; +use OCP\TaskProcessing\ShapeEnumValue; use OCP\TaskProcessing\Task; use RuntimeException; @@ -67,26 +67,35 @@ class TaskProcessingApiController extends \OCP\AppFramework\OCSController { #[PublicPage] #[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')] public function taskTypes(): DataResponse { - $taskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); - - $serializedTaskTypes = []; - foreach ($taskTypes as $key => $taskType) { - $serializedTaskTypes[$key] = [ - 'name' => $taskType['name'], - 'description' => $taskType['description'], - 'inputShape' => array_map(fn (ShapeDescriptor $descriptor) => - $descriptor->jsonSerialize() + ['mandatory' => true], $taskType['inputShape']) - + array_map(fn (ShapeDescriptor $descriptor) => - $descriptor->jsonSerialize() + ['mandatory' => false], $taskType['optionalInputShape']), - 'outputShape' => array_map(fn (ShapeDescriptor $descriptor) => - $descriptor->jsonSerialize() + ['mandatory' => true], $taskType['outputShape']) - + array_map(fn (ShapeDescriptor $descriptor) => - $descriptor->jsonSerialize() + ['mandatory' => false], $taskType['optionalOutputShape']), - ]; - } - + $taskTypes = array_map(function (array $tt) { + $tt['inputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['inputShape']); + $tt['outputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['outputShape']); + $tt['optionalInputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['optionalInputShape']); + $tt['optionalOutputShape'] = array_map(function ($descriptor) { + return $descriptor->jsonSerialize(); + }, $tt['optionalOutputShape']); + $tt['inputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['inputShapeEnumValues']); + $tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['optionalInputShapeEnumValues']); + $tt['outputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['outputShapeEnumValues']); + $tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) { + return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues); + }, $tt['optionalOutputShapeEnumValues']); + return $tt; + }, $this->taskProcessingManager->getAvailableTaskTypes()); return new DataResponse([ - 'types' => $serializedTaskTypes, + 'types' => $taskTypes, ]); } diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index 1108e8013a6..4edde2dde36 100644 --- a/core/ResponseDefinitions.php +++ b/core/ResponseDefinitions.php @@ -165,15 +165,22 @@ namespace OC\Core; * @psalm-type CoreTaskProcessingShape = array{ * name: string, * description: string, - * type: "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles", - * mandatory: bool, + * type: "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"Enum"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles", * } * * @psalm-type CoreTaskProcessingTaskType = array{ * name: string, * description: string, * inputShape: CoreTaskProcessingShape[], + * inputShapeEnumValues: array{name: string, value: string}[][], + * inputShapeDefaults: array, + * optionalInputShape: CoreTaskProcessingShape[], + * optionalInputShapeEnumValues: array{name: string, value: string}[][], + * optionalInputShapeDefaults: array, * outputShape: CoreTaskProcessingShape[], + * outputShapeEnumValues: array{name: string, value: string}[][], + * optionalOutputShape: CoreTaskProcessingShape[], + * optionalOutputShapeEnumValues: array{name: string, value: string}[][]} * } * * @psalm-type CoreTaskProcessingIO = array|string|list> diff --git a/core/openapi-full.json b/core/openapi-full.json index a90cc1efaaf..e52b62d6fa6 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -496,8 +496,7 @@ "required": [ "name", "description", - "type", - "mandatory" + "type" ], "properties": { "name": { @@ -515,6 +514,7 @@ "Image", "Video", "File", + "Enum", "ListOfNumbers", "ListOfTexts", "ListOfImages", @@ -522,9 +522,6 @@ "ListOfVideos", "ListOfFiles" ] - }, - "mandatory": { - "type": "boolean" } } }, @@ -602,7 +599,15 @@ "name", "description", "inputShape", - "outputShape" + "inputShapeEnumValues", + "inputShapeDefaults", + "optionalInputShape", + "optionalInputShapeEnumValues", + "optionalInputShapeDefaults", + "outputShape", + "outputShapeEnumValues", + "optionalOutputShape", + "optionalOutputShapeEnumValues" ], "properties": { "name": { @@ -617,11 +622,133 @@ "$ref": "#/components/schemas/TaskProcessingShape" } }, + "inputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "inputShapeDefaults": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + "optionalInputShape": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskProcessingShape" + } + }, + "optionalInputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "optionalInputShapeDefaults": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, "outputShape": { "type": "array", "items": { "$ref": "#/components/schemas/TaskProcessingShape" } + }, + "outputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "optionalOutputShape": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskProcessingShape" + } + }, + "optionalOutputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } } } }, diff --git a/core/openapi.json b/core/openapi.json index 869bc4ef2c5..d597796f94e 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -496,8 +496,7 @@ "required": [ "name", "description", - "type", - "mandatory" + "type" ], "properties": { "name": { @@ -515,6 +514,7 @@ "Image", "Video", "File", + "Enum", "ListOfNumbers", "ListOfTexts", "ListOfImages", @@ -522,9 +522,6 @@ "ListOfVideos", "ListOfFiles" ] - }, - "mandatory": { - "type": "boolean" } } }, @@ -602,7 +599,15 @@ "name", "description", "inputShape", - "outputShape" + "inputShapeEnumValues", + "inputShapeDefaults", + "optionalInputShape", + "optionalInputShapeEnumValues", + "optionalInputShapeDefaults", + "outputShape", + "outputShapeEnumValues", + "optionalOutputShape", + "optionalOutputShapeEnumValues" ], "properties": { "name": { @@ -617,11 +622,133 @@ "$ref": "#/components/schemas/TaskProcessingShape" } }, + "inputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "inputShapeDefaults": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + "optionalInputShape": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskProcessingShape" + } + }, + "optionalInputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "optionalInputShapeDefaults": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, "outputShape": { "type": "array", "items": { "$ref": "#/components/schemas/TaskProcessingShape" } + }, + "outputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "optionalOutputShape": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TaskProcessingShape" + } + }, + "optionalOutputShapeEnumValues": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } } } }, diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 379a1edbe63..4abb976e1a9 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -749,6 +749,7 @@ return array( 'OCP\\TaskProcessing\\ISynchronousProvider' => $baseDir . '/lib/public/TaskProcessing/ISynchronousProvider.php', 'OCP\\TaskProcessing\\ITaskType' => $baseDir . '/lib/public/TaskProcessing/ITaskType.php', 'OCP\\TaskProcessing\\ShapeDescriptor' => $baseDir . '/lib/public/TaskProcessing/ShapeDescriptor.php', + 'OCP\\TaskProcessing\\ShapeEnumValue' => $baseDir . '/lib/public/TaskProcessing/ShapeEnumValue.php', 'OCP\\TaskProcessing\\Task' => $baseDir . '/lib/public/TaskProcessing/Task.php', 'OCP\\TaskProcessing\\TaskTypes\\AudioToText' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/AudioToText.php', 'OCP\\TaskProcessing\\TaskTypes\\ContextWrite' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/ContextWrite.php', @@ -762,6 +763,7 @@ return array( 'OCP\\TaskProcessing\\TaskTypes\\TextToTextSimplification' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/TextToTextSimplification.php', 'OCP\\TaskProcessing\\TaskTypes\\TextToTextSummary' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/TextToTextSummary.php', 'OCP\\TaskProcessing\\TaskTypes\\TextToTextTopics' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/TextToTextTopics.php', + 'OCP\\TaskProcessing\\TaskTypes\\TextToTextTranslate' => $baseDir . '/lib/public/TaskProcessing/TaskTypes/TextToTextTranslate.php', 'OCP\\Teams\\ITeamManager' => $baseDir . '/lib/public/Teams/ITeamManager.php', 'OCP\\Teams\\ITeamResourceProvider' => $baseDir . '/lib/public/Teams/ITeamResourceProvider.php', 'OCP\\Teams\\Team' => $baseDir . '/lib/public/Teams/Team.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 41109fd5f3d..d7a58bff1a2 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -782,6 +782,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\TaskProcessing\\ISynchronousProvider' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ISynchronousProvider.php', 'OCP\\TaskProcessing\\ITaskType' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ITaskType.php', 'OCP\\TaskProcessing\\ShapeDescriptor' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ShapeDescriptor.php', + 'OCP\\TaskProcessing\\ShapeEnumValue' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/ShapeEnumValue.php', 'OCP\\TaskProcessing\\Task' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/Task.php', 'OCP\\TaskProcessing\\TaskTypes\\AudioToText' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/AudioToText.php', 'OCP\\TaskProcessing\\TaskTypes\\ContextWrite' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/ContextWrite.php', @@ -795,6 +796,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\TaskProcessing\\TaskTypes\\TextToTextSimplification' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/TextToTextSimplification.php', 'OCP\\TaskProcessing\\TaskTypes\\TextToTextSummary' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/TextToTextSummary.php', 'OCP\\TaskProcessing\\TaskTypes\\TextToTextTopics' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/TextToTextTopics.php', + 'OCP\\TaskProcessing\\TaskTypes\\TextToTextTranslate' => __DIR__ . '/../../..' . '/lib/public/TaskProcessing/TaskTypes/TextToTextTranslate.php', 'OCP\\Teams\\ITeamManager' => __DIR__ . '/../../..' . '/lib/public/Teams/ITeamManager.php', 'OCP\\Teams\\ITeamResourceProvider' => __DIR__ . '/../../..' . '/lib/public/Teams/ITeamResourceProvider.php', 'OCP\\Teams\\Team' => __DIR__ . '/../../..' . '/lib/public/Teams/Team.php', diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index ad690acefd7..73086b55e56 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -50,6 +50,7 @@ use OCP\TaskProcessing\IProvider; use OCP\TaskProcessing\ISynchronousProvider; use OCP\TaskProcessing\ITaskType; use OCP\TaskProcessing\ShapeDescriptor; +use OCP\TaskProcessing\ShapeEnumValue; use OCP\TaskProcessing\Task; use OCP\TaskProcessing\TaskTypes\AudioToText; use OCP\TaskProcessing\TaskTypes\TextToImage; @@ -70,11 +71,12 @@ class Manager implements IManager { /** @var list|null */ private ?array $providers = null; - /** @var array, optionalInputShape: array, outputShape: array, optionalOutputShape: array}>|null */ + /** + * @var array, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}> + */ private ?array $availableTaskTypes = null; private IAppData $appData; - public function __construct( private IConfig $config, private Coordinator $coordinator, @@ -95,9 +97,7 @@ class Manager implements IManager { $this->appData = $appDataFactory->get('core'); } - /** - * @return IProvider[] - */ + private function _getTextProcessingProviders(): array { $oldProviders = $this->textProcessingManager->getProviders(); $newProviders = []; @@ -155,6 +155,30 @@ class Manager implements IManager { throw new ProcessingException($e->getMessage(), 0, $e); } } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } }; $newProviders[$provider->getId()] = $provider; } @@ -289,6 +313,30 @@ class Manager implements IManager { } return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)]; } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } }; $newProviders[$newProvider->getId()] = $newProvider; } @@ -351,6 +399,30 @@ class Manager implements IManager { } return ['output' => $result]; } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } }; $newProviders[$newProvider->getId()] = $newProvider; } @@ -439,35 +511,67 @@ class Manager implements IManager { /** * @param ShapeDescriptor[] $spec + * @param array $defaults + * @param array $enumValues * @param array $io + * @param bool $optional * @return void * @throws ValidationException */ - private function validateInput(array $spec, array $io, bool $optional = false): void { + private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void { foreach ($spec as $key => $descriptor) { $type = $descriptor->getShapeType(); if (!isset($io[$key])) { if ($optional) { continue; } + if (isset($defaults[$key])) { + if (EShapeType::getScalarType($type) !== $type) { + throw new ValidationException('Provider tried to set a default value for a non-scalar slot'); + } + if (EShapeType::isFileType($type)) { + throw new ValidationException('Provider tried to set a default value for a slot that is not text or number'); + } + $type->validateInput($defaults[$key]); + continue; + } throw new ValidationException('Missing key: "' . $key . '"'); } try { $type->validateInput($io[$key]); + if ($type === EShapeType::Enum) { + if (!isset($enumValues[$key])) { + throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key .'"'); + } + $type->validateEnum($io[$key], $enumValues[$key]); + } } catch (ValidationException $e) { throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage()); } } } + /** + * Takes task input data and replaces fileIds with File objects + * + * @param array|numeric|string> $input + * @param array ...$defaultSpecs the specs + * @return array|numeric|string> + */ + public function fillInputDefaults(array $input, ...$defaultSpecs): array { + $spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []); + return array_merge($spec, $input); + } + /** * @param ShapeDescriptor[] $spec + * @param array $enumValues * @param array $io * @param bool $optional * @return void * @throws ValidationException */ - private function validateOutputWithFileIds(array $spec, array $io, bool $optional = false): void { + private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void { foreach ($spec as $key => $descriptor) { $type = $descriptor->getShapeType(); if (!isset($io[$key])) { @@ -478,6 +582,9 @@ class Manager implements IManager { } try { $type->validateOutputWithFileIds($io[$key]); + if (isset($enumValues[$key])) { + $type->validateEnum($io[$key], $enumValues[$key]); + } } catch (ValidationException $e) { throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage()); } @@ -486,12 +593,13 @@ class Manager implements IManager { /** * @param ShapeDescriptor[] $spec + * @param array $enumValues * @param array $io * @param bool $optional * @return void * @throws ValidationException */ - private function validateOutputWithFileData(array $spec, array $io, bool $optional = false): void { + private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void { foreach ($spec as $key => $descriptor) { $type = $descriptor->getShapeType(); if (!isset($io[$key])) { @@ -502,6 +610,9 @@ class Manager implements IManager { } try { $type->validateOutputWithFileData($io[$key]); + if (isset($enumValues[$key])) { + $type->validateEnum($io[$key], $enumValues[$key]); + } } catch (ValidationException $e) { throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage()); } @@ -569,10 +680,16 @@ class Manager implements IManager { $availableTaskTypes[$provider->getTaskTypeId()] = [ 'name' => $taskType->getName(), 'description' => $taskType->getDescription(), - 'inputShape' => $taskType->getInputShape(), 'optionalInputShape' => $provider->getOptionalInputShape(), + 'inputShapeEnumValues' => $provider->getInputShapeEnumValues(), + 'inputShapeDefaults' => $provider->getInputShapeDefaults(), + 'inputShape' => $taskType->getInputShape(), + 'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(), + 'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(), 'outputShape' => $taskType->getOutputShape(), + 'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(), 'optionalOutputShape' => $provider->getOptionalOutputShape(), + 'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(), ]; } @@ -592,10 +709,14 @@ class Manager implements IManager { } $taskTypes = $this->getAvailableTaskTypes(); $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape']; + $inputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['inputShapeDefaults']; + $inputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['inputShapeEnumValues']; $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape']; + $optionalInputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeEnumValues']; + $optionalInputShapeDefaults = $taskTypes[$task->getTaskTypeId()]['optionalInputShapeDefaults']; // validate input - $this->validateInput($inputShape, $task->getInput()); - $this->validateInput($optionalInputShape, $task->getInput(), true); + $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput()); + $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true); // authenticate access to mentioned files $ids = []; foreach ($inputShape + $optionalInputShape as $key => $descriptor) { @@ -614,7 +735,9 @@ class Manager implements IManager { $this->validateUserAccessToFile($fileId, $task->getUserId()); } // remove superfluous keys and set input - $task->setInput($this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape)); + $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape); + $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults); + $task->setInput($inputWithDefaults); $task->setStatus(Task::STATUS_SCHEDULED); $task->setScheduledAt(time()); $provider = $this->getPreferredProvider($task->getTaskTypeId()); @@ -703,15 +826,17 @@ class Manager implements IManager { } elseif ($result !== null) { $taskTypes = $this->getAvailableTaskTypes(); $outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape']; + $outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues']; $optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape']; + $optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues']; try { // validate output if (!$isUsingFileIds) { - $this->validateOutputWithFileData($outputShape, $result); - $this->validateOutputWithFileData($optionalOutputShape, $result, true); + $this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result); + $this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true); } else { - $this->validateOutputWithFileIds($outputShape, $result); - $this->validateOutputWithFileIds($optionalOutputShape, $result, true); + $this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result); + $this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true); } $output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape); // extract raw data and put it in files, replace it with file ids @@ -929,9 +1054,6 @@ class Manager implements IManager { $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape']; $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape']; $input = $task->getInput(); - // validate input, again for good measure (should have been validated in scheduleTask) - $this->validateInput($inputShape, $input); - $this->validateInput($optionalInputShape, $input, true); $input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape); $input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape); return $input; diff --git a/lib/public/TaskProcessing/EShapeType.php b/lib/public/TaskProcessing/EShapeType.php index 059f9d0c3c7..cd8d6d837da 100644 --- a/lib/public/TaskProcessing/EShapeType.php +++ b/lib/public/TaskProcessing/EShapeType.php @@ -23,6 +23,7 @@ enum EShapeType: int { case Audio = 3; case Video = 4; case File = 5; + case Enum = 6; case ListOfNumbers = 10; case ListOfTexts = 11; case ListOfImages = 12; @@ -30,6 +31,25 @@ enum EShapeType: int { case ListOfVideos = 14; case ListOfFiles = 15; + /** + * @param mixed $value + * @param ShapeEnumValue[] $enumValues + * @return void + * @throws ValidationException + * @since 30.0.0 + */ + public function validateEnum(mixed $value, array $enumValues): void { + if ($this !== EShapeType::Enum) { + throw new ValidationException('Provider provided enum values for non-enum slot'); + } + foreach ($enumValues as $enumValue) { + if ($value === $enumValue->getValue()) { + return; + } + } + throw new ValidationException('Wrong value given for Enum slot. Got "' . $value . '", but expected one of the provided enum values: "' . implode('", "', array_map(fn ($enumValue) => $enumValue->getValue(), $enumValues)) . '"'); + } + /** * @param mixed $value * @return void @@ -37,6 +57,9 @@ enum EShapeType: int { * @since 30.0.0 */ private function validateNonFileType(mixed $value): void { + if ($this === EShapeType::Enum && !is_string($value)) { + throw new ValidationException('Non-text item provided for Enum slot'); + } if ($this === EShapeType::Text && !is_string($value)) { throw new ValidationException('Non-text item provided for Text slot'); } @@ -159,4 +182,13 @@ enum EShapeType: int { public static function getScalarType(EShapeType $type): EShapeType { return EShapeType::from($type->value % 10); } + + /** + * @param EShapeType $type + * @return bool + * @since 30.0.0 + */ + public static function isFileType(EShapeType $type): bool { + return in_array(EShapeType::getScalarType($type), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true); + } } diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index d7cd96edc45..e3e6b3be09d 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -46,7 +46,7 @@ interface IManager { public function getPreferredProvider(string $taskType); /** - * @return array + * @return array, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}> * @since 30.0.0 */ public function getAvailableTaskTypes(): array; diff --git a/lib/public/TaskProcessing/IProvider.php b/lib/public/TaskProcessing/IProvider.php index 68a708ca834..a4e752216c7 100644 --- a/lib/public/TaskProcessing/IProvider.php +++ b/lib/public/TaskProcessing/IProvider.php @@ -58,4 +58,52 @@ interface IProvider { * @psalm-return ShapeDescriptor[] */ public function getOptionalOutputShape(): array; + + /** + * Returns the option list for each input shape ENUM slot + * + * @since 30.0.0 + * @psalm-return ShapeEnumValue[][] + */ + public function getInputShapeEnumValues(): array; + + /** + * Returns the default values for input shape slots + * + * @since 30.0.0 + * @psalm-return array + */ + public function getInputShapeDefaults(): array; + + /** + * Returns the option list for each optional input shape ENUM slot + * + * @since 30.0.0 + * @psalm-return ShapeEnumValue[][] + */ + public function getOptionalInputShapeEnumValues(): array; + + /** + * Returns the default values for optional input shape slots + * + * @since 30.0.0 + * @psalm-return array + */ + public function getOptionalInputShapeDefaults(): array; + + /** + * Returns the option list for each output shape ENUM slot + * + * @since 30.0.0 + * @psalm-return ShapeEnumValue[][] + */ + public function getOutputShapeEnumValues(): array; + + /** + * Returns the option list for each optional output shape ENUM slot + * + * @since 30.0.0 + * @psalm-return ShapeEnumValue[][] + */ + public function getOptionalOutputShapeEnumValues(): array; } diff --git a/lib/public/TaskProcessing/ShapeDescriptor.php b/lib/public/TaskProcessing/ShapeDescriptor.php index 5759b260865..19e57c8a91d 100644 --- a/lib/public/TaskProcessing/ShapeDescriptor.php +++ b/lib/public/TaskProcessing/ShapeDescriptor.php @@ -49,11 +49,11 @@ class ShapeDescriptor implements \JsonSerializable { } /** - * @return array{name: string, description: string, type: "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles"} + * @return array{name: string, description: string, type: "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"Enum"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles"} * @since 30.0.0 */ public function jsonSerialize(): array { - /** @var "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles" $type */ + /** @var "Number"|"Text"|"Audio"|"Image"|"Video"|"File"|"Enum"|"ListOfNumbers"|"ListOfTexts"|"ListOfImages"|"ListOfAudios"|"ListOfVideos"|"ListOfFiles" $type */ $type = $this->getShapeType()->name; return [ 'name' => $this->getName(), diff --git a/lib/public/TaskProcessing/ShapeEnumValue.php b/lib/public/TaskProcessing/ShapeEnumValue.php new file mode 100644 index 00000000000..bc500524304 --- /dev/null +++ b/lib/public/TaskProcessing/ShapeEnumValue.php @@ -0,0 +1,51 @@ +name; + } + + /** + * @return string + * @since 30.0.0 + */ + public function getValue(): string { + return $this->value; + } + + /** + * @return array{name: string, value: string} + * @since 30.0.0 + */ + public function jsonSerialize(): array { + return [ + 'name' => $this->getName(), + 'value' => $this->getValue(), + ]; + } +} diff --git a/lib/public/TaskProcessing/TaskTypes/TextToText.php b/lib/public/TaskProcessing/TaskTypes/TextToText.php index 7ac7f2eca05..92eaf5629e8 100644 --- a/lib/public/TaskProcessing/TaskTypes/TextToText.php +++ b/lib/public/TaskProcessing/TaskTypes/TextToText.php @@ -51,7 +51,7 @@ class TextToText implements ITaskType { * @since 30.0.0 */ public function getDescription(): string { - return $this->l->t('Runs an arbitrary prompt through a language model that retuns a reply'); + return $this->l->t('Runs an arbitrary prompt through a language model that returns a reply'); } /** diff --git a/lib/public/TaskProcessing/TaskTypes/TextToTextTranslate.php b/lib/public/TaskProcessing/TaskTypes/TextToTextTranslate.php new file mode 100644 index 00000000000..11b71ec3eb2 --- /dev/null +++ b/lib/public/TaskProcessing/TaskTypes/TextToTextTranslate.php @@ -0,0 +1,102 @@ +l = $l10nFactory->get('core'); + } + + + /** + * @inheritDoc + * @since 30.0.0 + */ + public function getName(): string { + return $this->l->t('Translate'); + } + + /** + * @inheritDoc + * @since 30.0.0 + */ + public function getDescription(): string { + return $this->l->t('Translate text from one language to another'); + } + + /** + * @return string + * @since 30.0.0 + */ + public function getId(): string { + return self::ID; + } + + /** + * @return ShapeDescriptor[] + * @since 30.0.0 + */ + public function getInputShape(): array { + return [ + 'input' => new ShapeDescriptor( + $this->l->t('Origin text'), + $this->l->t('The text to translate'), + EShapeType::Text + ), + 'origin_language' => new ShapeDescriptor( + $this->l->t('Origin language'), + $this->l->t('The language of the origin text'), + EShapeType::Enum + ), + 'target_language' => new ShapeDescriptor( + $this->l->t('Target language'), + $this->l->t('The desired language to translate the origin text in'), + EShapeType::Enum + ), + ]; + } + + /** + * @return ShapeDescriptor[] + * @since 30.0.0 + */ + public function getOutputShape(): array { + return [ + 'output' => new ShapeDescriptor( + $this->l->t('Result'), + $this->l->t('The translated text'), + EShapeType::Text + ), + ]; + } +} diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php index c88f73a861c..ac9dec1cd1d 100644 --- a/tests/lib/TaskProcessing/TaskProcessingTest.php +++ b/tests/lib/TaskProcessing/TaskProcessingTest.php @@ -104,6 +104,30 @@ class AsyncProvider implements IProvider { 'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text), ]; } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } } class SuccessfulSyncProvider implements IProvider, ISynchronousProvider { @@ -138,6 +162,30 @@ class SuccessfulSyncProvider implements IProvider, ISynchronousProvider { public function process(?string $userId, array $input, callable $reportProgress): array { return ['output' => $input['input']]; } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } } class FailingSyncProvider implements IProvider, ISynchronousProvider { @@ -173,6 +221,30 @@ class FailingSyncProvider implements IProvider, ISynchronousProvider { public function process(?string $userId, array $input, callable $reportProgress): array { throw new ProcessingException(self::ERROR_MESSAGE); } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } } class BrokenSyncProvider implements IProvider, ISynchronousProvider { @@ -207,6 +279,30 @@ class BrokenSyncProvider implements IProvider, ISynchronousProvider { public function process(?string $userId, array $input, callable $reportProgress): array { return []; } + + public function getInputShapeEnumValues(): array { + return []; + } + + public function getInputShapeDefaults(): array { + return []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } } class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {