Skip to content

Commit f175357

Browse files
committed
feat(file-actions): start implementing 3 file actions
Signed-off-by: Julien Veyssier <julien-nc@posteo.net>
1 parent ec6f230 commit f175357

File tree

11 files changed

+379
-14
lines changed

11 files changed

+379
-14
lines changed

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
['name' => 'assistantApi#saveOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/save', 'verb' => 'POST', 'requirements' => $requirements],
3535
['name' => 'assistantApi#getOutputFilePreview', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/preview', 'verb' => 'GET', 'requirements' => $requirements],
3636
['name' => 'assistantApi#getOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download', 'verb' => 'GET', 'requirements' => $requirements],
37+
['name' => 'assistantApi#runFileAction', 'url' => '/api/{apiVersion}/file-action/{fileId}/{taskTypeId}', 'verb' => 'POST', 'requirements' => $requirements],
3738

3839
['name' => 'chattyLLM#newSession', 'url' => '/chat/new_session', 'verb' => 'PUT'],
3940
['name' => 'chattyLLM#updateSessionTitle', 'url' => '/chat/update_session', 'verb' => 'PATCH'],

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\Assistant\Listener\ChattyLLMTaskListener;
1313
use OCA\Assistant\Listener\CSPListener;
1414
use OCA\Assistant\Listener\FreePrompt\FreePromptReferenceListener;
15+
use OCA\Assistant\Listener\LoadAdditionalScriptsListener;
1516
use OCA\Assistant\Listener\SpeechToText\SpeechToTextReferenceListener;
1617
use OCA\Assistant\Listener\TaskFailedListener;
1718
use OCA\Assistant\Listener\TaskOutputFileReferenceListener;
@@ -24,6 +25,7 @@
2425
use OCA\Assistant\Reference\Text2ImageReferenceProvider;
2526
use OCA\Assistant\TaskProcessing\AudioToAudioChatProvider;
2627
use OCA\Assistant\TaskProcessing\ContextAgentAudioInteractionProvider;
28+
use OCA\Files\Event\LoadAdditionalScriptsEvent;
2729
use OCP\AppFramework\App;
2830
use OCP\AppFramework\Bootstrap\IBootContext;
2931

@@ -64,6 +66,7 @@ public function register(IRegistrationContext $context): void {
6466
$context->registerEventListener(RenderReferenceEvent::class, TaskOutputFileReferenceListener::class);
6567

6668
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
69+
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class);
6770

6871
$context->registerEventListener(TaskSuccessfulEvent::class, TaskSuccessfulListener::class);
6972
$context->registerEventListener(TaskFailedEvent::class, TaskFailedListener::class);

lib/Controller/AssistantApiController.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use OCA\Assistant\ResponseDefinitions;
1111
use OCA\Assistant\Service\AssistantService;
12+
use OCA\Assistant\Service\TaskProcessingService;
1213
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
1314
use OCP\AppFramework\Http;
1415
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
@@ -41,6 +42,7 @@ public function __construct(
4142
IRequest $request,
4243
private IL10N $l10n,
4344
private AssistantService $assistantService,
45+
private TaskProcessingService $taskProcessingService,
4446
private LoggerInterface $logger,
4547
private ?string $userId,
4648
) {
@@ -408,4 +410,14 @@ public function getOutputFile(int $ocpTaskId, int $fileId): DataDownloadResponse
408410
return new DataResponse('', Http::STATUS_NOT_FOUND);
409411
}
410412
}
413+
414+
#[NoAdminRequired]
415+
public function runFileAction(int $fileId, string $taskTypeId): DataResponse {
416+
try {
417+
$taskId = $this->taskProcessingService->runFileAction($this->userId, $fileId, $taskTypeId);
418+
return new DataResponse(['taskId' => $taskId]);
419+
} catch (Exception|Throwable $e) {
420+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
421+
}
422+
}
411423
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Assistant\Listener;
11+
12+
use OCA\Assistant\AppInfo\Application;
13+
use OCA\Assistant\Service\TaskProcessingService;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
17+
18+
/**
19+
* @template-implements IEventListener<TaskSuccessfulEvent>
20+
*/
21+
class FileActionTaskListener implements IEventListener {
22+
23+
public function __construct(
24+
private TaskProcessingService $taskProcessingService,
25+
) {
26+
}
27+
28+
public function handle(Event $event): void {
29+
if (!($event instanceof TaskSuccessfulEvent)) {
30+
return;
31+
}
32+
33+
$task = $event->getTask();
34+
$customId = $task->getCustomId();
35+
$appId = $task->getAppId();
36+
$taskTypeId = $task->getTaskTypeId();
37+
38+
if ($customId === null || $appId !== (Application::APP_ID . ':file-action')) {
39+
return;
40+
}
41+
42+
if (!$this->taskProcessingService->isFileActionTaskTypeAuthorized($taskTypeId)) {
43+
return;
44+
}
45+
46+
if (preg_match('/^file-action:(\d+)$/', $customId, $matches)) {
47+
$sourceFileId = (int)$matches[1];
48+
// TODO get the task output, write it in the output file (in the same dir as the source one)
49+
// TODO maybe send a notification
50+
}
51+
}
52+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Assistant\Listener;
9+
10+
use OCA\Assistant\AppInfo\Application;
11+
use OCA\Files\Event\LoadAdditionalScriptsEvent;
12+
use OCP\AppFramework\Services\IInitialState;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\TaskProcessing\IManager as ITaskProcessingManager;
16+
use OCP\TaskProcessing\TaskTypes\AudioToText;
17+
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
18+
use OCP\Util;
19+
20+
/**
21+
* @implements IEventListener<Event>
22+
*/
23+
class LoadAdditionalScriptsListener implements IEventListener {
24+
25+
public function __construct(
26+
private IInitialState $initialStateService,
27+
private ITaskProcessingManager $taskProcessingManager,
28+
) {
29+
}
30+
31+
public function handle(Event $event): void {
32+
if (!$event instanceof LoadAdditionalScriptsEvent) {
33+
return;
34+
}
35+
36+
$availableTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
37+
$summarizeAvailable = array_key_exists(TextToTextSummary::ID, $availableTaskTypes);
38+
$sttAvailable = array_key_exists(AudioToText::ID, $availableTaskTypes);
39+
$ttsAvailable = class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech')
40+
&& array_key_exists(\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID, $availableTaskTypes);
41+
42+
$this->initialStateService->provideInitialState('stt-available', $sttAvailable);
43+
$this->initialStateService->provideInitialState('tts-available', $ttsAvailable);
44+
$this->initialStateService->provideInitialState('summarize-available', $summarizeAvailable);
45+
46+
Util::addInitScript(Application::APP_ID, Application::APP_ID . '-fileActions');
47+
}
48+
}

lib/Service/TaskProcessingService.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace OCA\Assistant\Service;
99

10+
use OC\User\NoUserException;
11+
use OCA\Assistant\AppInfo\Application;
1012
use OCP\Files\File;
1113
use OCP\Files\GenericFileException;
1214
use OCP\Files\IRootFolder;
@@ -19,6 +21,8 @@
1921
use OCP\TaskProcessing\Exception\ValidationException;
2022
use OCP\TaskProcessing\IManager;
2123
use OCP\TaskProcessing\Task;
24+
use OCP\TaskProcessing\TaskTypes\AudioToText;
25+
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
2226
use RuntimeException;
2327

2428
class TaskProcessingService {
@@ -66,4 +70,56 @@ public function getOutputFileContent(int $fileId): string {
6670
}
6771
return $node->getContent();
6872
}
73+
74+
public function isFileActionTaskTypeAuthorized(string $taskTypeId): bool {
75+
$authorizedTaskTypes = [AudioToText::ID, TextToTextSummary::ID];
76+
if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech')) {
77+
$authorizedTaskTypes[] = \OCP\TaskProcessing\TaskTypes\TextToSpeech::ID;
78+
}
79+
return in_array($taskTypeId, $authorizedTaskTypes, true);
80+
}
81+
82+
/**
83+
* Execute a file action
84+
*
85+
* @param string $userId
86+
* @param int $fileId
87+
* @param string $taskTypeId
88+
* @return int The scheduled task ID
89+
* @throws Exception
90+
* @throws GenericFileException
91+
* @throws LockedException
92+
* @throws NotFoundException
93+
* @throws NotPermittedException
94+
* @throws PreConditionNotMetException
95+
* @throws UnauthorizedException
96+
* @throws ValidationException
97+
* @throws NoUserException
98+
*/
99+
public function runFileAction(string $userId, int $fileId, string $taskTypeId): int {
100+
if (!$this->isFileActionTaskTypeAuthorized($taskTypeId)) {
101+
throw new PreConditionNotMetException();
102+
}
103+
$userFolder = $this->rootFolder->getUserFolder($userId);
104+
$file = $userFolder->getFirstNodeById($fileId);
105+
if (!$file instanceof File) {
106+
throw new NotFoundException('File is not a file');
107+
}
108+
$input = $taskTypeId === AudioToText::ID
109+
? ['input' => $fileId]
110+
: ['input' => $file->getContent()];
111+
$task = new Task(
112+
$taskTypeId,
113+
$input,
114+
Application::APP_ID . ':file-action:',
115+
$userId,
116+
'file-action:' . $fileId,
117+
);
118+
$this->taskProcessingManager->scheduleTask($task);
119+
$taskId = $task->getId();
120+
if ($taskId === null) {
121+
throw new Exception('The task could not be scheduled');
122+
}
123+
return $taskId;
124+
}
69125
}

package-lock.json

Lines changed: 52 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@nextcloud/axios": "^2.0.0",
4646
"@nextcloud/dialogs": "^7.0.0-rc.1",
4747
"@nextcloud/event-bus": "^3.1.0",
48+
"@nextcloud/files": "^3.11.0",
4849
"@nextcloud/initial-state": "^2.0.0",
4950
"@nextcloud/l10n": "^3.1.0",
5051
"@nextcloud/moment": "^1.3.1",

0 commit comments

Comments
 (0)