Skip to content

Commit

Permalink
feat: ai message summary
Browse files Browse the repository at this point in the history
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
  • Loading branch information
SebastianKrupinski committed Dec 9, 2024
1 parent a3ac382 commit 638daf4
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 1 deletion.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The rating depends on the installed text processing backend. See [the rating ove
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]></description>
<version>4.2.0-alpha.0</version>
<version>4.2.1-alpha.0</version>
<licence>agpl</licence>
<author homepage="https://github.com/ChristophWurst">Christoph Wurst</author>
<author homepage="https://github.com/GretaD">GretaD</author>
Expand Down
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@
use OCA\Mail\Listener\MoveJunkListener;
use OCA\Mail\Listener\NewMessageClassificationListener;
use OCA\Mail\Listener\NewMessagesNotifier;
use OCA\Mail\Listener\NewMessagesSummarizeListener;
use OCA\Mail\Listener\OauthTokenRefreshListener;
use OCA\Mail\Listener\OptionalIndicesListener;
use OCA\Mail\Listener\OutOfOfficeListener;
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\TaskProcessingListener;
use OCA\Mail\Listener\UserDeletedListener;
use OCA\Mail\Notification\Notifier;
use OCA\Mail\Provider\MailProvider;
Expand All @@ -72,6 +74,7 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\IServerContainer;
use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeEndedEvent;
Expand Down Expand Up @@ -133,6 +136,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, MessageKnownSinceListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, NewMessagesNotifier::class);
$context->registerEventListener(NewMessagesSynchronized::class, NewMessagesSummarizeListener::class);

Check failure on line 139 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidArgument

lib/AppInfo/Application.php:139:67: InvalidArgument: Argument 2 of OCP\AppFramework\Bootstrap\IRegistrationContext::registerEventListener expects class-string<OCP\EventDispatcher\IEventListener<OCP\EventDispatcher\Event>>, but OCA\Mail\Listener\NewMessagesSummarizeListener::class provided (see https://psalm.dev/004)

Check warning on line 139 in lib/AppInfo/Application.php

View check run for this annotation

Codecov / codecov/patch

lib/AppInfo/Application.php#L139

Added line #L139 was not covered by tests
$context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, FollowUpClassifierListener::class);
Expand All @@ -141,6 +145,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(TaskSuccessfulEvent::class, TaskProcessingListener::class);

Check failure on line 148 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidArgument

