diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e90a4c11..ca6161fc 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -14,6 +14,7 @@ use OCA\OpenAi\Reference\ImageReferenceProvider; use OCA\OpenAi\Reference\WhisperReferenceProvider; use OCA\OpenAi\SpeechToText\STTProvider; +use OCA\OpenAi\Translation\TranslationProvider; use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\IConfig; @@ -49,6 +50,7 @@ public function register(IRegistrationContext $context): void { $context->registerReferenceProvider(ImageReferenceProvider::class); $context->registerReferenceProvider(WhisperReferenceProvider::class); $context->registerEventListener(RenderReferenceEvent::class, OpenAiReferenceListener::class); + $context->registerTranslationProvider(TranslationProvider::class); if (version_compare($this->config->getSystemValueString('version', '0.0.0'), '27.0.0', '>=')) { $context->registerSpeechToTextProvider(STTProvider::class); diff --git a/lib/Service/OpenAiAPIService.php b/lib/Service/OpenAiAPIService.php index 785355ea..652d3e6a 100644 --- a/lib/Service/OpenAiAPIService.php +++ b/lib/Service/OpenAiAPIService.php @@ -80,10 +80,12 @@ public function getPromptHistory(string $userId, int $type): array { * @param int $n * @param string $model * @param int $maxTokens + * @param bool $storePrompt * @return array|string[] * @throws \OCP\DB\Exception */ - public function createCompletion(?string $userId, string $prompt, int $n, string $model, int $maxTokens = 1000): array { + public function createCompletion(?string $userId, string $prompt, int $n, string $model, int $maxTokens = 1000, + bool $storePrompt = true): array { $params = [ 'model' => $model, 'prompt' => $prompt, @@ -93,7 +95,9 @@ public function createCompletion(?string $userId, string $prompt, int $n, string if ($userId !== null) { $params['user'] = $userId; } - $this->promptMapper->createPrompt(Application::PROMPT_TYPE_TEXT, $userId, $prompt); + if ($storePrompt) { + $this->promptMapper->createPrompt(Application::PROMPT_TYPE_TEXT, $userId, $prompt); + } return $this->request('completions', $params, 'POST'); } @@ -103,10 +107,12 @@ public function createCompletion(?string $userId, string $prompt, int $n, string * @param int $n * @param string $model * @param int $maxTokens + * @param bool $storePrompt * @return array|string[] * @throws \OCP\DB\Exception */ - public function createChatCompletion(?string $userId, string $prompt, int $n, string $model, int $maxTokens = 1000): array { + public function createChatCompletion(?string $userId, string $prompt, int $n, string $model, int $maxTokens = 1000, + bool $storePrompt = true): array { $params = [ 'model' => $model, 'messages' => [['role' => 'user', 'content' => $prompt ]], @@ -116,7 +122,9 @@ public function createChatCompletion(?string $userId, string $prompt, int $n, st if ($userId !== null) { $params['user'] = $userId; } - $this->promptMapper->createPrompt(Application::PROMPT_TYPE_TEXT, $userId, $prompt); + if ($storePrompt) { + $this->promptMapper->createPrompt(Application::PROMPT_TYPE_TEXT, $userId, $prompt); + } return $this->request('chat/completions', $params, 'POST'); } diff --git a/lib/Translation/TranslationProvider.php b/lib/Translation/TranslationProvider.php new file mode 100644 index 00000000..1246c5d5 --- /dev/null +++ b/lib/Translation/TranslationProvider.php @@ -0,0 +1,142 @@ + + * + * @author Julien Veyssier + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +namespace OCA\OpenAi\Translation; + +use Exception; +use OCA\OpenAi\AppInfo\Application; +use OCA\OpenAi\Service\OpenAiAPIService; +use OCP\ICacheFactory; +use OCP\L10N\IFactory; +use OCP\Translation\IDetectLanguageProvider; +use OCP\Translation\ITranslationProvider; +use OCP\Translation\LanguageTuple; +use Psr\Log\LoggerInterface; +use RuntimeException; + +class TranslationProvider implements ITranslationProvider, IDetectLanguageProvider { + + public function __construct( + private ICacheFactory $cacheFactory, + private IFactory $l10nFactory, + private OpenAiAPIService $openAiAPIService, + private LoggerInterface $logger, + private ?string $userId + ) { + } + + public function getName(): string { + return 'OpenAI\'s ChatGpt 3.5'; + } + + public function getAvailableLanguages(): array { + $cache = $this->cacheFactory->createDistributed('integration_openai'); + if ($cached = $cache->get('languages')) { + return array_map(function ($entry) { + return $entry instanceof LanguageTuple ? $entry : LanguageTuple::fromArray($entry); + }, $cached); + } + + $coreL = $this->l10nFactory->getLanguages(); + $languages = array_merge($coreL['commonLanguages'], $coreL['otherLanguages']); + + $availableLanguages = []; + foreach ($languages as $sourceLanguage) { + foreach ($languages as $targetLanguage) { + if ($targetLanguage['code'] === $sourceLanguage['code']) { + continue; + } + + $availableLanguages[] = new LanguageTuple( + $sourceLanguage['code'], $sourceLanguage['name'], + $targetLanguage['code'], $targetLanguage['name'] + ); + } + } + + $cache->set('languages', $availableLanguages, 3600); + return $availableLanguages; + } + + public function detectLanguage(string $text): ?string { + $prompt = 'What language is this (answer with the language name only, in English): ' . $text; + $completion = $this->openAiAPIService->createChatCompletion($this->userId, $prompt, 1, 'gpt-3.5-turbo', 100, false); + if (isset($completion['choices']) && is_array($completion['choices']) && count($completion['choices']) > 0) { + $choice = $completion['choices'][0]; + if (isset($choice['message'], $choice['message']['content'])) { + return $choice['message']['content']; + } + } + return null; + } + + private function getCoreLanguagesByCode(): array { + $coreL = $this->l10nFactory->getLanguages(); + $coreLanguages = array_reduce(array_merge($coreL['commonLanguages'], $coreL['otherLanguages']), function ($carry, $val) { + $carry[$val['code']] = $val['name']; + return $carry; + }); + return $coreLanguages; + } + + public function translate(?string $fromLanguage, string $toLanguage, string $text): string { + $cacheKey = ($fromLanguage ?? '') . '/' . $toLanguage . '/' . md5($text); + + $cache = $this->cacheFactory->createDistributed('integration_openai'); + if ($cached = $cache->get($cacheKey)) { + return $cached; + } + + try { + $coreLanguages = $this->getCoreLanguagesByCode(); + + $toLanguage = $coreLanguages[$toLanguage]; + if ($fromLanguage !== null) { + $this->logger->debug('OpenAI translation FROM['.$fromLanguage.'] TO['.$toLanguage.']', ['app' => Application::APP_ID]); + $fromLanguage = $coreLanguages[$fromLanguage]; + $prompt = 'Translate from ' . $fromLanguage . ' to ' . $toLanguage . ': ' . $text; + } else { + $this->logger->debug('OpenAI translation TO['.$toLanguage.']', ['app' => Application::APP_ID]); + $prompt = 'Translate to ' . $toLanguage . ': ' . $text; + } + $completion = $this->openAiAPIService->createChatCompletion($this->userId, $prompt, 1, 'gpt-3.5-turbo', 4000, false); + if (isset($completion['choices']) && is_array($completion['choices']) && count($completion['choices']) > 0) { + $choice = $completion['choices'][0]; + if (isset($choice['message'], $choice['message']['content'])) { + $translation = $choice['message']['content']; + $cache->set($cacheKey, $translation, 3600); + return $translation; + } + } + if (isset($choice['body']['error']['message'])) { + throw new Exception($choice['body']['error']['message']); + } + } catch (Exception $e) { + throw new RuntimeException("Failed translate from {$fromLanguage} to {$toLanguage}", 0, $e); + } + throw new RuntimeException("Failed translate from {$fromLanguage} to {$toLanguage}"); + } +}