Skip to content

Commit

Permalink
add thread summary ui
Browse files Browse the repository at this point in the history
Signed-off-by: hamza221 <[email protected]>
  • Loading branch information
hamza221 committed Aug 8, 2023
1 parent cc86f36 commit 6528bad
Show file tree
Hide file tree
Showing 16 changed files with 394 additions and 10 deletions.
15 changes: 15 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,16 @@
'url' => '/api/settings/allownewaccounts',
'verb' => 'POST'
],
[
'name' => 'settings#setEnabledThreadSummary',
'url' => '/api/settings/threadsummary',
'verb' => 'PUT'
],
[
'name' => 'settings#isLlmConfigured',
'url' => '/api/settings/llmconfigured',
'verb' => 'GET'
],
[
'name' => 'trusted_senders#setTrusted',
'url' => '/api/trustedsenders/{email}',
Expand Down Expand Up @@ -360,6 +370,11 @@
'url' => '/api/thread/{id}',
'verb' => 'POST'
],
[
'name' => 'thread#summarize',
'url' => '/api/thread/{id}/summary',
'verb' => 'GET'
],
[
'name' => 'outbox#send',
'url' => '/api/outbox/{id}',
Expand Down
5 changes: 5 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ public function index(): TemplateResponse {
$this->config->getAppValue('mail', 'allow_new_mail_accounts', 'yes') === 'yes'
);

$this->initialStateService->provideInitialState(
'enabled_thread_summary',
$this->config->getAppValue('mail', 'enabled_thread_summary', 'no') === 'yes'
);

$this->initialStateService->provideInitialState(
'smime-certificates',
array_map(
Expand Down
22 changes: 21 additions & 1 deletion lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,29 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\SummaryTaskType;
use Psr\Container\ContainerInterface;

use function array_merge;

class SettingsController extends Controller {
private ProvisioningManager $provisioningManager;
private AntiSpamService $antiSpamService;
private ContainerInterface $container;

private IConfig $config;

public function __construct(IRequest $request,
ProvisioningManager $provisioningManager,
AntiSpamService $antiSpamService,
IConfig $config) {
IConfig $config,
ContainerInterface $container) {
parent::__construct(Application::APP_ID, $request);
$this->provisioningManager = $provisioningManager;
$this->antiSpamService = $antiSpamService;
$this->config = $config;
$this->container = $container;
}

public function index(): JSONResponse {
Expand Down Expand Up @@ -119,4 +126,17 @@ public function deleteAntiSpamEmail(): JSONResponse {
public function setAllowNewMailAccounts(bool $allowed) {
$this->config->setAppValue('mail', 'allow_new_mail_accounts', $allowed ? 'yes' : 'no');
}

public function setEnabledThreadSummary(bool $enabled) {
$this->config->setAppValue('mail', 'enabled_thread_summary', $enabled ? 'yes' : 'no');
}

public function isLlmConfigured() {
try {
$manager = $this->container->get(IManager::class);
} catch (\Throwable $e) {
return new JSONResponse(['data' => false]);
}
return new JSONResponse(['data' => in_array(SummaryTaskType::class, $manager->getAvailableTaskTypes(), true)]);
}
}
48 changes: 46 additions & 2 deletions lib/Controller/ThreadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,35 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\AiIntegrationsService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

class ThreadController extends Controller {
private string $currentUserId;
private AccountService $accountService;
private IMailManager $mailManager;
private AiIntegrationsService $aiIntergrationsService;
private LoggerInterface $logger;


public function __construct(string $appName,
IRequest $request,
string $UserId,
AccountService $accountService,
IMailManager $mailManager) {
IMailManager $mailManager,
AiIntegrationsService $aiIntergrationsService,
LoggerInterface $logger) {
parent::__construct($appName, $request);

$this->currentUserId = $UserId;
$this->accountService = $accountService;
$this->mailManager = $mailManager;
$this->aiIntergrationsService = $aiIntergrationsService;
$this->logger = $logger;
}

/**
Expand Down Expand Up @@ -111,4 +119,40 @@ public function delete(int $id): JSONResponse {

return new JSONResponse();
}

/**
* @NoAdminRequired
*
* @param int $id
*
* @return JSONResponse
*/
public function summarize(int $id): JSONResponse {
try {
$message = $this->mailManager->getMessage($this->currentUserId, $id);
$mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId());
$account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId());
} catch (DoesNotExistException $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
if (empty($message->getThreadRootId())) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
$thread = $this->mailManager->getThread($account, $message->getThreadRootId());
try {
$summary = $this->aiIntergrationsService->summarizeThread(
$message->getThreadRootId(),
$thread,
$this->currentUserId,
);
} catch (\Throwable $e) {
$this->logger->error('Summarizing thread failed: ' . $e->getMessage(), [
'exception' => $e,
]);
return new JSONResponse([], Http::STATUS_NO_CONTENT);
}

return new JSONResponse(['data' => $summary]);
}

}
71 changes: 71 additions & 0 deletions lib/Service/AiIntegrationsService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

/**
* @author Hamza Mahjoubi <[email protected]>
*
* Mail
*
* 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\Mail\Service;

use OCA\Mail\Exception\ServiceException;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\SummaryTaskType;
use OCP\TextProcessing\Task;
use Psr\Container\ContainerInterface;
use function array_map;

class AiIntegrationsService {

/** @var ContainerInterface */
private ContainerInterface $container;


public function __construct(ContainerInterface $container) {
$this->container = $container;
}
/**
* @param string $threadId
* @param array $messages
* @param string $currentUserId
*
* @return null|string
*
* @throws ServiceException
*/
public function summarizeThread(string $threadId, array $messages, string $currentUserId): null|string {
try {
$manager = $this->container->get(IManager::class);
} catch (\Throwable $e) {
throw new ServiceException('Text processing is not available in your current Nextcloud version', $e);
}
if(in_array(SummaryTaskType::class, $manager->getAvailableTaskTypes(), true)) {
$messagesBodies = array_map(function ($message) {
return $message->getPreviewText();
}, $messages);

$taskPrompt = implode("\n", $messagesBodies);
$summaryTask = new Task(SummaryTaskType::class, $taskPrompt, "mail", $currentUserId, $threadId);
$manager->runTask($summaryTask);

return $summaryTask->getOutput();
} else {
throw new ServiceException('No language model available for summary');
}
}
}
7 changes: 7 additions & 0 deletions lib/Settings/AdminSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ public function getForm() {
'allow_new_mail_accounts',
$this->config->getAppValue('mail', 'allow_new_mail_accounts', 'yes') === 'yes'
);

$this->initialStateService->provideInitialState(
Application::APP_ID,
'enabled_thread_summary',
$this->config->getAppValue('mail', 'enabled_thread_summary', 'no') === 'yes'
);

$this->initialStateService->provideLazyInitialState(
Application::APP_ID,
'ldap_aliases_integration',
Expand Down
4 changes: 4 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
<referencedClass name="Symfony\Component\Console\Input\InputInterface" />
<referencedClass name="Symfony\Component\Console\Input\InputOption" />
<referencedClass name="Symfony\Component\Console\Output\OutputInterface" />
<referencedClass name="OCP\TextProcessing\IManager" />
<referencedClass name="OCP\TextProcessing\SummaryTaskType" />
<referencedClass name="OCP\TextProcessing\Task" />
<referencedClass name="OCP\TextProcessing\SummaryTaskType" />
</errorLevel>
</UndefinedClass>
<UndefinedDocblockClass>
Expand Down
2 changes: 1 addition & 1 deletion src/components/MailboxThread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
</template>
</AppContentList>
</div>
<Thread v-if="showThread" @delete="deleteMessage" />
<Thread v-if="showThread" :account-id="account.accountId" @delete="deleteMessage" />
<NoMessageSelected v-else-if="hasEnvelopes && !isMobile" />
</AppContent>
</template>
Expand Down
34 changes: 34 additions & 0 deletions src/components/Thread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
</div>
</div>
</div>
<ThreadSummary v-if="showSummaryBox" :loading="summaryLoading" :summary="summaryText" />
<ThreadEnvelope v-for="env in thread"
:key="env.databaseId"
:envelope="env"
Expand All @@ -54,37 +55,52 @@

<script>
import { NcAppContentDetails as AppContentDetails, NcPopover as Popover } from '@nextcloud/vue'
import { showError } from '@nextcloud/dialogs'
import { prop, uniqBy } from 'ramda'
import debounce from 'lodash/fp/debounce'
import { loadState } from '@nextcloud/initial-state'
import { summarizeThread } from '../service/AiIntergrationsService'
import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory'
import Loading from './Loading'
import logger from '../logger'
import Error from './Error'
import RecipientBubble from './RecipientBubble'
import ThreadEnvelope from './ThreadEnvelope'
import ThreadSummary from './ThreadSummary'
export default {
name: 'Thread',
components: {
RecipientBubble,
ThreadSummary,
AppContentDetails,
Error,
Loading,
ThreadEnvelope,
Popover,
},
props: {
accountId: {
type: Number,
required: true,
},
},
data() {
return {
summaryLoading: false,
loading: true,
message: undefined,
errorMessage: '',
error: undefined,
expandedThreads: [],
participantsToDisplay: 999,
resizeDebounced: debounce(500, this.updateParticipantsToDisplay),
enabledThreadSummary: loadState('mail', 'enabled_thread_summary', false),
summaryText: '',
summaryError: false,
}
},
Expand Down Expand Up @@ -134,6 +150,9 @@ export default {
}
return thread[0].subject || this.t('mail', 'No subject')
},
showSummaryBox() {
return this.thread.length > 2 && this.enabledThreadSummary && !this.summaryError
},
},
watch: {
$route(to, from) {
Expand All @@ -158,6 +177,20 @@ export default {
window.removeEventListener('resize', this.resizeDebounced)
},
methods: {
async updateSummary() {
if (this.thread.length < 2 || !this.enabledThreadSummary) return
this.summaryLoading = true
try {
this.summaryText = await summarizeThread(this.thread[0].databaseId)
} catch (error) {
this.summaryError = true
showError(t('mail', 'Summarizing thread failed.'))
logger.error('Summarizing thread failed', { error })
} finally {
this.summaryLoading = false
}
},
updateParticipantsToDisplay() {
// Wait until everything is in place
if (!this.$refs.avatarHeader || !this.threadParticipants) {
Expand Down Expand Up @@ -226,6 +259,7 @@ export default {
this.error = undefined
await this.fetchThread()
this.updateParticipantsToDisplay()
this.updateSummary()
},
async fetchThread() {
Expand Down
Loading

0 comments on commit 6528bad

Please sign in to comment.