From f94ae80c0928decbe872aaf764d8ff11bb05bab5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Sitkiewicz Date: Fri, 25 Sep 2026 13:00:26 +0200 Subject: [PATCH 1/5] Revert "feat: make summary system prompt configurable" This reverts commit f9dfdf8fcf8c09aba9c55e9b63af6e00ce7e03aa. --- lib/AppInfo/Application.php | 2 -- lib/Service/OpenAiSettingsService.php | 29 +-------------------- lib/TaskProcessing/ProviderFactory.php | 4 +-- lib/TaskProcessing/SummaryProvider.php | 5 ++-- src/components/AdminSettings.vue | 24 ----------------- tests/unit/Providers/OpenAiProviderTest.php | 5 ++-- 6 files changed, 6 insertions(+), 63 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 708d1b35..c34ddfe6 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -46,8 +46,6 @@ class Application extends App implements IBootstrap { public const DEFAULT_QUOTA_PERIOD = 30; public const MAX_QUOTA_PERIOD_DAYS = 3650; public const MAX_QUOTA_PERIOD_MONTHS = 120; - public const DEFAULT_SUMMARY_SYSTEM_PROMPT = 'You are a helpful assistant that summarizes text in the same language as the text. ' - . 'You should only return the summary without any additional information.'; public const DEFAULT_QUOTA_CONFIG = ['length' => self::DEFAULT_QUOTA_PERIOD, 'unit' => 'day', 'day' => 1]; public const DEFAULT_OPENAI_TEXT_GENERATION_TIME = 10; // seconds diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index 33e17570..8e48a555 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -27,7 +27,6 @@ class OpenAiSettingsService { private const ADMIN_CONFIG_TYPES = [ 'quota_period' => 'array', 'usage_storage_time' => 'integer', - 'summary_system_prompt' => 'string', ]; private const USER_CONFIG_TYPES = [ @@ -136,19 +135,6 @@ public function getUsageStorageTime(): int { return $this->appConfig->getValueInt(Application::APP_ID, 'usage_storage_time', Application::DEFAULT_QUOTA_PERIOD, lazy: true); } - /** - * System prompt for the summary provider. Empty falls back to the default unless $raw is true. - */ - public function getSummarySystemPrompt(bool $raw = false): string { - $default = $raw ? '' : Application::DEFAULT_SUMMARY_SYSTEM_PROMPT; - return $this->appConfig->getValueString( - Application::APP_ID, - 'summary_system_prompt', - $default, - lazy: true - ) ?: $default; - } - /** * @param string|null $userId * @return string @@ -160,13 +146,12 @@ public function getUserSTTLanguage(?string $userId): string { /** * Get the instance-wide admin config for the settings page * - * @return array{quota_period: array, usage_storage_time: int, summary_system_prompt: string} + * @return array{quota_period: array, usage_storage_time: int} */ public function getAdminConfig(): array { return [ 'quota_period' => $this->getQuotaPeriod(), 'usage_storage_time' => $this->getUsageStorageTime(), - 'summary_system_prompt' => $this->getSummarySystemPrompt(true), ]; } @@ -233,15 +218,6 @@ public function setUsageStorageTime(int $usageStorageTime): void { $this->appConfig->setValueInt(Application::APP_ID, 'usage_storage_time', $usageStorageTime, lazy: true); } - public function setSummarySystemPrompt(string $summarySystemPrompt): void { - $this->appConfig->setValueString( - Application::APP_ID, - 'summary_system_prompt', - $summarySystemPrompt, - lazy: true - ); - } - /** * @param string $userId * @param string $language @@ -274,9 +250,6 @@ public function setAdminConfig(array $adminConfig): void { if (isset($adminConfig['usage_storage_time'])) { $this->setUsageStorageTime($adminConfig['usage_storage_time']); } - if (isset($adminConfig['summary_system_prompt'])) { - $this->setSummarySystemPrompt($adminConfig['summary_system_prompt']); - } } /** diff --git a/lib/TaskProcessing/ProviderFactory.php b/lib/TaskProcessing/ProviderFactory.php index 2f2949e7..43a2792e 100644 --- a/lib/TaskProcessing/ProviderFactory.php +++ b/lib/TaskProcessing/ProviderFactory.php @@ -12,7 +12,6 @@ use OCA\OpenAi\AppInfo\Application; use OCA\OpenAi\Service\ChunkService; use OCA\OpenAi\Service\OpenAiAPIService; -use OCA\OpenAi\Service\OpenAiSettingsService; use OCA\OpenAi\Service\ServiceConfig; use OCA\OpenAi\Service\ServicesService; use OCA\OpenAi\Service\TranslateService; @@ -36,7 +35,6 @@ class ProviderFactory { public function __construct( private ServicesService $servicesService, private OpenAiAPIService $openAiAPIService, - private OpenAiSettingsService $openAiSettingsService, private ChunkService $chunkService, private TranslateService $translateService, private WatermarkingService $watermarkingService, @@ -82,7 +80,7 @@ private function getTextProviders(ServiceConfig $service, string $model): array new TextToTextProvider($this->openAiAPIService, $this->l, $service, $model), new TextToTextChatProvider($this->openAiAPIService, $this->l, $service, $model), new TextToTextChatWithToolsProvider($this->openAiAPIService, $this->l, $service, $model), - new SummaryProvider($this->openAiAPIService, $this->l, $this->chunkService, $this->openAiSettingsService, $service, $model), + new SummaryProvider($this->openAiAPIService, $this->l, $this->chunkService, $service, $model), new HeadlineProvider($this->openAiAPIService, $this->l, $service, $model), new TopicsProvider($this->openAiAPIService, $this->l, $this->chunkService, $this->logger, $service, $model), new ContextWriteProvider($this->openAiAPIService, $this->chunkService, $this->l, $service, $model), diff --git a/lib/TaskProcessing/SummaryProvider.php b/lib/TaskProcessing/SummaryProvider.php index 63c1e140..f84709f5 100644 --- a/lib/TaskProcessing/SummaryProvider.php +++ b/lib/TaskProcessing/SummaryProvider.php @@ -11,7 +11,6 @@ use OCA\OpenAi\Service\ChunkService; use OCA\OpenAi\Service\OpenAiAPIService; -use OCA\OpenAi\Service\OpenAiSettingsService; use OCA\OpenAi\Service\ServiceConfig; use OCP\IL10N; use OCP\TaskProcessing\EShapeType; @@ -29,7 +28,6 @@ public function __construct( private OpenAiAPIService $openAiAPIService, private IL10N $l, private ChunkService $chunkService, - private OpenAiSettingsService $openAiSettingsService, private ServiceConfig $service, private string $model, ) { @@ -144,7 +142,8 @@ public function process(?string $userId, array $input, callable $reportProgress) try { $completions = []; - $summarySystemPrompt = $this->openAiSettingsService->getSummarySystemPrompt() . ' '; + $summarySystemPrompt = 'You are a helpful assistant that summarizes text in the same language as the text. ' + . 'You should only return the summary without any additional information. '; if (isset($input['format'])) { if ($input['format'] === 'paragraph') { $summarySystemPrompt .= 'Return the summary as a paragraph. '; diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 91f9c0b6..4503922c 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -58,19 +58,6 @@ -

{{ t('integration_openai', 'Text generation') }}

-
- -
-

{{ t('integration_openai', 'Usage limits') }}

openAiSettingsService->setSummarySystemPrompt('This is a custom summary system prompt'); $summaryProvider = new SummaryProvider( $this->openAiApiService, $this->createMock(\OCP\IL10N::class), $this->chunkService, - $this->openAiSettingsService, $this->service, self::TEXT_MODEL, ); @@ -578,7 +576,8 @@ public function testSummaryProvider(): void { $url = self::OPENAI_API_BASE . 'chat/completions'; $options = ['timeout' => Application::OPENAI_DEFAULT_REQUEST_TIMEOUT, 'headers' => ['User-Agent' => Application::USER_AGENT, 'Authorization' => self::AUTHORIZATION_HEADER, 'Content-Type' => 'application/json']]; - $systemPrompt = 'This is a custom summary system prompt '; + $systemPrompt = 'You are a helpful assistant that summarizes text in the same language as the text. ' + . 'You should only return the summary without any additional information. '; $options['body'] = json_encode([ 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, 'messages' => [ From 7fbbe02b46a4901e6bac8735182356eace83df24 Mon Sep 17 00:00:00 2001 From: Arkadiusz Sitkiewicz Date: Tue, 22 Sep 2026 14:50:00 +0200 Subject: [PATCH 2/5] feat(summary): add configurable system prompt for summary - user/admin/default Signed-off-by: Arkadiusz Sitkiewicz --- lib/Service/ServiceConfig.php | 13 +++++++ lib/TaskProcessing/SummaryProvider.php | 53 ++++++++++++++++++-------- src/components/AdminSettings.vue | 1 + src/components/ServiceForm.vue | 18 +++++++++ 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/lib/Service/ServiceConfig.php b/lib/Service/ServiceConfig.php index 0bd8b7bf..c03a3c6b 100644 --- a/lib/Service/ServiceConfig.php +++ b/lib/Service/ServiceConfig.php @@ -38,6 +38,7 @@ class ServiceConfig implements JsonSerializable { 'llm_extra_params' => 'string', 'max_tokens' => 'integer', 'chunk_size' => 'integer', + 'system_prompt_summary' => 'string', 'multimodal_image_enabled' => 'boolean', 'multimodal_audio_enabled' => 'boolean', 'multimodal_video_enabled' => 'boolean', @@ -83,6 +84,7 @@ public function __construct( private string $llmExtraParams = '', private int $maxTokens = Application::DEFAULT_MAX_NUM_OF_TOKENS, private int $chunkSize = Application::DEFAULT_CHUNK_SIZE, + private string $systemPromptSummary = '', // these mirror the defaults of the single-service configuration, so // that a migrated service and a newly connected one accept the same // kinds of attachment @@ -150,6 +152,8 @@ public function with(array $values): self { break; case 'chunk_size': $new->chunkSize = (int)$value === 0 ? 0 : max(Application::MIN_CHUNK_SIZE, (int)$value); break; + case 'system_prompt_summary': $new->systemPromptSummary = (string)$value; + break; case 'multimodal_image_enabled': $new->multimodalImageEnabled = (bool)$value; break; case 'multimodal_audio_enabled': $new->multimodalAudioEnabled = (bool)$value; @@ -349,6 +353,14 @@ public function getChunkSize(): int { return $this->chunkSize; } + /** + * The admin-configured fallback system prompt for the summary task type. + * An empty string means the built-in default prompt is used. + */ + public function getSystemPromptSummary(): string { + return $this->systemPromptSummary; + } + public function getMultimodalImageEnabled(): bool { return $this->multimodalImageEnabled; } @@ -466,6 +478,7 @@ public function jsonSerialize(): array { 'llm_extra_params' => $this->llmExtraParams, 'max_tokens' => $this->maxTokens, 'chunk_size' => $this->chunkSize, + 'system_prompt_summary' => $this->systemPromptSummary, 'multimodal_image_enabled' => $this->multimodalImageEnabled, 'multimodal_audio_enabled' => $this->multimodalAudioEnabled, 'multimodal_video_enabled' => $this->multimodalVideoEnabled, diff --git a/lib/TaskProcessing/SummaryProvider.php b/lib/TaskProcessing/SummaryProvider.php index f84709f5..46bfdfe2 100644 --- a/lib/TaskProcessing/SummaryProvider.php +++ b/lib/TaskProcessing/SummaryProvider.php @@ -24,6 +24,9 @@ class SummaryProvider implements ISynchronousProvider { use ProviderIdentity; + private const DEFAULT_SUMMARY_SYSTEM_PROMPT = 'You are a helpful assistant that summarizes text in the same language as the text. ' + . 'You should only return the summary without any additional information. '; + public function __construct( private OpenAiAPIService $openAiAPIService, private IL10N $l, @@ -74,6 +77,11 @@ public function getOptionalInputShape(): array { $this->l->t('The maximum number of words/tokens that can be generated in the completion.'), EShapeType::Number ), + 'system_prompt_summary' => new ShapeDescriptor( + $this->l->t('Summary system prompt'), + $this->l->t('Summary system prompt. When set, it overrides the default prompt and the Format and Complexity options are ignored.'), + EShapeType::Text + ), ]; } @@ -98,6 +106,7 @@ public function getOptionalInputShapeDefaults(): array { 'max_tokens' => $this->service->getMaxTokens(), 'format' => 'auto', 'complexity' => 'medium', + 'system_prompt_summary' => '', ]; } @@ -142,22 +151,36 @@ public function process(?string $userId, array $input, callable $reportProgress) try { $completions = []; - $summarySystemPrompt = 'You are a helpful assistant that summarizes text in the same language as the text. ' - . 'You should only return the summary without any additional information. '; - if (isset($input['format'])) { - if ($input['format'] === 'paragraph') { - $summarySystemPrompt .= 'Return the summary as a paragraph. '; - } elseif ($input['format'] === 'bullet_points') { - $summarySystemPrompt .= 'Return the summary as a list of bullet points. '; - } elseif ($input['format'] === 'sentence') { - $summarySystemPrompt .= 'Return the summary as a single sentence. Do not include more than one sentence. '; + $customSystemPrompt = isset($input['system_prompt_summary']) && is_string($input['system_prompt_summary']) + ? trim($input['system_prompt_summary']) + : ''; + if ($customSystemPrompt !== '') { + // A user provided prompt overrides the others; format and complexity are ignored + $summarySystemPrompt = $customSystemPrompt . ' '; + } else { + // Fallback to the admin-configured prompt, then to default. Format and complexity appended + $summarySystemPrompt = $this->service->getSystemPromptSummary(); + if ($summarySystemPrompt === '') { + $summarySystemPrompt = self::DEFAULT_SUMMARY_SYSTEM_PROMPT; + } else { + $summarySystemPrompt .= ' '; } - } - if (isset($input['complexity'])) { - if ($input['complexity'] === 'complex') { - $summarySystemPrompt .= 'Use complex language and vocabulary appropriate for an expert in the subject. '; - } elseif ($input['complexity'] === 'simple') { - $summarySystemPrompt .= 'Use simple language and vocabulary appropriate for a 5 year old. '; + + if (isset($input['format'])) { + if ($input['format'] === 'paragraph') { + $summarySystemPrompt .= 'Return the summary as a paragraph. '; + } elseif ($input['format'] === 'bullet_points') { + $summarySystemPrompt .= 'Return the summary as a list of bullet points. '; + } elseif ($input['format'] === 'sentence') { + $summarySystemPrompt .= 'Return the summary as a single sentence. Do not include more than one sentence. '; + } + } + if (isset($input['complexity'])) { + if ($input['complexity'] === 'complex') { + $summarySystemPrompt .= 'Use complex language and vocabulary appropriate for an expert in the subject. '; + } elseif ($input['complexity'] === 'simple') { + $summarySystemPrompt .= 'Use simple language and vocabulary appropriate for a 5 year old. '; + } } } if ($this->service->isUsingOpenAi() || $this->service->getChatEndpointEnabled()) { diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 4503922c..781a45ff 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -370,6 +370,7 @@ export default { llm_extra_params: service.llm_extra_params, max_tokens: parseInt(service.max_tokens) || 1, chunk_size: parseInt(service.chunk_size) || 0, + system_prompt_summary: service.system_prompt_summary, multimodal_image_enabled: service.multimodal_image_enabled, multimodal_audio_enabled: service.multimodal_audio_enabled, multimodal_video_enabled: service.multimodal_video_enabled, diff --git a/src/components/ServiceForm.vue b/src/components/ServiceForm.vue index 6ccd22b5..d3220ee1 100644 --- a/src/components/ServiceForm.vue +++ b/src/components/ServiceForm.vue @@ -269,6 +269,22 @@ @update:model-value="onInput({ translation_enabled: $event })"> {{ t('integration_openai', 'Offer translation') }} +
{{ t('integration_openai', 'System prompts') }}
+
+ + + + +
{{ t('integration_openai', 'Multimodal LLM Support') }}
{{ t('integration_openai', 'Which kinds of attachments the models of this service accept.') }} @@ -412,6 +428,7 @@ import NcInputField from '@nextcloud/vue/components/NcInputField' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import NcSelect from '@nextcloud/vue/components/NcSelect' +import NcTextArea from '@nextcloud/vue/components/NcTextArea' import NcTextField from '@nextcloud/vue/components/NcTextField' import axios from '@nextcloud/axios' @@ -439,6 +456,7 @@ export default { NcLoadingIcon, NcNoteCard, NcSelect, + NcTextArea, NcTextField, }, From 5f04f50e308ab83b80b70a2933dbc375d2cb2e41 Mon Sep 17 00:00:00 2001 From: Arkadiusz Sitkiewicz Date: Wed, 23 Sep 2026 14:15:49 +0200 Subject: [PATCH 3/5] feat(translation): add configurable system prompt for translation - admin/default Signed-off-by: Arkadiusz Sitkiewicz --- lib/Service/ServiceConfig.php | 15 +++++++++++++++ lib/Service/TranslateService.php | 10 ++++++++-- src/components/AdminSettings.vue | 1 + src/components/ServiceForm.vue | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/Service/ServiceConfig.php b/lib/Service/ServiceConfig.php index c03a3c6b..583be81b 100644 --- a/lib/Service/ServiceConfig.php +++ b/lib/Service/ServiceConfig.php @@ -39,6 +39,7 @@ class ServiceConfig implements JsonSerializable { 'max_tokens' => 'integer', 'chunk_size' => 'integer', 'system_prompt_summary' => 'string', + 'system_prompt_translate' => 'string', 'multimodal_image_enabled' => 'boolean', 'multimodal_audio_enabled' => 'boolean', 'multimodal_video_enabled' => 'boolean', @@ -85,6 +86,7 @@ public function __construct( private int $maxTokens = Application::DEFAULT_MAX_NUM_OF_TOKENS, private int $chunkSize = Application::DEFAULT_CHUNK_SIZE, private string $systemPromptSummary = '', + private string $systemPromptTranslate = '', // these mirror the defaults of the single-service configuration, so // that a migrated service and a newly connected one accept the same // kinds of attachment @@ -154,6 +156,8 @@ public function with(array $values): self { break; case 'system_prompt_summary': $new->systemPromptSummary = (string)$value; break; + case 'system_prompt_translate': $new->systemPromptTranslate = (string)$value; + break; case 'multimodal_image_enabled': $new->multimodalImageEnabled = (bool)$value; break; case 'multimodal_audio_enabled': $new->multimodalAudioEnabled = (bool)$value; @@ -361,6 +365,16 @@ public function getSystemPromptSummary(): string { return $this->systemPromptSummary; } + /** + * The admin-configured system prompt appended to the built-in translation + * prompt for the translation task type. The built-in prompt is always kept, + * as it enforces the expected JSON response format. + * An empty string means only the built-in prompt is used. + */ + public function getSystemPromptTranslate(): string { + return $this->systemPromptTranslate; + } + public function getMultimodalImageEnabled(): bool { return $this->multimodalImageEnabled; } @@ -479,6 +493,7 @@ public function jsonSerialize(): array { 'max_tokens' => $this->maxTokens, 'chunk_size' => $this->chunkSize, 'system_prompt_summary' => $this->systemPromptSummary, + 'system_prompt_translate' => $this->systemPromptTranslate, 'multimodal_image_enabled' => $this->multimodalImageEnabled, 'multimodal_audio_enabled' => $this->multimodalAudioEnabled, 'multimodal_video_enabled' => $this->multimodalVideoEnabled, diff --git a/lib/Service/TranslateService.php b/lib/Service/TranslateService.php index c3190688..fff9dc9e 100644 --- a/lib/Service/TranslateService.php +++ b/lib/Service/TranslateService.php @@ -85,6 +85,12 @@ public function translate( $promptStart = 'Translate the following text to ' . $toLanguage . ': '; } + $systemPrompt = self::SYSTEM_PROMPT; + $adminSystemPrompt = $service->getSystemPromptTranslate(); + if ($adminSystemPrompt !== '') { + $systemPrompt .= PHP_EOL . $adminSystemPrompt; + } + $cache = $this->cacheFactory->createDistributed('integration_openai'); foreach ($chunks as $chunk) { $progress += $increase; @@ -105,12 +111,12 @@ public function translate( if ($service->isUsingOpenAi() || $service->getChatEndpointEnabled()) { $completionsObj = $this->openAiAPIService->createChatCompletion( - $userId, $service, $model, $prompt, TranslateService::SYSTEM_PROMPT, null, 1, $maxTokens, TranslateService::JSON_RESPONSE_FORMAT + $userId, $service, $model, $prompt, $systemPrompt, null, 1, $maxTokens, TranslateService::JSON_RESPONSE_FORMAT ); $completions = $completionsObj['messages']; } else { $completions = $this->openAiAPIService->createCompletion( - $userId, $service, $prompt . PHP_EOL . TranslateService::SYSTEM_PROMPT . PHP_EOL . PHP_EOL, 1, $model, $maxTokens + $userId, $service, $prompt . PHP_EOL . $systemPrompt . PHP_EOL . PHP_EOL, 1, $model, $maxTokens ); } diff --git a/src/components/AdminSettings.vue b/src/components/AdminSettings.vue index 781a45ff..78803c7e 100644 --- a/src/components/AdminSettings.vue +++ b/src/components/AdminSettings.vue @@ -371,6 +371,7 @@ export default { max_tokens: parseInt(service.max_tokens) || 1, chunk_size: parseInt(service.chunk_size) || 0, system_prompt_summary: service.system_prompt_summary, + system_prompt_translate: service.system_prompt_translate, multimodal_image_enabled: service.multimodal_image_enabled, multimodal_audio_enabled: service.multimodal_audio_enabled, multimodal_video_enabled: service.multimodal_video_enabled, diff --git a/src/components/ServiceForm.vue b/src/components/ServiceForm.vue index d3220ee1..67784f78 100644 --- a/src/components/ServiceForm.vue +++ b/src/components/ServiceForm.vue @@ -285,6 +285,21 @@
+
+ + + + +
{{ t('integration_openai', 'Multimodal LLM Support') }}
{{ t('integration_openai', 'Which kinds of attachments the models of this service accept.') }} From e2ebd4c1fc6f2b5f20e4de993c77370764eb98b4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Sitkiewicz Date: Thu, 24 Sep 2026 12:48:08 +0200 Subject: [PATCH 4/5] tests(summary): add test for configurable summary system prompts - user/admin Signed-off-by: Arkadiusz Sitkiewicz --- lib/TaskProcessing/SummaryProvider.php | 4 +- tests/unit/Providers/OpenAiProviderTest.php | 156 +++++++++++++++++++- 2 files changed, 155 insertions(+), 5 deletions(-) diff --git a/lib/TaskProcessing/SummaryProvider.php b/lib/TaskProcessing/SummaryProvider.php index 46bfdfe2..e5524c48 100644 --- a/lib/TaskProcessing/SummaryProvider.php +++ b/lib/TaskProcessing/SummaryProvider.php @@ -24,7 +24,7 @@ class SummaryProvider implements ISynchronousProvider { use ProviderIdentity; - private const DEFAULT_SUMMARY_SYSTEM_PROMPT = 'You are a helpful assistant that summarizes text in the same language as the text. ' + public const DEFAULT_SYSTEM_PROMPT = 'You are a helpful assistant that summarizes text in the same language as the text. ' . 'You should only return the summary without any additional information. '; public function __construct( @@ -161,7 +161,7 @@ public function process(?string $userId, array $input, callable $reportProgress) // Fallback to the admin-configured prompt, then to default. Format and complexity appended $summarySystemPrompt = $this->service->getSystemPromptSummary(); if ($summarySystemPrompt === '') { - $summarySystemPrompt = self::DEFAULT_SUMMARY_SYSTEM_PROMPT; + $summarySystemPrompt = self::DEFAULT_SYSTEM_PROMPT; } else { $summarySystemPrompt .= ' '; } diff --git a/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index 5c5f9d02..46dcb315 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -576,12 +576,10 @@ public function testSummaryProvider(): void { $url = self::OPENAI_API_BASE . 'chat/completions'; $options = ['timeout' => Application::OPENAI_DEFAULT_REQUEST_TIMEOUT, 'headers' => ['User-Agent' => Application::USER_AGENT, 'Authorization' => self::AUTHORIZATION_HEADER, 'Content-Type' => 'application/json']]; - $systemPrompt = 'You are a helpful assistant that summarizes text in the same language as the text. ' - . 'You should only return the summary without any additional information. '; $options['body'] = json_encode([ 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, 'messages' => [ - ['role' => 'system', 'content' => $systemPrompt], + ['role' => 'system', 'content' => SummaryProvider::DEFAULT_SYSTEM_PROMPT], ['role' => 'user', 'content' => $prompt], ], 'n' => $n, @@ -607,6 +605,158 @@ public function testSummaryProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } + public function testSummaryProviderWithAdminSystemPrompt(): void { + $adminSystemPrompt = 'You are a helpful assistant that summarizes text in Polish.'; + $service = $this->service->with(['system_prompt_summary' => $adminSystemPrompt]); + + $summaryProvider = new SummaryProvider( + $this->openAiApiService, + $this->createMock(\OCP\IL10N::class), + $this->chunkService, + $service, + self::TEXT_MODEL, + ); + + $prompt = 'This is a test prompt'; + $n = 1; + + $response = '{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo-0613", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This is a test response." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + }'; + + $url = self::OPENAI_API_BASE . 'chat/completions'; + + $options = ['timeout' => Application::OPENAI_DEFAULT_REQUEST_TIMEOUT, 'headers' => ['User-Agent' => Application::USER_AGENT, 'Authorization' => self::AUTHORIZATION_HEADER, 'Content-Type' => 'application/json']]; + $options['body'] = json_encode([ + 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, + 'messages' => [ + ['role' => 'system', 'content' => $adminSystemPrompt . ' '], + ['role' => 'user', 'content' => $prompt], + ], + 'n' => $n, + 'stream' => false, + 'max_completion_tokens' => Application::DEFAULT_MAX_NUM_OF_TOKENS, + 'user' => self::TEST_USER1, + ]); + + $iResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $iResponse->method('getBody')->willReturn($response); + $iResponse->method('getStatusCode')->willReturn(200); + $iResponse->method('getHeader')->with('Content-Type')->willReturn('application/json'); + + $this->iClient->expects($this->once())->method('post')->with($url, $options)->willReturn($iResponse); + + $result = $summaryProvider->process(self::TEST_USER1, ['input' => $prompt], fn () => true); + $this->assertEquals('This is a test response.', $result['output']); + + // Check that token usage is logged properly + $usage = $this->quotaUsageMapper->getQuotaUnitsOfUser(self::TEST_USER1, Application::QUOTA_TYPE_TEXT); + $this->assertEquals(21, $usage); + // Clear quota usage + $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); + } + + public function testSummaryProviderWithUserSystemPrompt(): void { + // Define admin system prompt to ensure it is overwritten by user system prompt + $adminSystemPrompt = 'This is an admin system prompt for summarization.'; + $service = $this->service->with(['system_prompt_summary' => $adminSystemPrompt]); + + $summaryProvider = new SummaryProvider( + $this->openAiApiService, + $this->createMock(\OCP\IL10N::class), + $this->chunkService, + $service, + self::TEXT_MODEL, + ); + + // Ensure that complexity and format are ignored when user system prompt is set + $userSystemPrompt = 'This is a user system prompt for summarization.'; + $complexity = 'complex'; + $format = 'bullet_points'; + $prompt = 'This is a test prompt'; + $input = [ + 'input' => $prompt, + 'system_prompt_summary' => $userSystemPrompt, + 'format' => $format, + 'complexity' => $complexity, + ]; + + $n = 1; + + $response = '{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-3.5-turbo-0613", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This is a test response." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + }'; + + $url = self::OPENAI_API_BASE . 'chat/completions'; + + $options = ['timeout' => Application::OPENAI_DEFAULT_REQUEST_TIMEOUT, 'headers' => ['User-Agent' => Application::USER_AGENT, 'Authorization' => self::AUTHORIZATION_HEADER, 'Content-Type' => 'application/json']]; + $options['body'] = json_encode([ + 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, + 'messages' => [ + ['role' => 'system', 'content' => $userSystemPrompt . ' '], + ['role' => 'user', 'content' => $prompt], + ], + 'n' => $n, + 'stream' => false, + 'max_completion_tokens' => Application::DEFAULT_MAX_NUM_OF_TOKENS, + 'user' => self::TEST_USER1, + ]); + + $iResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $iResponse->method('getBody')->willReturn($response); + $iResponse->method('getStatusCode')->willReturn(200); + $iResponse->method('getHeader')->with('Content-Type')->willReturn('application/json'); + + $this->iClient->expects($this->once())->method('post')->with($url, $options)->willReturn($iResponse); + + $result = $summaryProvider->process(self::TEST_USER1, $input, fn () => true); + $this->assertEquals('This is a test response.', $result['output']); + + // Check that token usage is logged properly + $usage = $this->quotaUsageMapper->getQuotaUnitsOfUser(self::TEST_USER1, Application::QUOTA_TYPE_TEXT); + $this->assertEquals(21, $usage); + // Clear quota usage + $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); + } + public function testProofreadProvider(): void { $proofreadProvider = new ProofreadProvider( $this->openAiApiService, From 3d530f1c760fa0f596dae294a01685445681bd9e Mon Sep 17 00:00:00 2001 From: Arkadiusz Sitkiewicz Date: Fri, 25 Sep 2026 10:08:26 +0200 Subject: [PATCH 5/5] tests(translation): add test for configurable translation system prompt - admin Signed-off-by: Arkadiusz Sitkiewicz --- tests/unit/Providers/OpenAiProviderTest.php | 83 +++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index 46dcb315..e44faba4 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -905,6 +905,89 @@ public function testTranslationProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } + public function testTranslationProviderWithAdminSystemPrompt(): void { + $adminSystemPrompt = 'This is an admin system prompt for translation.'; + $service = $this->service->with(['system_prompt_translate' => $adminSystemPrompt]); + + $translationProvider = new TranslateProvider( + $this->openAiApiService, + $this->createMock(\OCP\IL10N::class), + $this->translateService, + $service, + self::TEXT_MODEL, + ); + + $inputText = 'This is a test prompt'; + $n = 1; + $fromLang = 'English'; + $toLang = 'Polish'; + $aiContent = ['translation' => 'This is a test response.']; + + $response = '{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4.1-mini", + "system_fingerprint": "fp_44709d6fcb", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": ' . json_encode(json_encode($aiContent)) . ' + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + }'; + + $url = self::OPENAI_API_BASE . 'chat/completions'; + $prompt = 'Translate the following text from ' . $fromLang . ' to ' . $toLang . ': ' . PHP_EOL . PHP_EOL . $inputText; + + $options = ['timeout' => Application::OPENAI_DEFAULT_REQUEST_TIMEOUT, 'headers' => ['User-Agent' => Application::USER_AGENT, 'Authorization' => self::AUTHORIZATION_HEADER, 'Content-Type' => 'application/json']]; + $options['body'] = json_encode([ + 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, + 'messages' => [ + ['role' => 'system', 'content' => TranslateService::SYSTEM_PROMPT . PHP_EOL . $adminSystemPrompt], + ['role' => 'user', 'content' => $prompt], + ], + 'n' => $n, + 'stream' => false, + 'max_completion_tokens' => Application::DEFAULT_MAX_NUM_OF_TOKENS, + 'user' => self::TEST_USER1, + ...TranslateService::JSON_RESPONSE_FORMAT, + ]); + + $iResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $iResponse->method('getBody')->willReturn($response); + $iResponse->method('getStatusCode')->willReturn(200); + $iResponse->method('getHeader')->with('Content-Type')->willReturn('application/json'); + + $this->iClient->expects($this->once())->method('post')->with( + $this->equalTo($url), + $this->callback(function ($revdOptions) use ($options) { + $body = json_decode($revdOptions['body'], true); + $expectedBody = json_decode($options['body'], true); + $this->assertEquals($expectedBody, $body); + return true; + }), + )->willReturn($iResponse); + + $result = $translationProvider->process(self::TEST_USER1, ['input' => $inputText, 'origin_language' => $fromLang, 'target_language' => $toLang], fn () => true, new SynchronousProviderOptions(preferStreaming: false)); + $this->assertEquals(['output' => $aiContent['translation']], $result); + + // Check that token usage is logged properly + $usage = $this->quotaUsageMapper->getQuotaUnitsOfUser(self::TEST_USER1, Application::QUOTA_TYPE_TEXT); + $this->assertEquals(21, $usage); + // Clear quota usage + $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); + } + public function testAudioToAudioTranslateProvider(): void { $l10n = $this->createMock(\OCP\IL10N::class); $l10n->method('t')->willReturnCallback(fn ($text) => $text);