diff --git a/appinfo/routes.php b/appinfo/routes.php index 89037fcabe..a06af47f77 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -495,6 +495,16 @@ 'url' => '/api/follow-up/check-message-ids', 'verb' => 'POST', ], + [ + 'name' => 'AiPrompt#getPrompts', + 'url' => '/api/prompts', + 'verb' => 'GET', + ], + [ + 'name' => 'AiPrompt#setPrompt', + 'url' => '/api/prompts/{key}', + 'verb' => 'POST', + ], ], 'resources' => [ 'accounts' => ['url' => '/api/accounts'], diff --git a/lib/Controller/AiPromptController.php b/lib/Controller/AiPromptController.php new file mode 100644 index 0000000000..7b6eb98010 --- /dev/null +++ b/lib/Controller/AiPromptController.php @@ -0,0 +1,91 @@ +service->getEventDataPromptPreamble(); + $prompts['summarize_email_prompt'] = $this->service->getSummarizeEmailPrompt(); + $prompts['smart_reply_prompt_preamble'] = $this->service->getSmartReplyPromptPreamble(); + $prompts['smart_reply_prompt_postamble'] = $this->service->getSmartReplyPromptPostamble(); + $prompts['requires_followup_prompt_preamble'] = $this->service->getRequiresFollowupPromptPreamble(); + $prompts['requires_followup_prompt_postamble'] = $this->service->getRequiresFollowupPromptPostamble(); + + return JsonResponse::success($prompts, Http::STATUS_CREATED); + } + + /** + * @NoAdminRequired + * + * @param string $email + * @param string $type + * @return JsonResponse + */ + #[TrapError] + public function setPrompt(string $key, string $value): JsonResponse { + switch ($key) { + case 'event_data_prompt_preamble': + $this->service->setEventDataPromptPreamble($value); + break; + case 'summarize_email_prompt': + $this->service->setSummarizeEmailPrompt($value); + break; + case 'smart_reply_prompt_preamble': + $this->service->setSmartReplyPreamble($value); + break; + case 'smart_reply_prompt_postamble': + $this->service->setSmartReplyPostamble($value); + break; + case 'requires_followup_prompt_preamble': + $this->service->setRequiresFollowupPreamble($value); + break; + case 'requires_followup_prompt_postamble': + $this->service->setRequiresFollowupPostamble($value); + break; + default: + return JsonResponse::error('Invalid prompt key', Http::STATUS_BAD_REQUEST); + } + + return JsonResponse::success(null); + } + /** + * @NoAdminRequired + * + * @return JsonResponse + */ + #[TrapError] + public function list(): JsonResponse { + $list = $this->trustedSenderService->getTrusted( + $this->uid + ); + + return JsonResponse::success($list); + } +} diff --git a/lib/Service/AiIntegrations/AiIntegrationsPromptsService.php b/lib/Service/AiIntegrations/AiIntegrationsPromptsService.php new file mode 100644 index 0000000000..72a7b7ffd9 --- /dev/null +++ b/lib/Service/AiIntegrations/AiIntegrationsPromptsService.php @@ -0,0 +1,107 @@ +; +--- +Tell me what the function outputs for the following parameters. + +emailText:\r\n"; + + private const REQUIRES_FOLLOWUP_PROMPT_POSTAMBLE = "\r\nThe JSON output should be in the form: {\"expectsReply\": true} +Never return null or undefined."; + + public function getEventDataPromptPreamble(): string { + return $this->appConfig->getValueString('mail', 'event_data_prompt_preamble', self::EVENT_DATA_PROMPT_PREAMBLE); + } + public function getSummarizeEmailPrompt(): string { + return $this->appConfig->getValueString('mail', 'summarize_email_prompt', self::SUMMARIZE_EMAIL_PROMPT); + } + public function getSmartReplyPrompt(string $message): string { + return $this->appConfig->getValueString('mail', 'smart_reply_prompt_preamble', self::SMART_REPLY_PROMPT_PREAMPBLE) . $message . $this->appConfig->getAppValue('mail', 'smart_reply_prompt_postamble', self::SMART_REPLY_PROMPT_POSTAMBLE); + } + public function getSmartReplyPromptPreamble(): string { + return $this->appConfig->getValueString('mail', 'smart_reply_prompt_preamble', self::SMART_REPLY_PROMPT_PREAMPBLE); + } + public function getSmartReplyPromptPostamble(): string { + return $this->appConfig->getValueString('mail', 'smart_reply_prompt_postamble', self::SMART_REPLY_PROMPT_POSTAMBLE); + } + + public function getRequiresFollowupPrompt(string $message): string { + return $this->appConfig->getValueString('mail', 'requires_followup_prompt_preamble', self::REQUIRES_FOLLOWUP_PROMPT_PREAMPBLE) . $message . $this->appConfig->getAppValue('mail', 'requires_followup_prompt_postamble', self::REQUIRES_FOLLOWUP_PROMPT_POSTAMBLE); + } + public function getRequiresFollowupPromptPreamble(): string { + return $this->appConfig->getValueString('mail', 'requires_followup_prompt_preamble', self::REQUIRES_FOLLOWUP_PROMPT_PREAMPBLE); + } + public function getRequiresFollowupPromptPostamble(): string { + return $this->appConfig->getValueString('mail', 'requires_followup_prompt_postamble', self::REQUIRES_FOLLOWUP_PROMPT_POSTAMBLE); + } + + public function setEventDataPromptPreamble(string $preamble): void { + $this->appConfig->setValueString('mail', 'event_data_prompt_preamble', $preamble); + } + public function setSummarizeEmailPrompt(string $prompt): void { + $this->appConfig->setValueString('mail', 'summarize_email_prompt', $prompt); + } + public function setSmartReplyPreamble(string $prompt): void { + $this->appConfig->setValueString('mail', 'smart_reply_prompt_preamble', $prompt); + } + + public function setSmartReplyPostamble(string $postamble): void { + $this->appConfig->setValueString('mail', 'smart_reply_prompt_postamble', $postamble); + } + public function setRequiresFollowupPreamble(string $preamble): void { + $this->appConfig->setValueString('mail', 'requires_followup_prompt_preamble', $preamble); + } + public function setRequiresFollowupPostamble(string $postamble): void { + $this->appConfig->setValueString('mail', 'requires_followup_prompt_postamble', $postamble); + } + + +} diff --git a/lib/Service/AiIntegrations/AiIntegrationsService.php b/lib/Service/AiIntegrations/AiIntegrationsService.php index 30f414ae03..a354d64b6c 100644 --- a/lib/Service/AiIntegrations/AiIntegrationsService.php +++ b/lib/Service/AiIntegrations/AiIntegrationsService.php @@ -38,13 +38,6 @@ class AiIntegrationsService { - private const EVENT_DATA_PROMPT_PREAMBLE = <<getPlainBody(); - $prompt = "You are tasked with formulating a helpful summary of a email message. \r\n" - . "The summary should be less than 160 characters. \r\n" - . "Output *ONLY* the summary itself, leave out any introduction. \r\n" - . "Here is the ***E-MAIL*** for which you must generate a helpful summary: \r\n" + $prompt = $this->promptsService->getSummarizeEmailPrompt() . "***START_OF_E-MAIL***\r\n$messageBody\r\n***END_OF_E-MAIL***\r\n"; $task = new TaskProcessingTask( TextToText::ID, @@ -203,7 +194,7 @@ public function generateEventData(Account $account, string $threadId, array $mes $task = new Task( FreePromptTaskType::class, - self::EVENT_DATA_PROMPT_PREAMBLE . implode("\n\n---\n\n", $messageBodies), + $this->promptsService->getEventDataPromptPreamble() . implode("\n\n---\n\n", $messageBodies), 'mail', $currentUserId, "event_data_$threadId", @@ -248,20 +239,7 @@ public function getSmartReply(Account $account, Mailbox $mailbox, Message $messa } finally { $client->logout(); } - $prompt = "You are tasked with formulating helpful replies or reply templates to e-mails provided that have been sent to me. If you don't know some relevant information for answering the e-mails (like my schedule) leave blanks in the text that can later be filled by me. You must write the replies from my point of view as replies to the original sender of the provided e-mail! - - Formulate two extremely succinct reply suggestions to the provided ***E-MAIL***. Please, do not invent any context for the replies but, rather, leave blanks for me to fill in with relevant information where necessary. Provide the output formatted as valid JSON with the keys 'reply1' and 'reply2' for the reply suggestions. - - Each suggestion must be of 25 characters or less. - - Here is the ***E-MAIL*** for which you must suggest the replies to: - - ***START_OF_E-MAIL***" . $messageBody . " - - ***END_OF_E-MAIL*** - - Please, output *ONLY* a valid JSON string with the keys 'reply1' and 'reply2' for the reply suggestions. Leave out any other text besides the JSON! Be extremely succinct and write the replies from my point of view. - "; + $prompt = $this->promptsService->getSmartReplyPrompt($messageBody); $task = new Task(FreePromptTaskType::class, $prompt, 'mail,', $currentUserId); $manager->runTask($task); $replies = $task->getOutput(); @@ -323,21 +301,7 @@ public function requiresFollowUp( $messageBody = $imapMessage->getPlainBody(); $messageBody = str_replace('"', '\"', $messageBody); - $prompt = "Consider the following TypeScript function prototype: ---- -/** - * This function takes in an email text and returns a boolean indicating whether the email author expects a response. - * - * @param emailText - string with the email text - * @returns boolean true if the email expects a reply, false if not - */ -declare function doesEmailExpectReply(emailText: string): Promise; ---- -Tell me what the function outputs for the following parameters. - -emailText: \"$messageBody\" -The JSON output should be in the form: {\"expectsReply\": true} -Never return null or undefined."; + $prompt = $this->promptsService->getRequiresFollowupPrompt($messageBody); $task = new Task(FreePromptTaskType::class, $prompt, Application::APP_ID, $currentUserId); $manager->runTask($task); diff --git a/lib/Settings/AiPromptSettings.php b/lib/Settings/AiPromptSettings.php new file mode 100644 index 0000000000..64440f4df1 --- /dev/null +++ b/lib/Settings/AiPromptSettings.php @@ -0,0 +1,56 @@ +initialStateService->provideInitialState( + Application::APP_ID, + 'llm_processing', + $this->aiIntegrationsService->isLlmProcessingEnabled(), + ); + $this->initialStateService->provideInitialState( + Application::APP_ID, + 'enabled_llm_free_prompt_backend', + $this->aiIntegrationsService->isLlmAvailable(FreePromptTaskType::class) + ); + + return new TemplateResponse(Application::APP_ID, 'ai-prompt-settings'); + } + + public function getSection(): string { + if ($this->aiIntegrationsService->isLlmProcessingEnabled()) { + return null; + } + return 'ai'; + } + + public function getPriority(): int { + return 11; + } +} diff --git a/src/ai-prompt-settings.js b/src/ai-prompt-settings.js new file mode 100644 index 0000000000..f5382b97e7 --- /dev/null +++ b/src/ai-prompt-settings.js @@ -0,0 +1,13 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import Vue from 'vue' +import { translate } from '@nextcloud/l10n' +import AdminAiPrommptsSettings from './components/settings/AdminAiPrommptsSettings.vue' + +Vue.prototype.$t = translate + +const View = Vue.extend(AdminAiPrommptsSettings); + +(new View({})).$mount('#ai-prompts-settings') diff --git a/src/components/settings/AdminAiPrommptsSettings.vue b/src/components/settings/AdminAiPrommptsSettings.vue new file mode 100644 index 0000000000..2e00d746f8 --- /dev/null +++ b/src/components/settings/AdminAiPrommptsSettings.vue @@ -0,0 +1,209 @@ + + + + + + diff --git a/src/service/AiPromptsService.js b/src/service/AiPromptsService.js new file mode 100644 index 0000000000..727f0c7c17 --- /dev/null +++ b/src/service/AiPromptsService.js @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { generateUrl } from '@nextcloud/router' +import axios from '@nextcloud/axios' +import { convertAxiosError } from '../errors/convert.js' + +export const getPromptes = async () => { + const url = generateUrl('/apps/mail/api/prompts') + + try { + const resp = await axios.get(url) + return resp.data.data + } catch (e) { + throw convertAxiosError(e) + } +} + +export const savePromptValue = async (value, key) => { + const url = generateUrl('/apps/mail/api/prompts/{key}', { key }) + + try { + await axios.post(url, { value }) + } catch (e) { + throw convertAxiosError(e) + } +} diff --git a/templates/ai-prompt-settings.php b/templates/ai-prompt-settings.php new file mode 100644 index 0000000000..faded4f74f --- /dev/null +++ b/templates/ai-prompt-settings.php @@ -0,0 +1,12 @@ + +
+