@@ -20,89 +20,89 @@ |
||
| 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 | } |
@@ -20,93 +20,93 @@ |
||
| 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 | } |
@@ -20,99 +20,99 @@ |
||
| 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 | } |
@@ -20,98 +20,98 @@ |
||
| 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 | } |
@@ -27,230 +27,230 @@ |
||
| 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 | } |
@@ -71,1594 +71,1594 @@ |
||
| 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 | } |