diff --git a/composer.json b/composer.json index 7f54d4dd..3b7f9071 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "nextcloud/ocp": "dev-master", "nextcloud/openapi-extractor": "^1.0.0", "phpunit/phpunit": "^9.5", - "psalm/phar": "6.4.0" + "psalm/phar": "6.7" }, "config": { "sort-packages": true, diff --git a/composer.lock b/composer.lock index 4424dc23..d1b9e973 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "82ecee0e36d1b1a3fba5a93982fd6d62", + "content-hash": "9dbd950eb30116a6d75e71a01e759622", "packages": [ { "name": "erusev/parsedown", @@ -1586,16 +1586,16 @@ }, { "name": "psalm/phar", - "version": "6.4.0", + "version": "6.7.0", "source": { "type": "git", "url": "https://github.com/psalm/phar.git", - "reference": "38771dadf1f2c64cadc3a81d12a73c23d040380f" + "reference": "8eb3a469fa888c862b8a382f1fe1671b4e989f06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/psalm/phar/zipball/38771dadf1f2c64cadc3a81d12a73c23d040380f", - "reference": "38771dadf1f2c64cadc3a81d12a73c23d040380f", + "url": "https://api.github.com/repos/psalm/phar/zipball/8eb3a469fa888c862b8a382f1fe1671b4e989f06", + "reference": "8eb3a469fa888c862b8a382f1fe1671b4e989f06", "shasum": "" }, "require": { @@ -1615,9 +1615,9 @@ "description": "Composer-based Psalm Phar", "support": { "issues": "https://github.com/psalm/phar/issues", - "source": "https://github.com/psalm/phar/tree/6.4.0" + "source": "https://github.com/psalm/phar/tree/6.7.0" }, - "time": "2025-02-05T14:15:07+00:00" + "time": "2025-02-17T10:44:04+00:00" }, { "name": "psr/clock", diff --git a/lib/Controller/ChattyLLMController.php b/lib/Controller/ChattyLLMController.php index b3bda2ac..2af5b57f 100644 --- a/lib/Controller/ChattyLLMController.php +++ b/lib/Controller/ChattyLLMController.php @@ -500,11 +500,15 @@ public function generateForSession(int $sessionId, int $agencyConfirm = 0): JSON $lastAttachments = $lastUserMessage->jsonSerialize()['attachments']; $audioAttachment = $lastAttachments[0] ?? null; + // see https://github.com/vimeo/psalm/issues/7980 + $isContextAgentAudioAvailable = false; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction')) { + $isContextAgentAudioAvailable = isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID]); + } if ($audioAttachment !== null && isset($audioAttachment['type']) && $audioAttachment['type'] === 'Audio' - && class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction') - && isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID]) + && $isContextAgentAudioAvailable ) { // audio agency $fileId = $audioAttachment['file_id']; @@ -536,11 +540,14 @@ public function generateForSession(int $sessionId, int $agencyConfirm = 0): JSON $lastAttachments = $lastUserMessage->jsonSerialize()['attachments']; $audioAttachment = $lastAttachments[0] ?? null; + $isAudioToAudioAvailable = false; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat')) { + $isAudioToAudioAvailable = isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID]); + } if ($audioAttachment !== null && isset($audioAttachment['type']) && $audioAttachment['type'] === 'Audio' - && class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat') - && isset($this->taskProcessingManager->getAvailableTaskTypes()[\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID]) + && $isAudioToAudioAvailable ) { // for an audio chat task, let's try to get the remote audio IDs for all the previous audio messages $history = $this->getAudioHistory($history); @@ -1006,6 +1013,7 @@ private function scheduleAgencyTask(string $content, int $confirmation, string $ 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID, $taskInput, @@ -1027,6 +1035,7 @@ private function scheduleAudioChatTask( 'system_prompt' => $systemPrompt, 'history' => $history, ]; + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, $input, @@ -1048,6 +1057,7 @@ private function scheduleAgencyAudioTask( 'confirmation' => $confirmation, 'conversation_token' => $conversationToken, ]; + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID, $taskInput, diff --git a/lib/Listener/ChattyLLMTaskListener.php b/lib/Listener/ChattyLLMTaskListener.php index 47f552ef..464a99e5 100644 --- a/lib/Listener/ChattyLLMTaskListener.php +++ b/lib/Listener/ChattyLLMTaskListener.php @@ -105,6 +105,8 @@ public function handle(Event $event): void { } else { $content = trim($taskOutput['output'] ?? ''); $message->setContent($content); + // the task is not an audio one, but we might still need to Tts the answer + // if it is a response to a ContextAgentInteraction confirmation that was asked about an audio message $this->runTtsIfNeeded($sessionId, $message, $taskTypeId, $task->getUserId()); } try { @@ -135,7 +137,8 @@ public function handle(Event $event): void { * @return void */ private function runTtsIfNeeded(int $sessionId, Message $message, string $taskTypeId, ?string $userId): void { - if ($taskTypeId !== \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID) { + if (!class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentInteraction') + || $taskTypeId !== \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID) { return; } // is the last non-empty user message an audio one? @@ -157,6 +160,7 @@ private function runTtsIfNeeded(int $sessionId, Message $message, string $taskTy */ private function runTtsTask(Message $message, ?string $userId): void { try { + /** @psalm-suppress UndefinedClass */ $task = new Task( \OCP\TaskProcessing\TaskTypes\TextToSpeech::ID, ['input' => $message->getContent()], diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index acacaf2f..1998549f 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -214,12 +214,19 @@ public function cancelNotifyWhenReady(int $taskId, string $userId): void { public function isAudioChatAvailable(): bool { $availableTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); + $ttsAvailable = false; + // see https://github.com/vimeo/psalm/issues/7980 + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech')) { + $ttsAvailable = array_key_exists(\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID, $availableTaskTypes); + } + $audioToAudioAvailable = false; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat')) { + $audioToAudioAvailable = array_key_exists(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, $availableTaskTypes); + } // we have at least the simple audio chat task type and the 3 sub task types available - return class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat') - && array_key_exists(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID, $availableTaskTypes) + return $audioToAudioAvailable + && $ttsAvailable && array_key_exists(AudioToText::ID, $availableTaskTypes) - && class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToSpeech') - && array_key_exists(\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID, $availableTaskTypes) && array_key_exists(TextToTextChat::ID, $availableTaskTypes); } @@ -291,21 +298,26 @@ public function getAvailableTaskTypes(): array { if ($taskTypeArray['isInternal'] ?? false) { continue; } - if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToTextChatWithTools') - && $typeId === \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID) { - continue; + // see https://github.com/vimeo/psalm/issues/7980 + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToTextChatWithTools')) { + if ($typeId === \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID) { + continue; + } } - if (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentInteraction') - && $typeId === \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID) { - continue; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentInteraction')) { + if ($typeId === \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID) { + continue; + } } - if (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction') - && $typeId === \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID) { - continue; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\ContextAgentAudioInteraction')) { + if ($typeId === \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID) { + continue; + } } - if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat') - && $typeId === \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID) { - continue; + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat')) { + if ($typeId === \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID) { + continue; + } } } if ($typeId === TextToTextChat::ID) { diff --git a/lib/TaskProcessing/AudioToAudioChatProvider.php b/lib/TaskProcessing/AudioToAudioChatProvider.php index b2d74a4c..336e72e3 100644 --- a/lib/TaskProcessing/AudioToAudioChatProvider.php +++ b/lib/TaskProcessing/AudioToAudioChatProvider.php @@ -41,6 +41,7 @@ public function getName(): string { } public function getTaskTypeId(): string { + /** @psalm-suppress UndefinedClass */ return AudioToAudioChat::ID; } @@ -134,6 +135,8 @@ public function process(?string $userId, array $input, callable $reportProgress) // text to speech try { + // this provider is not declared if TextToSpeech does not exist so we know it's fine + /** @psalm-suppress UndefinedClass */ $task = new Task( TextToSpeech::ID, ['input' => $llmResult], diff --git a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php index 5d57865a..fae2a81f 100644 --- a/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php +++ b/lib/TaskProcessing/ContextAgentAudioInteractionProvider.php @@ -41,6 +41,7 @@ public function getName(): string { } public function getTaskTypeId(): string { + /** @psalm-suppress UndefinedClass */ return ContextAgentAudioInteraction::ID; } @@ -115,6 +116,7 @@ public function process(?string $userId, array $input, callable $reportProgress) // context agent try { + /** @psalm-suppress UndefinedClass */ $task = new Task( ContextAgentInteraction::ID, [ @@ -134,6 +136,7 @@ public function process(?string $userId, array $input, callable $reportProgress) if ($agencyTaskOutput['output'] !== '') { // text to speech try { + /** @psalm-suppress UndefinedClass */ $task = new Task( TextToSpeech::ID, ['input' => $agencyTaskOutput['output']], diff --git a/psalm.xml b/psalm.xml index 29986094..4eae0b07 100644 --- a/psalm.xml +++ b/psalm.xml @@ -9,6 +9,8 @@ findUnusedBaselineEntry="true" findUnusedCode="false" resolveFromConfigFile="true" + ensureOverrideAttribute="false" + strictBinaryOperands="false" phpVersion="8.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" @@ -35,12 +37,6 @@ - - - - - -