diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 27be6312..0781286b 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -64,6 +64,9 @@ class Application extends App implements IBootstrap { public const DEFAULT_QUOTA_PERIOD = 30; public const DEFAULT_QUOTA_CONFIG = ['length' => self::DEFAULT_QUOTA_PERIOD, 'unit' => 'day', 'day' => 1]; + 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_OPENAI_TEXT_GENERATION_TIME = 10; // seconds public const DEFAULT_LOCALAI_TEXT_GENERATION_TIME = 60; // seconds public const DEFAULT_OPENAI_IMAGE_GENERATION_TIME = 20; // seconds diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index f63705c1..d4226afd 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -318,6 +318,20 @@ public function getQuotaPeriod(): array { return $value; } + /** + * @return string + */ + public function getSummarySystemPrompt(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'system_prompt_summary', Application::DEFAULT_SUMMARY_SYSTEM_PROMPT, lazy: true) ?: Application::DEFAULT_SUMMARY_SYSTEM_PROMPT; + } + + /** + * @return string + */ + public function getTalkSummarySystemPrompt(): string { + return $this->appConfig->getValueString(Application::APP_ID, 'system_prompt_talk_summary', Application::DEFAULT_SUMMARY_SYSTEM_PROMPT, lazy: true) ?: Application::DEFAULT_SUMMARY_SYSTEM_PROMPT; + } + /** * @return int[] */ diff --git a/lib/TaskProcessing/SummaryProvider.php b/lib/TaskProcessing/SummaryProvider.php index ab9dce24..b74720b1 100644 --- a/lib/TaskProcessing/SummaryProvider.php +++ b/lib/TaskProcessing/SummaryProvider.php @@ -79,6 +79,11 @@ public function getOptionalInputShape(): array { $this->l->t('The model used to generate the completion'), EShapeType::Enum ), + 'talk_meeting' => new ShapeDescriptor( + $this->l->t("Talk meeting flag"), + $this->l->t('Flag that indicates if the summary is generated from a Talk meeting recording.'), + EShapeType::Number + ), ]; } @@ -106,6 +111,7 @@ public function getOptionalInputShapeDefaults(): array { 'model' => $adminModel, 'format' => 'auto', 'complexity' => 'medium', + 'talk_meeting' => 0, ]; } @@ -139,6 +145,7 @@ public function process(?string $userId, array $input, callable $reportProgress) $model = $input['model']; } + $isTalkMeeting = (bool)($input['talk_meeting'] ?? 0); $prompts = $this->chunkService->chunkSplitPrompt($prompt); $newNumChunks = count($prompts); $progress = 0.0; @@ -153,8 +160,11 @@ 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 ($isTalkMeeting) { + $summarySystemPrompt = $this->openAiSettingsService->getTalkSummarySystemPrompt(); + } else { + $summarySystemPrompt = $this->openAiSettingsService->getSummarySystemPrompt(); + } if (isset($input['format'])) { if ($input['format'] === 'paragraph') { $summarySystemPrompt .= 'Return the summary as a paragraph. '; diff --git a/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index d739eb65..a314cbcc 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -574,6 +574,170 @@ public function testSummaryProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } + public function testSummaryProviderConfigurableSystemPrompt(): void { + try { + $configurableSystemPrompt = 'Configurable summary system prompt from app config'; + $appConfig = \OCP\Server::get(IAppConfig::class); + $appConfig->setValueString( + Application::APP_ID, + 'system_prompt_summary', + $configurableSystemPrompt, + ); + + $summaryProvider = new SummaryProvider( + $this->openAiApiService, + $this->openAiSettingsService, + $this->createMock(\OCP\IL10N::class), + $this->chunkService, + self::TEST_USER1, + ); + + $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' => $configurableSystemPrompt], + ['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); + } finally { + $appConfig->deleteKey( + Application::APP_ID, + 'system_prompt_summary' + ); + } + } + + public function testSummaryProviderConfigurableTalkSystemPrompt(): void { + try { + $configurableSystemPrompt = 'Configurable Talk summary system prompt from app config'; + $appConfig = \OCP\Server::get(IAppConfig::class); + $appConfig->setValueString( + Application::APP_ID, + 'system_prompt_talk_summary', + $configurableSystemPrompt, + ); + + $summaryProvider = new SummaryProvider( + $this->openAiApiService, + $this->openAiSettingsService, + $this->createMock(\OCP\IL10N::class), + $this->chunkService, + self::TEST_USER1, + ); + + $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' => $configurableSystemPrompt], + ['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, 'talk_meeting' => 1], 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); + } finally { + $appConfig->deleteKey( + Application::APP_ID, + 'system_prompt_talk_summary' + ); + } + } + public function testProofreadProvider(): void { $proofreadProvider = new ProofreadProvider( $this->openAiApiService,