-
Notifications
You must be signed in to change notification settings - Fork 295
feat: add context chat provider #11150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
edward-ly
wants to merge
30
commits into
main
Choose a base branch
from
feat/context-chat
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
a8b1ba2
feat: add context chat provider
edward-ly 397cd43
feat(ContextChat): execute initial import as queued jobs
edward-ly d5a5c3d
feat(ContextChat): add database table and related classes for context…
edward-ly e50bf5e
refactor(ContextChat): replace IJobList with JobsService for managing…
edward-ly cbce697
test: add partial unit tests for context chat
edward-ly 09671ec
fix(Application): Move context_chat constants to ContextChatProvider
marcelklehr a547457
fix(SubmitContentJob): Ensure client connection is closed
marcelklehr ad5d1af
refactor: context chat integration
marcelklehr 386e6a3
test: Add unit tests for ContextChatProvider and SubmitContentJob
marcelklehr 72f77a7
fix: Shorten table name
marcelklehr 58b68db
test: Complete unit tests for Context Chat integration
marcelklehr 827e196
test: Add tests for MessageMapper#findIdsAfter
marcelklehr 2fb22c5
test: Add tests for TaskMapper and TaskService
marcelklehr fa02ea5
test(SubmitContentJob): More failure tests
marcelklehr 7c0d92f
fix: update context chat constants
edward-ly 05222ac
ci(workflows): remove stable31 from psalm, unit, and integration tests
edward-ly 854442a
feat: Add opt-in setting for context chat
marcelklehr 3d308be
fix: Don't error if getItemUrl fails
marcelklehr 50b1387
fix: Add TaskService#setLastMessage to avoid loop
marcelklehr 86c6b58
fix: Log error
marcelklehr da32896
fix: Add a ScheduleJob to schedule SubmitContentJob when the user has…
marcelklehr 29040e4
fix: Mistake from rebasing
marcelklehr 95973bf
fix: Mistake from rebasing
marcelklehr db4353f
fix: Mistake from rebasing
marcelklehr bf2f0bc
fix: Appease psalm
marcelklehr ceaaca3
fix: Appease eslint
marcelklehr b019e83
fix: Fix tests
marcelklehr 34d4ad1
fix: Fix integration test
marcelklehr b7fd55e
fix: Fix integration test
marcelklehr 94afb80
fix: Fix integration test
marcelklehr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Mail\BackgroundJob\ContextChat; | ||
|
|
||
| use OCA\Mail\Exception\ServiceException; | ||
| use OCA\Mail\Service\AccountService; | ||
| use OCA\Mail\Service\ContextChat\ContextChatSettingsService; | ||
| use OCA\Mail\Service\ContextChat\TaskService; | ||
| use OCA\Mail\Service\MailManager; | ||
| use OCP\AppFramework\Db\DoesNotExistException; | ||
| use OCP\AppFramework\Db\MultipleObjectsReturnedException; | ||
| use OCP\AppFramework\Utility\ITimeFactory; | ||
| use OCP\BackgroundJob\IJob; | ||
| use OCP\BackgroundJob\IJobList; | ||
| use OCP\BackgroundJob\TimedJob; | ||
| use OCP\ContextChat\IContentManager; | ||
| use OCP\DB\Exception; | ||
| use Psr\Log\LoggerInterface; | ||
|
|
||
| class ScheduleJob extends TimedJob { | ||
| public function __construct( | ||
| ITimeFactory $time, | ||
| private TaskService $taskService, | ||
| private AccountService $accountService, | ||
| private MailManager $mailManager, | ||
| private LoggerInterface $logger, | ||
| private IJobList $jobList, | ||
| private ContextChatSettingsService $contextChatSettingsService, | ||
| private IContentManager $contentManager, | ||
| ) { | ||
| parent::__construct($time); | ||
|
|
||
| $this->setInterval(60 * 60 * 24); | ||
| $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); | ||
| } | ||
|
|
||
| #[\Override] | ||
| protected function run($argument): void { | ||
| $accountId = $argument['accountId']; | ||
|
|
||
| if (!$this->contentManager->isContextChatAvailable()) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| $account = $this->accountService->findById($accountId); | ||
| } catch (DoesNotExistException $e) { | ||
| $this->logger->debug('Could not find account <' . $accountId . '> removing from jobs'); | ||
| $this->jobList->remove(self::class, $argument); | ||
| return; | ||
| } | ||
|
|
||
| if (!$this->contextChatSettingsService->isIndexingEnabled($account->getUserId())) { | ||
| $this->logger->debug("indexing is turned off for account $accountId"); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| $mailboxes = $this->mailManager->getMailboxes($account); | ||
| } catch (ServiceException $e) { | ||
| $this->logger->debug('Could not find mailboxes for account <' . $accountId . '>'); | ||
| return; | ||
| } | ||
|
|
||
| foreach ($mailboxes as $mailbox) { | ||
| try { | ||
| $this->taskService->findByMailboxId($mailbox->getId()); | ||
| $this->taskService->updateOrCreate($mailbox->getId(), 0); | ||
| } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception $e) { | ||
| $this->logger->warning('Could not schedule context chat indexing tasks for mailbox <' . $mailbox->getId() . '>'); | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| namespace OCA\Mail\BackgroundJob\ContextChat; | ||
|
|
||
| use OCA\Mail\ContextChat\ContextChatProvider; | ||
| use OCA\Mail\Db\MailboxMapper; | ||
| use OCA\Mail\Db\Message; | ||
| use OCA\Mail\Db\MessageMapper; | ||
| use OCA\Mail\Exception\ServiceException; | ||
| use OCA\Mail\Exception\SmimeDecryptException; | ||
| use OCA\Mail\IMAP\IMAPClientFactory; | ||
| use OCA\Mail\Service\AccountService; | ||
| use OCA\Mail\Service\ContextChat\TaskService; | ||
| use OCA\Mail\Service\MailManager; | ||
| use OCP\AppFramework\Db\DoesNotExistException; | ||
| use OCP\AppFramework\Db\MultipleObjectsReturnedException; | ||
| use OCP\AppFramework\Utility\ITimeFactory; | ||
| use OCP\BackgroundJob\IJob; | ||
| use OCP\BackgroundJob\TimedJob; | ||
| use OCP\ContextChat\ContentItem; | ||
| use OCP\ContextChat\IContentManager; | ||
| use OCP\DB\Exception; | ||
| use Psr\Log\LoggerInterface; | ||
|
|
||
| class SubmitContentJob extends TimedJob { | ||
| public function __construct( | ||
| ITimeFactory $time, | ||
| private TaskService $taskService, | ||
| private AccountService $accountService, | ||
| private MailManager $mailManager, | ||
| private MessageMapper $messageMapper, | ||
| private IMAPClientFactory $clientFactory, | ||
| private ContextChatProvider $contextChatProvider, | ||
| private IContentManager $contentManager, | ||
| private LoggerInterface $logger, | ||
| private MailboxMapper $mailboxMapper, | ||
| ) { | ||
| parent::__construct($time); | ||
|
|
||
| $this->setAllowParallelRuns(false); | ||
| $this->setInterval(ContextChatProvider::CONTEXT_CHAT_JOB_INTERVAL); | ||
| $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); | ||
| } | ||
|
|
||
| #[\Override] | ||
| protected function run($argument): void { | ||
| if (!$this->contentManager->isContextChatAvailable()) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| $task = $this->taskService->findNext(); | ||
| } catch (Exception $e) { | ||
| $this->logger->warning('Exception occurred when trying to fetch next task', ['exception' => $e]); | ||
| return; | ||
| } catch (DoesNotExistException $e) { | ||
| // nothing to be done, let's defer to the next iteration of this job | ||
| return; | ||
| } catch (MultipleObjectsReturnedException $e) { | ||
| $this->logger->warning('Multiple tasks found for context chat. This is unexpected.', ['exception' => $e]); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| $mailbox = $this->mailboxMapper->findById($task->getMailboxId()); | ||
| } catch (ServiceException $e) { | ||
| $this->logger->warning('Multiple mailboxes found for context chat task, but only one expected. ERROR!', ['exception' => $e]); | ||
| return; | ||
| } catch (DoesNotExistException $e) { | ||
| // mailbox does not exist, lets wait for this task to be removed | ||
| return; | ||
| } | ||
|
|
||
| $processMailsAfter = $this->time->getTime() - ContextChatProvider::CONTEXT_CHAT_MESSAGE_MAX_AGE; | ||
| $messageIds = $this->messageMapper->findIdsAfter($mailbox, $task->getLastMessageId(), $processMailsAfter, ContextChatProvider::CONTEXT_CHAT_IMPORT_MAX_ITEMS); | ||
|
|
||
| if (empty($messageIds)) { | ||
| try { | ||
| $this->taskService->delete($task->getId()); | ||
| } catch (MultipleObjectsReturnedException|Exception $e) { | ||
| $this->logger->warning('Exception occurred when trying to delete task', ['exception' => $e]); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| $account = $this->accountService->findById($mailbox->getAccountId()); | ||
| } catch (DoesNotExistException $e) { | ||
| // well, what do you know. Then let's just skip this and wait for the next iteration of this job. tasks should be cascade deleted anyway | ||
| return; | ||
| } | ||
|
|
||
| $messages = $this->messageMapper->findByIds($account->getUserId(), $messageIds, 'asc', 'id'); | ||
|
|
||
| if (empty($messages)) { | ||
| try { | ||
| $this->taskService->delete($task->getId()); | ||
| } catch (MultipleObjectsReturnedException|Exception $e) { | ||
| $this->logger->warning('Exception occurred when trying to delete task', ['exception' => $e]); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
|
|
||
| $client = $this->clientFactory->getClient($account); | ||
marcelklehr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| $items = []; | ||
|
|
||
| try { | ||
| $startTime = $this->time->getTime(); | ||
| foreach ($messages as $message) { | ||
| if ($this->time->getTime() - $startTime > ContextChatProvider::CONTEXT_CHAT_JOB_INTERVAL) { | ||
| break; | ||
| } | ||
| try { | ||
| $imapMessage = $this->mailManager->getImapMessage($client, $account, $mailbox, $message->getUid(), true); | ||
| } catch (ServiceException $e) { | ||
| // couldn't load message, let's skip it. Retrying would be too costly | ||
| continue; | ||
| } catch (SmimeDecryptException $e) { | ||
| // encryption problem, skip this message | ||
| continue; | ||
| } | ||
|
|
||
|
|
||
| // Skip encrypted messages | ||
| if ($imapMessage->isEncrypted()) { | ||
| continue; | ||
| } | ||
|
|
||
|
|
||
| $fullMessage = $imapMessage->getFullMessage($imapMessage->getUid(), true); | ||
|
|
||
|
|
||
| $items[] = new ContentItem( | ||
| $mailbox->getId() . ':' . $message->getId(), | ||
| $this->contextChatProvider->getId(), | ||
| $imapMessage->getSubject(), | ||
| $fullMessage['body'] ?? '', | ||
| 'E-Mail', | ||
| $imapMessage->getSentDate(), | ||
| [$account->getUserId()], | ||
| ); | ||
| } | ||
| } catch (\Throwable $e) { | ||
| $this->logger->warning('Exception occurred when trying to fetch messages for context chat', ['exception' => $e]); | ||
| } finally { | ||
| try { | ||
| $client->close(); | ||
| } catch (\Horde_Imap_Client_Exception $e) { | ||
| $this->logger->debug('Failed to close IMAP client', ['exception' => $e]); | ||
| } | ||
| } | ||
|
|
||
| if (count($items) > 0) { | ||
| $this->contentManager->submitContent($this->contextChatProvider->getAppId(), $items); | ||
| } | ||
|
|
||
| try { | ||
| $this->taskService->setLastMessage($task->getMailboxId(), $message?->getId() ?? $messageIds[0]); | ||
| } catch (MultipleObjectsReturnedException|Exception $e) { | ||
| $this->logger->warning('Exception occurred when trying to update task', ['exception' => $e]); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.