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