diff --git a/docs/developer/web-integration.md b/docs/developer/web-integration.md index 631bd90d..b1b62484 100644 --- a/docs/developer/web-integration.md +++ b/docs/developer/web-integration.md @@ -28,10 +28,9 @@ A helper function is exposed as `OCA.Assistant.openAssistantForm`. It opens the It accepts one parameter which is an object that can contain those keys: * appId: [string, mandatory] app id of the app currently displayed -* identifier: [string, optional, default: ''] the task identifier (if the task is scheduled, this helps to identify the task when receiving the "task finished" event in the backend) +* customId: [string, optional, default: ''] the task custom ID (if the task is scheduled, this helps to identify the task when receiving the "task finished" event in the backend) * taskType: [string, optional, default: last used task type] initially selected task type. It can be a text processing task type class or `speech-to-text` or `OCP\TextToImage\Task` -* input: [string, optional, default: '', DEPRECATED] initial input prompt (for task types that only require a prompt) -* inputs: [object, optional, default: {}] initial inputs (specific to each task type) +* input: [object, optional, default: {}] initial inputs (specific to each task type) * isInsideViewer: [boolean, optional, default: false] should be true if this function is called while the Viewer is displayed * closeOnResult: [boolean, optional, default: false] If true, the modal will be closed after running a synchronous task and getting its result * actionButtons: [array, optional, default: empty list] List of extra buttons to show in the assistant result form (only used if closeOnResult is false) @@ -47,15 +46,17 @@ The promise resolves with a task object which looks like: ```javascript { appId: 'text', - category: 1, // 0: text generation, 1: image generation, 2: speech-to-text id: 310, // the assistant task ID - identifier: 'my custom identifier', - inputs: { prompt: 'give me a short summary of a simple settings section about GitHub' }, + customId: 'my custom identifier', + input: { input: 'give me a short summary of a simple settings section about GitHub' }, ocpTaskId: 152, // the underlying OCP task ID - output: 'blabla', - status: 3, // 0: unknown, 1: scheduled, 2: running, 3: sucessful, 4: failed - taskType: 'OCP\\TextProcessing\\FreePromptTaskType', - timestamp: 1711545305, + output: { output: 'blabla' }, + status: 'STATUS_SUCCESSFUL', // 0: unknown, 1: scheduled, 2: running, 3: sucessful, 4: failed + type: 'core:text2text', + lastUpdated: 1711545305, + scheduledAt: 1711545301, + startedAt: 1711545302, + endedAt: 1711545303, userId: 'janedoe', } ``` @@ -64,9 +65,9 @@ Complete example: ``` javascript OCA.Assistant.openAssistantForm({ appId: 'my_app_id', - identifier: 'my custom identifier', - taskType: 'OCP\\TextProcessing\\FreePromptTaskType', - inputs: { prompt: 'count to 3' }, + customId: 'my custom identifier', + taskType: 'core:text2text', + inputs: { input: 'count to 3' }, actionButtons: [ { label: 'Label 1', @@ -87,3 +88,23 @@ OCA.Assistant.openAssistantForm({ console.debug('assistant promise failure', error) }) ``` + +### Populate input fields with the content of a file + +You might want to initialize an input field with the content of a file. +This is possible by passing a file path or ID like this: + +``` javascript +OCA.Assistant.openAssistantForm({ + appId: 'my_app_id', + customId: 'my custom identifier', + taskType: 'core:text2text', + inputs: { input: { fileId: 123 } }, +}) +OCA.Assistant.openAssistantForm({ + appId: 'my_app_id', + customId: 'my custom identifier', + taskType: 'core:text2text', + inputs: { input: { filePath: '/path/to/file.txt' } }, +}) +``` diff --git a/lib/Controller/AssistantApiController.php b/lib/Controller/AssistantApiController.php index 305d2f4e..b867bf92 100644 --- a/lib/Controller/AssistantApiController.php +++ b/lib/Controller/AssistantApiController.php @@ -121,20 +121,24 @@ public function getUserTasks(?string $taskTypeId = null): DataResponse { * * Parse and extract text content of a file (if the file type is supported) * - * @param string $filePath Path of the file to parse in the user's storage + * @param string|null $filePath Path of the file to parse in the user's storage + * @param int|null $fileId Id of the file to parse in the user's storage * @return DataResponse|DataResponse * * 200: Text parsed from file successfully * 400: Parsing text from file is not possible */ #[NoAdminRequired] - public function parseTextFromFile(string $filePath): DataResponse { + public function parseTextFromFile(?string $filePath = null, ?int $fileId = null): DataResponse { if ($this->userId === null) { return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST); } + if ($fileId === null && $filePath === null) { + return new DataResponse('Invalid parameters', Http::STATUS_BAD_REQUEST); + } try { - $text = $this->assistantService->parseTextFromFile($filePath, $this->userId); + $text = $this->assistantService->parseTextFromFile($this->userId, $filePath, $fileId); } catch (\Exception | \Throwable $e) { return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); } diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index 856635a8..1710bdc5 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -545,12 +545,14 @@ private function sanitizeInputs(string $type, array $inputs): array { /** * Parse text from file (if parsing the file type is supported) - * @param string $filePath * @param string $userId + * @param string|null $filePath + * @param int|null $fileId * @return string - * @throws \Exception + * @throws NotPermittedException + * @throws \OCP\Files\NotFoundException */ - public function parseTextFromFile(string $filePath, string $userId): string { + public function parseTextFromFile(string $userId, ?string $filePath = null, ?int $fileId = null): string { try { $userFolder = $this->rootFolder->getUserFolder($userId); @@ -559,7 +561,11 @@ public function parseTextFromFile(string $filePath, string $userId): string { } try { - $file = $userFolder->get($filePath); + if ($filePath !== null) { + $file = $userFolder->get($filePath); + } else { + $file = $userFolder->getFirstNodeById($fileId); + } } catch (NotFoundException $e) { throw new \Exception('File not found.'); } diff --git a/openapi.json b/openapi.json index fd142c1b..a0eeaa08 100644 --- a/openapi.json +++ b/openapi.json @@ -432,18 +432,22 @@ } ], "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "filePath" - ], "properties": { "filePath": { "type": "string", + "nullable": true, "description": "Path of the file to parse in the user's storage" + }, + "fileId": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "Id of the file to parse in the user's storage" } } } diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue index 6a1f4cbb..c5bf9501 100644 --- a/src/components/AssistantTextProcessingForm.vue +++ b/src/components/AssistantTextProcessingForm.vue @@ -125,6 +125,7 @@ import { SHAPE_TYPE_NAMES } from '../constants.js' import axios from '@nextcloud/axios' import { generateOcsUrl, generateUrl } from '@nextcloud/router' +import { showError } from '@nextcloud/dialogs' import Vue from 'vue' import VueClipboard from 'vue-clipboard2' @@ -315,23 +316,57 @@ export default { console.debug('[assistant] form\'s myoutputs', this.myOutputs) }, methods: { + // Parse the file if a fileId is passed as initial value to a text field + parseTextFileInputs(taskType) { + if (taskType === undefined || taskType === null) { + return + } + Object.keys(this.myInputs).forEach(k => { + if (taskType.inputShape[k]?.type === 'Text') { + if (this.myInputs[k]?.fileId || this.myInputs[k]?.filePath) { + const { filePath, fileId } = { fileId: this.myInputs[k]?.fileId, filePath: this.myInputs[k]?.filePath } + this.myInputs[k] = '' + this.parseFile({ fileId, filePath }) + .then(response => { + if (response.data?.ocs?.data?.parsedText) { + this.myInputs[k] = response.data?.ocs?.data?.parsedText + } + }) + .catch(error => { + console.error(error) + showError(t('assistant', 'Failed to parse some files')) + }) + } + } + }) + }, + parseFile({ filePath, fileId }) { + const url = generateOcsUrl('/apps/assistant/api/v1/parse-file') + return axios.post(url, { + filePath, + fileId, + }) + }, getTaskTypes() { this.loadingTaskTypes = true axios.get(generateOcsUrl('/apps/assistant/api/v1/task-types')) .then((response) => { - this.taskTypes = response.data.ocs.data.types + const taskTypes = response.data.ocs.data.types // check if selected task type is in the list, fallback to text2text - const taskType = this.taskTypes.find(tt => tt.id === this.mySelectedTaskTypeId) + const taskType = taskTypes.find(tt => tt.id === this.mySelectedTaskTypeId) if (taskType === undefined) { - const text2textType = this.taskTypes.find(tt => tt.id === TEXT2TEXT_TASK_TYPE_ID) + const text2textType = taskTypes.find(tt => tt.id === TEXT2TEXT_TASK_TYPE_ID) if (text2textType) { + this.parseTextFileInputs(text2textType) this.mySelectedTaskTypeId = TEXT2TEXT_TASK_TYPE_ID } else { this.mySelectedTaskTypeId = null } + } else { + this.parseTextFileInputs(taskType) } // add placeholders - this.taskTypes.forEach(tt => { + taskTypes.forEach(tt => { if (tt.id === TEXT2TEXT_TASK_TYPE_ID && tt.inputShape.input) { tt.inputShape.input.placeholder = t('assistant', 'Generate a first draft for a blog post about privacy') } else if (tt.id === 'context_chat:context_chat' && tt.inputShape.prompt) { @@ -350,6 +385,7 @@ export default { tt.inputShape.source_input.placeholder = t('assistant', 'A description of what you need or some original content') } }) + this.taskTypes = taskTypes }) .catch((error) => { console.error(error)