From ebcf1f33d3bab1c9d62ed94ba401dd3de6f6300b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 13:57:01 +0100 Subject: [PATCH 01/22] feat: Implement the backend for memories Signed-off-by: Marcel Klehr --- appinfo/routes.php | 1 + .../GenerateNewChatSummaries.php | 21 ++++ .../RegenerateOutdatedChatSummariesJob.php | 22 +++++ lib/Controller/ChattyLLMController.php | 44 +++++++++ lib/Db/ChattyLLM/Session.php | 40 ++++++++ lib/Db/ChattyLLM/SessionMapper.php | 71 ++++++++++++++ lib/Listener/ChattyLLMTaskListener.php | 8 +- .../Version021201Date20251210130151.php | 59 +++++++++++ lib/ResponseDefinitions.php | 3 + lib/Service/SessionSummaryService.php | 97 +++++++++++++++++++ .../AudioToAudioChatProvider.php | 27 ++++-- .../ContextAgentAudioInteractionProvider.php | 27 ++++-- 12 files changed, 405 insertions(+), 15 deletions(-) create mode 100644 lib/BackgroundJob/GenerateNewChatSummaries.php create mode 100644 lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php create mode 100644 lib/Migration/Version021201Date20251210130151.php create mode 100644 lib/Service/SessionSummaryService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index d076cbc5..9d1792a7 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -38,6 +38,7 @@ ['name' => 'chattyLLM#newSession', 'url' => '/chat/new_session', 'verb' => 'PUT'], ['name' => 'chattyLLM#updateSessionTitle', 'url' => '/chat/update_session', 'verb' => 'PATCH'], + ['name' => 'chattyLLM#updateSessionIsRemembered', 'url' => '/chat/update_is_remembered', 'verb' => 'PATCH'], ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'], ['name' => 'chattyLLM#getSessions', 'url' => '/chat/sessions', 'verb' => 'GET'], ['name' => 'chattyLLM#newMessage', 'url' => '/chat/new_message', 'verb' => 'PUT'], diff --git a/lib/BackgroundJob/GenerateNewChatSummaries.php b/lib/BackgroundJob/GenerateNewChatSummaries.php new file mode 100644 index 00000000..934f7632 --- /dev/null +++ b/lib/BackgroundJob/GenerateNewChatSummaries.php @@ -0,0 +1,21 @@ +setInterval(60 * 10); // 10min + } + public function run($argument) { + $userId = $argument['userId']; + $this->sessionSummaryService->generateSummariesForNewSessions($userId); + } +} \ No newline at end of file diff --git a/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php new file mode 100644 index 00000000..34bbafa1 --- /dev/null +++ b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php @@ -0,0 +1,22 @@ +setInterval(60 * 60 * 24); // 24h + } + public function run($argument) { + $userId = $argument['userId']; + $this->sessionSummaryService->regenerateSummariesForOutdatedSessions($userId); + } +} \ No newline at end of file diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index d359312a..9fd89431 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -13,6 +13,7 @@ use OCA\Assistant\Db\ChattyLLM\Session; use OCA\Assistant\Db\ChattyLLM\SessionMapper; use OCA\Assistant\ResponseDefinitions; +use OCA\Assistant\Service\SessionSummaryService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Http; @@ -55,6 +56,7 @@ public function __construct( private IAppConfig $appConfig, private IUserManager $userManager, private ?string $userId, + private SessionSummaryService $sessionSummaryService ) { parent::__construct($appName, $request); $this->agencyActionData = [ @@ -190,6 +192,36 @@ public function updateSessionTitle(int $sessionId, string $title): JSONResponse } } + /** + * Update session is_remembered status + * + * @param integer $sessionId The chat session ID + * @param bool $is_remembered The new is_remembered status: Whether to remember the insights from this chat session across all chat sessiosn + * @return JSONResponse|JSONResponse + * + * 200: The title has been updated successfully + * 401: Not logged in + */ + #[NoAdminRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] + public function updateSessionIsRemembered(int $sessionId, bool $is_remembered): JSONResponse { + if ($this->userId === null) { + return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED); + } + + try { + $this->sessionMapper->updateSessionIsRemembered($this->userId, $sessionId, $is_remembered); + // schedule summarizer jobs for this chat user + if ($is_remembered) { + $this->sessionSummaryService->scheduleJobsForUser($this->userId); + } + return new JSONResponse(); + } catch (\OCP\DB\Exception|\RuntimeException $e) { + $this->logger->warning('Failed to update the chat session', ['exception' => $e]); + return new JSONResponse(['error' => $this->l10n->t('Failed to update the chat session')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Delete a chat session * @@ -990,6 +1022,9 @@ private function scheduleLLMChatTask( 'system_prompt' => $systemPrompt, 'history' => $history, ]; + if (isset($this->taskProcessingManager->getAvailableTaskTypes()[TextToTextChat::ID]['optionalInputShape']['memories'])) { + $input['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); + } $task = new Task(TextToTextChat::ID, $input, Application::APP_ID . ':chatty-llm', $this->userId, $customId); $this->taskProcessingManager->scheduleTask($task); return $task->getId() ?? 0; @@ -1016,6 +1051,9 @@ private function scheduleAgencyTask(string $content, int $confirmation, string $ 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID]['optionalInputShape']['memories'])) { + $taskInput['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); + } /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID, @@ -1038,6 +1076,9 @@ private function scheduleAudioChatTask( 'system_prompt' => $systemPrompt, 'history' => $history, ]; + if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID]['optionalInputShape']['memories'])) { + $input['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); + } /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, @@ -1060,6 +1101,9 @@ private function scheduleAgencyAudioTask( 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { + $taskInput['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); + } /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, diff --git a/lib/Db/ChattyLLM/Session.php b/lib/Db/ChattyLLM/Session.php index 11ba78c4..ccac4381 100644 --- a/lib/Db/ChattyLLM/Session.php +++ b/lib/Db/ChattyLLM/Session.php @@ -17,6 +17,8 @@ * @method \void setUserId(string $userId) * @method \string|null getTitle() * @method \void setTitle(?string $title) + * @method \string|null getSummary() + * @method \void setSummary(?string $title) * @method \int getTimestamp() * @method \void setTimestamp(int $timestamp) * @method \string|null getAgencyConversationToken() @@ -36,6 +38,16 @@ class Session extends Entity implements \JsonSerializable { /** @var ?string */ protected $agencyPendingActions; + /** @var ?string */ + protected $summary; + + /** @var int */ + protected $isSummaryUpToDate; + + /** @var int */ + protected $isRemembered; + + public static $columns = [ 'id', 'user_id', @@ -43,6 +55,9 @@ class Session extends Entity implements \JsonSerializable { 'timestamp', 'agency_conversation_token', 'agency_pending_actions', + 'summary', + 'is_summary_up_to_date', + 'is_remembered', ]; public static $fields = [ 'id', @@ -51,6 +66,9 @@ class Session extends Entity implements \JsonSerializable { 'timestamp', 'agencyConversationToken', 'agencyPendingActions', + 'summary', + 'isSummaryUpToDate', + 'isRemembered', ]; public function __construct() { @@ -59,6 +77,9 @@ public function __construct() { $this->addType('timestamp', Types::INTEGER); $this->addType('agency_conversation_token', Types::STRING); $this->addType('agency_pending_actions', Types::STRING); + $this->addType('summary', Types::TEXT); + $this->addType('is_summary_up_to_date', Types::SMALLINT); + $this->addType('is_remembered', Types::SMALLINT); } #[\ReturnTypeWillChange] @@ -70,6 +91,25 @@ public function jsonSerialize() { 'timestamp' => $this->getTimestamp(), 'agency_conversation_token' => $this->getAgencyConversationToken(), 'agency_pending_actions' => $this->getAgencyPendingActions(), + 'summary' => $this->getSummary(), + 'is_summary_up_to_date' => $this->getIsSummaryUpToDate(), + 'is_remembered' => $this->getIsRemembered(), ]; } + + public function setIsSummaryUpToDate(bool $value): void { + $this->setter('isSummaryUpToDate', [$value ? 1 : 0]); + } + + public function setIsRemembered(bool $value): void { + $this->setter('isRemembered', [$value ? 1 : 0]); + } + + public function getIsSummaryUpToDate(): bool { + return $this->getter('isSummaryUpToDate') === 1; + } + + public function getIsRemembered(): bool { + return $this->getter('isRemembered') === 1; + } } diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 7133db86..9a3d8a81 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -79,6 +79,71 @@ public function getUserSessions(string $userId): array { return $this->findEntities($qb); } + /** + * @return list + * @throws \OCP\DB\Exception + */ + public function getRememberedUserSessions(string $userId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) + ->orderBy('timestamp', 'DESC'); + + return $this->findEntities($qb); + } + + /** + * @return list + * @throws \OCP\DB\Exception + */ + public function getRememberedUserSessionsWithOutdatedSummaries(string $userId, int $limit): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('is_summary_up_to_date', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT))) + ->setMaxResults($limit) + ->orderBy('timestamp', 'DESC'); + + return $this->findEntities($qb); + } + + /** + * @return list + * @throws \OCP\DB\Exception + */ + public function getRememberedSessionsWithOutdatedSummaries(int $limit): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('is_summary_up_to_date', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT))) + ->setMaxResults($limit) + ->orderBy('timestamp', 'DESC'); + + return $this->findEntities($qb); + } + + /** + * @return list + * @throws \OCP\DB\Exception + */ + public function getRememberedUserSessionsWithoutSummaries(string $userId, int $limit): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) + ->where($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->isNull('summary')) + ->setMaxResults($limit) + ->orderBy('timestamp', 'DESC'); + + return $this->findEntities($qb); + } + /** * @param string $userId * @param integer $sessionId @@ -110,4 +175,10 @@ public function deleteSession(string $userId, int $sessionId) { $qb->executeStatement(); } + + public function updateSessionIsRemembered(?string $userId, int $sessionId, bool $is_remembered) { + $session = $this->getUserSession($userId, $sessionId); + $session->setIsRemembered($is_remembered); + $this->update($session); + } } diff --git a/lib/Listener/ChattyLLMTaskListener.php b/lib/Listener/ChattyLLMTaskListener.php index 464a99e5..46490878 100644 --- a/lib/Listener/ChattyLLMTaskListener.php +++ b/lib/Listener/ChattyLLMTaskListener.php @@ -115,15 +115,19 @@ public function handle(Event $event): void { $this->logger->error('Message insertion error in chattyllm task listener', ['exception' => $e]); } + $session = $this->sessionMapper->getUserSession($task->getUserId(), $sessionId); + // store the conversation token and the actions if we are using the agency feature if ($isAgency || $isAgencyAudioChat) { - $session = $this->sessionMapper->getUserSession($task->getUserId(), $sessionId); $conversationToken = ($taskOutput['conversation_token'] ?? null) ?: null; $pendingActions = ($taskOutput['actions'] ?? null) ?: null; $session->setAgencyConversationToken($conversationToken); $session->setAgencyPendingActions($pendingActions); - $this->sessionMapper->update($session); } + // Set flag that the conversation summary needs to be regenerated + $session->setIsSummaryUpToDate(false); + + $this->sessionMapper->update($session); } } diff --git a/lib/Migration/Version021201Date20251210130151.php b/lib/Migration/Version021201Date20251210130151.php new file mode 100644 index 00000000..a75aa185 --- /dev/null +++ b/lib/Migration/Version021201Date20251210130151.php @@ -0,0 +1,59 @@ +hasTable('assistant_chat_sns')) { + $table = $schema->getTable('assistant_chat_sns'); + if (!$table->hasColumn('summary')) { + $table->addColumn('summary', Types::TEXT, [ + 'notnull' => false, + 'default' => null, + ]); + $schemaChanged = true; + } + if (!$table->hasColumn('is_remembered')) { + $table->addColumn('is_remembered', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 0, + ]); + $schemaChanged = true; + } + if (!$table->hasColumn('is_summary_up_to_date')) { + $table->addColumn('is_summary_up_to_date', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 0, + ]); + $schemaChanged = true; + } + } + + return $schemaChanged ? $schema : null; + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 51aff909..c7407eb1 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -55,6 +55,9 @@ * timestamp: ?int, * agency_conversation_token: ?string, * agency_pending_actions: ?string, + * summary: ?string, + * is_remembered: bool, + * is_summary_up_to_date: bool, * } * * @psalm-type AssistantChatMessage = array{ diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php new file mode 100644 index 00000000..e184469c --- /dev/null +++ b/lib/Service/SessionSummaryService.php @@ -0,0 +1,97 @@ +messageMapper->getMessages($session->getId(), 0, self::SUMMARY_MESSAGE_LIMIT); + if ($messages[0]->getRole() === 'system') { + array_shift($messages); + } + + $prompt = "Summarize insights about the user's circumstances, preferences and choices from the following conversation. Be as concise as possible. Do not add an introductory sentence or any other remarks.\n\n"; + + foreach ($messages as $message) { + $prompt .= $message->getRole() . ': ' . $message->getContent() . "\n\n"; + } + + $task = new Task(TextToText::ID, [ + 'input' => $prompt, + ], 'assistant', $session->getUserId()); + $output = $this->taskProcessingService->runTaskProcessingTask($task); + $session->setSummary($output['output']); + $session->setIsSummaryUpToDate(true); + $this->sessionMapper->update($session); + } catch(\Throwable $e) { + $this->logger->warning('Failed to generate summary for chat session ' . $session->getId(), ['exception' => $e]); + } + } + } + + public function regenerateSummariesForOutdatedSessions(string $userId): void { + try { + $sessions = $this->sessionMapper->getRememberedUserSessionsWithOutdatedSummaries($userId, self::BAtCH_SIZE); + $this->generateSummaries($sessions); + } catch (Exception $e) { + $this->logger->warning('Failed to generate chat summaries for outdated sessions', ['exception' => $e]); + } + } + + public function generateSummariesForNewSessions(string $userId): void{ + try { + $sessions = $this->sessionMapper->getRememberedUserSessionsWithoutSummaries($userId, self::BAtCH_SIZE); + $this->generateSummaries($sessions); + } catch (Exception $e) { + $this->logger->warning('Failed to generate chat summaries for new sessions', ['exception' => $e]); + } + } + + public function scheduleJobsForUser(string $userId) { + if (!$this->jobList->has(GenerateNewChatSummaries::class, ['userId' => $userId])) { + $this->jobList->add(GenerateNewChatSummaries::class, ['userId' => $userId]); + } + if (!$this->jobList->has(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId])) { + $this->jobList->add(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId]); + } + } + + /** + * @return list + */ + public function getUserSessionSummaries(?string $userId): array { + try { + $sessions = $this->sessionMapper->getRememberedUserSessions($userId); + return array_map(fn (Session $session) => $session->getSummary(), $sessions); + } catch (Exception $e) { + $this->logger->error('Failed to get remembered user sessions', ['exception' => $e]); + return []; + } + } + +} \ No newline at end of file diff --git a/lib/TaskProcessing/AudioToAudioChatProvider.php b/lib/TaskProcessing/AudioToAudioChatProvider.php index 61c7576a..cc99aa18 100644 --- a/lib/TaskProcessing/AudioToAudioChatProvider.php +++ b/lib/TaskProcessing/AudioToAudioChatProvider.php @@ -10,11 +10,14 @@ namespace OCA\Assistant\TaskProcessing; use Exception; +use OC\TaskProcessing\Manager; use OCA\Assistant\AppInfo\Application; use OCA\Assistant\Service\TaskProcessingService; use OCP\Files\File; use OCP\IL10N; +use OCP\TaskProcessing\EShapeType; use OCP\TaskProcessing\ISynchronousProvider; +use OCP\TaskProcessing\ShapeDescriptor; use OCP\TaskProcessing\Task; use OCP\TaskProcessing\TaskTypes\AudioToAudioChat; use OCP\TaskProcessing\TaskTypes\AudioToText; @@ -29,6 +32,7 @@ public function __construct( private IL10N $l, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, + private Manager $taskProcessingManager, ) { } @@ -57,9 +61,14 @@ public function getInputShapeDefaults(): array { return []; } - public function getOptionalInputShape(): array { - return []; + return [ + 'memories' => new ShapeDescriptor( + $this->l->t('Memories'), + $this->l->t('The memories to be injected into the chat session.'), + EShapeType::ListOfTexts + ), + ]; } public function getOptionalInputShapeEnumValues(): array { @@ -117,13 +126,17 @@ public function process(?string $userId, array $input, callable $reportProgress) // free prompt try { + $chatTaskInput = [ + 'input' => $inputTranscription, + 'system_prompt' => $systemPrompt, + 'history' => $history, + ]; + if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[TextToTextChat::ID]['optionalInputShape']['memories'])) { + $chatTaskInput['memories'] = $input['memories']; + } $task = new Task( TextToTextChat::ID, - [ - 'input' => $inputTranscription, - 'system_prompt' => $systemPrompt, - 'history' => $history, - ], + $chatTaskInput, Application::APP_ID . ':internal', $userId, ); diff --git a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php index 07288a33..63459618 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -10,11 +10,15 @@ namespace OCA\Assistant\TaskProcessing; use Exception; +use OC\TaskProcessing\Manager; use OCA\Assistant\AppInfo\Application; +use OCA\Assistant\Service\SessionSummaryService; use OCA\Assistant\Service\TaskProcessingService; use OCP\Files\File; use OCP\IL10N; +use OCP\TaskProcessing\EShapeType; use OCP\TaskProcessing\ISynchronousProvider; +use OCP\TaskProcessing\ShapeDescriptor; use OCP\TaskProcessing\Task; use OCP\TaskProcessing\TaskTypes\AudioToText; use OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction; @@ -29,6 +33,7 @@ public function __construct( private IL10N $l, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, + private Manager $taskProcessingManager, ) { } @@ -59,7 +64,13 @@ public function getInputShapeDefaults(): array { public function getOptionalInputShape(): array { - return []; + return [ + 'memories' => new ShapeDescriptor( + $this->l->t('Memories'), + $this->l->t('The memories to be injected into the chat session.'), + EShapeType::ListOfTexts + ), + ]; } public function getOptionalInputShapeEnumValues(): array { @@ -116,14 +127,18 @@ public function process(?string $userId, array $input, callable $reportProgress) // context agent try { + $contextAgentTaskInput = [ + 'input' => $inputTranscription, + 'confirmation' => $confirmation, + 'conversation_token' => $conversationToken, + ]; + if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { + $contextAgentTaskInput['memories'] = $input['memories']; + } /** @psalm-suppress UndefinedClass */ $task = new Task( ContextAgentInteraction::ID, - [ - 'input' => $inputTranscription, - 'confirmation' => $confirmation, - 'conversation_token' => $conversationToken, - ], + $contextAgentTaskInput, Application::APP_ID . ':internal', $userId, ); From 75f95dc0ebfbe93e4e7844fddb63e300211cebc6 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 14:14:09 +0100 Subject: [PATCH 02/22] fix: run cs:fix Signed-off-by: Marcel Klehr --- lib/BackgroundJob/GenerateNewChatSummaries.php | 4 ++-- lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php | 4 ++-- lib/Controller/ChattyLLMController.php | 2 +- lib/Migration/Version021201Date20251210130151.php | 2 +- lib/Service/SessionSummaryService.php | 9 ++++----- .../ContextAgentAudioInteractionProvider.php | 1 - 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/BackgroundJob/GenerateNewChatSummaries.php b/lib/BackgroundJob/GenerateNewChatSummaries.php index 934f7632..39c22214 100644 --- a/lib/BackgroundJob/GenerateNewChatSummaries.php +++ b/lib/BackgroundJob/GenerateNewChatSummaries.php @@ -9,7 +9,7 @@ class GenerateNewChatSummaries extends TimedJob { public function __construct( ITimeFactory $timeFactory, - private SessionSummaryService $sessionSummaryService + private SessionSummaryService $sessionSummaryService, ) { parent::__construct($timeFactory); $this->setInterval(60 * 10); // 10min @@ -18,4 +18,4 @@ public function run($argument) { $userId = $argument['userId']; $this->sessionSummaryService->generateSummariesForNewSessions($userId); } -} \ No newline at end of file +} diff --git a/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php index 34bbafa1..8a99dc0e 100644 --- a/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php +++ b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php @@ -10,7 +10,7 @@ class RegenerateOutdatedChatSummariesJob extends TimedJob { public function __construct( ITimeFactory $timeFactory, - private SessionSummaryService $sessionSummaryService + private SessionSummaryService $sessionSummaryService, ) { parent::__construct($timeFactory); $this->setInterval(60 * 60 * 24); // 24h @@ -19,4 +19,4 @@ public function run($argument) { $userId = $argument['userId']; $this->sessionSummaryService->regenerateSummariesForOutdatedSessions($userId); } -} \ No newline at end of file +} diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 9fd89431..39b2288c 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -56,7 +56,7 @@ public function __construct( private IAppConfig $appConfig, private IUserManager $userManager, private ?string $userId, - private SessionSummaryService $sessionSummaryService + private SessionSummaryService $sessionSummaryService, ) { parent::__construct($appName, $request); $this->agencyActionData = [ diff --git a/lib/Migration/Version021201Date20251210130151.php b/lib/Migration/Version021201Date20251210130151.php index a75aa185..b5fde3d4 100644 --- a/lib/Migration/Version021201Date20251210130151.php +++ b/lib/Migration/Version021201Date20251210130151.php @@ -10,9 +10,9 @@ use Closure; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; -use OCP\DB\Types; class Version021201Date20251210130151 extends SimpleMigrationStep { diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php index e184469c..132eea1b 100644 --- a/lib/Service/SessionSummaryService.php +++ b/lib/Service/SessionSummaryService.php @@ -11,7 +11,6 @@ use OCP\DB\Exception; use OCP\TaskProcessing\Task; use OCP\TaskProcessing\TaskTypes\TextToText; -use OCP\TaskProcessing\TaskTypes\TextToTextSummary; use Psr\Log\LoggerInterface; class SessionSummaryService { @@ -23,7 +22,7 @@ public function __construct( private MessageMapper $messageMapper, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, - private IJobList $jobList + private IJobList $jobList, ) { } @@ -48,7 +47,7 @@ private function generateSummaries(array $sessions): void { $session->setSummary($output['output']); $session->setIsSummaryUpToDate(true); $this->sessionMapper->update($session); - } catch(\Throwable $e) { + } catch (\Throwable $e) { $this->logger->warning('Failed to generate summary for chat session ' . $session->getId(), ['exception' => $e]); } } @@ -63,7 +62,7 @@ public function regenerateSummariesForOutdatedSessions(string $userId): void { } } - public function generateSummariesForNewSessions(string $userId): void{ + public function generateSummariesForNewSessions(string $userId): void { try { $sessions = $this->sessionMapper->getRememberedUserSessionsWithoutSummaries($userId, self::BAtCH_SIZE); $this->generateSummaries($sessions); @@ -94,4 +93,4 @@ public function getUserSessionSummaries(?string $userId): array { } } -} \ No newline at end of file +} diff --git a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php index 63459618..b04e03a8 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -12,7 +12,6 @@ use Exception; use OC\TaskProcessing\Manager; use OCA\Assistant\AppInfo\Application; -use OCA\Assistant\Service\SessionSummaryService; use OCA\Assistant\Service\TaskProcessingService; use OCP\Files\File; use OCP\IL10N; From 3ed329b75d423516e9573d99d61ab15149c45e71 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 14:29:15 +0100 Subject: [PATCH 03/22] fix: Fix psalm errors Signed-off-by: Marcel Klehr --- lib/Db/ChattyLLM/SessionMapper.php | 8 ++++---- lib/Service/SessionSummaryService.php | 4 ++-- lib/TaskProcessing/AudioToAudioChatProvider.php | 9 ++++++--- .../ContextAgentAudioInteractionProvider.php | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 9a3d8a81..39075711 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -80,7 +80,7 @@ public function getUserSessions(string $userId): array { } /** - * @return list + * @return array * @throws \OCP\DB\Exception */ public function getRememberedUserSessions(string $userId): array { @@ -95,7 +95,7 @@ public function getRememberedUserSessions(string $userId): array { } /** - * @return list + * @return array * @throws \OCP\DB\Exception */ public function getRememberedUserSessionsWithOutdatedSummaries(string $userId, int $limit): array { @@ -112,7 +112,7 @@ public function getRememberedUserSessionsWithOutdatedSummaries(string $userId, i } /** - * @return list + * @return array * @throws \OCP\DB\Exception */ public function getRememberedSessionsWithOutdatedSummaries(int $limit): array { @@ -128,7 +128,7 @@ public function getRememberedSessionsWithOutdatedSummaries(int $limit): array { } /** - * @return list + * @return array * @throws \OCP\DB\Exception */ public function getRememberedUserSessionsWithoutSummaries(string $userId, int $limit): array { diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php index 132eea1b..609e05cc 100644 --- a/lib/Service/SessionSummaryService.php +++ b/lib/Service/SessionSummaryService.php @@ -81,12 +81,12 @@ public function scheduleJobsForUser(string $userId) { } /** - * @return list + * @return array */ public function getUserSessionSummaries(?string $userId): array { try { $sessions = $this->sessionMapper->getRememberedUserSessions($userId); - return array_map(fn (Session $session) => $session->getSummary(), $sessions); + return array_filter(array_map(fn (Session $session) => $session->getSummary(), $sessions), fn ($summary) => $summary !== null); } catch (Exception $e) { $this->logger->error('Failed to get remembered user sessions', ['exception' => $e]); return []; diff --git a/lib/TaskProcessing/AudioToAudioChatProvider.php b/lib/TaskProcessing/AudioToAudioChatProvider.php index cc99aa18..7d5f3d0b 100644 --- a/lib/TaskProcessing/AudioToAudioChatProvider.php +++ b/lib/TaskProcessing/AudioToAudioChatProvider.php @@ -10,12 +10,12 @@ namespace OCA\Assistant\TaskProcessing; use Exception; -use OC\TaskProcessing\Manager; use OCA\Assistant\AppInfo\Application; use OCA\Assistant\Service\TaskProcessingService; use OCP\Files\File; use OCP\IL10N; use OCP\TaskProcessing\EShapeType; +use OCP\TaskProcessing\IManager; use OCP\TaskProcessing\ISynchronousProvider; use OCP\TaskProcessing\ShapeDescriptor; use OCP\TaskProcessing\Task; @@ -32,7 +32,7 @@ public function __construct( private IL10N $l, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, - private Manager $taskProcessingManager, + private IManager $taskProcessingManager, ) { } @@ -131,9 +131,12 @@ public function process(?string $userId, array $input, callable $reportProgress) 'system_prompt' => $systemPrompt, 'history' => $history, ]; - if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[TextToTextChat::ID]['optionalInputShape']['memories'])) { + if ( + isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[TextToTextChat::ID]['optionalInputShape']['memories']) + ) { $chatTaskInput['memories'] = $input['memories']; } + /** @psalm-suppress InvalidArgument */ $task = new Task( TextToTextChat::ID, $chatTaskInput, diff --git a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php index b04e03a8..4f3aed30 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -10,7 +10,7 @@ namespace OCA\Assistant\TaskProcessing; use Exception; -use OC\TaskProcessing\Manager; +use OCP\TaskProcessing\IManager; use OCA\Assistant\AppInfo\Application; use OCA\Assistant\Service\TaskProcessingService; use OCP\Files\File; @@ -32,7 +32,7 @@ public function __construct( private IL10N $l, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, - private Manager $taskProcessingManager, + private IManager $taskProcessingManager, ) { } @@ -134,7 +134,7 @@ public function process(?string $userId, array $input, callable $reportProgress) if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { $contextAgentTaskInput['memories'] = $input['memories']; } - /** @psalm-suppress UndefinedClass */ + /** @psalm-suppress UndefinedClass,InvalidArgument */ $task = new Task( ContextAgentInteraction::ID, $contextAgentTaskInput, From 9e58feaac1f49c8378f62059b5d83b54b37b108b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 14:42:48 +0100 Subject: [PATCH 04/22] fix: Fix psalm errors pre 32 Signed-off-by: Marcel Klehr --- lib/Controller/ChattyLLMController.php | 3 +++ lib/TaskProcessing/ContextAgentAudioInteractionProvider.php | 1 + 2 files changed, 4 insertions(+) diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 39b2288c..b94ef673 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -1051,6 +1051,7 @@ private function scheduleAgencyTask(string $content, int $confirmation, string $ 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + /** @psalm-suppress UndefinedClass */ if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID]['optionalInputShape']['memories'])) { $taskInput['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); } @@ -1076,6 +1077,7 @@ private function scheduleAudioChatTask( 'system_prompt' => $systemPrompt, 'history' => $history, ]; + /** @psalm-suppress UndefinedClass */ if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID]['optionalInputShape']['memories'])) { $input['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); } @@ -1101,6 +1103,7 @@ private function scheduleAgencyAudioTask( 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + /** @psalm-suppress UndefinedClass */ if (isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { $taskInput['memories'] = $this->sessionSummaryService->getUserSessionSummaries($this->userId); } diff --git a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php index 4f3aed30..26bd37ef 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -131,6 +131,7 @@ public function process(?string $userId, array $input, callable $reportProgress) 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + /** @psalm-suppress UndefinedClass */ if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { $contextAgentTaskInput['memories'] = $input['memories']; } From 40a9419659036ee61e6f5ca13c25b46584078ec6 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 14:44:25 +0100 Subject: [PATCH 05/22] fix: Fix REUSE compatibility Signed-off-by: Marcel Klehr --- lib/BackgroundJob/GenerateNewChatSummaries.php | 7 +++++++ lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php | 7 +++++++ lib/Service/SessionSummaryService.php | 7 +++++++ .../ContextAgentAudioInteractionProvider.php | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/BackgroundJob/GenerateNewChatSummaries.php b/lib/BackgroundJob/GenerateNewChatSummaries.php index 39c22214..422d0846 100644 --- a/lib/BackgroundJob/GenerateNewChatSummaries.php +++ b/lib/BackgroundJob/GenerateNewChatSummaries.php @@ -1,5 +1,12 @@ Date: Wed, 10 Dec 2025 14:45:32 +0100 Subject: [PATCH 06/22] fix: Update openapi.json Signed-off-by: Marcel Klehr --- openapi.json | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/openapi.json b/openapi.json index f8c9d98f..c1540350 100644 --- a/openapi.json +++ b/openapi.json @@ -108,7 +108,10 @@ "title", "timestamp", "agency_conversation_token", - "agency_pending_actions" + "agency_pending_actions", + "summary", + "is_remembered", + "is_summary_up_to_date" ], "properties": { "id": { @@ -134,6 +137,16 @@ "agency_pending_actions": { "type": "string", "nullable": true + }, + "summary": { + "type": "string", + "nullable": true + }, + "is_remembered": { + "type": "boolean" + }, + "is_summary_up_to_date": { + "type": "boolean" } } }, @@ -2920,6 +2933,133 @@ } } }, + "/ocs/v2.php/apps/assistant/chat/update_is_remembered": { + "patch": { + "operationId": "chattyllm-update-session-is-remembered", + "summary": "Update session is_remembered status", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "is_remembered" + ], + "properties": { + "sessionId": { + "type": "integer", + "format": "int64", + "description": "The chat session ID" + }, + "is_remembered": { + "type": "boolean", + "description": "The new is_remembered status: Whether to remember the insights from this chat session across all chat sessiosn" + } + } + } + } + } + }, + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The title has been updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + } + } + } + }, "/ocs/v2.php/apps/assistant/chat/delete_session": { "delete": { "operationId": "chattyllm-delete-session", From daa1acd43c9712e291e8e86309bb9601f2e35ce6 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 10 Dec 2025 14:49:50 +0100 Subject: [PATCH 07/22] fix: Fix copy-pasta Signed-off-by: Marcel Klehr --- lib/Db/ChattyLLM/SessionMapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 39075711..2d468216 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -136,7 +136,7 @@ public function getRememberedUserSessionsWithoutSummaries(string $userId, int $l $qb->select(Session::$columns) ->from($this->getTableName()) ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))) - ->where($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->isNull('summary')) ->setMaxResults($limit) ->orderBy('timestamp', 'DESC'); From 1f2676291b007c286ded0167da3e7d9f8f759721 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 11 Dec 2025 12:54:12 +0100 Subject: [PATCH 08/22] Fix: Address review comments Signed-off-by: Marcel Klehr --- lib/Db/ChattyLLM/Session.php | 12 ++++++++++-- lib/Migration/Version021201Date20251210130151.php | 2 -- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/Db/ChattyLLM/Session.php b/lib/Db/ChattyLLM/Session.php index ccac4381..414e5cad 100644 --- a/lib/Db/ChattyLLM/Session.php +++ b/lib/Db/ChattyLLM/Session.php @@ -38,13 +38,21 @@ class Session extends Entity implements \JsonSerializable { /** @var ?string */ protected $agencyPendingActions; - /** @var ?string */ + /** + * Will be used to inject into assistant memories upon calling LLM + * + * @var ?string + */ protected $summary; /** @var int */ protected $isSummaryUpToDate; - /** @var int */ + /** + * Whether to remember the insights from this chat session across all chat sessions + * + * @var int + */ protected $isRemembered; diff --git a/lib/Migration/Version021201Date20251210130151.php b/lib/Migration/Version021201Date20251210130151.php index b5fde3d4..7eb97e1f 100644 --- a/lib/Migration/Version021201Date20251210130151.php +++ b/lib/Migration/Version021201Date20251210130151.php @@ -27,8 +27,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $schema = $schemaClosure(); $schemaChanged = false; - // some MariaDB/MySQL instances upgraded successfully to 2.6.0 with notNull=true - // this makes sure we bring everybody to the same notNull value for sources and attachments if ($schema->hasTable('assistant_chat_sns')) { $table = $schema->getTable('assistant_chat_sns'); if (!$table->hasColumn('summary')) { From 2611c47327278b21d346bcf0cf158c8e877b2fbb Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 11 Dec 2025 12:54:37 +0100 Subject: [PATCH 09/22] feat: Implement RESTful session endpoints Signed-off-by: Marcel Klehr --- appinfo/routes.php | 5 +- lib/Controller/ChattyLLMController.php | 15 +- openapi.json | 370 +++++++++++++++++++++---- 3 files changed, 329 insertions(+), 61 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 9d1792a7..1022f74c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,9 +36,12 @@ ['name' => 'assistantApi#getOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download', 'verb' => 'GET', 'requirements' => $requirements], ['name' => 'assistantApi#runFileAction', 'url' => '/api/{apiVersion}/file-action/{fileId}/{taskTypeId}', 'verb' => 'POST', 'requirements' => $requirements], + ['name' => 'chattyLLM#newSession', 'url' => '/chat/sessions', 'verb' => 'POST', 'postfix' => 'restful'], + ['name' => 'chattyLLM#updateChatSession', 'url' => '/chat/sessions/{sessionId}', 'verb' => 'PUT', 'postfix' => 'restful'], + ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/sessions/{sessionId}', 'verb' => 'DELETE', 'postfix' => 'restful'], + ['name' => 'chattyLLM#newSession', 'url' => '/chat/new_session', 'verb' => 'PUT'], ['name' => 'chattyLLM#updateSessionTitle', 'url' => '/chat/update_session', 'verb' => 'PATCH'], - ['name' => 'chattyLLM#updateSessionIsRemembered', 'url' => '/chat/update_is_remembered', 'verb' => 'PATCH'], ['name' => 'chattyLLM#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'], ['name' => 'chattyLLM#getSessions', 'url' => '/chat/sessions', 'verb' => 'GET'], ['name' => 'chattyLLM#newMessage', 'url' => '/chat/new_message', 'verb' => 'PUT'], diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index b94ef673..d7f44b92 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -193,10 +193,11 @@ public function updateSessionTitle(int $sessionId, string $title): JSONResponse } /** - * Update session is_remembered status + * Update session * * @param integer $sessionId The chat session ID - * @param bool $is_remembered The new is_remembered status: Whether to remember the insights from this chat session across all chat sessiosn + * @param string $title The new chat session title + * @param bool $is_remembered The new is_remembered status: Whether to remember the insights from this chat session across all chat session * @return JSONResponse|JSONResponse * * 200: The title has been updated successfully @@ -204,13 +205,15 @@ public function updateSessionTitle(int $sessionId, string $title): JSONResponse */ #[NoAdminRequired] #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] - public function updateSessionIsRemembered(int $sessionId, bool $is_remembered): JSONResponse { + public function updateChatSession(int $sessionId, string $title, bool $is_remembered): JSONResponse { if ($this->userId === null) { - return new JSONResponse(['error' => $this->l10n->t('User not logged in')], Http::STATUS_UNAUTHORIZED); + return new JSONResponse(['error' => $this->l10n->t('Could not find session')], Http::STATUS_NOT_FOUND); } try { - $this->sessionMapper->updateSessionIsRemembered($this->userId, $sessionId, $is_remembered); + $session = $this->sessionMapper->getUserSession($this->userId, $sessionId); + $session->setIsRemembered($is_remembered); + $session->setTitle($title); // schedule summarizer jobs for this chat user if ($is_remembered) { $this->sessionSummaryService->scheduleJobsForUser($this->userId); @@ -219,6 +222,8 @@ public function updateSessionIsRemembered(int $sessionId, bool $is_remembered): } catch (\OCP\DB\Exception|\RuntimeException $e) { $this->logger->warning('Failed to update the chat session', ['exception' => $e]); return new JSONResponse(['error' => $this->l10n->t('Failed to update the chat session')], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + return new JSONResponse(['error' => $this->l10n->t('Could not find session')], Http::STATUS_NOT_FOUND); } } diff --git a/openapi.json b/openapi.json index c1540350..b78e6c1d 100644 --- a/openapi.json +++ b/openapi.json @@ -2663,9 +2663,9 @@ } } }, - "/ocs/v2.php/apps/assistant/chat/new_session": { - "put": { - "operationId": "chattyllm-new-session", + "/ocs/v2.php/apps/assistant/chat/sessions": { + "post": { + "operationId": "chattyllm-new-session-restful", "summary": "Create chat session", "description": "Create a new chat session, add a system message with user instructions", "tags": [ @@ -2803,13 +2803,116 @@ } } } + }, + "get": { + "operationId": "chattyllm-get-sessions", + "summary": "Get chat sessions", + "description": "Get all chat sessions for the current user", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The session list has been obtained successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatSession" + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + } + } } }, - "/ocs/v2.php/apps/assistant/chat/update_session": { - "patch": { - "operationId": "chattyllm-update-session-title", - "summary": "Update session title", - "description": "Update the title of a chat session", + "/ocs/v2.php/apps/assistant/chat/sessions/{sessionId}": { + "put": { + "operationId": "chattyllm-update-chat-session-restful", + "summary": "Update session", "tags": [ "chat_api" ], @@ -2828,18 +2931,17 @@ "schema": { "type": "object", "required": [ - "sessionId", - "title" + "title", + "is_remembered" ], "properties": { - "sessionId": { - "type": "integer", - "format": "int64", - "description": "The chat session ID" - }, "title": { "type": "string", "description": "The new chat session title" + }, + "is_remembered": { + "type": "boolean", + "description": "The new is_remembered status: Whether to remember the insights from this chat session across all chat session" } } } @@ -2847,6 +2949,16 @@ } }, "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The chat session ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -2931,12 +3043,124 @@ } } } + }, + "delete": { + "operationId": "chattyllm-delete-session-restful", + "summary": "Delete a chat session", + "description": "Delete a chat session by ID", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The session ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "The session has been deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Not logged in", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + ] + } + } + } + } + } } }, - "/ocs/v2.php/apps/assistant/chat/update_is_remembered": { - "patch": { - "operationId": "chattyllm-update-session-is-remembered", - "summary": "Update session is_remembered status", + "/ocs/v2.php/apps/assistant/chat/new_session": { + "put": { + "operationId": "chattyllm-new-session", + "summary": "Create chat session", + "description": "Create a new chat session, add a system message with user instructions", "tags": [ "chat_api" ], @@ -2955,18 +3179,19 @@ "schema": { "type": "object", "required": [ - "sessionId", - "is_remembered" + "timestamp" ], "properties": { - "sessionId": { + "timestamp": { "type": "integer", "format": "int64", - "description": "The chat session ID" + "description": "The session creation date" }, - "is_remembered": { - "type": "boolean", - "description": "The new is_remembered status: Whether to remember the insights from this chat session across all chat sessiosn" + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "The session title" } } } @@ -2987,11 +3212,19 @@ ], "responses": { "200": { - "description": "The title has been updated successfully", + "description": "Chat session has been successfully created", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "required": [ + "session" + ], + "properties": { + "session": { + "$ref": "#/components/schemas/ChatSession" + } + } } } } @@ -3011,11 +3244,16 @@ } } } + }, + "text/plain": { + "schema": { + "type": "string" + } } } }, "401": { - "description": "Not logged in", + "description": "User is either not logged in or not found", "content": { "application/json": { "schema": { @@ -3060,11 +3298,11 @@ } } }, - "/ocs/v2.php/apps/assistant/chat/delete_session": { - "delete": { - "operationId": "chattyllm-delete-session", - "summary": "Delete a chat session", - "description": "Delete a chat session by ID", + "/ocs/v2.php/apps/assistant/chat/update_session": { + "patch": { + "operationId": "chattyllm-update-session-title", + "summary": "Update session title", + "description": "Update the title of a chat session", "tags": [ "chat_api" ], @@ -3076,17 +3314,32 @@ "basic_auth": [] } ], - "parameters": [ - { - "name": "sessionId", - "in": "query", - "description": "The session ID", - "required": true, - "schema": { - "type": "integer", - "format": "int64" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sessionId", + "title" + ], + "properties": { + "sessionId": { + "type": "integer", + "format": "int64", + "description": "The chat session ID" + }, + "title": { + "type": "string", + "description": "The new chat session title" + } + } + } } - }, + } + }, + "parameters": [ { "name": "OCS-APIRequest", "in": "header", @@ -3100,7 +3353,7 @@ ], "responses": { "200": { - "description": "The session has been deleted successfully", + "description": "The title has been updated successfully", "content": { "application/json": { "schema": { @@ -3173,11 +3426,11 @@ } } }, - "/ocs/v2.php/apps/assistant/chat/sessions": { - "get": { - "operationId": "chattyllm-get-sessions", - "summary": "Get chat sessions", - "description": "Get all chat sessions for the current user", + "/ocs/v2.php/apps/assistant/chat/delete_session": { + "delete": { + "operationId": "chattyllm-delete-session", + "summary": "Delete a chat session", + "description": "Delete a chat session by ID", "tags": [ "chat_api" ], @@ -3190,6 +3443,16 @@ } ], "parameters": [ + { + "name": "sessionId", + "in": "query", + "description": "The session ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -3203,14 +3466,11 @@ ], "responses": { "200": { - "description": "The session list has been obtained successfully", + "description": "The session has been deleted successfully", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChatSession" - } + "type": "object" } } } From 7980915c62cc3ed09c607fc53631c03f11f86d5f Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 11 Dec 2025 12:59:11 +0100 Subject: [PATCH 10/22] fix: Inject at most 10 summaries into one LLM call Signed-off-by: Marcel Klehr --- lib/Db/ChattyLLM/SessionMapper.php | 6 +++++- lib/Service/SessionSummaryService.php | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 2d468216..9006bcf6 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -83,7 +83,7 @@ public function getUserSessions(string $userId): array { * @return array * @throws \OCP\DB\Exception */ - public function getRememberedUserSessions(string $userId): array { + public function getRememberedUserSessions(string $userId, int $limit = 0): array { $qb = $this->db->getQueryBuilder(); $qb->select(Session::$columns) ->from($this->getTableName()) @@ -91,6 +91,10 @@ public function getRememberedUserSessions(string $userId): array { ->andWhere($qb->expr()->eq('is_remembered', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))) ->orderBy('timestamp', 'DESC'); + if ($limit > 0) { + $qb->setMaxResults($limit); + } + return $this->findEntities($qb); } diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php index 7e7d18a4..6a9574e5 100644 --- a/lib/Service/SessionSummaryService.php +++ b/lib/Service/SessionSummaryService.php @@ -20,10 +20,15 @@ use OCP\TaskProcessing\TaskTypes\TextToText; use Psr\Log\LoggerInterface; +/** + * We summarize chat sessions that are toggled to be remembered to inject the summaries as memories into LLM calls + */ class SessionSummaryService { public const BAtCH_SIZE = 10; public const SUMMARY_MESSAGE_LIMIT = 150; + public const MAX_INJECTED_SUMMARIES = 10; + public function __construct( private SessionMapper $sessionMapper, private MessageMapper $messageMapper, @@ -92,7 +97,7 @@ public function scheduleJobsForUser(string $userId) { */ public function getUserSessionSummaries(?string $userId): array { try { - $sessions = $this->sessionMapper->getRememberedUserSessions($userId); + $sessions = $this->sessionMapper->getRememberedUserSessions($userId, self::MAX_INJECTED_SUMMARIES); return array_filter(array_map(fn (Session $session) => $session->getSummary(), $sessions), fn ($summary) => $summary !== null); } catch (Exception $e) { $this->logger->error('Failed to get remembered user sessions', ['exception' => $e]); From 04a4d7a1a6ba7ea6a8c4cd2a96c6ae966f23d09f Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Fri, 12 Dec 2025 10:48:48 +0100 Subject: [PATCH 11/22] feat: adjust updateChatSession, only update what's necessary, fix OpenAPI annotation Signed-off-by: Julien Veyssier --- lib/Controller/ChattyLLMController.php | 27 ++++++---- openapi.json | 71 ++++++++++++++------------ 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index d7f44b92..50bd9e5d 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -196,27 +196,34 @@ public function updateSessionTitle(int $sessionId, string $title): JSONResponse * Update session * * @param integer $sessionId The chat session ID - * @param string $title The new chat session title - * @param bool $is_remembered The new is_remembered status: Whether to remember the insights from this chat session across all chat session - * @return JSONResponse|JSONResponse + * @param string|null $title The new chat session title + * @param bool|null $is_remembered The new is_remembered status: Whether to remember the insights from this chat session across all chat session + * @return JSONResponse|JSONResponse * * 200: The title has been updated successfully - * 401: Not logged in + * 404: The session was not found */ #[NoAdminRequired] #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] - public function updateChatSession(int $sessionId, string $title, bool $is_remembered): JSONResponse { + public function updateChatSession(int $sessionId, ?string $title = null, ?bool $is_remembered = null): JSONResponse { if ($this->userId === null) { return new JSONResponse(['error' => $this->l10n->t('Could not find session')], Http::STATUS_NOT_FOUND); } + if ($title === null && $is_remembered === null) { + return new JSONResponse(); + } try { $session = $this->sessionMapper->getUserSession($this->userId, $sessionId); - $session->setIsRemembered($is_remembered); - $session->setTitle($title); - // schedule summarizer jobs for this chat user - if ($is_remembered) { - $this->sessionSummaryService->scheduleJobsForUser($this->userId); + if ($title !== null) { + $session->setTitle($title); + } + if ($is_remembered !== null) { + $session->setIsRemembered($is_remembered); + // schedule summarizer jobs for this chat user + if ($is_remembered) { + $this->sessionSummaryService->scheduleJobsForUser($this->userId); + } } return new JSONResponse(); } catch (\OCP\DB\Exception|\RuntimeException $e) { diff --git a/openapi.json b/openapi.json index b78e6c1d..ea6bdd1d 100644 --- a/openapi.json +++ b/openapi.json @@ -2925,22 +2925,22 @@ } ], "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "title", - "is_remembered" - ], "properties": { "title": { "type": "string", + "nullable": true, + "default": null, "description": "The new chat session title" }, "is_remembered": { "type": "boolean", + "nullable": true, + "default": null, "description": "The new is_remembered status: Whether to remember the insights from this chat session across all chat session" } } @@ -2999,45 +2999,48 @@ } } }, + "404": { + "description": "The session was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, "401": { - "description": "Not logged in", + "description": "Current user is not logged in", "content": { "application/json": { "schema": { - "anyOf": [ - { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" - } - } - }, - { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { "type": "object", "required": [ - "ocs" + "meta", + "data" ], "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} } } - ] + } } } } From 24e3c2269ff3e3478383aa13fc8e6ef0d89d2cf5 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Fri, 12 Dec 2025 10:52:36 +0100 Subject: [PATCH 12/22] fix: typo in SessionSummaryService Signed-off-by: Julien Veyssier --- lib/Service/SessionSummaryService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php index 6a9574e5..fd3b3f85 100644 --- a/lib/Service/SessionSummaryService.php +++ b/lib/Service/SessionSummaryService.php @@ -24,7 +24,7 @@ * We summarize chat sessions that are toggled to be remembered to inject the summaries as memories into LLM calls */ class SessionSummaryService { - public const BAtCH_SIZE = 10; + public const BATCH_SIZE = 10; public const SUMMARY_MESSAGE_LIMIT = 150; public const MAX_INJECTED_SUMMARIES = 10; @@ -67,7 +67,7 @@ private function generateSummaries(array $sessions): void { public function regenerateSummariesForOutdatedSessions(string $userId): void { try { - $sessions = $this->sessionMapper->getRememberedUserSessionsWithOutdatedSummaries($userId, self::BAtCH_SIZE); + $sessions = $this->sessionMapper->getRememberedUserSessionsWithOutdatedSummaries($userId, self::BATCH_SIZE); $this->generateSummaries($sessions); } catch (Exception $e) { $this->logger->warning('Failed to generate chat summaries for outdated sessions', ['exception' => $e]); @@ -76,7 +76,7 @@ public function regenerateSummariesForOutdatedSessions(string $userId): void { public function generateSummariesForNewSessions(string $userId): void { try { - $sessions = $this->sessionMapper->getRememberedUserSessionsWithoutSummaries($userId, self::BAtCH_SIZE); + $sessions = $this->sessionMapper->getRememberedUserSessionsWithoutSummaries($userId, self::BATCH_SIZE); $this->generateSummaries($sessions); } catch (Exception $e) { $this->logger->warning('Failed to generate chat summaries for new sessions', ['exception' => $e]); From 86b68e582c9c7e647505903d25b953ea05905e4f Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Fri, 12 Dec 2025 13:48:24 +0100 Subject: [PATCH 13/22] feat: add remember button in Chat UI Signed-off-by: Jana Peper --- lib/Controller/ChattyLLMController.php | 1 + .../ChattyLLM/ChattyLLMInputForm.vue | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index 50bd9e5d..98b2a767 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -823,6 +823,7 @@ public function checkSession(int $sessionId): JSONResponse { 'messageTaskId' => null, 'titleTaskId' => null, 'sessionTitle' => $session->getTitle(), + 'sessionIsRemembered' => $session->getIsRemembered(), 'sessionAgencyPendingActions' => $p, ]; if (!empty($messageTasks)) { diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 3dbcc2b3..ceeee9da 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -55,7 +55,14 @@ @submit-text="onEditSessionTitle" />
- + + + + {{ t('assistant', 'Remember this') }} + @@ -68,8 +82,11 @@ import AssistantIcon from './icons/AssistantIcon.vue' import NcFormGroup from '@nextcloud/vue/components/NcFormGroup' import NcFormBox from '@nextcloud/vue/components/NcFormBox' import NcFormBoxSwitch from '@nextcloud/vue/components/NcFormBoxSwitch' +import NcFormBoxButton from '@nextcloud/vue/components/NcFormBoxButton' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import MemoryIcon from 'vue-material-design-icons/Memory.vue' + import { loadState } from '@nextcloud/initial-state' import { generateUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' @@ -83,7 +100,9 @@ export default { NcFormGroup, NcFormBox, NcFormBoxSwitch, + NcFormBoxButton, NcNoteCard, + MemoryIcon, }, props: [], @@ -92,6 +111,7 @@ export default { return { state: loadState('assistant', 'config'), providers: loadState('assistant', 'availableProviders'), + rememberedConversations: loadState('assistant', 'rememberedSessions'), } }, From 0705128867eff7b4663a93b8ed45290957a6b34f Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Tue, 23 Dec 2025 10:56:58 +0100 Subject: [PATCH 22/22] feat(ui): change button icon and make text reactive Signed-off-by: Jana Peper --- lib/Settings/Personal.php | 2 +- src/components/ChattyLLM/ChattyLLMInputForm.vue | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 98df7ff1..6e1e2ff1 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -63,7 +63,7 @@ public function getForm(): TemplateResponse { $speechToTextPickerAvailable = $speechToTextAvailable && $this->appConfig->getValueString(Application::APP_ID, 'speech_to_text_picker_enabled', '1') === '1'; $speechToTextPickerEnabled = $this->config->getUserValue($this->userId, Application::APP_ID, 'speech_to_text_picker_enabled', '1') === '1'; - + $userConfig = [ 'task_processing_available' => $taskProcessingAvailable, diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 440dd90f..cdbbda3d 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -58,10 +58,9 @@ - {{ t('assistant', 'Remember this') }} + {{ active.is_remembered ? t('assistant', 'Remembered') : t('assistant', 'Remember this') }}