Completed
Push — master ( 008e9f...e7fbfa )
by Joas
22:52 queued 14s
created
lib/private/TaskProcessing/Manager.php 1 patch
Indentation   +1404 added lines, -1404 removed lines patch added patch discarded remove patch
@@ -67,1408 +67,1408 @@
 block discarded – undo
67 67
 
68 68
 class Manager implements IManager {
69 69
 
70
-	public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
71
-	public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
72
-	public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
73
-
74
-	/** @var list<IProvider>|null */
75
-	private ?array $providers = null;
76
-
77
-	/**
78
-	 * @var array<array-key,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[][]}>
79
-	 */
80
-	private ?array $availableTaskTypes = null;
81
-
82
-	private IAppData $appData;
83
-	private ?array $preferences = null;
84
-	private ?array $providersById = null;
85
-
86
-	/** @var ITaskType[]|null */
87
-	private ?array $taskTypes = null;
88
-	private ICache $distributedCache;
89
-
90
-	private ?GetTaskProcessingProvidersEvent $eventResult = null;
91
-
92
-	public function __construct(
93
-		private IConfig $config,
94
-		private Coordinator $coordinator,
95
-		private IServerContainer $serverContainer,
96
-		private LoggerInterface $logger,
97
-		private TaskMapper $taskMapper,
98
-		private IJobList $jobList,
99
-		private IEventDispatcher $dispatcher,
100
-		IAppDataFactory $appDataFactory,
101
-		private IRootFolder $rootFolder,
102
-		private \OCP\TextToImage\IManager $textToImageManager,
103
-		private IUserMountCache $userMountCache,
104
-		private IClientService $clientService,
105
-		private IAppManager $appManager,
106
-		ICacheFactory $cacheFactory,
107
-	) {
108
-		$this->appData = $appDataFactory->get('core');
109
-		$this->distributedCache = $cacheFactory->createDistributed('task_processing::');
110
-	}
111
-
112
-
113
-	/**
114
-	 * This is almost a copy of textProcessingManager->getProviders
115
-	 * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
116
-	 */
117
-	private function _getRawTextProcessingProviders(): array {
118
-		$context = $this->coordinator->getRegistrationContext();
119
-		if ($context === null) {
120
-			return [];
121
-		}
122
-
123
-		$providers = [];
124
-
125
-		foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
126
-			$class = $providerServiceRegistration->getService();
127
-			try {
128
-				$providers[$class] = $this->serverContainer->get($class);
129
-			} catch (\Throwable $e) {
130
-				$this->logger->error('Failed to load Text processing provider ' . $class, [
131
-					'exception' => $e,
132
-				]);
133
-			}
134
-		}
135
-
136
-		return $providers;
137
-	}
138
-
139
-	private function _getTextProcessingProviders(): array {
140
-		$oldProviders = $this->_getRawTextProcessingProviders();
141
-		$newProviders = [];
142
-		foreach ($oldProviders as $oldProvider) {
143
-			$provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
144
-				private \OCP\TextProcessing\IProvider $provider;
145
-
146
-				public function __construct(\OCP\TextProcessing\IProvider $provider) {
147
-					$this->provider = $provider;
148
-				}
149
-
150
-				public function getId(): string {
151
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
152
-						return $this->provider->getId();
153
-					}
154
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
155
-				}
156
-
157
-				public function getName(): string {
158
-					return $this->provider->getName();
159
-				}
160
-
161
-				public function getTaskTypeId(): string {
162
-					return match ($this->provider->getTaskType()) {
163
-						\OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
164
-						\OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
165
-						\OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
166
-						\OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
167
-						default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
168
-					};
169
-				}
170
-
171
-				public function getExpectedRuntime(): int {
172
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
173
-						return $this->provider->getExpectedRuntime();
174
-					}
175
-					return 60;
176
-				}
177
-
178
-				public function getOptionalInputShape(): array {
179
-					return [];
180
-				}
181
-
182
-				public function getOptionalOutputShape(): array {
183
-					return [];
184
-				}
185
-
186
-				public function process(?string $userId, array $input, callable $reportProgress): array {
187
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
188
-						$this->provider->setUserId($userId);
189
-					}
190
-					try {
191
-						return ['output' => $this->provider->process($input['input'])];
192
-					} catch (\RuntimeException $e) {
193
-						throw new ProcessingException($e->getMessage(), 0, $e);
194
-					}
195
-				}
196
-
197
-				public function getInputShapeEnumValues(): array {
198
-					return [];
199
-				}
200
-
201
-				public function getInputShapeDefaults(): array {
202
-					return [];
203
-				}
204
-
205
-				public function getOptionalInputShapeEnumValues(): array {
206
-					return [];
207
-				}
208
-
209
-				public function getOptionalInputShapeDefaults(): array {
210
-					return [];
211
-				}
212
-
213
-				public function getOutputShapeEnumValues(): array {
214
-					return [];
215
-				}
216
-
217
-				public function getOptionalOutputShapeEnumValues(): array {
218
-					return [];
219
-				}
220
-			};
221
-			$newProviders[$provider->getId()] = $provider;
222
-		}
223
-
224
-		return $newProviders;
225
-	}
226
-
227
-	/**
228
-	 * @return ITaskType[]
229
-	 */
230
-	private function _getTextProcessingTaskTypes(): array {
231
-		$oldProviders = $this->_getRawTextProcessingProviders();
232
-		$newTaskTypes = [];
233
-		foreach ($oldProviders as $oldProvider) {
234
-			// These are already implemented in the TaskProcessing realm
235
-			if (in_array($oldProvider->getTaskType(), [
236
-				\OCP\TextProcessing\FreePromptTaskType::class,
237
-				\OCP\TextProcessing\HeadlineTaskType::class,
238
-				\OCP\TextProcessing\TopicsTaskType::class,
239
-				\OCP\TextProcessing\SummaryTaskType::class
240
-			], true)) {
241
-				continue;
242
-			}
243
-			$taskType = new class($oldProvider->getTaskType()) implements ITaskType {
244
-				private string $oldTaskTypeClass;
245
-				private \OCP\TextProcessing\ITaskType $oldTaskType;
246
-				private IL10N $l;
247
-
248
-				public function __construct(string $oldTaskTypeClass) {
249
-					$this->oldTaskTypeClass = $oldTaskTypeClass;
250
-					$this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
251
-					$this->l = \OCP\Server::get(IFactory::class)->get('core');
252
-				}
253
-
254
-				public function getId(): string {
255
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
256
-				}
257
-
258
-				public function getName(): string {
259
-					return $this->oldTaskType->getName();
260
-				}
261
-
262
-				public function getDescription(): string {
263
-					return $this->oldTaskType->getDescription();
264
-				}
265
-
266
-				public function getInputShape(): array {
267
-					return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
268
-				}
269
-
270
-				public function getOutputShape(): array {
271
-					return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
272
-				}
273
-			};
274
-			$newTaskTypes[$taskType->getId()] = $taskType;
275
-		}
276
-
277
-		return $newTaskTypes;
278
-	}
279
-
280
-	/**
281
-	 * @return IProvider[]
282
-	 */
283
-	private function _getTextToImageProviders(): array {
284
-		$oldProviders = $this->textToImageManager->getProviders();
285
-		$newProviders = [];
286
-		foreach ($oldProviders as $oldProvider) {
287
-			$newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
288
-				private \OCP\TextToImage\IProvider $provider;
289
-				private IAppData $appData;
290
-
291
-				public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
292
-					$this->provider = $provider;
293
-					$this->appData = $appData;
294
-				}
295
-
296
-				public function getId(): string {
297
-					return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
298
-				}
299
-
300
-				public function getName(): string {
301
-					return $this->provider->getName();
302
-				}
303
-
304
-				public function getTaskTypeId(): string {
305
-					return TextToImage::ID;
306
-				}
307
-
308
-				public function getExpectedRuntime(): int {
309
-					return $this->provider->getExpectedRuntime();
310
-				}
311
-
312
-				public function getOptionalInputShape(): array {
313
-					return [];
314
-				}
315
-
316
-				public function getOptionalOutputShape(): array {
317
-					return [];
318
-				}
319
-
320
-				public function process(?string $userId, array $input, callable $reportProgress): array {
321
-					try {
322
-						$folder = $this->appData->getFolder('text2image');
323
-					} catch (\OCP\Files\NotFoundException) {
324
-						$folder = $this->appData->newFolder('text2image');
325
-					}
326
-					$resources = [];
327
-					$files = [];
328
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
329
-						$file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
330
-						$files[] = $file;
331
-						$resource = $file->write();
332
-						if ($resource !== false && $resource !== true && is_resource($resource)) {
333
-							$resources[] = $resource;
334
-						} else {
335
-							throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
336
-						}
337
-					}
338
-					if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
339
-						$this->provider->setUserId($userId);
340
-					}
341
-					try {
342
-						$this->provider->generate($input['input'], $resources);
343
-					} catch (\RuntimeException $e) {
344
-						throw new ProcessingException($e->getMessage(), 0, $e);
345
-					}
346
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
347
-						if (is_resource($resources[$i])) {
348
-							// If $resource hasn't been closed yet, we'll do that here
349
-							fclose($resources[$i]);
350
-						}
351
-					}
352
-					return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
353
-				}
354
-
355
-				public function getInputShapeEnumValues(): array {
356
-					return [];
357
-				}
358
-
359
-				public function getInputShapeDefaults(): array {
360
-					return [];
361
-				}
362
-
363
-				public function getOptionalInputShapeEnumValues(): array {
364
-					return [];
365
-				}
366
-
367
-				public function getOptionalInputShapeDefaults(): array {
368
-					return [];
369
-				}
370
-
371
-				public function getOutputShapeEnumValues(): array {
372
-					return [];
373
-				}
374
-
375
-				public function getOptionalOutputShapeEnumValues(): array {
376
-					return [];
377
-				}
378
-			};
379
-			$newProviders[$newProvider->getId()] = $newProvider;
380
-		}
381
-
382
-		return $newProviders;
383
-	}
384
-
385
-	/**
386
-	 * This is almost a copy of SpeechToTextManager->getProviders
387
-	 * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
388
-	 */
389
-	private function _getRawSpeechToTextProviders(): array {
390
-		$context = $this->coordinator->getRegistrationContext();
391
-		if ($context === null) {
392
-			return [];
393
-		}
394
-		$providers = [];
395
-		foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
396
-			$class = $providerServiceRegistration->getService();
397
-			try {
398
-				$providers[$class] = $this->serverContainer->get($class);
399
-			} catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
400
-				$this->logger->error('Failed to load SpeechToText provider ' . $class, [
401
-					'exception' => $e,
402
-				]);
403
-			}
404
-		}
405
-
406
-		return $providers;
407
-	}
408
-
409
-	/**
410
-	 * @return IProvider[]
411
-	 */
412
-	private function _getSpeechToTextProviders(): array {
413
-		$oldProviders = $this->_getRawSpeechToTextProviders();
414
-		$newProviders = [];
415
-		foreach ($oldProviders as $oldProvider) {
416
-			$newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
417
-				private ISpeechToTextProvider $provider;
418
-				private IAppData $appData;
419
-
420
-				private IRootFolder $rootFolder;
421
-
422
-				public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
423
-					$this->provider = $provider;
424
-					$this->rootFolder = $rootFolder;
425
-					$this->appData = $appData;
426
-				}
427
-
428
-				public function getId(): string {
429
-					if ($this->provider instanceof ISpeechToTextProviderWithId) {
430
-						return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
431
-					}
432
-					return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
433
-				}
434
-
435
-				public function getName(): string {
436
-					return $this->provider->getName();
437
-				}
438
-
439
-				public function getTaskTypeId(): string {
440
-					return AudioToText::ID;
441
-				}
442
-
443
-				public function getExpectedRuntime(): int {
444
-					return 60;
445
-				}
446
-
447
-				public function getOptionalInputShape(): array {
448
-					return [];
449
-				}
450
-
451
-				public function getOptionalOutputShape(): array {
452
-					return [];
453
-				}
454
-
455
-				public function process(?string $userId, array $input, callable $reportProgress): array {
456
-					if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
457
-						$this->provider->setUserId($userId);
458
-					}
459
-					try {
460
-						$result = $this->provider->transcribeFile($input['input']);
461
-					} catch (\RuntimeException $e) {
462
-						throw new ProcessingException($e->getMessage(), 0, $e);
463
-					}
464
-					return ['output' => $result];
465
-				}
466
-
467
-				public function getInputShapeEnumValues(): array {
468
-					return [];
469
-				}
470
-
471
-				public function getInputShapeDefaults(): array {
472
-					return [];
473
-				}
474
-
475
-				public function getOptionalInputShapeEnumValues(): array {
476
-					return [];
477
-				}
478
-
479
-				public function getOptionalInputShapeDefaults(): array {
480
-					return [];
481
-				}
482
-
483
-				public function getOutputShapeEnumValues(): array {
484
-					return [];
485
-				}
486
-
487
-				public function getOptionalOutputShapeEnumValues(): array {
488
-					return [];
489
-				}
490
-			};
491
-			$newProviders[$newProvider->getId()] = $newProvider;
492
-		}
493
-
494
-		return $newProviders;
495
-	}
496
-
497
-	/**
498
-	 * Dispatches the event to collect external providers and task types.
499
-	 * Caches the result within the request.
500
-	 */
501
-	private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
502
-		if ($this->eventResult !== null) {
503
-			return $this->eventResult;
504
-		}
505
-
506
-		$this->eventResult = new GetTaskProcessingProvidersEvent();
507
-		$this->dispatcher->dispatchTyped($this->eventResult);
508
-		return $this->eventResult ;
509
-	}
510
-
511
-	/**
512
-	 * @return IProvider[]
513
-	 */
514
-	private function _getProviders(): array {
515
-		$context = $this->coordinator->getRegistrationContext();
516
-
517
-		if ($context === null) {
518
-			return [];
519
-		}
520
-
521
-		$providers = [];
522
-
523
-		foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
524
-			$class = $providerServiceRegistration->getService();
525
-			try {
526
-				/** @var IProvider $provider */
527
-				$provider = $this->serverContainer->get($class);
528
-				if (isset($providers[$provider->getId()])) {
529
-					$this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
530
-				}
531
-				$providers[$provider->getId()] = $provider;
532
-			} catch (\Throwable $e) {
533
-				$this->logger->error('Failed to load task processing provider ' . $class, [
534
-					'exception' => $e,
535
-				]);
536
-			}
537
-		}
538
-
539
-		$event = $this->dispatchGetProvidersEvent();
540
-		$externalProviders = $event->getProviders();
541
-		foreach ($externalProviders as $provider) {
542
-			if (!isset($providers[$provider->getId()])) {
543
-				$providers[$provider->getId()] = $provider;
544
-			} else {
545
-				$this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
546
-			}
547
-		}
548
-
549
-		$providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
550
-
551
-		return $providers;
552
-	}
553
-
554
-	/**
555
-	 * @return ITaskType[]
556
-	 */
557
-	private function _getTaskTypes(): array {
558
-		$context = $this->coordinator->getRegistrationContext();
559
-
560
-		if ($context === null) {
561
-			return [];
562
-		}
563
-
564
-		if ($this->taskTypes !== null) {
565
-			return $this->taskTypes;
566
-		}
567
-
568
-		// Default task types
569
-		$taskTypes = [
570
-			\OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
571
-			\OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
572
-			\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
573
-			\OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
574
-			\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
575
-			\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
576
-			\OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
577
-			\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
578
-			\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
579
-			\OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
580
-			\OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
581
-			\OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
582
-			\OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
583
-			\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
584
-			\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
585
-			\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
586
-			\OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
587
-			\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
588
-		];
589
-
590
-		foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
591
-			$class = $providerServiceRegistration->getService();
592
-			try {
593
-				/** @var ITaskType $provider */
594
-				$taskType = $this->serverContainer->get($class);
595
-				if (isset($taskTypes[$taskType->getId()])) {
596
-					$this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
597
-				}
598
-				$taskTypes[$taskType->getId()] = $taskType;
599
-			} catch (\Throwable $e) {
600
-				$this->logger->error('Failed to load task processing task type ' . $class, [
601
-					'exception' => $e,
602
-				]);
603
-			}
604
-		}
605
-
606
-		$event = $this->dispatchGetProvidersEvent();
607
-		$externalTaskTypes = $event->getTaskTypes();
608
-		foreach ($externalTaskTypes as $taskType) {
609
-			if (isset($taskTypes[$taskType->getId()])) {
610
-				$this->logger->warning('External task processing task type is using ID ' . $taskType->getId() . ' which is already used by a locally registered task type (' . get_class($taskTypes[$taskType->getId()]) . ')');
611
-			}
612
-			$taskTypes[$taskType->getId()] = $taskType;
613
-		}
614
-
615
-		$taskTypes += $this->_getTextProcessingTaskTypes();
616
-
617
-		$this->taskTypes = $taskTypes;
618
-		return $this->taskTypes;
619
-	}
620
-
621
-	/**
622
-	 * @return array
623
-	 */
624
-	private function _getTaskTypeSettings(): array {
625
-		try {
626
-			$json = $this->config->getAppValue('core', 'ai.taskprocessing_type_preferences', '');
627
-			if ($json === '') {
628
-				return [];
629
-			}
630
-			return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
631
-		} catch (\JsonException $e) {
632
-			$this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
633
-			$taskTypeSettings = [];
634
-			$taskTypes = $this->_getTaskTypes();
635
-			foreach ($taskTypes as $taskType) {
636
-				$taskTypeSettings[$taskType->getId()] = false;
637
-			};
638
-
639
-			return $taskTypeSettings;
640
-		}
641
-
642
-	}
643
-
644
-	/**
645
-	 * @param ShapeDescriptor[] $spec
646
-	 * @param array<array-key, string|numeric> $defaults
647
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
648
-	 * @param array $io
649
-	 * @param bool $optional
650
-	 * @return void
651
-	 * @throws ValidationException
652
-	 */
653
-	private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
654
-		foreach ($spec as $key => $descriptor) {
655
-			$type = $descriptor->getShapeType();
656
-			if (!isset($io[$key])) {
657
-				if ($optional) {
658
-					continue;
659
-				}
660
-				if (isset($defaults[$key])) {
661
-					if (EShapeType::getScalarType($type) !== $type) {
662
-						throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
663
-					}
664
-					if (EShapeType::isFileType($type)) {
665
-						throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
666
-					}
667
-					$type->validateInput($defaults[$key]);
668
-					continue;
669
-				}
670
-				throw new ValidationException('Missing key: "' . $key . '"');
671
-			}
672
-			try {
673
-				$type->validateInput($io[$key]);
674
-				if ($type === EShapeType::Enum) {
675
-					if (!isset($enumValues[$key])) {
676
-						throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
677
-					}
678
-					$type->validateEnum($io[$key], $enumValues[$key]);
679
-				}
680
-			} catch (ValidationException $e) {
681
-				throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
682
-			}
683
-		}
684
-	}
685
-
686
-	/**
687
-	 * Takes task input data and replaces fileIds with File objects
688
-	 *
689
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
690
-	 * @param array<array-key, numeric|string> ...$defaultSpecs the specs
691
-	 * @return array<array-key, list<numeric|string>|numeric|string>
692
-	 */
693
-	public function fillInputDefaults(array $input, ...$defaultSpecs): array {
694
-		$spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
695
-		return array_merge($spec, $input);
696
-	}
697
-
698
-	/**
699
-	 * @param ShapeDescriptor[] $spec
700
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
701
-	 * @param array $io
702
-	 * @param bool $optional
703
-	 * @return void
704
-	 * @throws ValidationException
705
-	 */
706
-	private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
707
-		foreach ($spec as $key => $descriptor) {
708
-			$type = $descriptor->getShapeType();
709
-			if (!isset($io[$key])) {
710
-				if ($optional) {
711
-					continue;
712
-				}
713
-				throw new ValidationException('Missing key: "' . $key . '"');
714
-			}
715
-			try {
716
-				$type->validateOutputWithFileIds($io[$key]);
717
-				if (isset($enumValues[$key])) {
718
-					$type->validateEnum($io[$key], $enumValues[$key]);
719
-				}
720
-			} catch (ValidationException $e) {
721
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
722
-			}
723
-		}
724
-	}
725
-
726
-	/**
727
-	 * @param ShapeDescriptor[] $spec
728
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
729
-	 * @param array $io
730
-	 * @param bool $optional
731
-	 * @return void
732
-	 * @throws ValidationException
733
-	 */
734
-	private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
735
-		foreach ($spec as $key => $descriptor) {
736
-			$type = $descriptor->getShapeType();
737
-			if (!isset($io[$key])) {
738
-				if ($optional) {
739
-					continue;
740
-				}
741
-				throw new ValidationException('Missing key: "' . $key . '"');
742
-			}
743
-			try {
744
-				$type->validateOutputWithFileData($io[$key]);
745
-				if (isset($enumValues[$key])) {
746
-					$type->validateEnum($io[$key], $enumValues[$key]);
747
-				}
748
-			} catch (ValidationException $e) {
749
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
750
-			}
751
-		}
752
-	}
753
-
754
-	/**
755
-	 * @param array<array-key, T> $array The array to filter
756
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
757
-	 * @return array<array-key, T>
758
-	 * @psalm-template T
759
-	 */
760
-	private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
761
-		$keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
762
-		$keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
763
-		$values = array_map(fn (string $key) => $array[$key], $keys);
764
-		return array_combine($keys, $values);
765
-	}
766
-
767
-	public function hasProviders(): bool {
768
-		return count($this->getProviders()) !== 0;
769
-	}
770
-
771
-	public function getProviders(): array {
772
-		if ($this->providers === null) {
773
-			$this->providers = $this->_getProviders();
774
-		}
775
-
776
-		return $this->providers;
777
-	}
778
-
779
-	public function getPreferredProvider(string $taskTypeId) {
780
-		try {
781
-			if ($this->preferences === null) {
782
-				$this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
783
-				if ($this->preferences === null) {
784
-					$this->preferences = json_decode($this->config->getAppValue('core', 'ai.taskprocessing_provider_preferences', 'null'), associative: true, flags: JSON_THROW_ON_ERROR);
785
-					$this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
786
-				}
787
-			}
788
-
789
-			$providers = $this->getProviders();
790
-			if (isset($this->preferences[$taskTypeId])) {
791
-				$providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
792
-					$carry[$provider->getId()] = $provider;
793
-					return $carry;
794
-				}, []);
795
-				$this->providersById = $providersById;
796
-				if (isset($providersById[$this->preferences[$taskTypeId]])) {
797
-					return $providersById[$this->preferences[$taskTypeId]];
798
-				}
799
-			}
800
-			// By default, use the first available provider
801
-			foreach ($providers as $provider) {
802
-				if ($provider->getTaskTypeId() === $taskTypeId) {
803
-					return $provider;
804
-				}
805
-			}
806
-		} catch (\JsonException $e) {
807
-			$this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
808
-		}
809
-		throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
810
-	}
811
-
812
-	public function getAvailableTaskTypes(bool $showDisabled = false): array {
813
-		if ($this->availableTaskTypes === null) {
814
-			$cachedValue = $this->distributedCache->get('available_task_types_v2');
815
-			if ($cachedValue !== null) {
816
-				$this->availableTaskTypes = unserialize($cachedValue);
817
-			}
818
-		}
819
-		// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
820
-		if ($this->availableTaskTypes === null || $showDisabled) {
821
-			$taskTypes = $this->_getTaskTypes();
822
-			$taskTypeSettings = $this->_getTaskTypeSettings();
823
-
824
-			$availableTaskTypes = [];
825
-			foreach ($taskTypes as $taskType) {
826
-				if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
827
-					continue;
828
-				}
829
-				try {
830
-					$provider = $this->getPreferredProvider($taskType->getId());
831
-				} catch (\OCP\TaskProcessing\Exception\Exception $e) {
832
-					continue;
833
-				}
834
-				try {
835
-					$availableTaskTypes[$provider->getTaskTypeId()] = [
836
-						'name' => $taskType->getName(),
837
-						'description' => $taskType->getDescription(),
838
-						'optionalInputShape' => $provider->getOptionalInputShape(),
839
-						'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
840
-						'inputShapeDefaults' => $provider->getInputShapeDefaults(),
841
-						'inputShape' => $taskType->getInputShape(),
842
-						'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
843
-						'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
844
-						'outputShape' => $taskType->getOutputShape(),
845
-						'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
846
-						'optionalOutputShape' => $provider->getOptionalOutputShape(),
847
-						'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
848
-					];
849
-				} catch (\Throwable $e) {
850
-					$this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
851
-				}
852
-			}
853
-
854
-			if ($showDisabled) {
855
-				// Do not cache showDisabled, ever.
856
-				return $availableTaskTypes;
857
-			}
858
-
859
-			$this->availableTaskTypes = $availableTaskTypes;
860
-			$this->distributedCache->set('available_task_types_v2', serialize($this->availableTaskTypes), 60);
861
-		}
862
-
863
-
864
-		return $this->availableTaskTypes;
865
-	}
866
-
867
-	public function canHandleTask(Task $task): bool {
868
-		return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
869
-	}
870
-
871
-	public function scheduleTask(Task $task): void {
872
-		if (!$this->canHandleTask($task)) {
873
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
874
-		}
875
-		$this->prepareTask($task);
876
-		$task->setStatus(Task::STATUS_SCHEDULED);
877
-		$this->storeTask($task);
878
-		// schedule synchronous job if the provider is synchronous
879
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
880
-		if ($provider instanceof ISynchronousProvider) {
881
-			$this->jobList->add(SynchronousBackgroundJob::class, null);
882
-		}
883
-	}
884
-
885
-	public function runTask(Task $task): Task {
886
-		if (!$this->canHandleTask($task)) {
887
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
888
-		}
889
-
890
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
891
-		if ($provider instanceof ISynchronousProvider) {
892
-			$this->prepareTask($task);
893
-			$task->setStatus(Task::STATUS_SCHEDULED);
894
-			$this->storeTask($task);
895
-			$this->processTask($task, $provider);
896
-			$task = $this->getTask($task->getId());
897
-		} else {
898
-			$this->scheduleTask($task);
899
-			// poll task
900
-			while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
901
-				sleep(1);
902
-				$task = $this->getTask($task->getId());
903
-			}
904
-		}
905
-		return $task;
906
-	}
907
-
908
-	public function processTask(Task $task, ISynchronousProvider $provider): bool {
909
-		try {
910
-			try {
911
-				$input = $this->prepareInputData($task);
912
-			} catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
913
-				$this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
914
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
915
-				return false;
916
-			}
917
-			try {
918
-				$this->setTaskStatus($task, Task::STATUS_RUNNING);
919
-				$output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
920
-			} catch (ProcessingException $e) {
921
-				$this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
922
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
923
-				return false;
924
-			} catch (\Throwable $e) {
925
-				$this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
926
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
927
-				return false;
928
-			}
929
-			$this->setTaskResult($task->getId(), null, $output);
930
-		} catch (NotFoundException $e) {
931
-			$this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
932
-		} catch (Exception $e) {
933
-			$this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
934
-		}
935
-		return true;
936
-	}
937
-
938
-	public function deleteTask(Task $task): void {
939
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
940
-		$this->taskMapper->delete($taskEntity);
941
-	}
942
-
943
-	public function getTask(int $id): Task {
944
-		try {
945
-			$taskEntity = $this->taskMapper->find($id);
946
-			return $taskEntity->toPublicTask();
947
-		} catch (DoesNotExistException $e) {
948
-			throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
949
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
950
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
951
-		} catch (\JsonException $e) {
952
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
953
-		}
954
-	}
955
-
956
-	public function cancelTask(int $id): void {
957
-		$task = $this->getTask($id);
958
-		if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
959
-			return;
960
-		}
961
-		$task->setStatus(Task::STATUS_CANCELLED);
962
-		$task->setEndedAt(time());
963
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
964
-		try {
965
-			$this->taskMapper->update($taskEntity);
966
-			$this->runWebhook($task);
967
-		} catch (\OCP\DB\Exception $e) {
968
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
969
-		}
970
-	}
971
-
972
-	public function setTaskProgress(int $id, float $progress): bool {
973
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
974
-		$task = $this->getTask($id);
975
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
976
-			return false;
977
-		}
978
-		// only set the start time if the task is going from scheduled to running
979
-		if ($task->getstatus() === Task::STATUS_SCHEDULED) {
980
-			$task->setStartedAt(time());
981
-		}
982
-		$task->setStatus(Task::STATUS_RUNNING);
983
-		$task->setProgress($progress);
984
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
985
-		try {
986
-			$this->taskMapper->update($taskEntity);
987
-		} catch (\OCP\DB\Exception $e) {
988
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
989
-		}
990
-		return true;
991
-	}
992
-
993
-	public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
994
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
995
-		$task = $this->getTask($id);
996
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
997
-			$this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
998
-			return;
999
-		}
1000
-		if ($error !== null) {
1001
-			$task->setStatus(Task::STATUS_FAILED);
1002
-			$task->setEndedAt(time());
1003
-			// truncate error message to 1000 characters
1004
-			$task->setErrorMessage(mb_substr($error, 0, 1000));
1005
-			$this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1006
-		} elseif ($result !== null) {
1007
-			$taskTypes = $this->getAvailableTaskTypes();
1008
-			$outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1009
-			$outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1010
-			$optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1011
-			$optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1012
-			try {
1013
-				// validate output
1014
-				if (!$isUsingFileIds) {
1015
-					$this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1016
-					$this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1017
-				} else {
1018
-					$this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1019
-					$this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1020
-				}
1021
-				$output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1022
-				// extract raw data and put it in files, replace it with file ids
1023
-				if (!$isUsingFileIds) {
1024
-					$output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1025
-				} else {
1026
-					$this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1027
-				}
1028
-				// Turn file objects into IDs
1029
-				foreach ($output as $key => $value) {
1030
-					if ($value instanceof Node) {
1031
-						$output[$key] = $value->getId();
1032
-					}
1033
-					if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1034
-						$output[$key] = array_map(fn ($node) => $node->getId(), $value);
1035
-					}
1036
-				}
1037
-				$task->setOutput($output);
1038
-				$task->setProgress(1);
1039
-				$task->setStatus(Task::STATUS_SUCCESSFUL);
1040
-				$task->setEndedAt(time());
1041
-			} catch (ValidationException $e) {
1042
-				$task->setProgress(1);
1043
-				$task->setStatus(Task::STATUS_FAILED);
1044
-				$task->setEndedAt(time());
1045
-				$error = 'The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec';
1046
-				$task->setErrorMessage($error);
1047
-				$this->logger->error($error, ['exception' => $e, 'output' => $result]);
1048
-			} catch (NotPermittedException $e) {
1049
-				$task->setProgress(1);
1050
-				$task->setStatus(Task::STATUS_FAILED);
1051
-				$task->setEndedAt(time());
1052
-				$error = 'The task was processed successfully but storing the output in a file failed';
1053
-				$task->setErrorMessage($error);
1054
-				$this->logger->error($error, ['exception' => $e]);
1055
-			} catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1056
-				$task->setProgress(1);
1057
-				$task->setStatus(Task::STATUS_FAILED);
1058
-				$task->setEndedAt(time());
1059
-				$error = 'The task was processed successfully but the result file could not be found';
1060
-				$task->setErrorMessage($error);
1061
-				$this->logger->error($error, ['exception' => $e]);
1062
-			}
1063
-		}
1064
-		try {
1065
-			$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1066
-		} catch (\JsonException $e) {
1067
-			throw new \OCP\TaskProcessing\Exception\Exception('The task was processed successfully but the provider\'s output could not be encoded as JSON for the database.', 0, $e);
1068
-		}
1069
-		try {
1070
-			$this->taskMapper->update($taskEntity);
1071
-			$this->runWebhook($task);
1072
-		} catch (\OCP\DB\Exception $e) {
1073
-			throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1074
-		}
1075
-		if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1076
-			$event = new TaskSuccessfulEvent($task);
1077
-		} else {
1078
-			$event = new TaskFailedEvent($task, $error);
1079
-		}
1080
-		$this->dispatcher->dispatchTyped($event);
1081
-	}
1082
-
1083
-	public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1084
-		try {
1085
-			$taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1086
-			return $taskEntity->toPublicTask();
1087
-		} catch (DoesNotExistException $e) {
1088
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1089
-		} catch (\OCP\DB\Exception $e) {
1090
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1091
-		} catch (\JsonException $e) {
1092
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1093
-		}
1094
-	}
1095
-
1096
-	/**
1097
-	 * Takes task input data and replaces fileIds with File objects
1098
-	 *
1099
-	 * @param string|null $userId
1100
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
1101
-	 * @param ShapeDescriptor[] ...$specs the specs
1102
-	 * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1103
-	 * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1104
-	 */
1105
-	public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1106
-		if ($userId !== null) {
1107
-			\OC_Util::setupFS($userId);
1108
-		}
1109
-		$newInputOutput = [];
1110
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1111
-		foreach ($spec as $key => $descriptor) {
1112
-			$type = $descriptor->getShapeType();
1113
-			if (!isset($input[$key])) {
1114
-				continue;
1115
-			}
1116
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1117
-				$newInputOutput[$key] = $input[$key];
1118
-				continue;
1119
-			}
1120
-			if (EShapeType::getScalarType($type) === $type) {
1121
-				// is scalar
1122
-				$node = $this->validateFileId((int)$input[$key]);
1123
-				$this->validateUserAccessToFile($input[$key], $userId);
1124
-				$newInputOutput[$key] = $node;
1125
-			} else {
1126
-				// is list
1127
-				$newInputOutput[$key] = [];
1128
-				foreach ($input[$key] as $item) {
1129
-					$node = $this->validateFileId((int)$item);
1130
-					$this->validateUserAccessToFile($item, $userId);
1131
-					$newInputOutput[$key][] = $node;
1132
-				}
1133
-			}
1134
-		}
1135
-		return $newInputOutput;
1136
-	}
1137
-
1138
-	public function getUserTask(int $id, ?string $userId): Task {
1139
-		try {
1140
-			$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1141
-			return $taskEntity->toPublicTask();
1142
-		} catch (DoesNotExistException $e) {
1143
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1144
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1145
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1146
-		} catch (\JsonException $e) {
1147
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1148
-		}
1149
-	}
1150
-
1151
-	public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1152
-		try {
1153
-			$taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1154
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1155
-		} catch (\OCP\DB\Exception $e) {
1156
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1157
-		} catch (\JsonException $e) {
1158
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1159
-		}
1160
-	}
1161
-
1162
-	public function getTasks(
1163
-		?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1164
-		?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1165
-	): array {
1166
-		try {
1167
-			$taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1168
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1169
-		} catch (\OCP\DB\Exception $e) {
1170
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1171
-		} catch (\JsonException $e) {
1172
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1173
-		}
1174
-	}
1175
-
1176
-	public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1177
-		try {
1178
-			$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1179
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1180
-		} catch (\OCP\DB\Exception $e) {
1181
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1182
-		} catch (\JsonException $e) {
1183
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1184
-		}
1185
-	}
1186
-
1187
-	/**
1188
-	 *Takes task input or output and replaces base64 data with file ids
1189
-	 *
1190
-	 * @param array $output
1191
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1192
-	 * @return array
1193
-	 * @throws NotPermittedException
1194
-	 */
1195
-	public function encapsulateOutputFileData(array $output, ...$specs): array {
1196
-		$newOutput = [];
1197
-		try {
1198
-			$folder = $this->appData->getFolder('TaskProcessing');
1199
-		} catch (\OCP\Files\NotFoundException) {
1200
-			$folder = $this->appData->newFolder('TaskProcessing');
1201
-		}
1202
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1203
-		foreach ($spec as $key => $descriptor) {
1204
-			$type = $descriptor->getShapeType();
1205
-			if (!isset($output[$key])) {
1206
-				continue;
1207
-			}
1208
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1209
-				$newOutput[$key] = $output[$key];
1210
-				continue;
1211
-			}
1212
-			if (EShapeType::getScalarType($type) === $type) {
1213
-				/** @var SimpleFile $file */
1214
-				$file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1215
-				$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1216
-			} else {
1217
-				$newOutput = [];
1218
-				foreach ($output[$key] as $item) {
1219
-					/** @var SimpleFile $file */
1220
-					$file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1221
-					$newOutput[$key][] = $file->getId();
1222
-				}
1223
-			}
1224
-		}
1225
-		return $newOutput;
1226
-	}
1227
-
1228
-	/**
1229
-	 * @param Task $task
1230
-	 * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1231
-	 * @throws GenericFileException
1232
-	 * @throws LockedException
1233
-	 * @throws NotPermittedException
1234
-	 * @throws ValidationException|UnauthorizedException
1235
-	 */
1236
-	public function prepareInputData(Task $task): array {
1237
-		$taskTypes = $this->getAvailableTaskTypes();
1238
-		$inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1239
-		$optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1240
-		$input = $task->getInput();
1241
-		$input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1242
-		$input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1243
-		return $input;
1244
-	}
1245
-
1246
-	public function lockTask(Task $task): bool {
1247
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1248
-		if ($this->taskMapper->lockTask($taskEntity) === 0) {
1249
-			return false;
1250
-		}
1251
-		$task->setStatus(Task::STATUS_RUNNING);
1252
-		return true;
1253
-	}
1254
-
1255
-	/**
1256
-	 * @throws \JsonException
1257
-	 * @throws Exception
1258
-	 */
1259
-	public function setTaskStatus(Task $task, int $status): void {
1260
-		$currentTaskStatus = $task->getStatus();
1261
-		if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1262
-			$task->setStartedAt(time());
1263
-		} elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1264
-			$task->setEndedAt(time());
1265
-		} elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1266
-			$task->setScheduledAt(time());
1267
-		}
1268
-		$task->setStatus($status);
1269
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1270
-		$this->taskMapper->update($taskEntity);
1271
-	}
1272
-
1273
-	/**
1274
-	 * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1275
-	 *
1276
-	 * @param Task $task
1277
-	 * @return void
1278
-	 * @throws UnauthorizedException
1279
-	 * @throws ValidationException
1280
-	 * @throws \OCP\TaskProcessing\Exception\Exception
1281
-	 */
1282
-	private function prepareTask(Task $task): void {
1283
-		$taskTypes = $this->getAvailableTaskTypes();
1284
-		$taskType = $taskTypes[$task->getTaskTypeId()];
1285
-		$inputShape = $taskType['inputShape'];
1286
-		$inputShapeDefaults = $taskType['inputShapeDefaults'];
1287
-		$inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1288
-		$optionalInputShape = $taskType['optionalInputShape'];
1289
-		$optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1290
-		$optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1291
-		// validate input
1292
-		$this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1293
-		$this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1294
-		// authenticate access to mentioned files
1295
-		$ids = [];
1296
-		foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1297
-			if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1298
-				/** @var list<int>|int $inputSlot */
1299
-				$inputSlot = $task->getInput()[$key];
1300
-				if (is_array($inputSlot)) {
1301
-					$ids += $inputSlot;
1302
-				} else {
1303
-					$ids[] = $inputSlot;
1304
-				}
1305
-			}
1306
-		}
1307
-		foreach ($ids as $fileId) {
1308
-			$this->validateFileId($fileId);
1309
-			$this->validateUserAccessToFile($fileId, $task->getUserId());
1310
-		}
1311
-		// remove superfluous keys and set input
1312
-		$input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1313
-		$inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1314
-		$task->setInput($inputWithDefaults);
1315
-		$task->setScheduledAt(time());
1316
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
1317
-		// calculate expected completion time
1318
-		$completionExpectedAt = new \DateTime('now');
1319
-		$completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1320
-		$task->setCompletionExpectedAt($completionExpectedAt);
1321
-	}
1322
-
1323
-	/**
1324
-	 * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1325
-	 *
1326
-	 * @param Task $task
1327
-	 * @return void
1328
-	 * @throws Exception
1329
-	 * @throws \JsonException
1330
-	 */
1331
-	private function storeTask(Task $task): void {
1332
-		// create a db entity and insert into db table
1333
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1334
-		$this->taskMapper->insert($taskEntity);
1335
-		// make sure the scheduler knows the id
1336
-		$task->setId($taskEntity->getId());
1337
-	}
1338
-
1339
-	/**
1340
-	 * @param array $output
1341
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1342
-	 * @return array
1343
-	 * @throws NotPermittedException
1344
-	 */
1345
-	private function validateOutputFileIds(array $output, ...$specs): array {
1346
-		$newOutput = [];
1347
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1348
-		foreach ($spec as $key => $descriptor) {
1349
-			$type = $descriptor->getShapeType();
1350
-			if (!isset($output[$key])) {
1351
-				continue;
1352
-			}
1353
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1354
-				$newOutput[$key] = $output[$key];
1355
-				continue;
1356
-			}
1357
-			if (EShapeType::getScalarType($type) === $type) {
1358
-				// Is scalar file ID
1359
-				$newOutput[$key] = $this->validateFileId($output[$key]);
1360
-			} else {
1361
-				// Is list of file IDs
1362
-				$newOutput = [];
1363
-				foreach ($output[$key] as $item) {
1364
-					$newOutput[$key][] = $this->validateFileId($item);
1365
-				}
1366
-			}
1367
-		}
1368
-		return $newOutput;
1369
-	}
1370
-
1371
-	/**
1372
-	 * @param mixed $id
1373
-	 * @return File
1374
-	 * @throws ValidationException
1375
-	 */
1376
-	private function validateFileId(mixed $id): File {
1377
-		$node = $this->rootFolder->getFirstNodeById($id);
1378
-		if ($node === null) {
1379
-			$node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1380
-			if ($node === null) {
1381
-				throw new ValidationException('Could not find file ' . $id);
1382
-			} elseif (!$node instanceof File) {
1383
-				throw new ValidationException('File with id "' . $id . '" is not a file');
1384
-			}
1385
-		} elseif (!$node instanceof File) {
1386
-			throw new ValidationException('File with id "' . $id . '" is not a file');
1387
-		}
1388
-		return $node;
1389
-	}
1390
-
1391
-	/**
1392
-	 * @param mixed $fileId
1393
-	 * @param string|null $userId
1394
-	 * @return void
1395
-	 * @throws UnauthorizedException
1396
-	 */
1397
-	private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1398
-		if ($userId === null) {
1399
-			throw new UnauthorizedException('User does not have access to file ' . $fileId);
1400
-		}
1401
-		$mounts = $this->userMountCache->getMountsForFileId($fileId);
1402
-		$userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1403
-		if (!in_array($userId, $userIds)) {
1404
-			throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1405
-		}
1406
-	}
1407
-
1408
-	/**
1409
-	 * Make a request to the task's webhookUri if necessary
1410
-	 *
1411
-	 * @param Task $task
1412
-	 */
1413
-	private function runWebhook(Task $task): void {
1414
-		$uri = $task->getWebhookUri();
1415
-		$method = $task->getWebhookMethod();
1416
-
1417
-		if (!$uri || !$method) {
1418
-			return;
1419
-		}
1420
-
1421
-		if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1422
-			$client = $this->clientService->newClient();
1423
-			$httpMethod = preg_replace('/^HTTP:/', '', $method);
1424
-			$options = [
1425
-				'timeout' => 30,
1426
-				'body' => json_encode([
1427
-					'task' => $task->jsonSerialize(),
1428
-				]),
1429
-				'headers' => ['Content-Type' => 'application/json'],
1430
-			];
1431
-			try {
1432
-				$client->request($httpMethod, $uri, $options);
1433
-			} catch (ClientException|ServerException $e) {
1434
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1435
-			} catch (\Exception|\Throwable $e) {
1436
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1437
-			}
1438
-		} elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1439
-			$parsedMethod = explode(':', $method, 4);
1440
-			if (count($parsedMethod) < 3) {
1441
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1442
-			}
1443
-			[, $exAppId, $httpMethod] = $parsedMethod;
1444
-			if (!$this->appManager->isEnabledForAnyone('app_api')) {
1445
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1446
-				return;
1447
-			}
1448
-			try {
1449
-				$appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1450
-			} catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1451
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1452
-				return;
1453
-			}
1454
-			$exApp = $appApiFunctions->getExApp($exAppId);
1455
-			if ($exApp === null) {
1456
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1457
-				return;
1458
-			} elseif (!$exApp['enabled']) {
1459
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1460
-				return;
1461
-			}
1462
-			$requestParams = [
1463
-				'task' => $task->jsonSerialize(),
1464
-			];
1465
-			$requestOptions = [
1466
-				'timeout' => 30,
1467
-			];
1468
-			$response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1469
-			if (is_array($response) && isset($response['error'])) {
1470
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1471
-			}
1472
-		}
1473
-	}
70
+    public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
71
+    public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
72
+    public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
73
+
74
+    /** @var list<IProvider>|null */
75
+    private ?array $providers = null;
76
+
77
+    /**
78
+     * @var array<array-key,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[][]}>
79
+     */
80
+    private ?array $availableTaskTypes = null;
81
+
82
+    private IAppData $appData;
83
+    private ?array $preferences = null;
84
+    private ?array $providersById = null;
85
+
86
+    /** @var ITaskType[]|null */
87
+    private ?array $taskTypes = null;
88
+    private ICache $distributedCache;
89
+
90
+    private ?GetTaskProcessingProvidersEvent $eventResult = null;
91
+
92
+    public function __construct(
93
+        private IConfig $config,
94
+        private Coordinator $coordinator,
95
+        private IServerContainer $serverContainer,
96
+        private LoggerInterface $logger,
97
+        private TaskMapper $taskMapper,
98
+        private IJobList $jobList,
99
+        private IEventDispatcher $dispatcher,
100
+        IAppDataFactory $appDataFactory,
101
+        private IRootFolder $rootFolder,
102
+        private \OCP\TextToImage\IManager $textToImageManager,
103
+        private IUserMountCache $userMountCache,
104
+        private IClientService $clientService,
105
+        private IAppManager $appManager,
106
+        ICacheFactory $cacheFactory,
107
+    ) {
108
+        $this->appData = $appDataFactory->get('core');
109
+        $this->distributedCache = $cacheFactory->createDistributed('task_processing::');
110
+    }
111
+
112
+
113
+    /**
114
+     * This is almost a copy of textProcessingManager->getProviders
115
+     * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
116
+     */
117
+    private function _getRawTextProcessingProviders(): array {
118
+        $context = $this->coordinator->getRegistrationContext();
119
+        if ($context === null) {
120
+            return [];
121
+        }
122
+
123
+        $providers = [];
124
+
125
+        foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
126
+            $class = $providerServiceRegistration->getService();
127
+            try {
128
+                $providers[$class] = $this->serverContainer->get($class);
129
+            } catch (\Throwable $e) {
130
+                $this->logger->error('Failed to load Text processing provider ' . $class, [
131
+                    'exception' => $e,
132
+                ]);
133
+            }
134
+        }
135
+
136
+        return $providers;
137
+    }
138
+
139
+    private function _getTextProcessingProviders(): array {
140
+        $oldProviders = $this->_getRawTextProcessingProviders();
141
+        $newProviders = [];
142
+        foreach ($oldProviders as $oldProvider) {
143
+            $provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
144
+                private \OCP\TextProcessing\IProvider $provider;
145
+
146
+                public function __construct(\OCP\TextProcessing\IProvider $provider) {
147
+                    $this->provider = $provider;
148
+                }
149
+
150
+                public function getId(): string {
151
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
152
+                        return $this->provider->getId();
153
+                    }
154
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
155
+                }
156
+
157
+                public function getName(): string {
158
+                    return $this->provider->getName();
159
+                }
160
+
161
+                public function getTaskTypeId(): string {
162
+                    return match ($this->provider->getTaskType()) {
163
+                        \OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
164
+                        \OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
165
+                        \OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
166
+                        \OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
167
+                        default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
168
+                    };
169
+                }
170
+
171
+                public function getExpectedRuntime(): int {
172
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
173
+                        return $this->provider->getExpectedRuntime();
174
+                    }
175
+                    return 60;
176
+                }
177
+
178
+                public function getOptionalInputShape(): array {
179
+                    return [];
180
+                }
181
+
182
+                public function getOptionalOutputShape(): array {
183
+                    return [];
184
+                }
185
+
186
+                public function process(?string $userId, array $input, callable $reportProgress): array {
187
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
188
+                        $this->provider->setUserId($userId);
189
+                    }
190
+                    try {
191
+                        return ['output' => $this->provider->process($input['input'])];
192
+                    } catch (\RuntimeException $e) {
193
+                        throw new ProcessingException($e->getMessage(), 0, $e);
194
+                    }
195
+                }
196
+
197
+                public function getInputShapeEnumValues(): array {
198
+                    return [];
199
+                }
200
+
201
+                public function getInputShapeDefaults(): array {
202
+                    return [];
203
+                }
204
+
205
+                public function getOptionalInputShapeEnumValues(): array {
206
+                    return [];
207
+                }
208
+
209
+                public function getOptionalInputShapeDefaults(): array {
210
+                    return [];
211
+                }
212
+
213
+                public function getOutputShapeEnumValues(): array {
214
+                    return [];
215
+                }
216
+
217
+                public function getOptionalOutputShapeEnumValues(): array {
218
+                    return [];
219
+                }
220
+            };
221
+            $newProviders[$provider->getId()] = $provider;
222
+        }
223
+
224
+        return $newProviders;
225
+    }
226
+
227
+    /**
228
+     * @return ITaskType[]
229
+     */
230
+    private function _getTextProcessingTaskTypes(): array {
231
+        $oldProviders = $this->_getRawTextProcessingProviders();
232
+        $newTaskTypes = [];
233
+        foreach ($oldProviders as $oldProvider) {
234
+            // These are already implemented in the TaskProcessing realm
235
+            if (in_array($oldProvider->getTaskType(), [
236
+                \OCP\TextProcessing\FreePromptTaskType::class,
237
+                \OCP\TextProcessing\HeadlineTaskType::class,
238
+                \OCP\TextProcessing\TopicsTaskType::class,
239
+                \OCP\TextProcessing\SummaryTaskType::class
240
+            ], true)) {
241
+                continue;
242
+            }
243
+            $taskType = new class($oldProvider->getTaskType()) implements ITaskType {
244
+                private string $oldTaskTypeClass;
245
+                private \OCP\TextProcessing\ITaskType $oldTaskType;
246
+                private IL10N $l;
247
+
248
+                public function __construct(string $oldTaskTypeClass) {
249
+                    $this->oldTaskTypeClass = $oldTaskTypeClass;
250
+                    $this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
251
+                    $this->l = \OCP\Server::get(IFactory::class)->get('core');
252
+                }
253
+
254
+                public function getId(): string {
255
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
256
+                }
257
+
258
+                public function getName(): string {
259
+                    return $this->oldTaskType->getName();
260
+                }
261
+
262
+                public function getDescription(): string {
263
+                    return $this->oldTaskType->getDescription();
264
+                }
265
+
266
+                public function getInputShape(): array {
267
+                    return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
268
+                }
269
+
270
+                public function getOutputShape(): array {
271
+                    return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
272
+                }
273
+            };
274
+            $newTaskTypes[$taskType->getId()] = $taskType;
275
+        }
276
+
277
+        return $newTaskTypes;
278
+    }
279
+
280
+    /**
281
+     * @return IProvider[]
282
+     */
283
+    private function _getTextToImageProviders(): array {
284
+        $oldProviders = $this->textToImageManager->getProviders();
285
+        $newProviders = [];
286
+        foreach ($oldProviders as $oldProvider) {
287
+            $newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
288
+                private \OCP\TextToImage\IProvider $provider;
289
+                private IAppData $appData;
290
+
291
+                public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
292
+                    $this->provider = $provider;
293
+                    $this->appData = $appData;
294
+                }
295
+
296
+                public function getId(): string {
297
+                    return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
298
+                }
299
+
300
+                public function getName(): string {
301
+                    return $this->provider->getName();
302
+                }
303
+
304
+                public function getTaskTypeId(): string {
305
+                    return TextToImage::ID;
306
+                }
307
+
308
+                public function getExpectedRuntime(): int {
309
+                    return $this->provider->getExpectedRuntime();
310
+                }
311
+
312
+                public function getOptionalInputShape(): array {
313
+                    return [];
314
+                }
315
+
316
+                public function getOptionalOutputShape(): array {
317
+                    return [];
318
+                }
319
+
320
+                public function process(?string $userId, array $input, callable $reportProgress): array {
321
+                    try {
322
+                        $folder = $this->appData->getFolder('text2image');
323
+                    } catch (\OCP\Files\NotFoundException) {
324
+                        $folder = $this->appData->newFolder('text2image');
325
+                    }
326
+                    $resources = [];
327
+                    $files = [];
328
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
329
+                        $file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
330
+                        $files[] = $file;
331
+                        $resource = $file->write();
332
+                        if ($resource !== false && $resource !== true && is_resource($resource)) {
333
+                            $resources[] = $resource;
334
+                        } else {
335
+                            throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
336
+                        }
337
+                    }
338
+                    if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
339
+                        $this->provider->setUserId($userId);
340
+                    }
341
+                    try {
342
+                        $this->provider->generate($input['input'], $resources);
343
+                    } catch (\RuntimeException $e) {
344
+                        throw new ProcessingException($e->getMessage(), 0, $e);
345
+                    }
346
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
347
+                        if (is_resource($resources[$i])) {
348
+                            // If $resource hasn't been closed yet, we'll do that here
349
+                            fclose($resources[$i]);
350
+                        }
351
+                    }
352
+                    return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
353
+                }
354
+
355
+                public function getInputShapeEnumValues(): array {
356
+                    return [];
357
+                }
358
+
359
+                public function getInputShapeDefaults(): array {
360
+                    return [];
361
+                }
362
+
363
+                public function getOptionalInputShapeEnumValues(): array {
364
+                    return [];
365
+                }
366
+
367
+                public function getOptionalInputShapeDefaults(): array {
368
+                    return [];
369
+                }
370
+
371
+                public function getOutputShapeEnumValues(): array {
372
+                    return [];
373
+                }
374
+
375
+                public function getOptionalOutputShapeEnumValues(): array {
376
+                    return [];
377
+                }
378
+            };
379
+            $newProviders[$newProvider->getId()] = $newProvider;
380
+        }
381
+
382
+        return $newProviders;
383
+    }
384
+
385
+    /**
386
+     * This is almost a copy of SpeechToTextManager->getProviders
387
+     * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
388
+     */
389
+    private function _getRawSpeechToTextProviders(): array {
390
+        $context = $this->coordinator->getRegistrationContext();
391
+        if ($context === null) {
392
+            return [];
393
+        }
394
+        $providers = [];
395
+        foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
396
+            $class = $providerServiceRegistration->getService();
397
+            try {
398
+                $providers[$class] = $this->serverContainer->get($class);
399
+            } catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
400
+                $this->logger->error('Failed to load SpeechToText provider ' . $class, [
401
+                    'exception' => $e,
402
+                ]);
403
+            }
404
+        }
405
+
406
+        return $providers;
407
+    }
408
+
409
+    /**
410
+     * @return IProvider[]
411
+     */
412
+    private function _getSpeechToTextProviders(): array {
413
+        $oldProviders = $this->_getRawSpeechToTextProviders();
414
+        $newProviders = [];
415
+        foreach ($oldProviders as $oldProvider) {
416
+            $newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
417
+                private ISpeechToTextProvider $provider;
418
+                private IAppData $appData;
419
+
420
+                private IRootFolder $rootFolder;
421
+
422
+                public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
423
+                    $this->provider = $provider;
424
+                    $this->rootFolder = $rootFolder;
425
+                    $this->appData = $appData;
426
+                }
427
+
428
+                public function getId(): string {
429
+                    if ($this->provider instanceof ISpeechToTextProviderWithId) {
430
+                        return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
431
+                    }
432
+                    return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
433
+                }
434
+
435
+                public function getName(): string {
436
+                    return $this->provider->getName();
437
+                }
438
+
439
+                public function getTaskTypeId(): string {
440
+                    return AudioToText::ID;
441
+                }
442
+
443
+                public function getExpectedRuntime(): int {
444
+                    return 60;
445
+                }
446
+
447
+                public function getOptionalInputShape(): array {
448
+                    return [];
449
+                }
450
+
451
+                public function getOptionalOutputShape(): array {
452
+                    return [];
453
+                }
454
+
455
+                public function process(?string $userId, array $input, callable $reportProgress): array {
456
+                    if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
457
+                        $this->provider->setUserId($userId);
458
+                    }
459
+                    try {
460
+                        $result = $this->provider->transcribeFile($input['input']);
461
+                    } catch (\RuntimeException $e) {
462
+                        throw new ProcessingException($e->getMessage(), 0, $e);
463
+                    }
464
+                    return ['output' => $result];
465
+                }
466
+
467
+                public function getInputShapeEnumValues(): array {
468
+                    return [];
469
+                }
470
+
471
+                public function getInputShapeDefaults(): array {
472
+                    return [];
473
+                }
474
+
475
+                public function getOptionalInputShapeEnumValues(): array {
476
+                    return [];
477
+                }
478
+
479
+                public function getOptionalInputShapeDefaults(): array {
480
+                    return [];
481
+                }
482
+
483
+                public function getOutputShapeEnumValues(): array {
484
+                    return [];
485
+                }
486
+
487
+                public function getOptionalOutputShapeEnumValues(): array {
488
+                    return [];
489
+                }
490
+            };
491
+            $newProviders[$newProvider->getId()] = $newProvider;
492
+        }
493
+
494
+        return $newProviders;
495
+    }
496
+
497
+    /**
498
+     * Dispatches the event to collect external providers and task types.
499
+     * Caches the result within the request.
500
+     */
501
+    private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
502
+        if ($this->eventResult !== null) {
503
+            return $this->eventResult;
504
+        }
505
+
506
+        $this->eventResult = new GetTaskProcessingProvidersEvent();
507
+        $this->dispatcher->dispatchTyped($this->eventResult);
508
+        return $this->eventResult ;
509
+    }
510
+
511
+    /**
512
+     * @return IProvider[]
513
+     */
514
+    private function _getProviders(): array {
515
+        $context = $this->coordinator->getRegistrationContext();
516
+
517
+        if ($context === null) {
518
+            return [];
519
+        }
520
+
521
+        $providers = [];
522
+
523
+        foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
524
+            $class = $providerServiceRegistration->getService();
525
+            try {
526
+                /** @var IProvider $provider */
527
+                $provider = $this->serverContainer->get($class);
528
+                if (isset($providers[$provider->getId()])) {
529
+                    $this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
530
+                }
531
+                $providers[$provider->getId()] = $provider;
532
+            } catch (\Throwable $e) {
533
+                $this->logger->error('Failed to load task processing provider ' . $class, [
534
+                    'exception' => $e,
535
+                ]);
536
+            }
537
+        }
538
+
539
+        $event = $this->dispatchGetProvidersEvent();
540
+        $externalProviders = $event->getProviders();
541
+        foreach ($externalProviders as $provider) {
542
+            if (!isset($providers[$provider->getId()])) {
543
+                $providers[$provider->getId()] = $provider;
544
+            } else {
545
+                $this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
546
+            }
547
+        }
548
+
549
+        $providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
550
+
551
+        return $providers;
552
+    }
553
+
554
+    /**
555
+     * @return ITaskType[]
556
+     */
557
+    private function _getTaskTypes(): array {
558
+        $context = $this->coordinator->getRegistrationContext();
559
+
560
+        if ($context === null) {
561
+            return [];
562
+        }
563
+
564
+        if ($this->taskTypes !== null) {
565
+            return $this->taskTypes;
566
+        }
567
+
568
+        // Default task types
569
+        $taskTypes = [
570
+            \OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
571
+            \OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
572
+            \OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
573
+            \OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
574
+            \OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
575
+            \OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
576
+            \OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
577
+            \OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
578
+            \OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
579
+            \OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
580
+            \OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
581
+            \OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
582
+            \OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
583
+            \OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
584
+            \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
585
+            \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
586
+            \OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
587
+            \OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
588
+        ];
589
+
590
+        foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
591
+            $class = $providerServiceRegistration->getService();
592
+            try {
593
+                /** @var ITaskType $provider */
594
+                $taskType = $this->serverContainer->get($class);
595
+                if (isset($taskTypes[$taskType->getId()])) {
596
+                    $this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
597
+                }
598
+                $taskTypes[$taskType->getId()] = $taskType;
599
+            } catch (\Throwable $e) {
600
+                $this->logger->error('Failed to load task processing task type ' . $class, [
601
+                    'exception' => $e,
602
+                ]);
603
+            }
604
+        }
605
+
606
+        $event = $this->dispatchGetProvidersEvent();
607
+        $externalTaskTypes = $event->getTaskTypes();
608
+        foreach ($externalTaskTypes as $taskType) {
609
+            if (isset($taskTypes[$taskType->getId()])) {
610
+                $this->logger->warning('External task processing task type is using ID ' . $taskType->getId() . ' which is already used by a locally registered task type (' . get_class($taskTypes[$taskType->getId()]) . ')');
611
+            }
612
+            $taskTypes[$taskType->getId()] = $taskType;
613
+        }
614
+
615
+        $taskTypes += $this->_getTextProcessingTaskTypes();
616
+
617
+        $this->taskTypes = $taskTypes;
618
+        return $this->taskTypes;
619
+    }
620
+
621
+    /**
622
+     * @return array
623
+     */
624
+    private function _getTaskTypeSettings(): array {
625
+        try {
626
+            $json = $this->config->getAppValue('core', 'ai.taskprocessing_type_preferences', '');
627
+            if ($json === '') {
628
+                return [];
629
+            }
630
+            return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
631
+        } catch (\JsonException $e) {
632
+            $this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
633
+            $taskTypeSettings = [];
634
+            $taskTypes = $this->_getTaskTypes();
635
+            foreach ($taskTypes as $taskType) {
636
+                $taskTypeSettings[$taskType->getId()] = false;
637
+            };
638
+
639
+            return $taskTypeSettings;
640
+        }
641
+
642
+    }
643
+
644
+    /**
645
+     * @param ShapeDescriptor[] $spec
646
+     * @param array<array-key, string|numeric> $defaults
647
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
648
+     * @param array $io
649
+     * @param bool $optional
650
+     * @return void
651
+     * @throws ValidationException
652
+     */
653
+    private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
654
+        foreach ($spec as $key => $descriptor) {
655
+            $type = $descriptor->getShapeType();
656
+            if (!isset($io[$key])) {
657
+                if ($optional) {
658
+                    continue;
659
+                }
660
+                if (isset($defaults[$key])) {
661
+                    if (EShapeType::getScalarType($type) !== $type) {
662
+                        throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
663
+                    }
664
+                    if (EShapeType::isFileType($type)) {
665
+                        throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
666
+                    }
667
+                    $type->validateInput($defaults[$key]);
668
+                    continue;
669
+                }
670
+                throw new ValidationException('Missing key: "' . $key . '"');
671
+            }
672
+            try {
673
+                $type->validateInput($io[$key]);
674
+                if ($type === EShapeType::Enum) {
675
+                    if (!isset($enumValues[$key])) {
676
+                        throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
677
+                    }
678
+                    $type->validateEnum($io[$key], $enumValues[$key]);
679
+                }
680
+            } catch (ValidationException $e) {
681
+                throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
682
+            }
683
+        }
684
+    }
685
+
686
+    /**
687
+     * Takes task input data and replaces fileIds with File objects
688
+     *
689
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
690
+     * @param array<array-key, numeric|string> ...$defaultSpecs the specs
691
+     * @return array<array-key, list<numeric|string>|numeric|string>
692
+     */
693
+    public function fillInputDefaults(array $input, ...$defaultSpecs): array {
694
+        $spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
695
+        return array_merge($spec, $input);
696
+    }
697
+
698
+    /**
699
+     * @param ShapeDescriptor[] $spec
700
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
701
+     * @param array $io
702
+     * @param bool $optional
703
+     * @return void
704
+     * @throws ValidationException
705
+     */
706
+    private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
707
+        foreach ($spec as $key => $descriptor) {
708
+            $type = $descriptor->getShapeType();
709
+            if (!isset($io[$key])) {
710
+                if ($optional) {
711
+                    continue;
712
+                }
713
+                throw new ValidationException('Missing key: "' . $key . '"');
714
+            }
715
+            try {
716
+                $type->validateOutputWithFileIds($io[$key]);
717
+                if (isset($enumValues[$key])) {
718
+                    $type->validateEnum($io[$key], $enumValues[$key]);
719
+                }
720
+            } catch (ValidationException $e) {
721
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
722
+            }
723
+        }
724
+    }
725
+
726
+    /**
727
+     * @param ShapeDescriptor[] $spec
728
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
729
+     * @param array $io
730
+     * @param bool $optional
731
+     * @return void
732
+     * @throws ValidationException
733
+     */
734
+    private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
735
+        foreach ($spec as $key => $descriptor) {
736
+            $type = $descriptor->getShapeType();
737
+            if (!isset($io[$key])) {
738
+                if ($optional) {
739
+                    continue;
740
+                }
741
+                throw new ValidationException('Missing key: "' . $key . '"');
742
+            }
743
+            try {
744
+                $type->validateOutputWithFileData($io[$key]);
745
+                if (isset($enumValues[$key])) {
746
+                    $type->validateEnum($io[$key], $enumValues[$key]);
747
+                }
748
+            } catch (ValidationException $e) {
749
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
750
+            }
751
+        }
752
+    }
753
+
754
+    /**
755
+     * @param array<array-key, T> $array The array to filter
756
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
757
+     * @return array<array-key, T>
758
+     * @psalm-template T
759
+     */
760
+    private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
761
+        $keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
762
+        $keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
763
+        $values = array_map(fn (string $key) => $array[$key], $keys);
764
+        return array_combine($keys, $values);
765
+    }
766
+
767
+    public function hasProviders(): bool {
768
+        return count($this->getProviders()) !== 0;
769
+    }
770
+
771
+    public function getProviders(): array {
772
+        if ($this->providers === null) {
773
+            $this->providers = $this->_getProviders();
774
+        }
775
+
776
+        return $this->providers;
777
+    }
778
+
779
+    public function getPreferredProvider(string $taskTypeId) {
780
+        try {
781
+            if ($this->preferences === null) {
782
+                $this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
783
+                if ($this->preferences === null) {
784
+                    $this->preferences = json_decode($this->config->getAppValue('core', 'ai.taskprocessing_provider_preferences', 'null'), associative: true, flags: JSON_THROW_ON_ERROR);
785
+                    $this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
786
+                }
787
+            }
788
+
789
+            $providers = $this->getProviders();
790
+            if (isset($this->preferences[$taskTypeId])) {
791
+                $providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
792
+                    $carry[$provider->getId()] = $provider;
793
+                    return $carry;
794
+                }, []);
795
+                $this->providersById = $providersById;
796
+                if (isset($providersById[$this->preferences[$taskTypeId]])) {
797
+                    return $providersById[$this->preferences[$taskTypeId]];
798
+                }
799
+            }
800
+            // By default, use the first available provider
801
+            foreach ($providers as $provider) {
802
+                if ($provider->getTaskTypeId() === $taskTypeId) {
803
+                    return $provider;
804
+                }
805
+            }
806
+        } catch (\JsonException $e) {
807
+            $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
808
+        }
809
+        throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
810
+    }
811
+
812
+    public function getAvailableTaskTypes(bool $showDisabled = false): array {
813
+        if ($this->availableTaskTypes === null) {
814
+            $cachedValue = $this->distributedCache->get('available_task_types_v2');
815
+            if ($cachedValue !== null) {
816
+                $this->availableTaskTypes = unserialize($cachedValue);
817
+            }
818
+        }
819
+        // Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
820
+        if ($this->availableTaskTypes === null || $showDisabled) {
821
+            $taskTypes = $this->_getTaskTypes();
822
+            $taskTypeSettings = $this->_getTaskTypeSettings();
823
+
824
+            $availableTaskTypes = [];
825
+            foreach ($taskTypes as $taskType) {
826
+                if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
827
+                    continue;
828
+                }
829
+                try {
830
+                    $provider = $this->getPreferredProvider($taskType->getId());
831
+                } catch (\OCP\TaskProcessing\Exception\Exception $e) {
832
+                    continue;
833
+                }
834
+                try {
835
+                    $availableTaskTypes[$provider->getTaskTypeId()] = [
836
+                        'name' => $taskType->getName(),
837
+                        'description' => $taskType->getDescription(),
838
+                        'optionalInputShape' => $provider->getOptionalInputShape(),
839
+                        'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
840
+                        'inputShapeDefaults' => $provider->getInputShapeDefaults(),
841
+                        'inputShape' => $taskType->getInputShape(),
842
+                        'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
843
+                        'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
844
+                        'outputShape' => $taskType->getOutputShape(),
845
+                        'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
846
+                        'optionalOutputShape' => $provider->getOptionalOutputShape(),
847
+                        'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
848
+                    ];
849
+                } catch (\Throwable $e) {
850
+                    $this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
851
+                }
852
+            }
853
+
854
+            if ($showDisabled) {
855
+                // Do not cache showDisabled, ever.
856
+                return $availableTaskTypes;
857
+            }
858
+
859
+            $this->availableTaskTypes = $availableTaskTypes;
860
+            $this->distributedCache->set('available_task_types_v2', serialize($this->availableTaskTypes), 60);
861
+        }
862
+
863
+
864
+        return $this->availableTaskTypes;
865
+    }
866
+
867
+    public function canHandleTask(Task $task): bool {
868
+        return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
869
+    }
870
+
871
+    public function scheduleTask(Task $task): void {
872
+        if (!$this->canHandleTask($task)) {
873
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
874
+        }
875
+        $this->prepareTask($task);
876
+        $task->setStatus(Task::STATUS_SCHEDULED);
877
+        $this->storeTask($task);
878
+        // schedule synchronous job if the provider is synchronous
879
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
880
+        if ($provider instanceof ISynchronousProvider) {
881
+            $this->jobList->add(SynchronousBackgroundJob::class, null);
882
+        }
883
+    }
884
+
885
+    public function runTask(Task $task): Task {
886
+        if (!$this->canHandleTask($task)) {
887
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
888
+        }
889
+
890
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
891
+        if ($provider instanceof ISynchronousProvider) {
892
+            $this->prepareTask($task);
893
+            $task->setStatus(Task::STATUS_SCHEDULED);
894
+            $this->storeTask($task);
895
+            $this->processTask($task, $provider);
896
+            $task = $this->getTask($task->getId());
897
+        } else {
898
+            $this->scheduleTask($task);
899
+            // poll task
900
+            while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
901
+                sleep(1);
902
+                $task = $this->getTask($task->getId());
903
+            }
904
+        }
905
+        return $task;
906
+    }
907
+
908
+    public function processTask(Task $task, ISynchronousProvider $provider): bool {
909
+        try {
910
+            try {
911
+                $input = $this->prepareInputData($task);
912
+            } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
913
+                $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
914
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
915
+                return false;
916
+            }
917
+            try {
918
+                $this->setTaskStatus($task, Task::STATUS_RUNNING);
919
+                $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
920
+            } catch (ProcessingException $e) {
921
+                $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
922
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
923
+                return false;
924
+            } catch (\Throwable $e) {
925
+                $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
926
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
927
+                return false;
928
+            }
929
+            $this->setTaskResult($task->getId(), null, $output);
930
+        } catch (NotFoundException $e) {
931
+            $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
932
+        } catch (Exception $e) {
933
+            $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
934
+        }
935
+        return true;
936
+    }
937
+
938
+    public function deleteTask(Task $task): void {
939
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
940
+        $this->taskMapper->delete($taskEntity);
941
+    }
942
+
943
+    public function getTask(int $id): Task {
944
+        try {
945
+            $taskEntity = $this->taskMapper->find($id);
946
+            return $taskEntity->toPublicTask();
947
+        } catch (DoesNotExistException $e) {
948
+            throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
949
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
950
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
951
+        } catch (\JsonException $e) {
952
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
953
+        }
954
+    }
955
+
956
+    public function cancelTask(int $id): void {
957
+        $task = $this->getTask($id);
958
+        if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
959
+            return;
960
+        }
961
+        $task->setStatus(Task::STATUS_CANCELLED);
962
+        $task->setEndedAt(time());
963
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
964
+        try {
965
+            $this->taskMapper->update($taskEntity);
966
+            $this->runWebhook($task);
967
+        } catch (\OCP\DB\Exception $e) {
968
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
969
+        }
970
+    }
971
+
972
+    public function setTaskProgress(int $id, float $progress): bool {
973
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
974
+        $task = $this->getTask($id);
975
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
976
+            return false;
977
+        }
978
+        // only set the start time if the task is going from scheduled to running
979
+        if ($task->getstatus() === Task::STATUS_SCHEDULED) {
980
+            $task->setStartedAt(time());
981
+        }
982
+        $task->setStatus(Task::STATUS_RUNNING);
983
+        $task->setProgress($progress);
984
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
985
+        try {
986
+            $this->taskMapper->update($taskEntity);
987
+        } catch (\OCP\DB\Exception $e) {
988
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
989
+        }
990
+        return true;
991
+    }
992
+
993
+    public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
994
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
995
+        $task = $this->getTask($id);
996
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
997
+            $this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
998
+            return;
999
+        }
1000
+        if ($error !== null) {
1001
+            $task->setStatus(Task::STATUS_FAILED);
1002
+            $task->setEndedAt(time());
1003
+            // truncate error message to 1000 characters
1004
+            $task->setErrorMessage(mb_substr($error, 0, 1000));
1005
+            $this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1006
+        } elseif ($result !== null) {
1007
+            $taskTypes = $this->getAvailableTaskTypes();
1008
+            $outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1009
+            $outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1010
+            $optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1011
+            $optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1012
+            try {
1013
+                // validate output
1014
+                if (!$isUsingFileIds) {
1015
+                    $this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1016
+                    $this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1017
+                } else {
1018
+                    $this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1019
+                    $this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1020
+                }
1021
+                $output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1022
+                // extract raw data and put it in files, replace it with file ids
1023
+                if (!$isUsingFileIds) {
1024
+                    $output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1025
+                } else {
1026
+                    $this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1027
+                }
1028
+                // Turn file objects into IDs
1029
+                foreach ($output as $key => $value) {
1030
+                    if ($value instanceof Node) {
1031
+                        $output[$key] = $value->getId();
1032
+                    }
1033
+                    if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1034
+                        $output[$key] = array_map(fn ($node) => $node->getId(), $value);
1035
+                    }
1036
+                }
1037
+                $task->setOutput($output);
1038
+                $task->setProgress(1);
1039
+                $task->setStatus(Task::STATUS_SUCCESSFUL);
1040
+                $task->setEndedAt(time());
1041
+            } catch (ValidationException $e) {
1042
+                $task->setProgress(1);
1043
+                $task->setStatus(Task::STATUS_FAILED);
1044
+                $task->setEndedAt(time());
1045
+                $error = 'The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec';
1046
+                $task->setErrorMessage($error);
1047
+                $this->logger->error($error, ['exception' => $e, 'output' => $result]);
1048
+            } catch (NotPermittedException $e) {
1049
+                $task->setProgress(1);
1050
+                $task->setStatus(Task::STATUS_FAILED);
1051
+                $task->setEndedAt(time());
1052
+                $error = 'The task was processed successfully but storing the output in a file failed';
1053
+                $task->setErrorMessage($error);
1054
+                $this->logger->error($error, ['exception' => $e]);
1055
+            } catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1056
+                $task->setProgress(1);
1057
+                $task->setStatus(Task::STATUS_FAILED);
1058
+                $task->setEndedAt(time());
1059
+                $error = 'The task was processed successfully but the result file could not be found';
1060
+                $task->setErrorMessage($error);
1061
+                $this->logger->error($error, ['exception' => $e]);
1062
+            }
1063
+        }
1064
+        try {
1065
+            $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1066
+        } catch (\JsonException $e) {
1067
+            throw new \OCP\TaskProcessing\Exception\Exception('The task was processed successfully but the provider\'s output could not be encoded as JSON for the database.', 0, $e);
1068
+        }
1069
+        try {
1070
+            $this->taskMapper->update($taskEntity);
1071
+            $this->runWebhook($task);
1072
+        } catch (\OCP\DB\Exception $e) {
1073
+            throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1074
+        }
1075
+        if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1076
+            $event = new TaskSuccessfulEvent($task);
1077
+        } else {
1078
+            $event = new TaskFailedEvent($task, $error);
1079
+        }
1080
+        $this->dispatcher->dispatchTyped($event);
1081
+    }
1082
+
1083
+    public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1084
+        try {
1085
+            $taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1086
+            return $taskEntity->toPublicTask();
1087
+        } catch (DoesNotExistException $e) {
1088
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1089
+        } catch (\OCP\DB\Exception $e) {
1090
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1091
+        } catch (\JsonException $e) {
1092
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1093
+        }
1094
+    }
1095
+
1096
+    /**
1097
+     * Takes task input data and replaces fileIds with File objects
1098
+     *
1099
+     * @param string|null $userId
1100
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
1101
+     * @param ShapeDescriptor[] ...$specs the specs
1102
+     * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1103
+     * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1104
+     */
1105
+    public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1106
+        if ($userId !== null) {
1107
+            \OC_Util::setupFS($userId);
1108
+        }
1109
+        $newInputOutput = [];
1110
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1111
+        foreach ($spec as $key => $descriptor) {
1112
+            $type = $descriptor->getShapeType();
1113
+            if (!isset($input[$key])) {
1114
+                continue;
1115
+            }
1116
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1117
+                $newInputOutput[$key] = $input[$key];
1118
+                continue;
1119
+            }
1120
+            if (EShapeType::getScalarType($type) === $type) {
1121
+                // is scalar
1122
+                $node = $this->validateFileId((int)$input[$key]);
1123
+                $this->validateUserAccessToFile($input[$key], $userId);
1124
+                $newInputOutput[$key] = $node;
1125
+            } else {
1126
+                // is list
1127
+                $newInputOutput[$key] = [];
1128
+                foreach ($input[$key] as $item) {
1129
+                    $node = $this->validateFileId((int)$item);
1130
+                    $this->validateUserAccessToFile($item, $userId);
1131
+                    $newInputOutput[$key][] = $node;
1132
+                }
1133
+            }
1134
+        }
1135
+        return $newInputOutput;
1136
+    }
1137
+
1138
+    public function getUserTask(int $id, ?string $userId): Task {
1139
+        try {
1140
+            $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1141
+            return $taskEntity->toPublicTask();
1142
+        } catch (DoesNotExistException $e) {
1143
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1144
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1145
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1146
+        } catch (\JsonException $e) {
1147
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1148
+        }
1149
+    }
1150
+
1151
+    public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1152
+        try {
1153
+            $taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1154
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1155
+        } catch (\OCP\DB\Exception $e) {
1156
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1157
+        } catch (\JsonException $e) {
1158
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1159
+        }
1160
+    }
1161
+
1162
+    public function getTasks(
1163
+        ?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1164
+        ?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1165
+    ): array {
1166
+        try {
1167
+            $taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1168
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1169
+        } catch (\OCP\DB\Exception $e) {
1170
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1171
+        } catch (\JsonException $e) {
1172
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1173
+        }
1174
+    }
1175
+
1176
+    public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1177
+        try {
1178
+            $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1179
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1180
+        } catch (\OCP\DB\Exception $e) {
1181
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1182
+        } catch (\JsonException $e) {
1183
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1184
+        }
1185
+    }
1186
+
1187
+    /**
1188
+     *Takes task input or output and replaces base64 data with file ids
1189
+     *
1190
+     * @param array $output
1191
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1192
+     * @return array
1193
+     * @throws NotPermittedException
1194
+     */
1195
+    public function encapsulateOutputFileData(array $output, ...$specs): array {
1196
+        $newOutput = [];
1197
+        try {
1198
+            $folder = $this->appData->getFolder('TaskProcessing');
1199
+        } catch (\OCP\Files\NotFoundException) {
1200
+            $folder = $this->appData->newFolder('TaskProcessing');
1201
+        }
1202
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1203
+        foreach ($spec as $key => $descriptor) {
1204
+            $type = $descriptor->getShapeType();
1205
+            if (!isset($output[$key])) {
1206
+                continue;
1207
+            }
1208
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1209
+                $newOutput[$key] = $output[$key];
1210
+                continue;
1211
+            }
1212
+            if (EShapeType::getScalarType($type) === $type) {
1213
+                /** @var SimpleFile $file */
1214
+                $file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1215
+                $newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1216
+            } else {
1217
+                $newOutput = [];
1218
+                foreach ($output[$key] as $item) {
1219
+                    /** @var SimpleFile $file */
1220
+                    $file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1221
+                    $newOutput[$key][] = $file->getId();
1222
+                }
1223
+            }
1224
+        }
1225
+        return $newOutput;
1226
+    }
1227
+
1228
+    /**
1229
+     * @param Task $task
1230
+     * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1231
+     * @throws GenericFileException
1232
+     * @throws LockedException
1233
+     * @throws NotPermittedException
1234
+     * @throws ValidationException|UnauthorizedException
1235
+     */
1236
+    public function prepareInputData(Task $task): array {
1237
+        $taskTypes = $this->getAvailableTaskTypes();
1238
+        $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1239
+        $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1240
+        $input = $task->getInput();
1241
+        $input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1242
+        $input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1243
+        return $input;
1244
+    }
1245
+
1246
+    public function lockTask(Task $task): bool {
1247
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1248
+        if ($this->taskMapper->lockTask($taskEntity) === 0) {
1249
+            return false;
1250
+        }
1251
+        $task->setStatus(Task::STATUS_RUNNING);
1252
+        return true;
1253
+    }
1254
+
1255
+    /**
1256
+     * @throws \JsonException
1257
+     * @throws Exception
1258
+     */
1259
+    public function setTaskStatus(Task $task, int $status): void {
1260
+        $currentTaskStatus = $task->getStatus();
1261
+        if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1262
+            $task->setStartedAt(time());
1263
+        } elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1264
+            $task->setEndedAt(time());
1265
+        } elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1266
+            $task->setScheduledAt(time());
1267
+        }
1268
+        $task->setStatus($status);
1269
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1270
+        $this->taskMapper->update($taskEntity);
1271
+    }
1272
+
1273
+    /**
1274
+     * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1275
+     *
1276
+     * @param Task $task
1277
+     * @return void
1278
+     * @throws UnauthorizedException
1279
+     * @throws ValidationException
1280
+     * @throws \OCP\TaskProcessing\Exception\Exception
1281
+     */
1282
+    private function prepareTask(Task $task): void {
1283
+        $taskTypes = $this->getAvailableTaskTypes();
1284
+        $taskType = $taskTypes[$task->getTaskTypeId()];
1285
+        $inputShape = $taskType['inputShape'];
1286
+        $inputShapeDefaults = $taskType['inputShapeDefaults'];
1287
+        $inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1288
+        $optionalInputShape = $taskType['optionalInputShape'];
1289
+        $optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1290
+        $optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1291
+        // validate input
1292
+        $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1293
+        $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1294
+        // authenticate access to mentioned files
1295
+        $ids = [];
1296
+        foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1297
+            if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1298
+                /** @var list<int>|int $inputSlot */
1299
+                $inputSlot = $task->getInput()[$key];
1300
+                if (is_array($inputSlot)) {
1301
+                    $ids += $inputSlot;
1302
+                } else {
1303
+                    $ids[] = $inputSlot;
1304
+                }
1305
+            }
1306
+        }
1307
+        foreach ($ids as $fileId) {
1308
+            $this->validateFileId($fileId);
1309
+            $this->validateUserAccessToFile($fileId, $task->getUserId());
1310
+        }
1311
+        // remove superfluous keys and set input
1312
+        $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1313
+        $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1314
+        $task->setInput($inputWithDefaults);
1315
+        $task->setScheduledAt(time());
1316
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
1317
+        // calculate expected completion time
1318
+        $completionExpectedAt = new \DateTime('now');
1319
+        $completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1320
+        $task->setCompletionExpectedAt($completionExpectedAt);
1321
+    }
1322
+
1323
+    /**
1324
+     * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1325
+     *
1326
+     * @param Task $task
1327
+     * @return void
1328
+     * @throws Exception
1329
+     * @throws \JsonException
1330
+     */
1331
+    private function storeTask(Task $task): void {
1332
+        // create a db entity and insert into db table
1333
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1334
+        $this->taskMapper->insert($taskEntity);
1335
+        // make sure the scheduler knows the id
1336
+        $task->setId($taskEntity->getId());
1337
+    }
1338
+
1339
+    /**
1340
+     * @param array $output
1341
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1342
+     * @return array
1343
+     * @throws NotPermittedException
1344
+     */
1345
+    private function validateOutputFileIds(array $output, ...$specs): array {
1346
+        $newOutput = [];
1347
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1348
+        foreach ($spec as $key => $descriptor) {
1349
+            $type = $descriptor->getShapeType();
1350
+            if (!isset($output[$key])) {
1351
+                continue;
1352
+            }
1353
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1354
+                $newOutput[$key] = $output[$key];
1355
+                continue;
1356
+            }
1357
+            if (EShapeType::getScalarType($type) === $type) {
1358
+                // Is scalar file ID
1359
+                $newOutput[$key] = $this->validateFileId($output[$key]);
1360
+            } else {
1361
+                // Is list of file IDs
1362
+                $newOutput = [];
1363
+                foreach ($output[$key] as $item) {
1364
+                    $newOutput[$key][] = $this->validateFileId($item);
1365
+                }
1366
+            }
1367
+        }
1368
+        return $newOutput;
1369
+    }
1370
+
1371
+    /**
1372
+     * @param mixed $id
1373
+     * @return File
1374
+     * @throws ValidationException
1375
+     */
1376
+    private function validateFileId(mixed $id): File {
1377
+        $node = $this->rootFolder->getFirstNodeById($id);
1378
+        if ($node === null) {
1379
+            $node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1380
+            if ($node === null) {
1381
+                throw new ValidationException('Could not find file ' . $id);
1382
+            } elseif (!$node instanceof File) {
1383
+                throw new ValidationException('File with id "' . $id . '" is not a file');
1384
+            }
1385
+        } elseif (!$node instanceof File) {
1386
+            throw new ValidationException('File with id "' . $id . '" is not a file');
1387
+        }
1388
+        return $node;
1389
+    }
1390
+
1391
+    /**
1392
+     * @param mixed $fileId
1393
+     * @param string|null $userId
1394
+     * @return void
1395
+     * @throws UnauthorizedException
1396
+     */
1397
+    private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1398
+        if ($userId === null) {
1399
+            throw new UnauthorizedException('User does not have access to file ' . $fileId);
1400
+        }
1401
+        $mounts = $this->userMountCache->getMountsForFileId($fileId);
1402
+        $userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1403
+        if (!in_array($userId, $userIds)) {
1404
+            throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1405
+        }
1406
+    }
1407
+
1408
+    /**
1409
+     * Make a request to the task's webhookUri if necessary
1410
+     *
1411
+     * @param Task $task
1412
+     */
1413
+    private function runWebhook(Task $task): void {
1414
+        $uri = $task->getWebhookUri();
1415
+        $method = $task->getWebhookMethod();
1416
+
1417
+        if (!$uri || !$method) {
1418
+            return;
1419
+        }
1420
+
1421
+        if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1422
+            $client = $this->clientService->newClient();
1423
+            $httpMethod = preg_replace('/^HTTP:/', '', $method);
1424
+            $options = [
1425
+                'timeout' => 30,
1426
+                'body' => json_encode([
1427
+                    'task' => $task->jsonSerialize(),
1428
+                ]),
1429
+                'headers' => ['Content-Type' => 'application/json'],
1430
+            ];
1431
+            try {
1432
+                $client->request($httpMethod, $uri, $options);
1433
+            } catch (ClientException|ServerException $e) {
1434
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1435
+            } catch (\Exception|\Throwable $e) {
1436
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1437
+            }
1438
+        } elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1439
+            $parsedMethod = explode(':', $method, 4);
1440
+            if (count($parsedMethod) < 3) {
1441
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1442
+            }
1443
+            [, $exAppId, $httpMethod] = $parsedMethod;
1444
+            if (!$this->appManager->isEnabledForAnyone('app_api')) {
1445
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1446
+                return;
1447
+            }
1448
+            try {
1449
+                $appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1450
+            } catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1451
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1452
+                return;
1453
+            }
1454
+            $exApp = $appApiFunctions->getExApp($exAppId);
1455
+            if ($exApp === null) {
1456
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1457
+                return;
1458
+            } elseif (!$exApp['enabled']) {
1459
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1460
+                return;
1461
+            }
1462
+            $requestParams = [
1463
+                'task' => $task->jsonSerialize(),
1464
+            ];
1465
+            $requestOptions = [
1466
+                'timeout' => 30,
1467
+            ];
1468
+            $response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1469
+            if (is_array($response) && isset($response['error'])) {
1470
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1471
+            }
1472
+        }
1473
+    }
1474 1474
 }
