Skip to content

Commit bd20b56

Browse files
authored
Merge pull request #161 from nextcloud/enh/noid/initial-text-input-file-id
Allow to pass file ID/path as initial input text field value
2 parents b699720 + abb8fb0 commit bd20b56

File tree

5 files changed

+99
-28
lines changed

5 files changed

+99
-28
lines changed

docs/developer/web-integration.md

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,9 @@ A helper function is exposed as `OCA.Assistant.openAssistantForm`. It opens the
2828

2929
It accepts one parameter which is an object that can contain those keys:
3030
* appId: [string, mandatory] app id of the app currently displayed
31-
* 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)
31+
* 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)
3232
* 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`
33-
* input: [string, optional, default: '', DEPRECATED] initial input prompt (for task types that only require a prompt)
34-
* inputs: [object, optional, default: {}] initial inputs (specific to each task type)
33+
* input: [object, optional, default: {}] initial inputs (specific to each task type)
3534
* isInsideViewer: [boolean, optional, default: false] should be true if this function is called while the Viewer is displayed
3635
* closeOnResult: [boolean, optional, default: false] If true, the modal will be closed after running a synchronous task and getting its result
3736
* 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:
4746
```javascript
4847
{
4948
appId: 'text',
50-
category: 1, // 0: text generation, 1: image generation, 2: speech-to-text
5149
id: 310, // the assistant task ID
52-
identifier: 'my custom identifier',
53-
inputs: { prompt: 'give me a short summary of a simple settings section about GitHub' },
50+
customId: 'my custom identifier',
51+
input: { input: 'give me a short summary of a simple settings section about GitHub' },
5452
ocpTaskId: 152, // the underlying OCP task ID
55-
output: 'blabla',
56-
status: 3, // 0: unknown, 1: scheduled, 2: running, 3: sucessful, 4: failed
57-
taskType: 'OCP\\TextProcessing\\FreePromptTaskType',
58-
timestamp: 1711545305,
53+
output: { output: 'blabla' },
54+
status: 'STATUS_SUCCESSFUL', // 0: unknown, 1: scheduled, 2: running, 3: sucessful, 4: failed
55+
type: 'core:text2text',
56+
lastUpdated: 1711545305,
57+
scheduledAt: 1711545301,
58+
startedAt: 1711545302,
59+
endedAt: 1711545303,
5960
userId: 'janedoe',
6061
}
6162
```
@@ -64,9 +65,9 @@ Complete example:
6465
``` javascript
6566
OCA.Assistant.openAssistantForm({
6667
appId: 'my_app_id',
67-
identifier: 'my custom identifier',
68-
taskType: 'OCP\\TextProcessing\\FreePromptTaskType',
69-
inputs: { prompt: 'count to 3' },
68+
customId: 'my custom identifier',
69+
taskType: 'core:text2text',
70+
inputs: { input: 'count to 3' },
7071
actionButtons: [
7172
{
7273
label: 'Label 1',
@@ -87,3 +88,23 @@ OCA.Assistant.openAssistantForm({
8788
console.debug('assistant promise failure', error)
8889
})
8990
```
91+
92+
### Populate input fields with the content of a file
93+
94+
You might want to initialize an input field with the content of a file.
95+
This is possible by passing a file path or ID like this:
96+
97+
``` javascript
98+
OCA.Assistant.openAssistantForm({
99+
appId: 'my_app_id',
100+
customId: 'my custom identifier',
101+
taskType: 'core:text2text',
102+
inputs: { input: { fileId: 123 } },
103+
})
104+
OCA.Assistant.openAssistantForm({
105+
appId: 'my_app_id',
106+
customId: 'my custom identifier',
107+
taskType: 'core:text2text',
108+
inputs: { input: { filePath: '/path/to/file.txt' } },
109+
})
110+
```

lib/Controller/AssistantApiController.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,20 +121,24 @@ public function getUserTasks(?string $taskTypeId = null): DataResponse {
121121
*
122122
* Parse and extract text content of a file (if the file type is supported)
123123
*
124-
* @param string $filePath Path of the file to parse in the user's storage
124+
* @param string|null $filePath Path of the file to parse in the user's storage
125+
* @param int|null $fileId Id of the file to parse in the user's storage
125126
* @return DataResponse<Http::STATUS_OK, array{parsedText: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
126127
*
127128
* 200: Text parsed from file successfully
128129
* 400: Parsing text from file is not possible
129130
*/
130131
#[NoAdminRequired]
131-
public function parseTextFromFile(string $filePath): DataResponse {
132+
public function parseTextFromFile(?string $filePath = null, ?int $fileId = null): DataResponse {
132133
if ($this->userId === null) {
133134
return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
134135
}
136+
if ($fileId === null && $filePath === null) {
137+
return new DataResponse('Invalid parameters', Http::STATUS_BAD_REQUEST);
138+
}
135139

136140
try {
137-
$text = $this->assistantService->parseTextFromFile($filePath, $this->userId);
141+
$text = $this->assistantService->parseTextFromFile($this->userId, $filePath, $fileId);
138142
} catch (\Exception | \Throwable $e) {
139143
return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
140144
}

lib/Service/AssistantService.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -545,12 +545,14 @@ private function sanitizeInputs(string $type, array $inputs): array {
545545

546546
/**
547547
* Parse text from file (if parsing the file type is supported)
548-
* @param string $filePath
549548
* @param string $userId
549+
* @param string|null $filePath
550+
* @param int|null $fileId
550551
* @return string
551-
* @throws \Exception
552+
* @throws NotPermittedException
553+
* @throws \OCP\Files\NotFoundException
552554
*/
553-
public function parseTextFromFile(string $filePath, string $userId): string {
555+
public function parseTextFromFile(string $userId, ?string $filePath = null, ?int $fileId = null): string {
554556

555557
try {
556558
$userFolder = $this->rootFolder->getUserFolder($userId);
@@ -559,7 +561,11 @@ public function parseTextFromFile(string $filePath, string $userId): string {
559561
}
560562

561563
try {
562-
$file = $userFolder->get($filePath);
564+
if ($filePath !== null) {
565+
$file = $userFolder->get($filePath);
566+
} else {
567+
$file = $userFolder->getFirstNodeById($fileId);
568+
}
563569
} catch (NotFoundException $e) {
564570
throw new \Exception('File not found.');
565571
}

openapi.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,18 +432,22 @@
432432
}
433433
],
434434
"requestBody": {
435-
"required": true,
435+
"required": false,
436436
"content": {
437437
"application/json": {
438438
"schema": {
439439
"type": "object",
440-
"required": [
441-
"filePath"
442-
],
443440
"properties": {
444441
"filePath": {
445442
"type": "string",
443+
"nullable": true,
446444
"description": "Path of the file to parse in the user's storage"
445+
},
446+
"fileId": {
447+
"type": "integer",
448+
"format": "int64",
449+
"nullable": true,
450+
"description": "Id of the file to parse in the user's storage"
447451
}
448452
}
449453
}

src/components/AssistantTextProcessingForm.vue

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import { SHAPE_TYPE_NAMES } from '../constants.js'
125125
126126
import axios from '@nextcloud/axios'
127127
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
128+
import { showError } from '@nextcloud/dialogs'
128129
import Vue from 'vue'
129130
import VueClipboard from 'vue-clipboard2'
130131
@@ -315,23 +316,57 @@ export default {
315316
console.debug('[assistant] form\'s myoutputs', this.myOutputs)
316317
},
317318
methods: {
319+
// Parse the file if a fileId is passed as initial value to a text field
320+
parseTextFileInputs(taskType) {
321+
if (taskType === undefined || taskType === null) {
322+
return
323+
}
324+
Object.keys(this.myInputs).forEach(k => {
325+
if (taskType.inputShape[k]?.type === 'Text') {
326+
if (this.myInputs[k]?.fileId || this.myInputs[k]?.filePath) {
327+
const { filePath, fileId } = { fileId: this.myInputs[k]?.fileId, filePath: this.myInputs[k]?.filePath }
328+
this.myInputs[k] = ''
329+
this.parseFile({ fileId, filePath })
330+
.then(response => {
331+
if (response.data?.ocs?.data?.parsedText) {
332+
this.myInputs[k] = response.data?.ocs?.data?.parsedText
333+
}
334+
})
335+
.catch(error => {
336+
console.error(error)
337+
showError(t('assistant', 'Failed to parse some files'))
338+
})
339+
}
340+
}
341+
})
342+
},
343+
parseFile({ filePath, fileId }) {
344+
const url = generateOcsUrl('/apps/assistant/api/v1/parse-file')
345+
return axios.post(url, {
346+
filePath,
347+
fileId,
348+
})
349+
},
318350
getTaskTypes() {
319351
this.loadingTaskTypes = true
320352
axios.get(generateOcsUrl('/apps/assistant/api/v1/task-types'))
321353
.then((response) => {
322-
this.taskTypes = response.data.ocs.data.types
354+
const taskTypes = response.data.ocs.data.types
323355
// check if selected task type is in the list, fallback to text2text
324-
const taskType = this.taskTypes.find(tt => tt.id === this.mySelectedTaskTypeId)
356+
const taskType = taskTypes.find(tt => tt.id === this.mySelectedTaskTypeId)
325357
if (taskType === undefined) {
326-
const text2textType = this.taskTypes.find(tt => tt.id === TEXT2TEXT_TASK_TYPE_ID)
358+
const text2textType = taskTypes.find(tt => tt.id === TEXT2TEXT_TASK_TYPE_ID)
327359
if (text2textType) {
360+
this.parseTextFileInputs(text2textType)
328361
this.mySelectedTaskTypeId = TEXT2TEXT_TASK_TYPE_ID
329362
} else {
330363
this.mySelectedTaskTypeId = null
331364
}
365+
} else {
366+
this.parseTextFileInputs(taskType)
332367
}
333368
// add placeholders
334-
this.taskTypes.forEach(tt => {
369+
taskTypes.forEach(tt => {
335370
if (tt.id === TEXT2TEXT_TASK_TYPE_ID && tt.inputShape.input) {
336371
tt.inputShape.input.placeholder = t('assistant', 'Generate a first draft for a blog post about privacy')
337372
} else if (tt.id === 'context_chat:context_chat' && tt.inputShape.prompt) {
@@ -350,6 +385,7 @@ export default {
350385
tt.inputShape.source_input.placeholder = t('assistant', 'A description of what you need or some original content')
351386
}
352387
})
388+
this.taskTypes = taskTypes
353389
})
354390
.catch((error) => {
355391
console.error(error)

0 commit comments

Comments
 (0)