Merge pull request #39776 from nextcloud/backport/39680/stable27

This commit is contained in:
Marcel Klehr 2023-08-09 15:47:55 +02:00 committed by GitHub
commit b2589c8f90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 3089 additions and 55 deletions

View file

@ -19,6 +19,7 @@
<settings>
<admin>OCA\Settings\Settings\Admin\Mail</admin>
<admin>OCA\Settings\Settings\Admin\Overview</admin>
<admin>OCA\Settings\Settings\Admin\ArtificialIntelligence</admin>
<admin>OCA\Settings\Settings\Admin\Server</admin>
<admin>OCA\Settings\Settings\Admin\Sharing</admin>
<admin>OCA\Settings\Settings\Admin\Security</admin>
@ -27,6 +28,7 @@
<admin-section>OCA\Settings\Sections\Admin\Delegation</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Overview</admin-section>
<admin-section>OCA\Settings\Sections\Admin\ArtificialIntelligence</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Security</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Server</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Sharing</admin-section>

View file

@ -75,6 +75,7 @@ return [
['name' => 'ChangePassword#changeUserPassword', 'url' => '/settings/users/changepassword', 'verb' => 'POST' , 'root' => ''],
['name' => 'TwoFactorSettings#index', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'GET' , 'root' => ''],
['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT' , 'root' => ''],
['name' => 'AISettings#update', 'url' => '/settings/api/admin/ai', 'verb' => 'PUT' , 'root' => ''],
['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => ''] , 'root' => ''],

View file

@ -16,6 +16,7 @@ return array(
'OCA\\Settings\\Activity\\Setting' => $baseDir . '/../lib/Activity/Setting.php',
'OCA\\Settings\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Settings\\BackgroundJobs\\VerifyUserData' => $baseDir . '/../lib/BackgroundJobs/VerifyUserData.php',
'OCA\\Settings\\Controller\\AISettingsController' => $baseDir . '/../lib/Controller/AISettingsController.php',
'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php',
'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php',
'OCA\\Settings\\Controller\\AuthSettingsController' => $baseDir . '/../lib/Controller/AuthSettingsController.php',
@ -42,6 +43,7 @@ return array(
'OCA\\Settings\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => $baseDir . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Sections\\Admin\\Additional' => $baseDir . '/../lib/Sections/Admin/Additional.php',
'OCA\\Settings\\Sections\\Admin\\ArtificialIntelligence' => $baseDir . '/../lib/Sections/Admin/ArtificialIntelligence.php',
'OCA\\Settings\\Sections\\Admin\\Delegation' => $baseDir . '/../lib/Sections/Admin/Delegation.php',
'OCA\\Settings\\Sections\\Admin\\Groupware' => $baseDir . '/../lib/Sections/Admin/Groupware.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => $baseDir . '/../lib/Sections/Admin/Overview.php',
@ -56,6 +58,7 @@ return array(
'OCA\\Settings\\Service\\AuthorizedGroupService' => $baseDir . '/../lib/Service/AuthorizedGroupService.php',
'OCA\\Settings\\Service\\NotFoundException' => $baseDir . '/../lib/Service/NotFoundException.php',
'OCA\\Settings\\Service\\ServiceException' => $baseDir . '/../lib/Service/ServiceException.php',
'OCA\\Settings\\Settings\\Admin\\ArtificialIntelligence' => $baseDir . '/../lib/Settings/Admin/ArtificialIntelligence.php',
'OCA\\Settings\\Settings\\Admin\\Delegation' => $baseDir . '/../lib/Settings/Admin/Delegation.php',
'OCA\\Settings\\Settings\\Admin\\Mail' => $baseDir . '/../lib/Settings/Admin/Mail.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => $baseDir . '/../lib/Settings/Admin/Overview.php',

View file

@ -31,6 +31,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/Activity/Setting.php',
'OCA\\Settings\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Settings\\BackgroundJobs\\VerifyUserData' => __DIR__ . '/..' . '/../lib/BackgroundJobs/VerifyUserData.php',
'OCA\\Settings\\Controller\\AISettingsController' => __DIR__ . '/..' . '/../lib/Controller/AISettingsController.php',
'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php',
'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php',
'OCA\\Settings\\Controller\\AuthSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AuthSettingsController.php',
@ -57,6 +58,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => __DIR__ . '/..' . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Sections\\Admin\\Additional' => __DIR__ . '/..' . '/../lib/Sections/Admin/Additional.php',
'OCA\\Settings\\Sections\\Admin\\ArtificialIntelligence' => __DIR__ . '/..' . '/../lib/Sections/Admin/ArtificialIntelligence.php',
'OCA\\Settings\\Sections\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Sections/Admin/Delegation.php',
'OCA\\Settings\\Sections\\Admin\\Groupware' => __DIR__ . '/..' . '/../lib/Sections/Admin/Groupware.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Sections/Admin/Overview.php',
@ -71,6 +73,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Service\\AuthorizedGroupService' => __DIR__ . '/..' . '/../lib/Service/AuthorizedGroupService.php',
'OCA\\Settings\\Service\\NotFoundException' => __DIR__ . '/..' . '/../lib/Service/NotFoundException.php',
'OCA\\Settings\\Service\\ServiceException' => __DIR__ . '/..' . '/../lib/Service/ServiceException.php',
'OCA\\Settings\\Settings\\Admin\\ArtificialIntelligence' => __DIR__ . '/..' . '/../lib/Settings/Admin/ArtificialIntelligence.php',
'OCA\\Settings\\Settings\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Settings/Admin/Delegation.php',
'OCA\\Settings\\Settings\\Admin\\Mail' => __DIR__ . '/..' . '/../lib/Settings/Admin/Mail.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Settings/Admin/Overview.php',

1
apps/settings/img/ai.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z" /></svg>

After

Width:  |  Height:  |  Size: 513 B

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Settings\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Mail\IMailer;
use function GuzzleHttp\Promise\queue;
class AISettingsController extends Controller {
/**
* @param string $appName
* @param IRequest $request
* @param IConfig $config
*/
public function __construct(
$appName,
IRequest $request,
private IConfig $config,
) {
parent::__construct($appName, $request);
}
/**
* Sets the email settings
*
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\ArtificialIntelligence)
*
* @param array $settings
* @return DataResponse
*/
public function update($settings) {
$keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.translation_provider_preferences'];
foreach ($keys as $key) {
if (!isset($settings[$key])) {
continue;
}
$this->config->setAppValue('core', $key, json_encode($settings[$key]));
}
return new DataResponse();
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Sections\Admin;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class ArtificialIntelligence implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
$this->l = $l;
$this->urlGenerator = $urlGenerator;
}
public function getIcon(): string {
return $this->urlGenerator->imagePath('settings', 'ai.svg');
}
public function getID(): string {
return 'ai';
}
public function getName(): string {
return $this->l->t('Artificial Intelligence');
}
public function getPriority(): int {
return 40;
}
}

View file

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
use OCP\SpeechToText\ISpeechToTextManager;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\IProvider;
use OCP\TextProcessing\ITaskType;
use OCP\Translation\ITranslationManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
class ArtificialIntelligence implements IDelegatedSettings {
public function __construct(
private IConfig $config,
private IL10N $l,
private IInitialState $initialState,
private ITranslationManager $translationManager,
private ISpeechToTextManager $sttManager,
private IManager $textProcessingManager,
private ContainerInterface $container,
) {
}
/**
* @return TemplateResponse
*/
public function getForm() {
$translationProviders = [];
$translationPreferences = [];
foreach ($this->translationManager->getProviders() as $provider) {
$translationProviders[] = [
'class' => $provider::class,
'name' => $provider->getName(),
];
$translationPreferences[] = $provider::class;
}
$sttProviders = [];
foreach ($this->sttManager->getProviders() as $provider) {
$sttProviders[] = [
'class' => $provider::class,
'name' => $provider->getName(),
];
}
$textProcessingProviders = [];
/** @var array<class-string<ITaskType>, class-string<IProvider>> $textProcessingSettings */
$textProcessingSettings = [];
foreach ($this->textProcessingManager->getProviders() as $provider) {
$textProcessingProviders[] = [
'class' => $provider::class,
'name' => $provider->getName(),
'taskType' => $provider->getTaskType(),
];
$textProcessingSettings[$provider->getTaskType()] = $provider::class;
}
$textProcessingTaskTypes = [];
foreach ($textProcessingSettings as $taskTypeClass => $providerClass) {
/** @var ITaskType $taskType */
try {
$taskType = $this->container->get($taskTypeClass);
} catch (NotFoundExceptionInterface $e) {
continue;
} catch (ContainerExceptionInterface $e) {
continue;
}
$textProcessingTaskTypes[] = [
'class' => $taskTypeClass,
'name' => $taskType->getName(),
'description' => $taskType->getDescription(),
];
}
$this->initialState->provideInitialState('ai-stt-providers', $sttProviders);
$this->initialState->provideInitialState('ai-translation-providers', $translationProviders);
$this->initialState->provideInitialState('ai-text-processing-providers', $textProcessingProviders);
$this->initialState->provideInitialState('ai-text-processing-task-types', $textProcessingTaskTypes);
$settings = [
'ai.stt_provider' => count($sttProviders) > 0 ? $sttProviders[0]['class'] : null,
'ai.textprocessing_provider_preferences' => $textProcessingSettings,
'ai.translation_provider_preferences' => $translationPreferences,
];
foreach ($settings as $key => $defaultValue) {
$value = $defaultValue;
$json = $this->config->getAppValue('core', $key, '');
if ($json !== '') {
$value = json_decode($json, true);
switch($key) {
case 'ai.textprocessing_provider_preferences':
// fill $value with $defaultValue values
$value = array_merge($defaultValue, $value);
break;
case 'ai.translation_provider_preferences':
$value += array_diff($defaultValue, $value); // Add entries from $defaultValue that are not in $value to the end of $value
break;
default:
break;
}
}
$settings[$key] = $value;
}
$this->initialState->provideInitialState('ai-settings', $settings);
return new TemplateResponse('settings', 'settings/admin/ai');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
return 'ai';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority() {
return 10;
}
public function getName(): ?string {
return $this->l->t('Artificial Intelligence');
}
public function getAuthorizedAppConfig(): array {
return [
'core' => ['/ai..*/'],
];
}
}

View file

@ -227,7 +227,7 @@ window.addEventListener('DOMContentLoaded', () => {
OC.SetupChecks.checkSetup(),
OC.SetupChecks.checkGeneric(),
OC.SetupChecks.checkWOFF2Loading(OC.filePath('core', '', 'fonts/NotoSans-Regular-latin.woff2'), OC.theme.docPlaceholderUrl),
OC.SetupChecks.checkDataProtected()
OC.SetupChecks.checkDataProtected(),
).then((check1, check2, check3, check4, check5, check6, check7, check8, check9, check10, check11) => {
const messages = [].concat(check1, check2, check3, check4, check5, check6, check7, check8, check9, check10, check11)
const $el = $('#postsetupchecks')

View file

@ -0,0 +1,173 @@
<template>
<div>
<NcSettingsSection :title="t('settings', 'Machine translation')"
:description="t('settings', 'Machine translation can be implemented by different apps. Here you can define the precedence of the machine translation apps you have installed at the moment.')">
<draggable v-model="settings['ai.translation_provider_preferences']" @change="saveChanges">
<div v-for="(providerClass, i) in settings['ai.translation_provider_preferences']" :key="providerClass" class="draggable__item">
<DragVerticalIcon /> <span class="draggable__number">{{ i + 1 }}</span> {{ translationProviders.find(p => p.class === providerClass)?.name }}
<NcButton aria-label="Move up" type="tertiary" @click="moveUp(i)">
<template #icon>
<ArrowUpIcon />
</template>
</NcButton>
<NcButton aria-label="Move down" type="tertiary" @click="moveDown(i)">
<template #icon>
<ArrowDownIcon />
</template>
</NcButton>
</div>
</draggable>
</NcSettingsSection>
<NcSettingsSection :title="t('settings', 'Speech-To-Text')"
:description="t('settings', 'Speech-To-Text can be implemented by different apps. Here you can set which app should be used.')">
<template v-for="provider in sttProviders">
<NcCheckboxRadioSwitch :key="provider.class"
:checked.sync="settings['ai.stt_provider']"
:value="provider.class"
name="stt_provider"
type="radio"
@update:checked="saveChanges">
{{ provider.name }}
</NcCheckboxRadioSwitch>
</template>
<template v-if="!hasStt">
<NcCheckboxRadioSwitch disabled type="radio">
{{ t('settings', 'None of your currently installed apps provide Speech-To-Text functionality') }}
</NcCheckboxRadioSwitch>
</template>
</NcSettingsSection>
<NcSettingsSection :title="t('settings', 'Text processing')"
:description="t('settings', 'Text processing tasks can be implemented by different apps. Here you can set which app should be used for which task.')">
<template v-for="type in Object.keys(settings['ai.textprocessing_provider_preferences'])">
<div :key="type">
<h3>{{ t('settings', 'Task:') }} {{ getTaskType(type).name }}</h3>
<p>{{ getTaskType(type).description }}</p>
<p>&nbsp;</p>
<NcSelect v-model="settings['ai.textprocessing_provider_preferences'][type]"
:clearable="false"
:options="textProcessingProviders.filter(p => p.taskType === type).map(p => p.class)"
@input="saveChanges">
<template #option="{label}">
{{ textProcessingProviders.find(p => p.class === label)?.name }}
</template>
<template #selected-option="{label}">
{{ textProcessingProviders.find(p => p.class === label)?.name }}
</template>
</NcSelect>
<p>&nbsp;</p>
</div>
</template>
<template v-if="!hasTextProcessing">
<p>{{ t('settings', 'None of your currently installed apps provide Text processing functionality') }}</p>
</template>
</NcSettingsSection>
</div>
</template>
<script>
import axios from '@nextcloud/axios'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import draggable from 'vuedraggable'
import DragVerticalIcon from 'vue-material-design-icons/DragVertical.vue'
import ArrowDownIcon from 'vue-material-design-icons/ArrowDown.vue'
import ArrowUpIcon from 'vue-material-design-icons/ArrowUp.vue'
import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router'
export default {
name: 'AdminAI',
components: {
NcCheckboxRadioSwitch,
NcSettingsSection,
NcSelect,
draggable,
DragVerticalIcon,
ArrowDownIcon,
ArrowUpIcon,
NcButton
},
data() {
return {
loading: false,
dirty: false,
groups: [],
loadingGroups: false,
sttProviders: loadState('settings', 'ai-stt-providers'),
translationProviders: loadState('settings', 'ai-translation-providers'),
textProcessingProviders: loadState('settings', 'ai-text-processing-providers'),
textProcessingTaskTypes: loadState('settings', 'ai-text-processing-task-types'),
settings: loadState('settings', 'ai-settings'),
}
},
computed: {
hasStt() {
return this.sttProviders.length > 0
},
hasTextProcessing() {
return Object.keys(this.settings['ai.textprocessing_provider_preferences']).length > 0 && Array.isArray(this.textProcessingTaskTypes)
},
},
methods: {
moveUp(i) {
this.settings['ai.translation_provider_preferences'].splice(
Math.min(i - 1, 0),
0,
...this.settings['ai.translation_provider_preferences'].splice(i, 1)
)
this.saveChanges()
},
moveDown(i) {
this.settings['ai.translation_provider_preferences'].splice(
i + 1,
0,
...this.settings['ai.translation_provider_preferences'].splice(i, 1)
)
this.saveChanges()
},
async saveChanges() {
this.loading = true
const data = { settings: this.settings }
try {
await axios.put(generateUrl('/settings/api/admin/ai'), data)
} catch (err) {
console.error('could not save changes', err)
}
this.loading = false
},
getTaskType(type) {
if (!Array.isArray(this.textProcessingTaskTypes)) {
return null
}
return this.textProcessingTaskTypes.find(taskType => taskType.class === type)
},
},
}
</script>
<style scoped>
.draggable__item {
margin-bottom: 5px;
display: flex;
align-items: center;
}
.draggable__item,
.draggable__item * {
cursor: grab;
}
.draggable__number {
border-radius: 20px;
border: 2px solid var(--color-primary-default);
color: var(--color-primary-default);
padding: 0px 7px;
margin-right: 3px;
}
.drag-vertical-icon {
float: left;
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<NcSelect :input-id="setting.id"
v-model="selected"
<NcSelect v-model="selected"
:input-id="setting.id"
class="group-select"
:placeholder="t('settings', 'None')"
label="displayName"

View file

@ -49,7 +49,7 @@ const confirm = () => {
t('settings', 'Do you really want to wipe your data from this device?'),
t('settings', 'Confirm wipe'),
resolve,
true
true,
)
})
}

View file

@ -100,7 +100,7 @@ export default {
'del',
'blockquote',
],
}
},
)
},
},

View file

@ -22,7 +22,9 @@
<template>
<section id="vue-avatar-section">
<h3 class="hidden-visually"> {{ t('settings', 'Your profile information') }} </h3>
<h3 class="hidden-visually">
{{ t('settings', 'Your profile information') }}
</h3>
<HeaderBar :input-id="avatarChangeSupported ? inputId : null"
:readable="avatar.readable"
:scope.sync="avatar.scope" />
@ -30,13 +32,13 @@
<div v-if="!showCropper" class="avatar__container">
<div class="avatar__preview">
<NcAvatar v-if="!loading"
:key="version"
:user="userId"
:aria-label="t('settings', 'Your profile picture')"
:disabled-menu="true"
:disabled-tooltip="true"
:show-user-status="false"
:size="180"
:key="version" />
:size="180" />
<div v-else class="icon-loading" />
</div>
<template v-if="avatarChangeSupported">
@ -62,8 +64,8 @@
</NcButton>
</div>
<span>{{ t('settings', 'The file must be a PNG or JPG') }}</span>
<input ref="input"
:id="inputId"
<input :id="inputId"
ref="input"
type="file"
:accept="validMimeTypes.join(',')"
@change="onChange">

View file

@ -29,7 +29,9 @@
<Account :size="20" />
<div class="details__groups-info">
<p>{{ t('settings', 'You are a member of the following groups:') }}</p>
<p class="details__groups-list">{{ groups.join(', ') }}</p>
<p class="details__groups-list">
{{ groups.join(', ') }}
</p>
</div>
</div>
<div class="details__quota">
@ -69,6 +71,13 @@ export default {
NcProgressBar,
},
data() {
return {
groups,
usageRelative,
}
},
computed: {
quotaText() {
if (quota === SPACE_UNLIMITED) {
@ -79,14 +88,7 @@ export default {
'You are using <strong>{usage}</strong> of <strong>{totalSpace}</strong> (<strong>{usageRelative}%</strong>)',
{ usage, totalSpace, usageRelative },
)
}
},
data() {
return {
groups,
usageRelative,
}
},
},
}
</script>

View file

@ -66,6 +66,6 @@ export default {
}
emit('settings:display-name:updated', value)
},
}
},
}
</script>

