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