lib/AppInfo/Application.php:148:63: InvalidArgument: Argument 2 of OCP\AppFramework\Bootstrap\IRegistrationContext::registerEventListener expects class-string<OCP\EventDispatcher\IEventListener<OCP\EventDispatcher\Event>>, but OCA\Mail\Listener\TaskProcessingListener::class provided (see https://psalm.dev/004)

Check warning on line 148 in lib/AppInfo/Application.php

View check run for this annotation

Codecov / codecov/patch

lib/AppInfo/Application.php#L148

Added line #L148 was not covered by tests

$context->registerMiddleWare(ErrorMiddleware::class);
$context->registerMiddleWare(ProvisioningMiddleware::class);
Expand Down
4 changes: 4 additions & 0 deletions lib/Db/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
* @method bool|null getFlagMdnsent()
* @method void setPreviewText(?string $subject)
* @method null|string getPreviewText()
* @method void setSummary(?string $summary)
* @method null|string getSummary()
* @method void setUpdatedAt(int $time)
* @method int getUpdatedAt()
* @method bool isImipMessage()
Expand Down Expand Up @@ -108,6 +110,7 @@ class Message extends Entity implements JsonSerializable {
protected $flagImportant = false;
protected $flagMdnsent;
protected $previewText;
protected $summary;
protected $imipMessage = false;
protected $imipProcessed = false;
protected $imipError = false;
Expand Down Expand Up @@ -325,6 +328,7 @@ static function (Tag $tag) {
'threadRootId' => $this->getThreadRootId(),
'imipMessage' => $this->isImipMessage(),
'previewText' => $this->getPreviewText(),
'summary' => $this->getSummary(),

Check warning on line 331 in lib/Db/Message.php

View check run for this annotation

Codecov / codecov/patch

lib/Db/Message.php#L331

Added line #L331 was not covered by tests
'encrypted' => ($this->isEncrypted() === true),
'mentionsMe' => $this->getMentionsMe(),
];
Expand Down
50 changes: 50 additions & 0 deletions lib/Listener/NewMessagesSummarizeListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Listener;

use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Events\NewMessagesSynchronized;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Service\AiIntegrations\AiIntegrationsService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<Event|NewMessagesSummarizeListener>
*/
class NewMessagesSummarizeListener implements IEventListener {

Check failure on line 24 in lib/Listener/NewMessagesSummarizeListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidTemplateParam

lib/Listener/NewMessagesSummarizeListener.php:24:47: InvalidTemplateParam: Extended template param T expects type OCP\EventDispatcher\Event, type OCA\Mail\Listener\NewMessagesSummarizeListener|OCP\EventDispatcher\Event given (see https://psalm.dev/183)

public function __construct(
protected LoggerInterface $logger,
protected IMAPClientFactory $imapFactory,
protected AiIntegrationsService $aiService,
protected IMailManager $mailManager
) { }

public function handle(Event $event): void {

Check failure on line 33 in lib/Listener/NewMessagesSummarizeListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

MoreSpecificImplementedParamType

lib/Listener/NewMessagesSummarizeListener.php:33:31: MoreSpecificImplementedParamType: Argument 1 of OCA\Mail\Listener\NewMessagesSummarizeListener::handle has the more specific type 'OCP\EventDispatcher\Event', expecting 'OCA\Mail\Listener\NewMessagesSummarizeListener|OCP\EventDispatcher\Event' as defined by OCP\EventDispatcher\IEventListener::handle (see https://psalm.dev/140)

if (!($event instanceof NewMessagesSynchronized)) {
return;

Check warning on line 36 in lib/Listener/NewMessagesSummarizeListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/NewMessagesSummarizeListener.php#L36

Added line #L36 was not covered by tests
}

try {
$this->aiService->summarizeMessages(
$event->getAccount(),
$event->getMessages(),
);
} catch (ServiceException $e) {
$this->logger->error('Could not classify incoming message importance: ' . $e->getMessage(), [
'exception' => $e,
]);
}
}
}
67 changes: 67 additions & 0 deletions lib/Listener/TaskProcessingListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Listener;

use OCA\Mail\AppInfo\Application;
use OCA\Mail\Db\MessageMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<Event|NewMessagesSummarizeListener>
*/
class TaskProcessingListener implements IEventListener {

Check failure on line 23 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidTemplateParam

lib/Listener/TaskProcessingListener.php:23:41: InvalidTemplateParam: Extended template param T expects type OCP\EventDispatcher\Event, type OCA\Mail\Listener\NewMessagesSummarizeListener|OCP\EventDispatcher\Event given (see https://psalm.dev/183)

public function __construct(

Check warning on line 25 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L25

Added line #L25 was not covered by tests
protected LoggerInterface $logger,
protected MessageMapper $messageStore,
) { }

Check warning on line 28 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L28

Added line #L28 was not covered by tests

public function handle(Event $event): void {

Check failure on line 30 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

MoreSpecificImplementedParamType

lib/Listener/TaskProcessingListener.php:30:31: MoreSpecificImplementedParamType: Argument 1 of OCA\Mail\Listener\TaskProcessingListener::handle has the more specific type 'OCP\EventDispatcher\Event', expecting 'OCA\Mail\Listener\NewMessagesSummarizeListener|OCP\EventDispatcher\Event' as defined by OCP\EventDispatcher\IEventListener::handle (see https://psalm.dev/140)

Check warning on line 30 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L30

Added line #L30 was not covered by tests

if (!($event instanceof TaskSuccessfulEvent)) {
return;

Check warning on line 33 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L32-L33

Added lines #L32 - L33 were not covered by tests
}

$task = $event->getTask();

Check warning on line 36 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L36

Added line #L36 was not covered by tests

if ($task->getAppId() !== Application::APP_ID) {
return;

Check warning on line 39 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L38-L39

Added lines #L38 - L39 were not covered by tests
}

if ($task->getTaskTypeId() !== TextToTextSummary::ID) {
return;

Check warning on line 43 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L42-L43

Added lines #L42 - L43 were not covered by tests
}

list($type, $id) = explode(':', $task->getCustomId());

Check failure on line 46 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyNullArgument

lib/Listener/TaskProcessingListener.php:46:35: PossiblyNullArgument: Argument 2 of explode cannot be null, possibly null value provided (see https://psalm.dev/078)
$userId = $task->getUserId();
$summary = $task->getOutput()['output'];

Check failure on line 48 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyNullArrayAccess

lib/Listener/TaskProcessingListener.php:48:14: PossiblyNullArrayAccess: Cannot access array value on possibly null variable of type array<array-key, list<numeric|string>|numeric|string>|null (see https://psalm.dev/079)

Check warning on line 48 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L46-L48

Added lines #L46 - L48 were not covered by tests

match ($type) {
'message' => $this->handleMessageSummary($userId, (int)$id, $summary),

Check failure on line 51 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyNullArgument

lib/Listener/TaskProcessingListener.php:51:45: PossiblyNullArgument: Argument 1 of OCA\Mail\Listener\TaskProcessingListener::handleMessageSummary cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 51 in lib/Listener/TaskProcessingListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

PossiblyInvalidArgument

lib/Listener/TaskProcessingListener.php:51:64: PossiblyInvalidArgument: Argument 3 of OCA\Mail\Listener\TaskProcessingListener::handleMessageSummary expects string, but possibly different type list<numeric|string>|null|numeric|string provided (see https://psalm.dev/092)
};

Check warning on line 52 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L50-L52

Added lines #L50 - L52 were not covered by tests

}

protected function handleMessageSummary(string $userId, int $id, string $summary) {
$messages = $this->messageStore->findByIds($userId, [$id], '');

Check warning on line 57 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L56-L57

Added lines #L56 - L57 were not covered by tests

if (count($messages) !== 1) {
return;

Check warning on line 60 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L59-L60

Added lines #L59 - L60 were not covered by tests
}

$message = $messages[0];
$message->setSummary($summary);
$this->messageStore->update($message);

Check warning on line 65 in lib/Listener/TaskProcessingListener.php

View check run for this annotation

Codecov / codecov/patch

lib/Listener/TaskProcessingListener.php#L63-L65

Added lines #L63 - L65 were not covered by tests
}
}
38 changes: 38 additions & 0 deletions lib/Migration/Version4100Date20241209000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version4100Date20241209000000 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();

Check warning on line 27 in lib/Migration/Version4100Date20241209000000.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version4100Date20241209000000.php#L26-L27

Added lines #L26 - L27 were not covered by tests

$outboxTable = $schema->getTable('mail_messages');
if (!$outboxTable->hasColumn('summary')) {
$outboxTable->addColumn('summary', Types::STRING, [
'length' => 1024,
'notnull' => false,
]);

Check warning on line 34 in lib/Migration/Version4100Date20241209000000.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version4100Date20241209000000.php#L29-L34

Added lines #L29 - L34 were not covered by tests
}
return $schema;

Check warning on line 36 in lib/Migration/Version4100Date20241209000000.php

View check run for this annotation

Codecov / codecov/patch

lib/Migration/Version4100Date20241209000000.php#L36

Added line #L36 was not covered by tests
}
}
73 changes: 73 additions & 0 deletions lib/Service/AiIntegrations/AiIntegrationsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Mail\Service\AiIntegrations;

use Horde_Imap_Client_Socket;
use JsonException;
use OCA\Mail\Account;
use OCA\Mail\AppInfo\Application;
Expand All @@ -20,6 +21,10 @@
use OCA\Mail\Model\EventData;
use OCA\Mail\Model\IMAPMessage;
use OCP\IConfig;
use OCP\TaskProcessing\IManager as TaskProcessingManager;
use OCP\TaskProcessing\Task as TaskProcessingTask;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use OCP\TaskProcessing\Exception\Exception as TaskProcessingException;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\SummaryTaskType;
Expand Down Expand Up @@ -61,6 +66,74 @@ public function __construct(ContainerInterface $container, Cache $cache, IMAPCli
$this->mailManager = $mailManager;
$this->config = $config;
}

/**
* generates summary for each message
*
* @param Account $account
* @param array<Message> $messages
* @param Horde_Imap_Client_Socket $client
*
* @return null|string
*
* @throws ServiceException
*/
public function summarizeMessages(Account $account, array $messages, Horde_Imap_Client_Socket $client = null): void {
try {
$manager = $this->container->get(TaskProcessingManager::class);
} catch (\Throwable $e) {
throw new ServiceException('Task processing is not available', 0, $e);

Check warning on line 85 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L84-L85

Added lines #L84 - L85 were not covered by tests
}
try {
$manager->getPreferredProvider(TextToTextSummary::ID);
} catch (TaskProcessingException $e) {
throw new ServiceException('No text summary provider available');
}
if (!$client) {
$client = $this->clientFactory->getClient($account);

Check warning on line 93 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L92-L93

Added lines #L92 - L93 were not covered by tests
}
try {
foreach ($messages as $entry) {

Check warning on line 96 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L96

Added line #L96 was not covered by tests

if (!empty($entry->getSummary())) {
continue;

Check warning on line 99 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L98-L99

Added lines #L98 - L99 were not covered by tests
}
// retrieve full message from server
$userId = $account->getUserId();
$mailboxId = $entry->getMailboxId();
$messageLocalId = $entry->getId();
$messageRemoteId = $entry->getUid();
$mailbox = $this->mailManager->getMailbox($userId, $mailboxId);
$message = $this->mailManager->getImapMessage(
$client,
$account,
$mailbox,
$messageRemoteId,
true
);
$messageBody = $message->getPlainBody();

Check warning on line 114 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L102-L114

Added lines #L102 - L114 were not covered by tests
// construct prompt and task
$prompt = "You are tasked with formulating a helpful summary of a email message. \r\n" .
"The summary should be less than 1024 characters. \r\n" .
"Here is the ***E-MAIL*** for which you must generate a helpful summary: \r\n" .
"***START_OF_E-MAIL***\r\n$messageBody\r\n***END_OF_E-MAIL***\r\n";
$task = new TaskProcessingTask(
TextToTextSummary::ID,
[
'max_tokens' => 1024,
'input' => $prompt,
],
Application::APP_ID,
$userId,
'message:' . (string)$messageLocalId
);
$manager->scheduleTask($task);

Check warning on line 130 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L116-L130

Added lines #L116 - L130 were not covered by tests
}
} finally {
$client->logout();

Check warning on line 133 in lib/Service/AiIntegrations/AiIntegrationsService.php

View check run for this annotation

Codecov / codecov/patch

lib/Service/AiIntegrations/AiIntegrationsService.php#L133

Added line #L133 was not covered by tests
}
}

/**
* @param Account $account
* @param string $threadId
Expand Down

0 comments on commit 638daf4

Please sign in to comment.