View file

@ -153,7 +153,7 @@ export default {
this.handleResponse(
'error',
t('settings', 'Unable to update primary email address'),
e
e,
)
}
},
@ -166,7 +166,7 @@ export default {
this.handleResponse(
'error',
t('settings', 'Unable to delete additional email address'),
e
e,
)
}
},
@ -178,7 +178,7 @@ export default {
this.handleResponse(
'error',
t('settings', 'Unable to delete additional email address'),
{}
{},
)
}
},

View file

@ -88,7 +88,7 @@ export default {
allLanguages() {
return Object.freeze(
[...this.commonLanguages, ...this.otherLanguages]
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {})
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}),
)
},
},

View file

@ -50,7 +50,7 @@
<span>{{ example.time }}</span>
</p>
<p>
{{ t('settings', 'Week starts on {firstDayOfWeek}', { firstDayOfWeek: this.example.firstDayOfWeek }) }}
{{ t('settings', 'Week starts on {firstDayOfWeek}', { firstDayOfWeek: example.firstDayOfWeek }) }}
</p>
</div>
</div>
@ -107,7 +107,7 @@ export default {
allLocales() {
return Object.freeze(
[...this.localesForLanguage, ...this.otherLocales]
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {})
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}),
)
},
},

View file

@ -38,8 +38,8 @@
autocorrect="off"
@input="onPropertyChange" />
<input v-else
ref="input"
:id="inputId"
ref="input"
:placeholder="placeholder"
:type="type"
:value="value"

View file

