diff --git a/src/Builders/PromptBuilder.php b/src/Builders/PromptBuilder.php index a6fb8cad..62872534 100644 --- a/src/Builders/PromptBuilder.php +++ b/src/Builders/PromptBuilder.php @@ -15,9 +15,7 @@ use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; use WordPress\AiClient\Providers\Models\DTO\ModelConfig; use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; -use WordPress\AiClient\Providers\Models\DTO\RequiredOption; use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum; -use WordPress\AiClient\Providers\Models\Enums\OptionEnum; use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface; use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface; use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; @@ -25,6 +23,7 @@ use WordPress\AiClient\Providers\ProviderRegistry; use WordPress\AiClient\Results\DTO\GenerativeAiResult; use WordPress\AiClient\Tools\DTO\FunctionResponse; +use WordPress\AiClient\Utils\RequirementsUtil; /** * Fluent builder for constructing AI prompts. @@ -387,68 +386,7 @@ public function asJsonResponse(?array $schema = null): self */ private function getModelRequirements(CapabilityEnum $capability): ModelRequirements { - $capabilities = [$capability]; - $inputModalities = []; - - // Check if we have chat history (multiple messages) - if (count($this->messages) > 1) { - $capabilities[] = CapabilityEnum::chatHistory(); - } - - // Analyze all messages to determine required input modalities - $hasFunctionMessageParts = false; - foreach ($this->messages as $message) { - foreach ($message->getParts() as $part) { - // Check for text input - if ($part->getType()->isText()) { - $inputModalities[] = ModalityEnum::text(); - } - - // Check for file inputs - if ($part->getType()->isFile()) { - $file = $part->getFile(); - - if ($file !== null) { - if ($file->isImage()) { - $inputModalities[] = ModalityEnum::image(); - } elseif ($file->isAudio()) { - $inputModalities[] = ModalityEnum::audio(); - } elseif ($file->isVideo()) { - $inputModalities[] = ModalityEnum::video(); - } elseif ($file->isDocument() || $file->isText()) { - $inputModalities[] = ModalityEnum::document(); - } - } - } - - // Check for function calls/responses (these might require special capabilities) - if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) { - $hasFunctionMessageParts = true; - } - } - } - - // Build required options from ModelConfig - $requiredOptions = $this->modelConfig->toRequiredOptions(); - - if ($hasFunctionMessageParts) { - // Add function declarations option if we have function calls/responses - $requiredOptions = $this->includeInRequiredOptions( - $requiredOptions, - new RequiredOption(OptionEnum::functionDeclarations(), true) - ); - } - - // Add input modalities if we have any inputs - $requiredOptions = $this->includeInRequiredOptions( - $requiredOptions, - new RequiredOption(OptionEnum::inputModalities(), $inputModalities) - ); - - return new ModelRequirements( - $capabilities, - $requiredOptions - ); + return RequirementsUtil::fromMessages($this->messages, $capability, $this->modelConfig); } /** @@ -1148,32 +1086,6 @@ private function isMessagesList($value): bool return true; } - /** - * Includes a required option in the list if not already present. - * - * Checks if a RequiredOption with the same name already exists in the list. - * If not, adds the new option. Returns the updated list. - * - * @since n.e.x.t - * - * @param list $options The existing list of required options. - * @param RequiredOption $option The option to potentially add. - * @return list The updated list of required options. - */ - private function includeInRequiredOptions(array $options, RequiredOption $option): array - { - // Check if an option with the same name already exists - foreach ($options as $existingOption) { - if ($existingOption->getName()->equals($option->getName())) { - // Option already exists, return unchanged list - return $options; - } - } - - // Add the new option - $options[] = $option; - return $options; - } /** * Includes output modalities if not already present. diff --git a/src/Utils/CapabilityUtil.php b/src/Utils/CapabilityUtil.php new file mode 100644 index 00000000..bfd70e45 --- /dev/null +++ b/src/Utils/CapabilityUtil.php @@ -0,0 +1,235 @@ +isTextGeneration()) { + return ModalityEnum::text(); + } + + if ($capability->isImageGeneration()) { + return ModalityEnum::image(); + } + + if ($capability->isSpeechGeneration() || $capability->isTextToSpeechConversion()) { + return ModalityEnum::audio(); + } + + if ($capability->isMusicGeneration()) { + return ModalityEnum::audio(); + } + + if ($capability->isVideoGeneration()) { + return ModalityEnum::video(); + } + + // Embedding generation doesn't have a traditional modality + return null; + } + + /** + * Determines if a capability requires specific input modalities. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability The capability to check. + * @return list Default input modalities for the capability. + */ + public static function getDefaultInputModalities(CapabilityEnum $capability): array + { + // Most generation types primarily use text input for prompts + if ( + $capability->isTextGeneration() || + $capability->isImageGeneration() || + $capability->isSpeechGeneration() || + $capability->isMusicGeneration() || + $capability->isVideoGeneration() + ) { + return [ModalityEnum::text()]; + } + + // Text-to-speech typically uses text input + if ($capability->isTextToSpeechConversion()) { + return [ModalityEnum::text()]; + } + + // Embedding generation can handle various inputs + if ($capability->isEmbeddingGeneration()) { + return [ModalityEnum::text()]; + } + + return []; + } + + /** + * Checks if two capabilities are compatible for the same operation. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability1 First capability. + * @param CapabilityEnum $capability2 Second capability. + * @return bool True if compatible, false otherwise. + */ + public static function areCompatible(CapabilityEnum $capability1, CapabilityEnum $capability2): bool + { + // Same capability is always compatible + if ($capability1->equals($capability2)) { + return true; + } + + // Chat history is compatible with most generation types + if ($capability1->isChatHistory() || $capability2->isChatHistory()) { + return true; + } + + // Different generation types are generally not compatible + $generationCapabilities = [ + $capability1->isTextGeneration(), + $capability1->isImageGeneration(), + $capability1->isSpeechGeneration(), + $capability1->isTextToSpeechConversion(), + $capability1->isMusicGeneration(), + $capability1->isVideoGeneration(), + $capability1->isEmbeddingGeneration(), + ]; + + $capability2Generations = [ + $capability2->isTextGeneration(), + $capability2->isImageGeneration(), + $capability2->isSpeechGeneration(), + $capability2->isTextToSpeechConversion(), + $capability2->isMusicGeneration(), + $capability2->isVideoGeneration(), + $capability2->isEmbeddingGeneration(), + ]; + + // If both are generation types, they're not compatible + if (in_array(true, $generationCapabilities, true) && in_array(true, $capability2Generations, true)) { + return false; + } + + return true; + } + + /** + * Gets all generation-type capabilities. + * + * @since n.e.x.t + * + * @return list All generation capabilities. + */ + public static function getAllGenerationCapabilities(): array + { + return [ + CapabilityEnum::textGeneration(), + CapabilityEnum::imageGeneration(), + CapabilityEnum::speechGeneration(), + CapabilityEnum::textToSpeechConversion(), + CapabilityEnum::musicGeneration(), + CapabilityEnum::videoGeneration(), + CapabilityEnum::embeddingGeneration(), + ]; + } + + /** + * Determines if a capability produces file output. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability The capability to check. + * @return bool True if the capability produces file output. + */ + public static function producesFileOutput(CapabilityEnum $capability): bool + { + return $capability->isImageGeneration() || + $capability->isSpeechGeneration() || + $capability->isTextToSpeechConversion() || + $capability->isMusicGeneration() || + $capability->isVideoGeneration(); + } + + /** + * Gets suggested file extensions for a capability's output. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability The capability. + * @return list Suggested file extensions (without dots). + */ + public static function getSuggestedFileExtensions(CapabilityEnum $capability): array + { + if ($capability->isImageGeneration()) { + return ['png', 'jpg', 'jpeg', 'webp']; + } + + if ($capability->isSpeechGeneration() || $capability->isTextToSpeechConversion()) { + return ['mp3', 'wav', 'ogg']; + } + + if ($capability->isMusicGeneration()) { + return ['mp3', 'wav', 'midi']; + } + + if ($capability->isVideoGeneration()) { + return ['mp4', 'webm', 'mov']; + } + + return []; + } +} diff --git a/src/Utils/RequirementsUtil.php b/src/Utils/RequirementsUtil.php new file mode 100644 index 00000000..c1bc27da --- /dev/null +++ b/src/Utils/RequirementsUtil.php @@ -0,0 +1,247 @@ + $messages The messages to analyze. + * @param CapabilityEnum $primaryCapability The primary capability required. + * @param ModelConfig|null $modelConfig Optional model configuration. + * @return ModelRequirements The inferred requirements. + */ + public static function fromMessages( + array $messages, + CapabilityEnum $primaryCapability, + ?ModelConfig $modelConfig = null + ): ModelRequirements { + $capabilities = [$primaryCapability]; + $requiredOptions = []; + + // Analyze message context + $messageAnalysis = self::analyzeMessages($messages); + + // Add chat history capability if multiple messages + if ($messageAnalysis['requiresChatHistory']) { + $capabilities[] = CapabilityEnum::chatHistory(); + } + + // Add input modalities requirement + if (!empty($messageAnalysis['inputModalities'])) { + $requiredOptions[] = new RequiredOption( + OptionEnum::inputModalities(), + $messageAnalysis['inputModalities'] + ); + } + + // Add function calling requirement if needed + if ($messageAnalysis['requiresFunctionCalling']) { + $requiredOptions[] = new RequiredOption( + OptionEnum::functionDeclarations(), + true + ); + } + + // Include requirements from model configuration + if ($modelConfig !== null) { + $configRequirements = $modelConfig->toRequiredOptions(); + $requiredOptions = self::mergeRequiredOptions($requiredOptions, $configRequirements); + } + + return new ModelRequirements($capabilities, $requiredOptions); + } + + /** + * Builds basic model requirements for a single capability. + * + * @since n.e.x.t + * + * @param CapabilityEnum $capability The required capability. + * @param ModelConfig|null $modelConfig Optional model configuration. + * @return ModelRequirements The basic requirements. + */ + public static function basic(CapabilityEnum $capability, ?ModelConfig $modelConfig = null): ModelRequirements + { + $requiredOptions = []; + + if ($modelConfig !== null) { + $requiredOptions = $modelConfig->toRequiredOptions(); + } + + return new ModelRequirements([$capability], $requiredOptions); + } + + /** + * Analyzes a list of messages to determine requirements context. + * + * @since n.e.x.t + * + * @param list $messages The messages to analyze. + * @return array{ + * requiresChatHistory: bool, + * inputModalities: list, + * requiresFunctionCalling: bool, + * hasTextInput: bool, + * hasFileInput: bool + * } Analysis results. + */ + public static function analyzeMessages(array $messages): array + { + $inputModalities = []; + $hasFunctionMessageParts = false; + $hasTextInput = false; + $hasFileInput = false; + + foreach ($messages as $message) { + foreach ($message->getParts() as $part) { + // Check for text input + if ($part->getType()->isText()) { + $hasTextInput = true; + $inputModalities[] = ModalityEnum::text(); + } + + // Check for file inputs + if ($part->getType()->isFile()) { + $hasFileInput = true; + $file = $part->getFile(); + + if ($file !== null) { + if ($file->isImage()) { + $inputModalities[] = ModalityEnum::image(); + } elseif ($file->isAudio()) { + $inputModalities[] = ModalityEnum::audio(); + } elseif ($file->isVideo()) { + $inputModalities[] = ModalityEnum::video(); + } elseif ($file->isDocument() || $file->isText()) { + $inputModalities[] = ModalityEnum::document(); + } + } + } + + // Check for function calls/responses + if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) { + $hasFunctionMessageParts = true; + } + } + } + + return [ + 'requiresChatHistory' => count($messages) > 1, + 'inputModalities' => array_values(array_unique($inputModalities, SORT_REGULAR)), + 'requiresFunctionCalling' => $hasFunctionMessageParts, + 'hasTextInput' => $hasTextInput, + 'hasFileInput' => $hasFileInput, + ]; + } + + /** + * Merges two arrays of required options, avoiding duplicates. + * + * @since n.e.x.t + * + * @param list $existing Existing required options. + * @param list $new New required options to merge. + * @return list Merged required options. + */ + public static function mergeRequiredOptions(array $existing, array $new): array + { + $merged = $existing; + + foreach ($new as $newOption) { + $merged = self::includeInRequiredOptions($merged, $newOption); + } + + return $merged; + } + + /** + * Includes a required option in the array, replacing existing ones with same key. + * + * @since n.e.x.t + * + * @param list $requiredOptions Existing options. + * @param RequiredOption $optionToInclude Option to include. + * @return list Updated options array. + */ + private static function includeInRequiredOptions(array $requiredOptions, RequiredOption $optionToInclude): array + { + // Remove any existing option with the same key + $filtered = array_filter( + $requiredOptions, + static fn(RequiredOption $option): bool => !$option->getName()->equals($optionToInclude->getName()) + ); + + // Add the new option + $filtered[] = $optionToInclude; + + return array_values($filtered); + } + + /** + * Creates requirements for multi-modal operations. + * + * @since n.e.x.t + * + * @param CapabilityEnum $primaryCapability The primary capability. + * @param list $inputModalities Required input modalities. + * @param list $outputModalities Required output modalities. + * @param ModelConfig|null $modelConfig Optional configuration. + * @return ModelRequirements Multi-modal requirements. + */ + public static function multiModal( + CapabilityEnum $primaryCapability, + array $inputModalities, + array $outputModalities = [], + ?ModelConfig $modelConfig = null + ): ModelRequirements { + $capabilities = [$primaryCapability]; + $requiredOptions = []; + + // Add input modalities + if (!empty($inputModalities)) { + $requiredOptions[] = new RequiredOption( + OptionEnum::inputModalities(), + $inputModalities + ); + } + + // Add output modalities if specified + if (!empty($outputModalities)) { + $requiredOptions[] = new RequiredOption( + OptionEnum::outputModalities(), + $outputModalities + ); + } + + // Include configuration requirements + if ($modelConfig !== null) { + $configRequirements = $modelConfig->toRequiredOptions(); + $requiredOptions = self::mergeRequiredOptions($requiredOptions, $configRequirements); + } + + return new ModelRequirements($capabilities, $requiredOptions); + } +} diff --git a/tests/unit/Utils/CapabilityUtilTest.php b/tests/unit/Utils/CapabilityUtilTest.php new file mode 100644 index 00000000..c2cadda7 --- /dev/null +++ b/tests/unit/Utils/CapabilityUtilTest.php @@ -0,0 +1,315 @@ +assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::textGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('image'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::imageGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('speech'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::speechGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('text-to-speech'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::textToSpeechConversion())); + + $result = CapabilityUtil::getCapabilityForGenerationType('tts'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::textToSpeechConversion())); + + $result = CapabilityUtil::getCapabilityForGenerationType('music'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::musicGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('video'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::videoGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('embedding'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::embeddingGeneration())); + + $result = CapabilityUtil::getCapabilityForGenerationType('embeddings'); + $this->assertNotNull($result); + $this->assertTrue($result->equals(CapabilityEnum::embeddingGeneration())); + } + + /** + * Tests case insensitive capability mapping. + */ + public function testGetCapabilityForGenerationTypeIsCaseInsensitive(): void + { + $textResult = CapabilityUtil::getCapabilityForGenerationType('TEXT'); + $this->assertNotNull($textResult); + $this->assertTrue($textResult->equals(CapabilityEnum::textGeneration())); + + $imageResult = CapabilityUtil::getCapabilityForGenerationType('Image'); + $this->assertNotNull($imageResult); + $this->assertTrue($imageResult->equals(CapabilityEnum::imageGeneration())); + + $speechResult = CapabilityUtil::getCapabilityForGenerationType('SPEECH'); + $this->assertNotNull($speechResult); + $this->assertTrue($speechResult->equals(CapabilityEnum::speechGeneration())); + } + + /** + * Tests unknown generation types return null. + */ + public function testGetCapabilityForGenerationTypeReturnsNullForUnknown(): void + { + $this->assertNull(CapabilityUtil::getCapabilityForGenerationType('unknown')); + $this->assertNull(CapabilityUtil::getCapabilityForGenerationType('')); + $this->assertNull(CapabilityUtil::getCapabilityForGenerationType('invalid-type')); + } + + /** + * Tests primary output modality mapping. + */ + public function testGetPrimaryOutputModality(): void + { + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::textGeneration()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::text())); + + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::imageGeneration()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::image())); + + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::speechGeneration()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::audio())); + + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::textToSpeechConversion()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::audio())); + + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::musicGeneration()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::audio())); + + $result = CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::videoGeneration()); + $this->assertNotNull($result); + $this->assertTrue($result->equals(ModalityEnum::video())); + } + + /** + * Tests embedding generation returns null for output modality. + */ + public function testGetPrimaryOutputModalityReturnsNullForEmbedding(): void + { + $this->assertNull(CapabilityUtil::getPrimaryOutputModality(CapabilityEnum::embeddingGeneration())); + } + + /** + * Tests default input modalities for capabilities. + */ + public function testGetDefaultInputModalities(): void + { + $expected = [ModalityEnum::text()]; + + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::textGeneration()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::imageGeneration()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::speechGeneration()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::textToSpeechConversion()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::musicGeneration()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::videoGeneration()) + ); + $this->assertEquals( + $expected, + CapabilityUtil::getDefaultInputModalities(CapabilityEnum::embeddingGeneration()) + ); + } + + /** + * Tests chat history default input modalities. + */ + public function testGetDefaultInputModalitiesForChatHistory(): void + { + $this->assertEquals([], CapabilityUtil::getDefaultInputModalities(CapabilityEnum::chatHistory())); + } + + /** + * Tests capability compatibility. + */ + public function testAreCompatible(): void + { + // Same capability is compatible + $this->assertTrue(CapabilityUtil::areCompatible( + CapabilityEnum::textGeneration(), + CapabilityEnum::textGeneration() + )); + + // Chat history is compatible with generation types + $this->assertTrue(CapabilityUtil::areCompatible( + CapabilityEnum::chatHistory(), + CapabilityEnum::textGeneration() + )); + $this->assertTrue(CapabilityUtil::areCompatible( + CapabilityEnum::textGeneration(), + CapabilityEnum::chatHistory() + )); + + // Different generation types are not compatible + $this->assertFalse(CapabilityUtil::areCompatible( + CapabilityEnum::textGeneration(), + CapabilityEnum::imageGeneration() + )); + $this->assertFalse(CapabilityUtil::areCompatible( + CapabilityEnum::speechGeneration(), + CapabilityEnum::videoGeneration() + )); + } + + /** + * Tests getting all generation capabilities. + */ + public function testGetAllGenerationCapabilities(): void + { + $capabilities = CapabilityUtil::getAllGenerationCapabilities(); + + $this->assertCount(7, $capabilities); + $this->assertContains(CapabilityEnum::textGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::imageGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::speechGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::textToSpeechConversion(), $capabilities); + $this->assertContains(CapabilityEnum::musicGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::videoGeneration(), $capabilities); + $this->assertContains(CapabilityEnum::embeddingGeneration(), $capabilities); + } + + /** + * Tests chat history is not in generation capabilities. + */ + public function testGetAllGenerationCapabilitiesExcludesChatHistory(): void + { + $capabilities = CapabilityUtil::getAllGenerationCapabilities(); + $this->assertNotContains(CapabilityEnum::chatHistory(), $capabilities); + } + + /** + * Tests file output detection. + */ + public function testProducesFileOutput(): void + { + // Capabilities that produce files + $this->assertTrue(CapabilityUtil::producesFileOutput(CapabilityEnum::imageGeneration())); + $this->assertTrue(CapabilityUtil::producesFileOutput(CapabilityEnum::speechGeneration())); + $this->assertTrue(CapabilityUtil::producesFileOutput(CapabilityEnum::textToSpeechConversion())); + $this->assertTrue(CapabilityUtil::producesFileOutput(CapabilityEnum::musicGeneration())); + $this->assertTrue(CapabilityUtil::producesFileOutput(CapabilityEnum::videoGeneration())); + + // Capabilities that don't produce files + $this->assertFalse(CapabilityUtil::producesFileOutput(CapabilityEnum::textGeneration())); + $this->assertFalse(CapabilityUtil::producesFileOutput(CapabilityEnum::embeddingGeneration())); + $this->assertFalse(CapabilityUtil::producesFileOutput(CapabilityEnum::chatHistory())); + } + + /** + * Tests file extension suggestions. + */ + public function testGetSuggestedFileExtensions(): void + { + // Image generation + $imageExtensions = CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::imageGeneration()); + $this->assertEquals(['png', 'jpg', 'jpeg', 'webp'], $imageExtensions); + + // Speech generation + $speechExtensions = CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::speechGeneration()); + $this->assertEquals(['mp3', 'wav', 'ogg'], $speechExtensions); + + // Text-to-speech + $ttsExtensions = CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::textToSpeechConversion()); + $this->assertEquals(['mp3', 'wav', 'ogg'], $ttsExtensions); + + // Music generation + $musicExtensions = CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::musicGeneration()); + $this->assertEquals(['mp3', 'wav', 'midi'], $musicExtensions); + + // Video generation + $videoExtensions = CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::videoGeneration()); + $this->assertEquals(['mp4', 'webm', 'mov'], $videoExtensions); + } + + /** + * Tests no file extensions for non-file-producing capabilities. + */ + public function testGetSuggestedFileExtensionsReturnsEmptyForNonFileCapabilities(): void + { + $this->assertEmpty(CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::textGeneration())); + $this->assertEmpty(CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::embeddingGeneration())); + $this->assertEmpty(CapabilityUtil::getSuggestedFileExtensions(CapabilityEnum::chatHistory())); + } + + /** + * Tests comprehensive compatibility matrix. + */ + public function testComprehensiveCompatibilityMatrix(): void + { + $generationCapabilities = CapabilityUtil::getAllGenerationCapabilities(); + + // Test that no two different generation capabilities are compatible + for ($i = 0; $i < count($generationCapabilities); $i++) { + for ($j = $i + 1; $j < count($generationCapabilities); $j++) { + $this->assertFalse( + CapabilityUtil::areCompatible($generationCapabilities[$i], $generationCapabilities[$j]), + sprintf( + 'Generation capabilities should not be compatible: %s vs %s', + $generationCapabilities[$i]->value, + $generationCapabilities[$j]->value + ) + ); + } + } + + // Test that all generation capabilities are compatible with chat history + foreach ($generationCapabilities as $capability) { + $this->assertTrue( + CapabilityUtil::areCompatible($capability, CapabilityEnum::chatHistory()), + sprintf('Generation capability %s should be compatible with chat history', $capability->value) + ); + $this->assertTrue( + CapabilityUtil::areCompatible(CapabilityEnum::chatHistory(), $capability), + sprintf('Chat history should be compatible with generation capability %s', $capability->value) + ); + } + } +} diff --git a/tests/unit/Utils/RequirementsUtilTest.php b/tests/unit/Utils/RequirementsUtilTest.php new file mode 100644 index 00000000..9ac23717 --- /dev/null +++ b/tests/unit/Utils/RequirementsUtilTest.php @@ -0,0 +1,275 @@ +getRequiredCapabilities(); + $this->assertCount(1, $capabilities); + $this->assertTrue($capabilities[0]->equals(CapabilityEnum::textGeneration())); + $this->assertEmpty($requirements->getRequiredOptions()); + } + + /** + * Tests basic requirements with model configuration. + */ + public function testBasicIncludesModelConfigRequirements(): void + { + $modelConfig = new ModelConfig(); + $modelConfig->setMaxTokens(100); + + $requirements = RequirementsUtil::basic(CapabilityEnum::textGeneration(), $modelConfig); + + $this->assertCount(1, $requirements->getRequiredCapabilities()); + $this->assertNotEmpty($requirements->getRequiredOptions()); + + // Should include max tokens from config + $hasMaxTokens = false; + foreach ($requirements->getRequiredOptions() as $option) { + if ($option->getName()->equals(OptionEnum::maxTokens())) { + $hasMaxTokens = true; + $this->assertEquals(100, $option->getValue()); + break; + } + } + $this->assertTrue($hasMaxTokens, 'Should include maxTokens requirement from config'); + } + + /** + * Tests requirements creation from single message. + */ + public function testFromMessagesWithSingleTextMessage(): void + { + $message = new UserMessage([new MessagePart('Hello, world!')]); + $messages = [$message]; + + $requirements = RequirementsUtil::fromMessages($messages, CapabilityEnum::textGeneration()); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertCount(1, $capabilities); + $this->assertTrue($capabilities[0]->equals(CapabilityEnum::textGeneration())); + + // Should include text input modality + $inputModalitiesOption = $this->findRequiredOption($requirements, OptionEnum::inputModalities()); + $this->assertNotNull($inputModalitiesOption); + $inputModalities = $inputModalitiesOption->getValue(); + $this->assertIsArray($inputModalities); + $this->assertContains(ModalityEnum::text(), $inputModalities); + } + + /** + * Tests requirements with multiple messages (chat history). + */ + public function testFromMessagesWithMultipleMessagesAddsChatHistory(): void + { + $messages = [ + new UserMessage([new MessagePart('First message')]), + new Message(MessageRoleEnum::model(), [new MessagePart('Response')]), + new UserMessage([new MessagePart('Second message')]), + ]; + + $requirements = RequirementsUtil::fromMessages($messages, CapabilityEnum::textGeneration()); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertCount(2, $capabilities); + + $hasTextGeneration = false; + $hasChatHistory = false; + foreach ($capabilities as $capability) { + if ($capability->equals(CapabilityEnum::textGeneration())) { + $hasTextGeneration = true; + } elseif ($capability->equals(CapabilityEnum::chatHistory())) { + $hasChatHistory = true; + } + } + + $this->assertTrue($hasTextGeneration, 'Should include primary capability'); + $this->assertTrue($hasChatHistory, 'Should include chat history capability'); + } + + /** + * Tests message analysis functionality. + */ + public function testAnalyzeMessagesWithTextOnly(): void + { + $messages = [new UserMessage([new MessagePart('Hello')])]; + + $analysis = RequirementsUtil::analyzeMessages($messages); + + $this->assertFalse($analysis['requiresChatHistory']); + $this->assertFalse($analysis['requiresFunctionCalling']); + $this->assertTrue($analysis['hasTextInput']); + $this->assertFalse($analysis['hasFileInput']); + $this->assertContains(ModalityEnum::text(), $analysis['inputModalities']); + } + + /** + * Tests message analysis with multiple messages. + */ + public function testAnalyzeMessagesWithMultipleMessages(): void + { + $messages = [ + new UserMessage([new MessagePart('First')]), + new UserMessage([new MessagePart('Second')]), + ]; + + $analysis = RequirementsUtil::analyzeMessages($messages); + + $this->assertTrue($analysis['requiresChatHistory']); + $this->assertTrue($analysis['hasTextInput']); + } + + /** + * Tests multi-modal requirements creation. + */ + public function testMultiModalCreatesCorrectRequirements(): void + { + $inputModalities = [ModalityEnum::text(), ModalityEnum::image()]; + $outputModalities = [ModalityEnum::text()]; + + $requirements = RequirementsUtil::multiModal( + CapabilityEnum::textGeneration(), + $inputModalities, + $outputModalities + ); + + // Should have primary capability + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertCount(1, $capabilities); + $this->assertTrue($capabilities[0]->equals(CapabilityEnum::textGeneration())); + + // Should include input modalities + $inputOption = $this->findRequiredOption($requirements, OptionEnum::inputModalities()); + $this->assertNotNull($inputOption); + $this->assertEquals($inputModalities, $inputOption->getValue()); + + // Should include output modalities + $outputOption = $this->findRequiredOption($requirements, OptionEnum::outputModalities()); + $this->assertNotNull($outputOption); + $this->assertEquals($outputModalities, $outputOption->getValue()); + } + + + /** + * Tests merging required options. + */ + public function testMergeRequiredOptionsReplacesExisting(): void + { + $existing = [ + new RequiredOption(OptionEnum::maxTokens(), 50), + new RequiredOption(OptionEnum::temperature(), 0.5), + ]; + + $new = [ + new RequiredOption(OptionEnum::maxTokens(), 100), // Should replace existing + new RequiredOption(OptionEnum::topP(), 0.9), // Should be added + ]; + + $merged = RequirementsUtil::mergeRequiredOptions($existing, $new); + + $this->assertCount(3, $merged); + + // Check maxTokens was replaced + $maxTokensOption = $this->findRequiredOptionInArray($merged, OptionEnum::maxTokens()); + $this->assertNotNull($maxTokensOption); + $this->assertEquals(100, $maxTokensOption->getValue()); + + // Check temperature was preserved + $tempOption = $this->findRequiredOptionInArray($merged, OptionEnum::temperature()); + $this->assertNotNull($tempOption); + $this->assertEquals(0.5, $tempOption->getValue()); + + // Check topP was added + $topPOption = $this->findRequiredOptionInArray($merged, OptionEnum::topP()); + $this->assertNotNull($topPOption); + $this->assertEquals(0.9, $topPOption->getValue()); + } + + /** + * Tests empty message array handling. + */ + public function testFromMessagesWithEmptyArray(): void + { + $requirements = RequirementsUtil::fromMessages([], CapabilityEnum::textGeneration()); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertCount(1, $capabilities); + $this->assertTrue($capabilities[0]->equals(CapabilityEnum::textGeneration())); + + // Should have minimal requirements + $this->assertEmpty($requirements->getRequiredOptions()); + } + + /** + * Tests multi-modal with empty modalities. + */ + public function testMultiModalWithEmptyModalities(): void + { + $requirements = RequirementsUtil::multiModal(CapabilityEnum::textGeneration(), []); + + $capabilities = $requirements->getRequiredCapabilities(); + $this->assertCount(1, $capabilities); + $this->assertTrue($capabilities[0]->equals(CapabilityEnum::textGeneration())); + + // Should not have modality requirements for empty arrays + $inputOption = $this->findRequiredOption($requirements, OptionEnum::inputModalities()); + $outputOption = $this->findRequiredOption($requirements, OptionEnum::outputModalities()); + + $this->assertNull($inputOption, 'Should not add input modalities option for empty array'); + $this->assertNull($outputOption, 'Should not add output modalities option when not specified'); + } + + /** + * Helper method to find a required option by option enum. + * + * @param ModelRequirements $requirements The requirements to search. + * @param OptionEnum $targetOption The option to find. + * @return RequiredOption|null The found option or null. + */ + private function findRequiredOption(ModelRequirements $requirements, OptionEnum $targetOption): ?RequiredOption + { + return $this->findRequiredOptionInArray($requirements->getRequiredOptions(), $targetOption); + } + + /** + * Helper method to find a required option in an array. + * + * @param list $options The options array to search. + * @param OptionEnum $targetOption The option to find. + * @return RequiredOption|null The found option or null. + */ + private function findRequiredOptionInArray(array $options, OptionEnum $targetOption): ?RequiredOption + { + foreach ($options as $option) { + if ($option->getName()->equals($targetOption)) { + return $option; + } + } + return null; + } +}