Please login to merge, or discard this patch.
lib/public/TaskProcessing/TaskTypes/TextToSpeech.php 1 patch
Indentation   +61 added lines, -61 removed lines patch added patch discarded remove patch
@@ -20,73 +20,73 @@
 block discarded – undo
20 20
  * @since 32.0.0
21 21
  */
22 22
 class TextToSpeech implements ITaskType {
23
-	/**
24
-	 * @since 32.0.0
25
-	 */
26
-	public const ID = 'core:text2speech';
23
+    /**
24
+     * @since 32.0.0
25
+     */
26
+    public const ID = 'core:text2speech';
27 27
 
28
-	private IL10N $l;
28
+    private IL10N $l;
29 29
 
30
-	/**
31
-	 * @param IFactory $l10nFactory
32
-	 * @since 32.0.0
33
-	 */
34
-	public function __construct(
35
-		IFactory $l10nFactory,
36
-	) {
37
-		$this->l = $l10nFactory->get('lib');
38
-	}
30
+    /**
31
+     * @param IFactory $l10nFactory
32
+     * @since 32.0.0
33
+     */
34
+    public function __construct(
35
+        IFactory $l10nFactory,
36
+    ) {
37
+        $this->l = $l10nFactory->get('lib');
38
+    }
39 39
 
40 40
 
41
-	/**
42
-	 * @inheritDoc
43
-	 * @since 32.0.0
44
-	 */
45
-	public function getName(): string {
46
-		return $this->l->t('Generate speech');
47
-	}
41
+    /**
42
+     * @inheritDoc
43
+     * @since 32.0.0
44
+     */
45
+    public function getName(): string {
46
+        return $this->l->t('Generate speech');
47
+    }
48 48
 
49
-	/**
50
-	 * @inheritDoc
51
-	 * @since 32.0.0
52
-	 */
53
-	public function getDescription(): string {
54
-		return $this->l->t('Generate speech from a transcript');
55
-	}
49
+    /**
50
+     * @inheritDoc
51
+     * @since 32.0.0
52
+     */
53
+    public function getDescription(): string {
54
+        return $this->l->t('Generate speech from a transcript');
55
+    }
56 56
 
57
-	/**
58
-	 * @return string
59
-	 * @since 32.0.0
60
-	 */
61
-	public function getId(): string {
62
-		return self::ID;
63
-	}
57
+    /**
58
+     * @return string
59
+     * @since 32.0.0
60
+     */
61
+    public function getId(): string {
62
+        return self::ID;
63
+    }
64 64
 
65
-	/**
66
-	 * @return ShapeDescriptor[]
67
-	 * @since 32.0.0
68
-	 */
69
-	public function getInputShape(): array {
70
-		return [
71
-			'input' => new ShapeDescriptor(
72
-				$this->l->t('Prompt'),
73
-				$this->l->t('Write transcript that you want the assistant to generate speech from'),
74
-				EShapeType::Text
75
-			),
76
-		];
77
-	}
65
+    /**
66
+     * @return ShapeDescriptor[]
67
+     * @since 32.0.0
68
+     */
69
+    public function getInputShape(): array {
70
+        return [
71
+            'input' => new ShapeDescriptor(
72
+                $this->l->t('Prompt'),
73
+                $this->l->t('Write transcript that you want the assistant to generate speech from'),
74
+                EShapeType::Text
75
+            ),
76
+        ];
77
+    }
78 78
 
79
-	/**
80
-	 * @return ShapeDescriptor[]
81
-	 * @since 32.0.0
82
-	 */
83
-	public function getOutputShape(): array {
84
-		return [
85
-			'speech' => new ShapeDescriptor(
86
-				$this->l->t('Output speech'),
87
-				$this->l->t('The generated speech'),
88
-				EShapeType::Audio
89
-			),
90
-		];
91
-	}
79
+    /**
80
+     * @return ShapeDescriptor[]
81
+     * @since 32.0.0
82
+     */
83
+    public function getOutputShape(): array {
84
+        return [
85
+            'speech' => new ShapeDescriptor(
86
+                $this->l->t('Output speech'),
87
+                $this->l->t('The generated speech'),
88
+                EShapeType::Audio
89
+            ),
90
+        ];
91
+    }
92 92
 }
Please login to merge, or discard this patch.