@ -100,7 +100,7 @@ export default {
isHeading: {
type: Boolean,
default: false,
}
},
},
data() {

View file

@ -0,0 +1,39 @@
/**
* @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import ArtificialIntelligence from './components/AdminAI.vue'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
Vue.prototype.t = t
// Not used here but required for legacy templates
window.OC = window.OC || {}
window.OC.Settings = window.OC.Settings || {}
const View = Vue.extend(ArtificialIntelligence)
new View().$mount('#ai-settings')

View file

@ -39,7 +39,7 @@ window.OC = window.OC || {}
window.OC.Settings = window.OC.Settings || {}
store.replaceState(
loadState('settings', 'mandatory2FAState')
loadState('settings', 'mandatory2FAState'),
)
const View = Vue.extend(AdminTwoFactor)

View file

@ -0,0 +1,28 @@
<?php
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
script('settings', [
'vue-settings-admin-ai',
]);
?>
<div id="ai-settings">
</div>

View file

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\Core\Controller;
use InvalidArgumentException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\Common\Exception\NotFoundException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\TextProcessing\ITaskType;
use OCP\TextProcessing\Task;
use OCP\TextProcessing\IManager;
use OCP\PreConditionNotMetException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class TextProcessingApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private IManager $textProcessingManager,
private IL10N $l,
private ?string $userId,
private ContainerInterface $container,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* This endpoint returns all available LanguageModel task types
*
* @return DataResponse
*/
#[PublicPage]
public function taskTypes(): DataResponse {
$typeClasses = $this->textProcessingManager->getAvailableTaskTypes();
$types = [];
/** @var string $typeClass */
foreach ($typeClasses as $typeClass) {
try {
/** @var ITaskType $object */
$object = $this->container->get($typeClass);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
$this->logger->warning('Could not find ' . $typeClass, ['exception' => $e]);
continue;
}
$types[] = [
'id' => $typeClass,
'name' => $object->getName(),
'description' => $object->getDescription(),
];
}
return new DataResponse([
'types' => $types,
]);
}
/**
* This endpoint allows scheduling a language model task
*
* @param string $input Input text
* @param string $type Type of the task
* @param string $appId ID of the app that will execute the task
* @param string $identifier An arbitrary identifier for the task
*
* @return DataResponse
*
* 200: Task scheduled successfully
* 400: Scheduling task is not possible
* 412: Scheduling task is not possible
*/
#[PublicPage]
#[UserRateLimit(limit: 20, period: 120)]
#[AnonRateLimit(limit: 5, period: 120)]
public function schedule(string $input, string $type, string $appId, string $identifier = ''): DataResponse {
try {
$task = new Task($type, $input, $appId, $this->userId, $identifier);
} catch (InvalidArgumentException) {
return new DataResponse(['message' => $this->l->t('Requested task type does not exist')], Http::STATUS_BAD_REQUEST);
}
try {
$this->textProcessingManager->scheduleTask($task);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (PreConditionNotMetException) {
return new DataResponse(['message' => $this->l->t('Necessary language model provider is not available')], Http::STATUS_PRECONDITION_FAILED);
}
}
/**
* This endpoint allows checking the status and results of a task.
* Tasks are removed 1 week after receiving their last update.
*
* @param int $id The id of the task
*
* @return DataResponse
*
* 200: Task returned
* 404: Task not found
*/
#[PublicPage]
public function getTask(int $id): DataResponse {
try {
$task = $this->textProcessingManager->getUserTask($id, $this->userId);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (NotFoundException $e) {
return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint allows to delete a scheduled task for a user
*
* @param int $id The id of the task
*
* @return DataResponse
*
* 200: Task returned
* 404: Task not found
*/
#[NoAdminRequired]
public function deleteTask(int $id): DataResponse {
try {
$task = $this->textProcessingManager->getUserTask($id, $this->userId);
$this->textProcessingManager->deleteTask($task);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (NotFoundException $e) {
return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint returns a list of tasks of a user that are related
* with a specific appId and optionally with an identifier
*
* @param string $appId
* @param string|null $identifier
* @return DataResponse
*
* 200: Task list returned
*/
#[NoAdminRequired]
public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse {
try {
$tasks = $this->textProcessingManager->getUserTasksByApp($this->userId, $appId, $identifier);
$json = array_map(static function (Task $task) {
return $task->jsonSerialize();
}, $tasks);
return new DataResponse([
'tasks' => $json,
]);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Introduce llm_tasks table
*/
class Version28000Date20230616104802 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('llm_tasks')) {
$table = $schema->createTable('llm_tasks');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'length' => 64,
'autoincrement' => true,
]);
$table->addColumn('type', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('input', Types::TEXT, [
'notnull' => true,
]);
$table->addColumn('output', Types::TEXT, [
'notnull' => false,
]);
$table->addColumn('status', Types::INTEGER, [
'notnull' => false,
'length' => 6,
'default' => 0,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('app_id', Types::STRING, [
'notnull' => true,
'length' => 32,
'default' => '',
]);
$table->addColumn('identifier', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('last_updated', 'integer', [
'notnull' => false,
'length' => 4,
'default' => 0,
'unsigned' => true,
]);
$table->setPrimaryKey(['id'], 'llm_tasks_id_index');
$table->addUniqueIndex(['status', 'type'], 'llm_tasks_status_type');
$table->addIndex(['last_updated'], 'llm_tasks_updated');
return $schema;
}
return null;
}
}

View file

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Introduce textprocessing_tasks table
*/
class Version28000Date20230728104802 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changed = false;
if ($schema->hasTable('llm_tasks')) {
$schema->dropTable('llm_tasks');
$changed = true;
}
if (!$schema->hasTable('textprocessing_tasks')) {
$table = $schema->createTable('textprocessing_tasks');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'length' => 64,
'autoincrement' => true,
]);
$table->addColumn('type', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('input', Types::TEXT, [
'notnull' => true,
]);
$table->addColumn('output', Types::TEXT, [
'notnull' => false,
]);
$table->addColumn('status', Types::INTEGER, [
'notnull' => false,
'length' => 6,
'default' => 0,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('app_id', Types::STRING, [
'notnull' => true,
'length' => 32,
'default' => '',
]);
$table->addColumn('identifier', Types::STRING, [
'notnull' => true,
'length' => 255,
'default' => '',
]);
$table->addColumn('last_updated', Types::INTEGER, [
'notnull' => false,
'length' => 4,
'default' => 0,
'unsigned' => true,
]);
$table->setPrimaryKey(['id'], 'tp_tasks_id_index');
$table->addIndex(['last_updated'], 'tp_tasks_updated');
$table->addIndex(['status', 'type'], 'tp_tasks_status_type_nonunique');
$changed = true;
}
if ($changed) {
return $schema;
}
return null;
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Julien Veyssier <julien-nc@posteo.net>
*
* @author Julien Veyssier <julien-nc@posteo.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Adjust textprocessing_tasks table
*/
class Version28000Date20230803221055 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$changed = false;
if ($schema->hasTable('textprocessing_tasks')) {
$table = $schema->getTable('textprocessing_tasks');
$column = $table->getColumn('user_id');
$column->setNotnull(false);
$table->addIndex(['user_id', 'app_id', 'identifier'], 'tp_tasks_uid_appid_ident');
$changed = true;
}
if ($changed) {
return $schema;
}
return null;
}
}

View file

@ -145,6 +145,12 @@ $application->registerRoutes($this, [
['root' => '/translation', 'name' => 'TranslationApi#languages', 'url' => '/languages', 'verb' => 'GET'],
['root' => '/translation', 'name' => 'TranslationApi#translate', 'url' => '/translate', 'verb' => 'POST'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#taskTypes', 'url' => '/tasktypes', 'verb' => 'GET'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#schedule', 'url' => '/schedule', 'verb' => 'POST'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#getTask', 'url' => '/task/{id}', 'verb' => 'GET'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#deleteTask', 'url' => '/task/{id}', 'verb' => 'DELETE'],
['root' => '/textprocessing', 'name' => 'TextProcessingApi#listTasksByApp', 'url' => '/tasks/app/{appId}', 'verb' => 'GET'],
],
]);

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,23 @@
/**
* @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -190,6 +190,7 @@ return array(
'OCP\\Comments\\IllegalIDChangeException' => $baseDir . '/lib/public/Comments/IllegalIDChangeException.php',
'OCP\\Comments\\MessageTooLongException' => $baseDir . '/lib/public/Comments/MessageTooLongException.php',
'OCP\\Comments\\NotFoundException' => $baseDir . '/lib/public/Comments/NotFoundException.php',
'OCP\\Common\\Exception\\NotFoundException' => $baseDir . '/lib/public/Common/Exception/NotFoundException.php',
'OCP\\Config\\BeforePreferenceDeletedEvent' => $baseDir . '/lib/public/Config/BeforePreferenceDeletedEvent.php',
'OCP\\Config\\BeforePreferenceSetEvent' => $baseDir . '/lib/public/Config/BeforePreferenceSetEvent.php',
'OCP\\Console\\ConsoleEvent' => $baseDir . '/lib/public/Console/ConsoleEvent.php',
@ -618,6 +619,17 @@ return array(
'OCP\\Talk\\IConversationOptions' => $baseDir . '/lib/public/Talk/IConversationOptions.php',
'OCP\\Talk\\ITalkBackend' => $baseDir . '/lib/public/Talk/ITalkBackend.php',
'OCP\\Template' => $baseDir . '/lib/public/Template.php',
'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => $baseDir . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php',
'OCP\\TextProcessing\\Events\\TaskFailedEvent' => $baseDir . '/lib/public/TextProcessing/Events/TaskFailedEvent.php',
'OCP\\TextProcessing\\Events\\TaskSuccessfulEvent' => $baseDir . '/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php',
'OCP\\TextProcessing\\FreePromptTaskType' => $baseDir . '/lib/public/TextProcessing/FreePromptTaskType.php',
'OCP\\TextProcessing\\HeadlineTaskType' => $baseDir . '/lib/public/TextProcessing/HeadlineTaskType.php',
'OCP\\TextProcessing\\IManager' => $baseDir . '/lib/public/TextProcessing/IManager.php',
'OCP\\TextProcessing\\IProvider' => $baseDir . '/lib/public/TextProcessing/IProvider.php',
'OCP\\TextProcessing\\ITaskType' => $baseDir . '/lib/public/TextProcessing/ITaskType.php',
'OCP\\TextProcessing\\SummaryTaskType' => $baseDir . '/lib/public/TextProcessing/SummaryTaskType.php',
'OCP\\TextProcessing\\Task' => $baseDir . '/lib/public/TextProcessing/Task.php',
'OCP\\TextProcessing\\TopicsTaskType' => $baseDir . '/lib/public/TextProcessing/TopicsTaskType.php',
'OCP\\Translation\\CouldNotTranslateException' => $baseDir . '/lib/public/Translation/CouldNotTranslateException.php',
'OCP\\Translation\\IDetectLanguageProvider' => $baseDir . '/lib/public/Translation/IDetectLanguageProvider.php',
'OCP\\Translation\\ITranslationManager' => $baseDir . '/lib/public/Translation/ITranslationManager.php',
@ -1040,6 +1052,7 @@ return array(
'OC\\Core\\Controller\\ReferenceController' => $baseDir . '/core/Controller/ReferenceController.php',
'OC\\Core\\Controller\\SearchController' => $baseDir . '/core/Controller/SearchController.php',
'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php',
'OC\\Core\\Controller\\TextProcessingApiController' => $baseDir . '/core/Controller/TextProcessingApiController.php',
'OC\\Core\\Controller\\TranslationApiController' => $baseDir . '/core/Controller/TranslationApiController.php',
'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UnifiedSearchController' => $baseDir . '/core/Controller/UnifiedSearchController.php',
@ -1118,6 +1131,9 @@ return array(
'OC\\Core\\Migrations\\Version27000Date20220613163520' => $baseDir . '/core/Migrations/Version27000Date20220613163520.php',
'OC\\Core\\Migrations\\Version27000Date20230309104325' => $baseDir . '/core/Migrations/Version27000Date20230309104325.php',
'OC\\Core\\Migrations\\Version27000Date20230309104802' => $baseDir . '/core/Migrations/Version27000Date20230309104802.php',
'OC\\Core\\Migrations\\Version28000Date20230616104802' => $baseDir . '/core/Migrations/Version28000Date20230616104802.php',
'OC\\Core\\Migrations\\Version28000Date20230728104802' => $baseDir . '/core/Migrations/Version28000Date20230728104802.php',
'OC\\Core\\Migrations\\Version28000Date20230803221055' => $baseDir . '/core/Migrations/Version28000Date20230803221055.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php',
@ -1490,6 +1506,7 @@ return array(
'OC\\RepairException' => $baseDir . '/lib/private/RepairException.php',
'OC\\Repair\\AddBruteForceCleanupJob' => $baseDir . '/lib/private/Repair/AddBruteForceCleanupJob.php',
'OC\\Repair\\AddCleanupUpdaterBackupsJob' => $baseDir . '/lib/private/Repair/AddCleanupUpdaterBackupsJob.php',
'OC\\Repair\\AddRemoveOldTasksBackgroundJob' => $baseDir . '/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php',
'OC\\Repair\\CleanTags' => $baseDir . '/lib/private/Repair/CleanTags.php',
'OC\\Repair\\CleanUpAbandonedApps' => $baseDir . '/lib/private/Repair/CleanUpAbandonedApps.php',
'OC\\Repair\\ClearFrontendCaches' => $baseDir . '/lib/private/Repair/ClearFrontendCaches.php',
@ -1641,6 +1658,11 @@ return array(
'OC\\Template\\ResourceLocator' => $baseDir . '/lib/private/Template/ResourceLocator.php',
'OC\\Template\\ResourceNotFoundException' => $baseDir . '/lib/private/Template/ResourceNotFoundException.php',
'OC\\Template\\TemplateFileLocator' => $baseDir . '/lib/private/Template/TemplateFileLocator.php',
'OC\\TextProcessing\\Db\\Task' => $baseDir . '/lib/private/TextProcessing/Db/Task.php',
'OC\\TextProcessing\\Db\\TaskMapper' => $baseDir . '/lib/private/TextProcessing/Db/TaskMapper.php',
'OC\\TextProcessing\\Manager' => $baseDir . '/lib/private/TextProcessing/Manager.php',
'OC\\TextProcessing\\RemoveOldTasksBackgroundJob' => $baseDir . '/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php',
'OC\\TextProcessing\\TaskBackgroundJob' => $baseDir . '/lib/private/TextProcessing/TaskBackgroundJob.php',
'OC\\Translation\\TranslationManager' => $baseDir . '/lib/private/Translation/TranslationManager.php',
'OC\\URLGenerator' => $baseDir . '/lib/private/URLGenerator.php',
'OC\\Updater' => $baseDir . '/lib/private/Updater.php',

View file

@ -223,6 +223,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Comments\\IllegalIDChangeException' => __DIR__ . '/../../..' . '/lib/public/Comments/IllegalIDChangeException.php',
'OCP\\Comments\\MessageTooLongException' => __DIR__ . '/../../..' . '/lib/public/Comments/MessageTooLongException.php',
'OCP\\Comments\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Comments/NotFoundException.php',
'OCP\\Common\\Exception\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Common/Exception/NotFoundException.php',
'OCP\\Config\\BeforePreferenceDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceDeletedEvent.php',
'OCP\\Config\\BeforePreferenceSetEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceSetEvent.php',
'OCP\\Console\\ConsoleEvent' => __DIR__ . '/../../..' . '/lib/public/Console/ConsoleEvent.php',
@ -651,6 +652,17 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Talk\\IConversationOptions' => __DIR__ . '/../../..' . '/lib/public/Talk/IConversationOptions.php',
'OCP\\Talk\\ITalkBackend' => __DIR__ . '/../../..' . '/lib/public/Talk/ITalkBackend.php',
'OCP\\Template' => __DIR__ . '/../../..' . '/lib/public/Template.php',
'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php',
'OCP\\TextProcessing\\Events\\TaskFailedEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/TaskFailedEvent.php',
'OCP\\TextProcessing\\Events\\TaskSuccessfulEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/TaskSuccessfulEvent.php',
'OCP\\TextProcessing\\FreePromptTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/FreePromptTaskType.php',
'OCP\\TextProcessing\\HeadlineTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/HeadlineTaskType.php',
'OCP\\TextProcessing\\IManager' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/IManager.php',
'OCP\\TextProcessing\\IProvider' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/IProvider.php',
'OCP\\TextProcessing\\ITaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/ITaskType.php',
'OCP\\TextProcessing\\SummaryTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/SummaryTaskType.php',
'OCP\\TextProcessing\\Task' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Task.php',
'OCP\\TextProcessing\\TopicsTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/TopicsTaskType.php',
'OCP\\Translation\\CouldNotTranslateException' => __DIR__ . '/../../..' . '/lib/public/Translation/CouldNotTranslateException.php',
'OCP\\Translation\\IDetectLanguageProvider' => __DIR__ . '/../../..' . '/lib/public/Translation/IDetectLanguageProvider.php',
'OCP\\Translation\\ITranslationManager' => __DIR__ . '/../../..' . '/lib/public/Translation/ITranslationManager.php',
@ -1073,6 +1085,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Controller\\ReferenceController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceController.php',
'OC\\Core\\Controller\\SearchController' => __DIR__ . '/../../..' . '/core/Controller/SearchController.php',
'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php',
'OC\\Core\\Controller\\TextProcessingApiController' => __DIR__ . '/../../..' . '/core/Controller/TextProcessingApiController.php',
'OC\\Core\\Controller\\TranslationApiController' => __DIR__ . '/../../..' . '/core/Controller/TranslationApiController.php',
'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php',
'OC\\Core\\Controller\\UnifiedSearchController' => __DIR__ . '/../../..' . '/core/Controller/UnifiedSearchController.php',
@ -1151,6 +1164,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version27000Date20220613163520' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20220613163520.php',
'OC\\Core\\Migrations\\Version27000Date20230309104325' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20230309104325.php',
'OC\\Core\\Migrations\\Version27000Date20230309104802' => __DIR__ . '/../../..' . '/core/Migrations/Version27000Date20230309104802.php',
'OC\\Core\\Migrations\\Version28000Date20230616104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230616104802.php',
'OC\\Core\\Migrations\\Version28000Date20230728104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230728104802.php',
'OC\\Core\\Migrations\\Version28000Date20230803221055' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230803221055.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php',
@ -1523,6 +1539,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\RepairException' => __DIR__ . '/../../..' . '/lib/private/RepairException.php',
'OC\\Repair\\AddBruteForceCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddBruteForceCleanupJob.php',
'OC\\Repair\\AddCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddCleanupUpdaterBackupsJob.php',
'OC\\Repair\\AddRemoveOldTasksBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php',
'OC\\Repair\\CleanTags' => __DIR__ . '/../../..' . '/lib/private/Repair/CleanTags.php',
'OC\\Repair\\CleanUpAbandonedApps' => __DIR__ . '/../../..' . '/lib/private/Repair/CleanUpAbandonedApps.php',
'OC\\Repair\\ClearFrontendCaches' => __DIR__ . '/../../..' . '/lib/private/Repair/ClearFrontendCaches.php',
@ -1674,6 +1691,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Template\\ResourceLocator' => __DIR__ . '/../../..' . '/lib/private/Template/ResourceLocator.php',
'OC\\Template\\ResourceNotFoundException' => __DIR__ . '/../../..' . '/lib/private/Template/ResourceNotFoundException.php',
'OC\\Template\\TemplateFileLocator' => __DIR__ . '/../../..' . '/lib/private/Template/TemplateFileLocator.php',
'OC\\TextProcessing\\Db\\Task' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Db/Task.php',
'OC\\TextProcessing\\Db\\TaskMapper' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Db/TaskMapper.php',
'OC\\TextProcessing\\Manager' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Manager.php',
'OC\\TextProcessing\\RemoveOldTasksBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php',
'OC\\TextProcessing\\TaskBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/TaskBackgroundJob.php',
'OC\\Translation\\TranslationManager' => __DIR__ . '/../../..' . '/lib/private/Translation/TranslationManager.php',
'OC\\URLGenerator' => __DIR__ . '/../../..' . '/lib/private/URLGenerator.php',
'OC\\Updater' => __DIR__ . '/../../..' . '/lib/private/Updater.php',

View file

@ -33,6 +33,7 @@ use Closure;
use OCP\Calendar\Resource\IBackend as IResourceBackend;
use OCP\Calendar\Room\IBackend as IRoomBackend;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\TextProcessing\IProvider as ITextProcessingProvider;
use OCP\SpeechToText\ISpeechToTextProvider;
use OCP\Talk\ITalkBackend;
use OCP\Translation\ITranslationProvider;
@ -115,6 +116,9 @@ class RegistrationContext {
/** @var ServiceRegistration<ISpeechToTextProvider>[] */
private $speechToTextProviders = [];
/** @var ServiceRegistration<ITextProcessingProvider>[] */
private $textProcessingProviders = [];
/** @var ServiceRegistration<ICustomTemplateProvider>[] */
private $templateProviders = [];
@ -262,6 +266,12 @@ class RegistrationContext {
$providerClass
);
}
public function registerTextProcessingProvider(string $providerClass): void {
$this->context->registerTextProcessingProvider(
$this->appId,
$providerClass
);
}
public function registerTemplateProvider(string $providerClass): void {
$this->context->registerTemplateProvider(
@ -429,6 +439,10 @@ class RegistrationContext {
$this->speechToTextProviders[] = new ServiceRegistration($appId, $class);
}
public function registerTextProcessingProvider(string $appId, string $class): void {
$this->textProcessingProviders[] = new ServiceRegistration($appId, $class);
}
public function registerTemplateProvider(string $appId, string $class): void {
$this->templateProviders[] = new ServiceRegistration($appId, $class);
}
@ -707,6 +721,13 @@ class RegistrationContext {
return $this->speechToTextProviders;
}
/**
* @return ServiceRegistration<ITextProcessingProvider>[]
*/
public function getTextProcessingProviders(): array {
return $this->textProcessingProviders;
}
/**
* @return ServiceRegistration<ICustomTemplateProvider>[]
*/

View file

@ -34,6 +34,7 @@
*/
namespace OC;
use OC\Repair\AddRemoveOldTasksBackgroundJob;
use OC\Repair\CleanUpAbandonedApps;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\Utility\ITimeFactory;
@ -210,6 +211,7 @@ class Repair implements IOutput {
\OCP\Server::get(AddTokenCleanupJob::class),
\OCP\Server::get(CleanUpAbandonedApps::class),
\OCP\Server::get(AddMissingSecretJob::class),
\OCP\Server::get(AddRemoveOldTasksBackgroundJob::class),
];
}

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Repair;
use OC\TextProcessing\RemoveOldTasksBackgroundJob;
use OCP\BackgroundJob\IJobList;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
class AddRemoveOldTasksBackgroundJob implements IRepairStep {
private IJobList $jobList;
public function __construct(IJobList $jobList) {
$this->jobList = $jobList;
}
public function getName(): string {
return 'Add language model tasks cleanup job';
}
public function run(IOutput $output) {
$this->jobList->add(RemoveOldTasksBackgroundJob::class);
}
}

View file

@ -1461,6 +1461,8 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(ISpeechToTextManager::class, SpeechToTextManager::class);
$this->registerAlias(\OCP\TextProcessing\IManager::class, \OC\TextProcessing\Manager::class);
$this->connectDispatcher();
}

View file

@ -53,6 +53,7 @@ use Exception;
use InvalidArgumentException;
use OC\Authentication\Token\PublicKeyTokenProvider;
use OC\Authentication\Token\TokenCleanupJob;
use OC\TextProcessing\RemoveOldTasksBackgroundJob;
use OC\Log\Rotate;
use OC\Preview\BackgroundCleanupJob;
use OCP\AppFramework\Utility\ITimeFactory;
@ -453,6 +454,7 @@ class Setup {
$jobList->add(TokenCleanupJob::class);
$jobList->add(Rotate::class);
$jobList->add(BackgroundCleanupJob::class);
$jobList->add(RemoveOldTasksBackgroundJob::class);
}
/**

View file

@ -34,6 +34,7 @@ use OCP\BackgroundJob\IJobList;
use OCP\Files\File;
use OCP\Files\InvalidPathException;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IServerContainer;
use OCP\PreConditionNotMetException;
use OCP\SpeechToText\ISpeechToTextManager;
@ -53,6 +54,7 @@ class SpeechToTextManager implements ISpeechToTextManager {
private Coordinator $coordinator,
private LoggerInterface $logger,
private IJobList $jobList,
private IConfig $config,
) {
}
@ -111,7 +113,18 @@ class SpeechToTextManager implements ISpeechToTextManager {
throw new PreConditionNotMetException('No SpeechToText providers have been registered');
}
foreach ($this->getProviders() as $provider) {
$providers = $this->getProviders();
$json = $this->config->getAppValue('core', 'ai.stt_provider', '');
if ($json !== '') {
$className = json_decode($json, true);
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $className));
if ($provider !== false) {
$providers = [$provider];
}
}
foreach ($providers as $provider) {
try {
return $provider->transcribeFile($file);
} catch (\Throwable $e) {

View file

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\TextProcessing\Db;
use OCP\AppFramework\Db\Entity;
use OCP\TextProcessing\Task as OCPTask;
/**
* @method setType(string $type)
* @method string getType()
* @method setLastUpdated(int $lastUpdated)
* @method int getLastUpdated()
* @method setInput(string $type)
* @method string getInput()
* @method setOutput(string $type)
* @method string getOutput()
* @method setStatus(int $type)
* @method int getStatus()
* @method setUserId(?string $userId)
* @method string|null getUserId()
* @method setAppId(string $type)
* @method string getAppId()
* @method setIdentifier(string $identifier)
* @method string getIdentifier()
*/
class Task extends Entity {
protected $lastUpdated;
protected $type;
protected $input;
protected $output;
protected $status;
protected $userId;
protected $appId;
protected $identifier;
/**
* @var string[]
*/
public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier'];
/**
* @var string[]
*/
public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier'];
public function __construct() {
// add types in constructor
$this->addType('id', 'integer');
$this->addType('lastUpdated', 'integer');
$this->addType('type', 'string');
$this->addType('input', 'string');
$this->addType('output', 'string');
$this->addType('status', 'integer');
$this->addType('userId', 'string');
$this->addType('appId', 'string');
$this->addType('identifier', 'string');
}
public function toRow(): array {
return array_combine(self::$columns, array_map(function ($field) {
return $this->{'get'.ucfirst($field)}();
}, self::$fields));
}
public static function fromPublicTask(OCPTask $task): Task {
/** @var Task $task */
$task = Task::fromParams([
'id' => $task->getId(),
'type' => $task->getType(),
'lastUpdated' => time(),
'status' => $task->getStatus(),
'input' => $task->getInput(),
'output' => $task->getOutput(),
'userId' => $task->getUserId(),
'appId' => $task->getAppId(),
'identifier' => $task->getIdentifier(),
]);
return $task;
}
public function toPublicTask(): OCPTask {
$task = new OCPTask($this->getType(), $this->getInput(), $this->getAppId(), $this->getuserId(), $this->getIdentifier());
$task->setId($this->getId());
$task->setStatus($this->getStatus());
$task->setOutput($this->getOutput());
return $task;
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\TextProcessing\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\IDBConnection;
/**
* @extends QBMapper<Task>
*/
class TaskMapper extends QBMapper {
public function __construct(
IDBConnection $db,
private ITimeFactory $timeFactory,
) {
parent::__construct($db, 'textprocessing_tasks', Task::class);
}
/**
* @param int $id
* @return Task
* @throws Exception
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function find(int $id): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
return $this->findEntity($qb);
}
/**
* @param int $id
* @param string|null $userId
* @return Task
* @throws DoesNotExistException
* @throws Exception
* @throws MultipleObjectsReturnedException
*/
public function findByIdAndUser(int $id, ?string $userId): Task {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createPositionalParameter($id)));
if ($userId === null) {
$qb->andWhere($qb->expr()->isNull('user_id'));
} else {
$qb->andWhere($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)));
}
return $this->findEntity($qb);
}
/**
* @param string $userId
* @param string $appId
* @param string|null $identifier
* @return array
* @throws Exception
*/
public function findUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select(Task::$columns)
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId)))
->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId)));
if ($identifier !== null) {
$qb->andWhere($qb->expr()->eq('identifier', $qb->createPositionalParameter($identifier)));
}
return $this->findEntities($qb);
}
/**
* @param int $timeout
* @return int the number of deleted tasks
* @throws Exception
*/
public function deleteOlderThan(int $timeout): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter(time() - $timeout)));
return $qb->executeStatement();
}
public function update(Entity $entity): Entity {
$entity->setLastUpdated($this->timeFactory->now()->getTimestamp());
return parent::update($entity);
}
}

