diff --git a/appinfo/routes.php b/appinfo/routes.php index d076cbc5..1022f74c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,6 +36,10 @@ ['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#deleteSession', 'url' => '/chat/delete_session', 'verb' => 'DELETE'], diff --git a/lib/BackgroundJob/GenerateNewChatSummaries.php b/lib/BackgroundJob/GenerateNewChatSummaries.php new file mode 100644 index 00000000..422d0846 --- /dev/null +++ b/lib/BackgroundJob/GenerateNewChatSummaries.php @@ -0,0 +1,28 @@ +setInterval(60 * 10); // 10min + } + public function run($argument) { + $userId = $argument['userId']; + $this->sessionSummaryService->generateSummariesForNewSessions($userId); + } +} diff --git a/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php new file mode 100644 index 00000000..bc886b79 --- /dev/null +++ b/lib/BackgroundJob/RegenerateOutdatedChatSummariesJob.php @@ -0,0 +1,29 @@ +setInterval(60 * 60 * 24); // 24h + } + public function run($argument) { + $userId = $argument['userId']; + $this->sessionSummaryService->regenerateSummariesForOutdatedSessions($userId); + } +} diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index d359312a..bde83575 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,49 @@ public function updateSessionTitle(int $sessionId, string $title): JSONResponse } } + /** + * Update session + * + * @param integer $sessionId The chat session ID + * @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 + * 404: The session was not found + */ + #[NoAdminRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['chat_api'])] + 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); + 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); + } + } + $this->sessionMapper->update($session); + 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); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + return new JSONResponse(['error' => $this->l10n->t('Could not find session')], Http::STATUS_NOT_FOUND); + } + } + /** * Delete a chat session * @@ -779,6 +824,7 @@ public function checkSession(int $sessionId): JSONResponse { 'messageTaskId' => null, 'titleTaskId' => null, 'sessionTitle' => $session->getTitle(), + 'is_remembered' => $session->getIsRemembered(), 'sessionAgencyPendingActions' => $p, ]; if (!empty($messageTasks)) { @@ -990,6 +1036,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; @@ -1017,6 +1066,10 @@ private function scheduleAgencyTask(string $content, int $confirmation, string $ '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); + } + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID, $taskInput, @@ -1039,6 +1092,10 @@ private function scheduleAudioChatTask( 'history' => $history, ]; /** @psalm-suppress UndefinedClass */ + 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, $input, @@ -1061,6 +1118,10 @@ private function scheduleAgencyAudioTask( '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); + } + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, $taskInput, diff --git a/lib/Db/ChattyLLM/Message.php b/lib/Db/ChattyLLM/Message.php index e01724ae..b60252bb 100644 --- a/lib/Db/ChattyLLM/Message.php +++ b/lib/Db/ChattyLLM/Message.php @@ -66,11 +66,11 @@ class Message extends Entity implements \JsonSerializable { ]; public function __construct() { - $this->addType('session_id', Types::INTEGER); + $this->addType('sessionId', Types::INTEGER); $this->addType('role', Types::STRING); $this->addType('content', Types::STRING); $this->addType('timestamp', Types::INTEGER); - $this->addType('ocp_task_id', Types::INTEGER); + $this->addType('ocpTaskId', Types::INTEGER); $this->addType('sources', Types::STRING); $this->addType('attachments', Types::STRING); } diff --git a/lib/Db/ChattyLLM/Session.php b/lib/Db/ChattyLLM/Session.php index 11ba78c4..fad23840 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,24 @@ class Session extends Entity implements \JsonSerializable { /** @var ?string */ protected $agencyPendingActions; + /** + * Will be used to inject into assistant memories upon calling LLM + * + * @var ?string + */ + protected $summary; + + /** @var int */ + protected $isSummaryUpToDate; + + /** + * Whether to remember the insights from this chat session across all chat sessions + * + * @var int + */ + protected $isRemembered; + + public static $columns = [ 'id', 'user_id', @@ -43,6 +63,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,14 +74,20 @@ class Session extends Entity implements \JsonSerializable { 'timestamp', 'agencyConversationToken', 'agencyPendingActions', + 'summary', + 'isSummaryUpToDate', + 'isRemembered', ]; public function __construct() { - $this->addType('user_id', Types::STRING); + $this->addType('userId', Types::STRING); $this->addType('title', Types::STRING); $this->addType('timestamp', Types::INTEGER); - $this->addType('agency_conversation_token', Types::STRING); - $this->addType('agency_pending_actions', Types::STRING); + $this->addType('agencyConversationToken', Types::STRING); + $this->addType('agencyPendingActions', Types::STRING); + $this->addType('summary', Types::TEXT); + $this->addType('isSummaryUpToDate', Types::SMALLINT); + $this->addType('isRemembered', Types::SMALLINT); } #[\ReturnTypeWillChange] @@ -70,6 +99,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..9006bcf6 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -79,6 +79,75 @@ public function getUserSessions(string $userId): array { return $this->findEntities($qb); } + /** + * @return array + * @throws \OCP\DB\Exception + */ + public function getRememberedUserSessions(string $userId, int $limit = 0): 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'); + + if ($limit > 0) { + $qb->setMaxResults($limit); + } + + return $this->findEntities($qb); + } + + /** + * @return array + * @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 array + * @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 array + * @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))) + ->andWhere($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 +179,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..7eb97e1f --- /dev/null +++ b/lib/Migration/Version021201Date20251210130151.php @@ -0,0 +1,57 @@ +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..7df97af5 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{ @@ -77,6 +80,7 @@ * titleTaskId: ?int, * sessionTitle: ?string, * sessionAgencyPendingActions: ?array, + * is_remembered: ?bool, * } */ class ResponseDefinitions { diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php new file mode 100644 index 00000000..fd3b3f85 --- /dev/null +++ b/lib/Service/SessionSummaryService.php @@ -0,0 +1,108 @@ +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 array + */ + public function getUserSessionSummaries(?string $userId): array { + try { + $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]); + return []; + } + } + +} diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 375160d5..6e1e2ff1 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -8,6 +8,7 @@ namespace OCA\Assistant\Settings; use OCA\Assistant\AppInfo\Application; +use OCA\Assistant\Db\ChattyLLM\SessionMapper; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\IAppConfig; @@ -27,6 +28,7 @@ public function __construct( private IInitialState $initialStateService, private ?string $userId, private ITaskProcessingManager $taskProcessingManager, + private SessionMapper $sessionMapper, ) { } @@ -61,6 +63,8 @@ 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, 'assistant_available' => $assistantAvailable, @@ -91,6 +95,9 @@ public function getForm(): TemplateResponse { } } $this->initialStateService->provideInitialState('availableProviders', $availableProviders); + + $rememberedSessions = $this->sessionMapper->getRememberedUserSessions($this->userId); + $this->initialStateService->provideInitialState('rememberedSessions', $rememberedSessions); return new TemplateResponse(Application::APP_ID, 'personalSettings'); } diff --git a/lib/TaskProcessing/AudioToAudioChatProvider.php b/lib/TaskProcessing/AudioToAudioChatProvider.php index 61c7576a..7d5f3d0b 100644 --- a/lib/TaskProcessing/AudioToAudioChatProvider.php +++ b/lib/TaskProcessing/AudioToAudioChatProvider.php @@ -14,7 +14,10 @@ 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; 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 IManager $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,20 @@ 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']; + } + /** @psalm-suppress InvalidArgument */ $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..a195aec4 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -14,7 +14,10 @@ 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; use OCP\TaskProcessing\TaskTypes\AudioToText; use OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction; @@ -29,6 +32,7 @@ public function __construct( private IL10N $l, private TaskProcessingService $taskProcessingService, private LoggerInterface $logger, + private IManager $taskProcessingManager, ) { } @@ -59,7 +63,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 +126,19 @@ public function process(?string $userId, array $input, callable $reportProgress) // context agent try { + $contextAgentTaskInput = [ + 'input' => $inputTranscription, + 'confirmation' => $confirmation, + 'conversation_token' => $conversationToken, + ]; /** @psalm-suppress UndefinedClass */ + if (isset($input['memories'], $this->taskProcessingManager->getAvailableTaskTypes()[ContextAgentAudioInteraction::ID]['optionalInputShape']['memories'])) { + $contextAgentTaskInput['memories'] = $input['memories']; + } + /** @psalm-suppress UndefinedClass,InvalidArgument */ $task = new Task( ContextAgentInteraction::ID, - [ - 'input' => $inputTranscription, - 'confirmation' => $confirmation, - 'conversation_token' => $conversationToken, - ], + $contextAgentTaskInput, Application::APP_ID . ':internal', $userId, ); diff --git a/openapi.json b/openapi.json index f8c9d98f..235a5539 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" } } }, @@ -143,7 +156,8 @@ "messageTaskId", "titleTaskId", "sessionTitle", - "sessionAgencyPendingActions" + "sessionAgencyPendingActions", + "is_remembered" ], "properties": { "messageTaskId": { @@ -166,6 +180,10 @@ "additionalProperties": { "type": "object" } + }, + "is_remembered": { + "type": "boolean", + "nullable": true } } }, @@ -2650,9 +2668,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": [ @@ -2790,13 +2808,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" ], @@ -2809,24 +2930,23 @@ } ], "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "sessionId", - "title" - ], "properties": { - "sessionId": { - "type": "integer", - "format": "int64", - "description": "The chat session ID" - }, "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" } } } @@ -2834,6 +2954,16 @@ } }, "parameters": [ + { + "name": "sessionId", + "in": "path", + "description": "The chat session ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -2874,6 +3004,120 @@ } } }, + "404": { + "description": "The session was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "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": { @@ -2920,11 +3164,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/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" ], @@ -2936,17 +3180,174 @@ "basic_auth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "timestamp" + ], + "properties": { + "timestamp": { + "type": "integer", + "format": "int64", + "description": "The session creation date" + }, + "title": { + "type": "string", + "nullable": true, + "default": null, + "description": "The session title" + } + } + } + } + } + }, "parameters": [ { - "name": "sessionId", - "in": "query", - "description": "The session ID", + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Chat session has been successfully created", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "session" + ], + "properties": { + "session": { + "$ref": "#/components/schemas/ChatSession" + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + } + } + }, + "text/plain": { + "schema": { + "type": "string" + } + } } }, + "401": { + "description": "User is either not logged in or not found", + "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", + "tags": [ + "chat_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "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", @@ -2960,7 +3361,7 @@ ], "responses": { "200": { - "description": "The session has been deleted successfully", + "description": "The title has been updated successfully", "content": { "application/json": { "schema": { @@ -3033,11 +3434,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" ], @@ -3050,6 +3451,16 @@ } ], "parameters": [ + { + "name": "sessionId", + "in": "query", + "description": "The session ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, { "name": "OCS-APIRequest", "in": "header", @@ -3063,14 +3474,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" } } } diff --git a/src/components/ChattyLLM/ChattyLLMInputForm.vue b/src/components/ChattyLLM/ChattyLLMInputForm.vue index 3dbcc2b3..9fa8f6e8 100644 --- a/src/components/ChattyLLM/ChattyLLMInputForm.vue +++ b/src/components/ChattyLLM/ChattyLLMInputForm.vue @@ -54,6 +54,11 @@ :max-length="100" @submit-text="onEditSessionTitle" /> +
+ + {{ t('assistant', 'Remember this') }} + +
@@ -171,6 +176,7 @@ import TrashCanOutlineIcon from 'vue-material-design-icons/TrashCanOutline.vue' import AssistantIcon from '../icons/AssistantIcon.vue' import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcActions from '@nextcloud/vue/components/NcActions' import NcAppContent from '@nextcloud/vue/components/NcAppContent' import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' @@ -215,6 +221,7 @@ export default { AssistantIcon, NcActionButton, + NcCheckboxRadioSwitch, NcActions, NcAppContent, NcAppNavigation, @@ -305,6 +312,7 @@ export default { console.debug('update session title with check result') } console.debug('check session response:', checkSessionResponseData) + this.active.is_remembered = checkSessionResponseData?.is_remembered // update the pending actions when switching conversations this.active.sessionAgencyPendingActions = checkSessionResponseData?.sessionAgencyPendingActions this.active.agencyAnswered = false @@ -779,6 +787,14 @@ export default { return false }, + async updateSession() { + await axios.put(getChatURL(`/sessions/${this.active.id}`), { + title: this.active.title, + is_remembered: this.active.is_remembered, + }) + + }, + async updateLastHumanMessageContent() { const lastHumanMessage = this.getLastHumanMessage() if (lastHumanMessage) { @@ -989,6 +1005,10 @@ export default { &__title { width: 100%; } + + &__remember { + white-space: nowrap; + } } &__chat-area { diff --git a/src/components/PersonalSettings.vue b/src/components/PersonalSettings.vue index 6c6b3543..5fd50c1d 100644 --- a/src/components/PersonalSettings.vue +++ b/src/components/PersonalSettings.vue @@ -58,6 +58,20 @@ {{ taskNames.join(', ') }}
+
+

{{ t('assistant', 'Remembered conversations') }}

+

{{ t('assistant', 'The following conversations are remembered by the Assistant Chat and will be taken into account for every new conversation:') }}

+ + + + + +
@@ -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'), } },