Skip to content

Commit

Permalink
feat(chat): Add API to summarize chat messages
Browse files Browse the repository at this point in the history
Signed-off-by: Joas Schilling <[email protected]>
  • Loading branch information
nickvergessen committed Oct 29, 2024
1 parent 0268a1c commit c3295bf
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 0 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes/routesChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
'ocs' => [
/** @see \OCA\Talk\Controller\ChatController::receiveMessages() */
['name' => 'Chat#receiveMessages', 'url' => '/api/{apiVersion}/chat/{token}', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\ChatController::summarizeChat() */
['name' => 'Chat#summarizeChat', 'url' => '/api/{apiVersion}/chat/{token}/summarize', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\ChatController::sendMessage() */
['name' => 'Chat#sendMessage', 'url' => '/api/{apiVersion}/chat/{token}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\ChatController::clearHistory() */
Expand Down
120 changes: 120 additions & 0 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@

namespace OCA\Talk\Controller;

use OCA\Talk\AppInfo\Application;
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Chat\ReactionManager;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\ChatSummaryException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
Expand Down Expand Up @@ -62,14 +64,20 @@
use OCP\IRequest;
use OCP\IUserManager;
use OCP\RichObjectStrings\InvalidObjectExeption;
use OCP\RichObjectStrings\IRichTextFormatter;
use OCP\RichObjectStrings\IValidator;
use OCP\Security\ITrustedDomainHelper;
use OCP\Security\RateLimiting\IRateLimitExceededException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IShare;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\IManager as ITaskProcessingManager;
use OCP\TaskProcessing\Task;
use OCP\TaskProcessing\TaskTypes\TextToTextSummary;
use OCP\User\Events\UserLiveStatusEvent;
use OCP\UserStatus\IManager as IUserStatusManager;
use OCP\UserStatus\IUserStatus;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type TalkChatMentionSuggestion from ResponseDefinitions
Expand Down Expand Up @@ -114,6 +122,9 @@ public function __construct(
protected Authenticator $federationAuthenticator,
protected ProxyCacheMessageService $pcmService,
protected Notifier $notifier,
protected IRichTextFormatter $richTextFormatter,
protected ITaskProcessingManager $taskProcessingManager,
protected LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -489,6 +500,115 @@ public function receiveMessages(int $lookIntoFuture,
return $this->prepareCommentsAsDataResponse($comments, $lastCommonReadId);
}

/**
* Summarize next bunch of chat messages from a given offset
*
* @param positive-int $fromMessageId Offset from where on the summary should be generated
* @return DataResponse<Http::STATUS_CREATED, array{taskId: int, nextOffset: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'ai-no-provider'|'ai-error'}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, array<empty>, array{}>
* @throws \InvalidArgumentException
*
* 201: Summary was scheduled
* 204: No messages found to summarize
* 400: No AI provider available or summarizing failed
*/
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
public function summarizeChat(
int $fromMessageId,
): DataResponse {
$fromMessageId = max(0, $fromMessageId);

$supportedTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes();
if (!isset($supportedTaskTypes[TextToTextSummary::ID])) {
return new DataResponse([
'error' => ChatSummaryException::REASON_AI_ERROR,
], Http::STATUS_BAD_REQUEST);
}

// if ($this->room->isFederatedConversation()) {
// /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
// $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
// return $proxy->summarizeChat(
// $this->room,
// $this->participant,
// $fromMessageId,
// );
// }

$currentUser = $this->userManager->get($this->userId);
$comments = $this->chatManager->waitForNewMessages($this->room, $fromMessageId, 500, 0, $currentUser, true, false);
$this->preloadShares($comments);

$messages = [];
$nextOffset = 0;
foreach ($comments as $comment) {
$message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
$this->messageParser->parseMessage($message);

if (!$message->getVisibility()) {
continue;
}

$parsedMessage = $this->richTextFormatter->richToParsed(
$message->getMessage(),
$message->getMessageParameters(),
);

$displayName = $message->getActorDisplayName();
if (in_array($message->getActorType(), [
Attendee::ACTOR_GUESTS,
Attendee::ACTOR_EMAILS,
], true)) {
if ($displayName === '') {
$displayName = $this->l->t('Guest');
} else {
$displayName = $this->l->t('%s (guest)', $displayName);
}
}

if ($comment->getParentId() !== '0') {
// FIXME should add something?
}

$messages[] = $displayName . ': ' . $parsedMessage;
$nextOffset = (int)$comment->getId();
}

if (empty($messages)) {
return new DataResponse([], Http::STATUS_NO_CONTENT);
}

$task = new Task(
TextToTextSummary::ID,
['input' => implode("\n\n", $messages)],
Application::APP_ID,
$this->userId,
'summary/' . $this->room->getToken(),
);

try {
$this->taskProcessingManager->scheduleTask($task);
} catch (Exception $e) {
$this->logger->error('An error occurred while trying to summarize unread messages', ['exception' => $e]);
return new DataResponse([
'error' => ChatSummaryException::REASON_AI_ERROR,
], Http::STATUS_BAD_REQUEST);
}

$taskId = $task->getId();
if ($taskId === null) {
return new DataResponse([
'error' => ChatSummaryException::REASON_AI_ERROR,
], Http::STATUS_BAD_REQUEST);
}

return new DataResponse([
'taskId' => $taskId,
'nextOffset' => $nextOffset,
], Http::STATUS_CREATED);
}

/**
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_MODIFIED, TalkChatMessageWithParent[], array{X-Chat-Last-Common-Read?: numeric-string, X-Chat-Last-Given?: numeric-string}>
*/
Expand Down
30 changes: 30 additions & 0 deletions lib/Exceptions/ChatSummaryException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Exceptions;

class ChatSummaryException extends \InvalidArgumentException {
public const REASON_NO_PROVIDER = 'ai-no-provider';
public const REASON_AI_ERROR = 'ai-error';

/**
* @param self::REASON_* $reason
*/
public function __construct(
protected string $reason,
) {
parent::__construct($reason);
}

/**
* @return self::REASON_*
*/
public function getReason(): string {
return $this->reason;
}
}

0 comments on commit c3295bf

Please sign in to comment.