View file

@ -0,0 +1,256 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\TextProcessing;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\TextProcessing\Db\Task as DbTask;
use OCP\IConfig;
use OCP\TextProcessing\Task;
use OCP\TextProcessing\Task as OCPTask;
use OC\TextProcessing\Db\TaskMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\BackgroundJob\IJobList;
use OCP\Common\Exception\NotFoundException;
use OCP\DB\Exception;
use OCP\IServerContainer;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\IProvider;
use OCP\PreConditionNotMetException;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
class Manager implements IManager {
/** @var ?IProvider[] */
private ?array $providers = null;
public function __construct(
private IServerContainer $serverContainer,
private Coordinator $coordinator,
private LoggerInterface $logger,
private IJobList $jobList,
private TaskMapper $taskMapper,
private IConfig $config,
) {
}
public function getProviders(): array {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
return [];
}
if ($this->providers !== null) {
return $this->providers;
}
$this->providers = [];
foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
$class = $providerServiceRegistration->getService();
try {
$this->providers[$class] = $this->serverContainer->get($class);
} catch (Throwable $e) {
$this->logger->error('Failed to load Text processing provider ' . $class, [
'exception' => $e,
]);
}
}
return $this->providers;
}
public function hasProviders(): bool {
$context = $this->coordinator->getRegistrationContext();
if ($context === null) {
return false;
}
return count($context->getTextProcessingProviders()) > 0;
}
/**
* @inheritDoc
*/
public function getAvailableTaskTypes(): array {
$tasks = [];
foreach ($this->getProviders() as $provider) {
$tasks[$provider->getTaskType()] = true;
}
return array_keys($tasks);
}
public function canHandleTask(OCPTask $task): bool {
return in_array($task->getType(), $this->getAvailableTaskTypes());
}
/**
* @inheritDoc
*/
public function runTask(OCPTask $task): string {
if (!$this->canHandleTask($task)) {
throw new PreConditionNotMetException('No text processing provider is installed that can handle this task');
}
$providers = $this->getProviders();
$json = $this->config->getAppValue('core', 'ai.textprocessing_provider_preferences', '');
if ($json !== '') {
$preferences = json_decode($json, true);
if (isset($preferences[$task->getType()])) {
// If a preference for this task type is set, move the preferred provider to the start
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $preferences[$task->getType()]));
if ($provider !== false) {
$providers = array_filter($providers, fn ($p) => $p !== $provider);
array_unshift($providers, $provider);
}
}
}
foreach ($providers as $provider) {
if (!$task->canUseProvider($provider)) {
continue;
}
try {
$task->setStatus(OCPTask::STATUS_RUNNING);
if ($task->getId() === null) {
$taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task));
$task->setId($taskEntity->getId());
} else {
$this->taskMapper->update(DbTask::fromPublicTask($task));
}
$output = $task->visitProvider($provider);
$task->setOutput($output);
$task->setStatus(OCPTask::STATUS_SUCCESSFUL);
$this->taskMapper->update(DbTask::fromPublicTask($task));
return $output;
} catch (\RuntimeException $e) {
$this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]);
$task->setStatus(OCPTask::STATUS_FAILED);
$this->taskMapper->update(DbTask::fromPublicTask($task));
throw $e;
} catch (\Throwable $e) {
$this->logger->info('LanguageModel call using provider ' . $provider->getName() . ' failed', ['exception' => $e]);
$task->setStatus(OCPTask::STATUS_FAILED);
$this->taskMapper->update(DbTask::fromPublicTask($task));
throw new RuntimeException('LanguageModel call using provider ' . $provider->getName() . ' failed: ' . $e->getMessage(), 0, $e);
}
}
throw new RuntimeException('Could not run task');
}
/**
* @inheritDoc
* @throws Exception
*/
public function scheduleTask(OCPTask $task): void {
if (!$this->canHandleTask($task)) {
throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task');
}
$task->setStatus(OCPTask::STATUS_SCHEDULED);
$taskEntity = DbTask::fromPublicTask($task);
$this->taskMapper->insert($taskEntity);
$task->setId($taskEntity->getId());
$this->jobList->add(TaskBackgroundJob::class, [
'taskId' => $task->getId()
]);
}
/**
* @inheritDoc
*/
public function deleteTask(Task $task): void {
$taskEntity = DbTask::fromPublicTask($task);
$this->taskMapper->delete($taskEntity);
$this->jobList->remove(TaskBackgroundJob::class, [
'taskId' => $task->getId()
]);
}
/**
* Get a task from its id
*
* @param int $id The id of the task
* @return OCPTask
* @throws RuntimeException If the query failed
* @throws NotFoundException If the task could not be found
*/
public function getTask(int $id): OCPTask {
try {
$taskEntity = $this->taskMapper->find($id);
return $taskEntity->toPublicTask();
} catch (DoesNotExistException $e) {
throw new NotFoundException('Could not find task with the provided id');
} catch (MultipleObjectsReturnedException $e) {
throw new RuntimeException('Could not uniquely identify task with given id', 0, $e);
} catch (Exception $e) {
throw new RuntimeException('Failure while trying to find task by id: ' . $e->getMessage(), 0, $e);
}
}
/**
* Get a task from its user id and task id
* If userId is null, this can only get a task that was scheduled anonymously
*
* @param int $id The id of the task
* @param string|null $userId The user id that scheduled the task
* @return OCPTask
* @throws RuntimeException If the query failed
* @throws NotFoundException If the task could not be found
*/
public function getUserTask(int $id, ?string $userId): OCPTask {
try {
$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
return $taskEntity->toPublicTask();
} catch (DoesNotExistException $e) {
throw new NotFoundException('Could not find task with the provided id and user id');
} catch (MultipleObjectsReturnedException $e) {
throw new RuntimeException('Could not uniquely identify task with given id and user id', 0, $e);
} catch (Exception $e) {
throw new RuntimeException('Failure while trying to find task by id and user id: ' . $e->getMessage(), 0, $e);
}
}
/**
* Get a list of tasks scheduled by a specific user for a specific app
* and optionally with a specific identifier.
* This cannot be used to get anonymously scheduled tasks
*
* @param string $userId
* @param string $appId
* @param string|null $identifier
* @return array
*/
public function getUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array {
try {
$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier);
return array_map(static function (DbTask $taskEntity) {
return $taskEntity->toPublicTask();
}, $taskEntities);
} catch (Exception $e) {
throw new RuntimeException('Failure while trying to find tasks by appId and identifier: ' . $e->getMessage(), 0, $e);
}
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\TextProcessing;
use OC\TextProcessing\Db\TaskMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\Exception;
use Psr\Log\LoggerInterface;
class RemoveOldTasksBackgroundJob extends TimedJob {
public const MAX_TASK_AGE_SECONDS = 60 * 50 * 24 * 7; // 1 week
public function __construct(
ITimeFactory $timeFactory,
private TaskMapper $taskMapper,
private LoggerInterface $logger,
) {
parent::__construct($timeFactory);
$this->setInterval(60 * 60 * 24);
}
/**
* @param mixed $argument
* @inheritDoc
*/
protected function run($argument) {
try {
$this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS);
} catch (Exception $e) {
$this->logger->warning('Failed to delete stale language model tasks', ['exception' => $e]);
}
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\TextProcessing;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\TextProcessing\Events\TaskFailedEvent;
use OCP\TextProcessing\Events\TaskSuccessfulEvent;
use OCP\TextProcessing\IManager;
class TaskBackgroundJob extends QueuedJob {
public function __construct(
ITimeFactory $timeFactory,
private IManager $textProcessingManager,
private IEventDispatcher $eventDispatcher,
) {
parent::__construct($timeFactory);
// We want to avoid overloading the machine with these jobs
// so we only allow running one job at a time
$this->setAllowParallelRuns(false);
}
/**
* @param array{taskId: int} $argument
* @inheritDoc
*/
protected function run($argument) {
$taskId = $argument['taskId'];
$task = $this->textProcessingManager->getTask($taskId);
try {
$this->textProcessingManager->runTask($task);
$event = new TaskSuccessfulEvent($task);
} catch (\Throwable $e) {
$event = new TaskFailedEvent($task, $e->getMessage());
}
$this->eventDispatcher->dispatchTyped($event);
}
}

View file

@ -28,6 +28,7 @@ namespace OC\Translation;
use InvalidArgumentException;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\IConfig;
use OCP\IServerContainer;
use OCP\PreConditionNotMetException;
use OCP\Translation\CouldNotTranslateException;
@ -48,6 +49,7 @@ class TranslationManager implements ITranslationManager {
private IServerContainer $serverContainer,
private Coordinator $coordinator,
private LoggerInterface $logger,
private IConfig $config,
) {
}
@ -64,8 +66,25 @@ class TranslationManager implements ITranslationManager {
throw new PreConditionNotMetException('No translation providers available');
}
$providers = $this->getProviders();
$json = $this->config->getAppValue('core', 'ai.translation_provider_preferences', '');
if ($json !== '') {
$precedence = json_decode($json, true);
$newProviders = [];
foreach ($precedence as $className) {
$provider = current(array_filter($providers, fn ($provider) => $provider::class === $className));
if ($provider !== false) {
$newProviders[] = $provider;
}
}
// Add all providers that haven't been added so far
$newProviders += array_udiff($providers, $newProviders, fn ($a, $b) => $a::class > $b::class ? 1 : ($a::class < $b::class ? -1 : 0));
$providers = $newProviders;
}
if ($fromLanguage === null) {
foreach ($this->getProviders() as $provider) {
foreach ($providers as $provider) {
if ($provider instanceof IDetectLanguageProvider) {
$fromLanguage = $provider->detectLanguage($text);
}
@ -84,11 +103,11 @@ class TranslationManager implements ITranslationManager {
return $text;
}
foreach ($this->getProviders() as $provider) {
foreach ($providers as $provider) {
try {
return $provider->translate($fromLanguage, $toLanguage, $text);
} catch (RuntimeException $e) {
$this->logger->warning("Failed to translate from {$fromLanguage} to {$toLanguage}", ['exception' => $e]);
$this->logger->warning("Failed to translate from {$fromLanguage} to {$toLanguage} using provider {$provider->getName()}", ['exception' => $e]);
}
}

View file

@ -37,6 +37,7 @@ use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Template\ICustomTemplateProvider;
use OCP\IContainer;
use OCP\TextProcessing\IProvider as ITextProcessingProvider;
use OCP\Notification\INotifier;
use OCP\Preview\IProviderV2;
use OCP\SpeechToText\ISpeechToTextProvider;
@ -219,6 +220,16 @@ interface IRegistrationContext {
*/
public function registerSpeechToTextProvider(string $providerClass): void;
/**
* Register a custom text processing provider class that provides a promptable language model
* through the OCP\TextProcessing APIs
*
* @param string $providerClass
* @psalm-param class-string<ITextProcessingProvider> $providerClass
* @since 27.1.0
*/
public function registerTextProcessingProvider(string $providerClass): void;
/**
* Register a custom template provider class that is able to inject custom templates
* in addition to the user defined ones

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCP\Common\Exception;
/**
* This is thrown whenever something was expected to exist but doesn't
*
* @since 27.1.0
*/
class NotFoundException extends \Exception {
/**
* Constructor
* @param string $msg the error message
* @since 27.1.0
*/
public function __construct(string $msg) {
parent::__construct($msg);
}
}

View file

@ -40,6 +40,12 @@ interface ISpeechToTextManager {
*/
public function hasProviders(): bool;
/**
* @return ISpeechToTextProvider[]
* @since 27.1.0
*/
public function getProviders(): array;
/**
* Will schedule a transcription process in the background. The result will become available
* with the \OCP\SpeechToText\Events\TranscriptionFinishedEvent

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\TextProcessing\Events;
use OCP\EventDispatcher\Event;
use OCP\TextProcessing\Task;
/**
* @since 27.1.0
*/
abstract class AbstractTextProcessingEvent extends Event {
/**
* @since 27.1.0
*/
public function __construct(
private Task $task
) {
parent::__construct();
}
/**
* @return Task
* @since 27.1.0
*/
public function getTask(): Task {
return $this->task;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace OCP\TextProcessing\Events;
use OCP\TextProcessing\Task;
/**
* @since 27.1.0
*/
class TaskFailedEvent extends AbstractTextProcessingEvent {
/**
* @param Task $task
* @param string $errorMessage
* @since 27.1.0
*/
public function __construct(
Task $task,
private string $errorMessage,
) {
parent::__construct($task);
}
/**
* @return string
* @since 27.1.0
*/
public function getErrorMessage(): string {
return $this->errorMessage;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace OCP\TextProcessing\Events;
/**
* @since 27.1.0
*/
class TaskSuccessfulEvent extends AbstractTextProcessingEvent {
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use OCP\IL10N;
use OCP\L10N\IFactory;
/**
* This is the text processing task type for free prompting
* @since 27.1.0
*/
class FreePromptTaskType implements ITaskType {
private IL10N $l;
/**
* Constructor for FreePromptTaskType
*
* @param IFactory $l10nFactory
* @since 27.1.0
*/
public function __construct(
IFactory $l10nFactory,
) {
$this->l = $l10nFactory->get('core');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getName(): string {
return $this->l->t('Free prompt');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getDescription(): string {
return $this->l->t('Runs an arbitrary prompt through the language model.');
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use OCP\IL10N;
use OCP\L10N\IFactory;
/**
* This is the text processing task type for creating headline
* @since 27.1.0
*/
class HeadlineTaskType implements ITaskType {
private IL10N $l;
/**
* Constructor for HeadlineTaskType
*
* @param IFactory $l10nFactory
* @since 27.1.0
*/
public function __construct(
IFactory $l10nFactory,
) {
$this->l = $l10nFactory->get('core');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getName(): string {
return $this->l->t('Generate headline');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getDescription(): string {
return $this->l->t('Generates a possible headline for a text.');
}
}

View file

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use OCP\Common\Exception\NotFoundException;
use OCP\PreConditionNotMetException;
use RuntimeException;
/**
* API surface for apps interacting with and making use of LanguageModel providers
* without known which providers are installed
* @since 27.1.0
*/
interface IManager {
/**
* @since 27.1.0
*/
public function hasProviders(): bool;
/**
* @return IProvider[]
* @since 27.1.0
*/
public function getProviders(): array;
/**
* @return class-string<ITaskType>[]
* @since 27.1.0
*/
public function getAvailableTaskTypes(): array;
/**
* @param Task $task The task to run
* @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
* @throws RuntimeException If something else failed
* @since 27.1.0
*/
public function runTask(Task $task): string;
/**
* Will schedule an LLM inference process in the background. The result will become available
* with the \OCP\LanguageModel\Events\TaskSuccessfulEvent
* If inference fails a \OCP\LanguageModel\Events\TaskFailedEvent will be dispatched instead
*
* @param Task $task The task to schedule
* @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
* @since 27.1.0
*/
public function scheduleTask(Task $task) : void;
/**
* Delete a task that has been scheduled before
*
* @param Task $task The task to delete
* @since 27.1.0
*/
public function deleteTask(Task $task): void;
/**
* @param int $id The id of the task
* @return Task
* @throws RuntimeException If the query failed
* @throws NotFoundException If the task could not be found
* @since 27.1.0
*/
public function getTask(int $id): Task;
/**
* @param int $id The id of the task
* @param string|null $userId The user id that scheduled the task
* @return Task
* @throws RuntimeException If the query failed
* @throws NotFoundException If the task could not be found
* @since 27.1.0
*/
public function getUserTask(int $id, ?string $userId): Task;
/**
* @param string $userId
* @param string $appId
* @param string|null $identifier
* @return array
* @since 27.1.0
*/
public function getUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array;
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use RuntimeException;
/**
* This is the interface that is implemented by apps that
* implement a text processing provider
* @template T of ITaskType
* @since 27.1.0
*/
interface IProvider {
/**
* The localized name of this provider
* @since 27.1.0
*/
public function getName(): string;
/**
* Processes a text
*
* @param string $prompt The input text
* @return string the output text
* @since 27.1.0
* @throws RuntimeException If the text could not be processed
*/
public function process(string $prompt): string;
/**
* Returns the task type class string of the task type, that this
* provider handles
*
* @since 27.1.0
* @return class-string<T>
*/
public function getTaskType(): string;
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
/**
* This is a task type interface that is implemented by text processing
* task types
* @since 27.1.0
*/
interface ITaskType {
/**
* Returns the localized name of this task type
*
* @since 27.1.0
* @return string
*/
public function getName(): string;
/**
* Returns the localized description of this task type
*
* @since 27.1.0
* @return string
*/
public function getDescription(): string;
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use OCP\IL10N;
use OCP\L10N\IFactory;
/**
* This is the text processing task type for summaries
* @since 27.1.0
*/
class SummaryTaskType implements ITaskType {
private IL10N $l;
/**
* Constructor for SummaryTaskType
*
* @param IFactory $l10nFactory
* @since 27.1.0
*/
public function __construct(
IFactory $l10nFactory,
) {
$this->l = $l10nFactory->get('core');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getName(): string {
return $this->l->t('Summarize');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getDescription(): string {
return $this->l->t('Summarizes text by reducing its length without losing key information.');
}
}

View file

@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
/**
* This is a text processing task
* @since 27.1.0
* @psalm-template T of ITaskType
* @psalm-template S as class-string<T>
* @psalm-template P as IProvider<T>
*/
final class Task implements \JsonSerializable {
protected ?int $id = null;
protected ?string $output = null;
/**
* @since 27.1.0
*/
public const TYPES = [
FreePromptTaskType::class,
SummaryTaskType::class,
HeadlineTaskType::class,
TopicsTaskType::class,
];
/**
* @since 27.1.0
*/
public const STATUS_FAILED = 4;
/**
* @since 27.1.0
*/
public const STATUS_SUCCESSFUL = 3;
/**
* @since 27.1.0
*/
public const STATUS_RUNNING = 2;
/**
* @since 27.1.0
*/
public const STATUS_SCHEDULED = 1;
/**
* @since 27.1.0
*/
public const STATUS_UNKNOWN = 0;
/**
* @psalm-var self::STATUS_*
*/
protected int $status = self::STATUS_UNKNOWN;
/**
* @psalm-param S $type
* @param string $type
* @param string $input
* @param string $appId
* @param string|null $userId
* @param string $identifier An arbitrary identifier for this task. max length: 255 chars
* @since 27.1.0
*/
final public function __construct(
protected string $type,
protected string $input,
protected string $appId,
protected ?string $userId,
protected string $identifier = '',
) {
}
/**
* @psalm-param P $provider
* @param IProvider $provider
* @return string
* @since 27.1.0
*/
public function visitProvider(IProvider $provider): string {
if ($this->canUseProvider($provider)) {
return $provider->process($this->getInput());
} else {
throw new \RuntimeException('Task of type ' . $this->getType() . ' cannot visit provider with task type ' . $provider->getTaskType());
}
}
/**
* @psalm-param P $provider
* @param IProvider $provider
* @return bool
* @since 27.1.0
*/
public function canUseProvider(IProvider $provider): bool {
return $provider->getTaskType() === $this->getType();
}
/**
* @psalm-return S
* @since 27.1.0
*/
final public function getType(): string {
return $this->type;
}
/**
* @return string|null
* @since 27.1.0
*/
final public function getOutput(): ?string {
return $this->output;
}
/**
* @param string|null $output
* @since 27.1.0
*/
final public function setOutput(?string $output): void {
$this->output = $output;
}
/**
* @psalm-return self::STATUS_*
* @since 27.1.0
*/
final public function getStatus(): int {
return $this->status;
}
/**
* @psalm-param self::STATUS_* $status
* @since 27.1.0
*/
final public function setStatus(int $status): void {
$this->status = $status;
}
/**
* @return int|null
* @since 27.1.0
*/
final public function getId(): ?int {
return $this->id;
}
/**
* @param int|null $id
* @since 27.1.0
*/
final public function setId(?int $id): void {
$this->id = $id;
}
/**
* @return string
* @since 27.1.0
*/
final public function getInput(): string {
return $this->input;
}
/**
* @return string
* @since 27.1.0
*/
final public function getAppId(): string {
return $this->appId;
}
/**
* @return string
* @since 27.1.0
*/
final public function getIdentifier(): string {
return $this->identifier;
}
/**
* @return string|null
* @since 27.1.0
*/
final public function getUserId(): ?string {
return $this->userId;
}
/**
* @psalm-return array{id: ?int, type: S, status: 0|1|2|3|4, userId: ?string, appId: string, input: string, output: ?string, identifier: string}
* @since 27.1.0
*/
public function jsonSerialize(): array {
return [
'id' => $this->getId(),
'type' => $this->getType(),
'status' => $this->getStatus(),
'userId' => $this->getUserId(),
'appId' => $this->getAppId(),
'input' => $this->getInput(),
'output' => $this->getOutput(),
'identifier' => $this->getIdentifier(),
];
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCP\TextProcessing;
use OCP\IL10N;
use OCP\L10N\IFactory;
/**
* This is the text processing task type for topics extraction
* @since 27.1.0
*/
class TopicsTaskType implements ITaskType {
private IL10N $l;
/**
* Constructor for TopicsTaskType
*
* @param IFactory $l10nFactory
* @since 27.1.0
*/
public function __construct(
IFactory $l10nFactory,
) {
$this->l = $l10nFactory->get('core');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getName(): string {
return $this->l->t('Extract topics');
}
/**
* @inheritDoc
* @since 27.1.0
*/
public function getDescription(): string {
return $this->l->t('Extracts topics from a text and outputs them separated by commas.');
}
}

View file

@ -38,6 +38,12 @@ interface ITranslationManager {
*/
public function hasProviders(): bool;
/**
* @return ITranslationProvider[]
* @since 27.1.0
*/
public function getProviders(): array;
/**
* @since 26.0.0
*/

View file

@ -141,7 +141,7 @@ class DummyJobList extends \OC\BackgroundJob\JobList {
}
public function hasReservedJob(?string $className = null): bool {
return $this->reserved[$className ?? ''];
return isset($this->reserved[$className ?? '']) && $this->reserved[$className ?? ''];
}
public function setHasReservedJob(?string $className, bool $hasReserved): void {

View file

@ -0,0 +1,345 @@
<?php
/**
* Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace Test\TextProcessing;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\RegistrationContext;
use OC\AppFramework\Bootstrap\ServiceRegistration;
use OC\EventDispatcher\EventDispatcher;
use OC\TextProcessing\Db\Task as DbTask;
use OC\TextProcessing\Db\TaskMapper;
use OC\TextProcessing\Manager;
use OC\TextProcessing\RemoveOldTasksBackgroundJob;
use OC\TextProcessing\TaskBackgroundJob;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Common\Exception\NotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IServerContainer;
use OCP\TextProcessing\Events\TaskFailedEvent;
use OCP\TextProcessing\Events\TaskSuccessfulEvent;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\IProvider;
use OCP\TextProcessing\SummaryTaskType;
use OCP\PreConditionNotMetException;
use OCP\TextProcessing\Task;
use OCP\TextProcessing\TopicsTaskType;
use PHPUnit\Framework\Constraint\IsInstanceOf;
use Psr\Log\LoggerInterface;
use Test\BackgroundJob\DummyJobList;
class SuccessfulSummaryProvider implements IProvider {
public bool $ran = false;
public function getName(): string {
return 'TEST Vanilla LLM Provider';
}
public function process(string $prompt): string {
$this->ran = true;
return $prompt . ' Summarize';
}
public function getTaskType(): string {
return SummaryTaskType::class;
}
}
class FailingSummaryProvider implements IProvider {
public bool $ran = false;
public function getName(): string {
return 'TEST Vanilla LLM Provider';
}
public function process(string $prompt): string {
$this->ran = true;
throw new \Exception('ERROR');
}
public function getTaskType(): string {
return SummaryTaskType::class;
}
}
class FreePromptProvider implements IProvider {
public bool $ran = false;
public function getName(): string {
return 'TEST Free Prompt Provider';
}
public function process(string $prompt): string {
$this->ran = true;
return $prompt . ' Free Prompt';
}
public function getTaskType(): string {
return FreePromptTaskType::class;
}
}
class TextProcessingTest extends \Test\TestCase {
private IManager $manager;
private Coordinator $coordinator;
protected function setUp(): void {
parent::setUp();
$this->providers = [
SuccessfulSummaryProvider::class => new SuccessfulSummaryProvider(),
FailingSummaryProvider::class => new FailingSummaryProvider(),
FreePromptProvider::class => new FreePromptProvider(),
];
$this->serverContainer = $this->createMock(IServerContainer::class);
$this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
return $this->providers[$class];
});
$this->eventDispatcher = new EventDispatcher(
new \Symfony\Component\EventDispatcher\EventDispatcher(),
$this->serverContainer,
\OC::$server->get(LoggerInterface::class),
);
$this->registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator = $this->createMock(Coordinator::class);
$this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
$this->currentTime = new \DateTimeImmutable('now');
$this->taskMapper = $this->createMock(TaskMapper::class);
$this->tasksDb = [];
$this->taskMapper
->expects($this->any())
->method('insert')
->willReturnCallback(function (DbTask $task) {
$task->setId(count($this->tasksDb) ? max(array_keys($this->tasksDb)) : 1);
$task->setLastUpdated($this->currentTime->getTimestamp());
$this->tasksDb[$task->getId()] = $task->toRow();
return $task;
});
$this->taskMapper
->expects($this->any())
->method('update')
->willReturnCallback(function (DbTask $task) {
$task->setLastUpdated($this->currentTime->getTimestamp());
$this->tasksDb[$task->getId()] = $task->toRow();
return $task;
});
$this->taskMapper
->expects($this->any())
->method('find')
->willReturnCallback(function (int $id) {
if (!isset($this->tasksDb[$id])) {
throw new DoesNotExistException('Could not find it');
}
return DbTask::fromRow($this->tasksDb[$id]);
});
$this->taskMapper
->expects($this->any())
->method('deleteOlderThan')
->willReturnCallback(function (int $timeout) {
$this->tasksDb = array_filter($this->tasksDb, function (array $task) use ($timeout) {
return $task['last_updated'] >= $this->currentTime->getTimestamp() - $timeout;
});
});
$this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
$this->jobList->expects($this->any())->method('add')->willReturnCallback(function () {
});
$config = $this->createMock(IConfig::class);
$config->method('getAppValue')
->with('core', 'ai.textprocessing_provider_preferences', '')
->willReturn('');
$this->manager = new Manager(
$this->serverContainer,
$this->coordinator,
\OC::$server->get(LoggerInterface::class),
$this->jobList,
$this->taskMapper,
$config
);
}
public function testShouldNotHaveAnyProviders() {
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
$this->assertCount(0, $this->manager->getAvailableTaskTypes());
$this->assertFalse($this->manager->hasProviders());
$this->expectException(PreConditionNotMetException::class);
$this->manager->runTask(new \OCP\TextProcessing\Task(FreePromptTaskType::class, 'Hello', 'test', null));
}
public function testProviderShouldBeRegisteredAndRun() {
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSummaryProvider::class)
]);
$this->assertCount(1, $this->manager->getAvailableTaskTypes());
$this->assertTrue($this->manager->hasProviders());
$this->assertEquals('Hello Summarize', $this->manager->runTask(new Task(SummaryTaskType::class, 'Hello', 'test', null)));
// Summaries are not implemented by the vanilla provider, only free prompt
$this->expectException(PreConditionNotMetException::class);
$this->manager->runTask(new Task(FreePromptTaskType::class, 'Hello', 'test', null));
}
public function testProviderShouldBeRegisteredAndScheduled() {
// register provider
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSummaryProvider::class)
]);
$this->assertCount(1, $this->manager->getAvailableTaskTypes());
$this->assertTrue($this->manager->hasProviders());
// create task object
$task = new Task(SummaryTaskType::class, 'Hello', 'test', null);
$this->assertNull($task->getId());
$this->assertNull($task->getOutput());
// schedule works
$this->assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
$this->manager->scheduleTask($task);
// Task object is up-to-date
$this->assertNotNull($task->getId());
$this->assertNull($task->getOutput());
$this->assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
// Task object retrieved from db is up-to-date
$task2 = $this->manager->getTask($task->getId());
$this->assertEquals($task->getId(), $task2->getId());
$this->assertEquals('Hello', $task2->getInput());
$this->assertNull($task2->getOutput());
$this->assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
// run background job
$bgJob = new TaskBackgroundJob(
\OC::$server->get(ITimeFactory::class),
$this->manager,
$this->eventDispatcher,
);
$bgJob->setArgument(['taskId' => $task->getId()]);
$bgJob->start($this->jobList);
$provider = $this->providers[SuccessfulSummaryProvider::class];
$this->assertTrue($provider->ran);
// Task object retrieved from db is up-to-date
$task3 = $this->manager->getTask($task->getId());
$this->assertEquals($task->getId(), $task3->getId());
$this->assertEquals('Hello', $task3->getInput());
$this->assertEquals('Hello Summarize', $task3->getOutput());
$this->assertEquals(Task::STATUS_SUCCESSFUL, $task3->getStatus());
}
public function testMultipleProvidersShouldBeRegisteredAndRunCorrectly() {
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSummaryProvider::class),
new ServiceRegistration('test', FreePromptProvider::class),
]);
$this->assertCount(2, $this->manager->getAvailableTaskTypes());
$this->assertTrue($this->manager->hasProviders());
// Try free prompt again
$this->assertEquals('Hello Free Prompt', $this->manager->runTask(new Task(FreePromptTaskType::class, 'Hello', 'test', null)));
// Try summary task
$this->assertEquals('Hello Summarize', $this->manager->runTask(new Task(SummaryTaskType::class, 'Hello', 'test', null)));
// Topics are not implemented by both the vanilla provider and the full provider
$this->expectException(PreConditionNotMetException::class);
$this->manager->runTask(new Task(TopicsTaskType::class, 'Hello', 'test', null));
}
public function testNonexistentTask() {
$this->expectException(NotFoundException::class);
$this->manager->getTask(2147483646);
}
public function testTaskFailure() {
// register provider
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
new ServiceRegistration('test', FailingSummaryProvider::class),
]);
$this->assertCount(1, $this->manager->getAvailableTaskTypes());
$this->assertTrue($this->manager->hasProviders());
// create task object
$task = new Task(SummaryTaskType::class, 'Hello', 'test', null);
$this->assertNull($task->getId());
$this->assertNull($task->getOutput());
// schedule works
$this->assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
$this->manager->scheduleTask($task);
// Task object is up-to-date
$this->assertNotNull($task->getId());
$this->assertNull($task->getOutput());
$this->assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
// Task object retrieved from db is up-to-date
$task2 = $this->manager->getTask($task->getId());
$this->assertEquals($task->getId(), $task2->getId());
$this->assertEquals('Hello', $task2->getInput());
$this->assertNull($task2->getOutput());
$this->assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
// run background job
$bgJob = new TaskBackgroundJob(
\OC::$server->get(ITimeFactory::class),
$this->manager,
$this->eventDispatcher,
);
$bgJob->setArgument(['taskId' => $task->getId()]);
$bgJob->start($this->jobList);
$provider = $this->providers[FailingSummaryProvider::class];
$this->assertTrue($provider->ran);
// Task object retrieved from db is up-to-date
$task3 = $this->manager->getTask($task->getId());
$this->assertEquals($task->getId(), $task3->getId());
$this->assertEquals('Hello', $task3->getInput());
$this->assertNull($task3->getOutput());
$this->assertEquals(Task::STATUS_FAILED, $task3->getStatus());
}
public function testOldTasksShouldBeCleanedUp() {
$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
new ServiceRegistration('test', SuccessfulSummaryProvider::class)
]);
$this->assertCount(1, $this->manager->getAvailableTaskTypes());
$this->assertTrue($this->manager->hasProviders());
$task = new Task(SummaryTaskType::class, 'Hello', 'test', null);
$this->assertEquals('Hello Summarize', $this->manager->runTask($task));
$this->currentTime = $this->currentTime->add(new \DateInterval('P1Y'));
// run background job
$bgJob = new RemoveOldTasksBackgroundJob(
\OC::$server->get(ITimeFactory::class),
$this->taskMapper,
\OC::$server->get(LoggerInterface::class),
);
$bgJob->setArgument([]);
$bgJob->start($this->jobList);
$this->expectException(NotFoundException::class);
$this->manager->getTask($task->getId());
}
}

View file

@ -30,7 +30,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
$OC_Version = [27, 0, 2, 1];
$OC_Version = [27, 0, 2, 2];
// The human-readable string
$OC_VersionString = '27.0.2';

View file

@ -79,6 +79,7 @@ module.exports = {
apps: path.join(__dirname, 'apps/settings/src', 'apps.js'),
'legacy-admin': path.join(__dirname, 'apps/settings/src', 'admin.js'),
'vue-settings-admin-basic-settings': path.join(__dirname, 'apps/settings/src', 'main-admin-basic-settings.js'),
'vue-settings-admin-ai': path.join(__dirname, 'apps/settings/src', 'main-admin-ai.js'),
'vue-settings-admin-delegation': path.join(__dirname, 'apps/settings/src', 'main-admin-delegation.js'),
'vue-settings-admin-security': path.join(__dirname, 'apps/settings/src', 'main-admin-security.js'),
'vue-settings-apps-users-management': path.join(__dirname, 'apps/settings/src', 'main-apps-users-management.js'),