Skip to content

Commit

Permalink
Merge pull request #161 from nextcloud/enh/noid/initial-text-input-fi…
Browse files Browse the repository at this point in the history
…le-id

Allow to pass file ID/path as initial input text field value
  • Loading branch information
julien-nc authored Dec 4, 2024
2 parents b699720 + abb8fb0 commit bd20b56
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 28 deletions.
47 changes: 34 additions & 13 deletions docs/developer/web-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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',
}
```
Expand All @@ -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',
Expand All @@ -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' } },
})
```
10 changes: 7 additions & 3 deletions lib/Controller/AssistantApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::STATUS_OK, array{parsedText: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
*
* 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);
}
Expand Down
14 changes: 10 additions & 4 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.');
}
Expand Down
12 changes: 8 additions & 4 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand Down
44 changes: 40 additions & 4 deletions src/components/AssistantTextProcessingForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down

0 comments on commit bd20b56

Please sign in to comment.