From 3c550b7f479f4bd580a3bd6dba74b8924fb969de Mon Sep 17 00:00:00 2001 From: Jan Kellermann <44717-werk21@users.noreply.drupalcode.org> Date: Thu, 16 Jan 2025 01:59:59 +0000 Subject: [PATCH 1/9] Invoke ai_operationtype_alter. --- src/AiProviderPluginManager.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AiProviderPluginManager.php b/src/AiProviderPluginManager.php index ecfbe8661..30651ff9e 100644 --- a/src/AiProviderPluginManager.php +++ b/src/AiProviderPluginManager.php @@ -295,6 +295,9 @@ final class AiProviderPluginManager extends DefaultPluginManager { } } + // Invoke ai_operationtype_alter. + $this->moduleHandler->invokeAll('ai_operationtype_alter', [&$operation_types]); + // Save to cache. $this->cacheBackend->set('ai_operation_types', $operation_types); -- GitLab From f466faf20c6c8c1baf7fb225316919174caedaac Mon Sep 17 00:00:00 2001 From: Jan Kellermann <44717-werk21@users.noreply.drupalcode.org> Date: Thu, 16 Jan 2025 08:55:29 +0000 Subject: [PATCH 2/9] cspell: Add operationtype --- .cspell-project-words.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt index f53924a2d..19daa0249 100644 --- a/.cspell-project-words.txt +++ b/.cspell-project-words.txt @@ -124,6 +124,7 @@ octect octocat oden oneshot +operationtype outro overriden paranthesis -- GitLab From 73d861ed2b420fc3ded6e3adeb79180d638066de Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Tue, 21 Jan 2025 11:08:35 +0100 Subject: [PATCH 3/9] Issue #3500120: Add test for hook_ai_operationtype_alter --- .../ai_test/src/Hook/OperationTypeHook.php | 23 +++++ .../src/OperationType/Echo/EchoInput.php | 37 ++++++++ .../src/OperationType/Echo/EchoInterface.php | 33 +++++++ .../src/OperationType/Echo/EchoOutput.php | 90 +++++++++++++++++++ .../src/Plugin/AiProvider/EchoProvider.php | 16 +++- .../Custom/EchoOperationHookTest.php | 47 ++++++++++ 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 tests/modules/ai_test/src/Hook/OperationTypeHook.php create mode 100644 tests/modules/ai_test/src/OperationType/Echo/EchoInput.php create mode 100644 tests/modules/ai_test/src/OperationType/Echo/EchoInterface.php create mode 100644 tests/modules/ai_test/src/OperationType/Echo/EchoOutput.php create mode 100644 tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php diff --git a/tests/modules/ai_test/src/Hook/OperationTypeHook.php b/tests/modules/ai_test/src/Hook/OperationTypeHook.php new file mode 100644 index 000000000..3561e7ff2 --- /dev/null +++ b/tests/modules/ai_test/src/Hook/OperationTypeHook.php @@ -0,0 +1,23 @@ +<?php + +namespace Drupal\ai_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hooks to interact with operation types. + */ +class OperationTypeHook { + + /** + * Implements hook_ai_operationtype_alter(). + */ + #[Hook('ai_operationtype_alter')] + public function echoOperationType(array &$operation_types) { + $operation_types[] = [ + 'id' => 'echo', + 'label' => 'Echo', + ]; + } + +} diff --git a/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php b/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php new file mode 100644 index 000000000..c38cfb8da --- /dev/null +++ b/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php @@ -0,0 +1,37 @@ +<?php + +namespace Drupal\ai_test\OperationType\Echo; + +use Drupal\ai\OperationType\InputInterface; + +/** + * Input object for echo operations. + */ +class EchoInput implements InputInterface { + + /** + * The constructor. + * + * @param string $input + * The echo input. + */ + public function __construct(private string $input) {} + + /** + * {@inheritdoc} + */ + public function toString(): string { + return $this->input; + } + + /** + * Return the input as string. + * + * @return string + * The input as string. + */ + public function __toString(): string { + return $this->toString(); + } + +} diff --git a/tests/modules/ai_test/src/OperationType/Echo/EchoInterface.php b/tests/modules/ai_test/src/OperationType/Echo/EchoInterface.php new file mode 100644 index 000000000..e86638a33 --- /dev/null +++ b/tests/modules/ai_test/src/OperationType/Echo/EchoInterface.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\ai_test\OperationType\Echo; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\ai\Attribute\OperationType; +use Drupal\ai\OperationType\OperationTypeInterface; + +/** + * Interface for text translation models. + */ +#[OperationType( + id: 'echo', + label: new TranslatableMarkup('Echo'), +)] +interface EchoInterface extends OperationTypeInterface { + + /** + * Translate the text. + * + * @param \Drupal\ai_test\OperationType\Echo\EchoInput|string $input + * The input to echo. + * @param string $model_id + * The model id to use. + * @param array $options + * Extra tags to set. + * + * @return \Drupal\ai_test\OperationType\Echo\EchoOutput + * The translation output. + */ + public function echo(string|EchoInput $input, string $model_id, array $options = []): EchoOutput; + +} diff --git a/tests/modules/ai_test/src/OperationType/Echo/EchoOutput.php b/tests/modules/ai_test/src/OperationType/Echo/EchoOutput.php new file mode 100644 index 000000000..0d8efcb9b --- /dev/null +++ b/tests/modules/ai_test/src/OperationType/Echo/EchoOutput.php @@ -0,0 +1,90 @@ +<?php + +namespace Drupal\ai_test\OperationType\Echo; + +use Drupal\ai\OperationType\OutputInterface; + +/** + * Data transfer output object text translation output. + */ +class EchoOutput implements OutputInterface { + + /** + * The normalized translated text. + * + * @var string + */ + private string $normalized; + + /** + * The raw output from the AI provider. + * + * @var mixed + */ + private mixed $rawOutput; + + /** + * The metadata from the AI provider. + * + * @var mixed + */ + private mixed $metadata; + + /** + * The constructor. + * + * @param string $normalized + * The metadata. + * @param mixed $rawOutput + * The raw output. + * @param mixed $metadata + * The metadata. + */ + public function __construct(string $normalized, mixed $rawOutput, mixed $metadata) { + $this->normalized = $normalized; + $this->rawOutput = $rawOutput; + $this->metadata = $metadata; + } + + /** + * Returns the translated text. + * + * @return string + * The text. + */ + public function getNormalized(): string { + return $this->normalized; + } + + /** + * Gets the raw output from the AI provider. + * + * @return mixed + * The raw output. + */ + public function getRawOutput(): mixed { + return $this->rawOutput; + } + + /** + * Gets the metadata from the AI provider. + * + * @return mixed + * The metadata. + */ + public function getMetadata(): mixed { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function toArray(): array { + return [ + 'normalized' => $this->normalized, + 'rawOutput' => $this->rawOutput, + 'metadata' => $this->metadata, + ]; + } + +} diff --git a/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php b/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php index 4427958df..0cde14848 100644 --- a/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php +++ b/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php @@ -34,6 +34,9 @@ use Drupal\ai\OperationType\TextToImage\TextToImageOutput; use Drupal\ai\OperationType\TextToSpeech\TextToSpeechInput; use Drupal\ai\OperationType\TextToSpeech\TextToSpeechInterface; use Drupal\ai\OperationType\TextToSpeech\TextToSpeechOutput; +use Drupal\ai_test\OperationType\Echo\EchoInput; +use Drupal\ai_test\OperationType\Echo\EchoInterface; +use Drupal\ai_test\OperationType\Echo\EchoOutput; use Symfony\Component\Yaml\Yaml; /** @@ -50,7 +53,8 @@ class EchoProvider extends AiProviderClientBase implements SpeechToTextInterface, TextToSpeechInterface, ImageClassificationInterface, - TextToImageInterface { + TextToImageInterface, + EchoInterface { /** * {@inheritdoc} @@ -230,4 +234,14 @@ class EchoProvider extends AiProviderClientBase implements return new ImageClassificationOutput($output, $response, []); } + /** + * {@inheritdoc} + */ + public function echo(string|EchoInput $input, string $model_id, array $options = []): EchoOutput { + if (!$input instanceof EchoInput) { + $input = new EchoInput($input); + } + return new EchoOutput((string) $input, ['echo' => (string) $input], []); + } + } diff --git a/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php b/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php new file mode 100644 index 000000000..47773fb6f --- /dev/null +++ b/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\ai\Kernel\OperationType\Custom; + +use Drupal\KernelTests\KernelTestBase; + +/** + * This tests the echo calling. + * + * @coversDefaultClass \Drupal\ai_test\OperationType\Echo\EchoInterface + * + * @group ai + */ +class EchoOperationHookTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + protected static $modules = [ + 'ai', + 'ai_test', + 'key', + 'file', + 'system', + ]; + + /** + * Test adding a provider via a hook. + */ + public function testCustomEchoOperation(): void { + /** @var \Drupal\ai\AiProviderPluginManager $manager */ + $manager = \Drupal::service('ai.provider'); + self::assertContains(['id' => 'echo', 'label' => 'Echo'], $manager->getOperationTypes()); + /** @var \Drupal\ai_test\Plugin\AiProvider\EchoProvider $provider */ + $provider = $manager->createInstance('echoai'); + + $input = $this->randomString(); + $output = $provider->echo($input, 'test'); + self::assertEquals($input, $output->getNormalized()); + self::assertEquals(['echo' => $input], $output->getRawOutput()); + } + +} -- GitLab From c4906b0695d35baaecc0dcab7c8f1b9ac828c2fa Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Tue, 21 Jan 2025 11:25:11 +0100 Subject: [PATCH 4/9] add echo as supported operation type --- tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php b/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php index 0cde14848..cf0520dff 100644 --- a/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php +++ b/tests/modules/ai_test/src/Plugin/AiProvider/EchoProvider.php @@ -106,6 +106,7 @@ class EchoProvider extends AiProviderClientBase implements 'text_to_speech', 'moderation', 'image_classification', + 'echo', ]; } -- GitLab From 9be17526f0a7bfd61e24073d97592a9788931593 Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Tue, 28 Jan 2025 14:06:20 +0100 Subject: [PATCH 5/9] Issue #3500120: Make operation types plugins without a plugin manager --- src/AiProviderPluginManager.php | 43 ++++++------------- .../ai_test/src/Hook/OperationTypeHook.php | 5 +-- .../Custom/EchoOperationHookTest.php | 4 +- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/AiProviderPluginManager.php b/src/AiProviderPluginManager.php index 30651ff9e..5a4b4ec8c 100644 --- a/src/AiProviderPluginManager.php +++ b/src/AiProviderPluginManager.php @@ -270,38 +270,21 @@ final class AiProviderPluginManager extends DefaultPluginManager { * The list of operation types. */ public function getOperationTypes(): array { - // Load from cache. - $data = $this->cacheBackend->get('ai_operation_types'); - if (!empty($data->data)) { - return $data->data; - } - // Look in the OperationType/** directories. - $operation_types = []; - $base_path = $this->moduleHandler->getModule('ai')->getPath() . '/src/OperationType'; - $directories = new \RecursiveDirectoryIterator($base_path); - $iterator = new \RecursiveIteratorIterator($directories); - $regex = new \RegexIterator($iterator, '/^.+\.php$/i', \RecursiveRegexIterator::GET_MATCH); - foreach ($regex as $file) { - $interface = $this->getInterfaceFromFile($file[0]); - if ($interface && $this->doesInterfaceExtend($interface, OperationTypeInterface::class)) { - $reflection = new \ReflectionClass($interface); - $attributes = $reflection->getAttributes(OperationType::class); - foreach ($attributes as $attribute) { - $operation_types[] = [ - 'id' => $attribute->newInstance()->id, - 'label' => $attribute->newInstance()->label->render(), - ]; - } + $manager = new class( + 'OperationType', + $this->namespaces, + $this->moduleHandler, + OperationTypeInterface::class, + OperationType::class + ) extends DefaultPluginManager { + public function finishSetup(): void { + $this->alterInfo('ai_operationtype'); } - } - - // Invoke ai_operationtype_alter. - $this->moduleHandler->invokeAll('ai_operationtype_alter', [&$operation_types]); - - // Save to cache. - $this->cacheBackend->set('ai_operation_types', $operation_types); + }; + $manager->finishSetup(); + $manager->setCacheBackend($this->cacheBackend, 'ai_operation_types'); - return $operation_types; + return $manager->getDefinitions(); } /** diff --git a/tests/modules/ai_test/src/Hook/OperationTypeHook.php b/tests/modules/ai_test/src/Hook/OperationTypeHook.php index 3561e7ff2..2e5bbca8e 100644 --- a/tests/modules/ai_test/src/Hook/OperationTypeHook.php +++ b/tests/modules/ai_test/src/Hook/OperationTypeHook.php @@ -14,10 +14,7 @@ class OperationTypeHook { */ #[Hook('ai_operationtype_alter')] public function echoOperationType(array &$operation_types) { - $operation_types[] = [ - 'id' => 'echo', - 'label' => 'Echo', - ]; + $operation_types['echo']['label'] = 'Echo altered'; } } diff --git a/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php b/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php index 47773fb6f..69622d521 100644 --- a/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php +++ b/tests/src/Kernel/OperationType/Custom/EchoOperationHookTest.php @@ -34,7 +34,9 @@ class EchoOperationHookTest extends KernelTestBase { public function testCustomEchoOperation(): void { /** @var \Drupal\ai\AiProviderPluginManager $manager */ $manager = \Drupal::service('ai.provider'); - self::assertContains(['id' => 'echo', 'label' => 'Echo'], $manager->getOperationTypes()); + $types = $manager->getOperationTypes(); + self::assertContains('echo', array_keys($types)); + self::assertEquals('Echo altered', $types['echo']['label']); /** @var \Drupal\ai_test\Plugin\AiProvider\EchoProvider $provider */ $provider = $manager->createInstance('echoai'); -- GitLab From 5f8b00611d728b24aecc7b9993168923807aa47e Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Tue, 28 Jan 2025 14:35:33 +0100 Subject: [PATCH 6/9] Issue #3500120: Add comments to operation types plugin manager --- src/AiProviderPluginManager.php | 65 +++++++-------------------------- 1 file changed, 13 insertions(+), 52 deletions(-) diff --git a/src/AiProviderPluginManager.php b/src/AiProviderPluginManager.php index 5a4b4ec8c..917d0c899 100644 --- a/src/AiProviderPluginManager.php +++ b/src/AiProviderPluginManager.php @@ -270,6 +270,13 @@ final class AiProviderPluginManager extends DefaultPluginManager { * The list of operation types. */ public function getOperationTypes(): array { + // Load from cache. + $data = $this->cacheBackend->get('ai_operation_types'); + if (!empty($data->data)) { + return $data->data; + } + + // We use a plugin manager only to discover the possible operation types. $manager = new class( 'OperationType', $this->namespaces, @@ -277,11 +284,17 @@ final class AiProviderPluginManager extends DefaultPluginManager { OperationTypeInterface::class, OperationType::class ) extends DefaultPluginManager { + + /** + * Call protected methods usually called in the constructor. + */ public function finishSetup(): void { $this->alterInfo('ai_operationtype'); } + }; $manager->finishSetup(); + // The in situ plugin manager uses the same cache backend. $manager->setCacheBackend($this->cacheBackend, 'ai_operation_types'); return $manager->getDefinitions(); @@ -355,58 +368,6 @@ final class AiProviderPluginManager extends DefaultPluginManager { return TRUE; } - /** - * Extracts the fully qualified interface name from a file. - * - * @param string $file - * The file path. - * - * @return string|null - * The fully qualified interface name, or NULL if not found. - */ - protected function getInterfaceFromFile($file) { - $contents = file_get_contents($file); - - // Match namespace and interface declarations. - if (preg_match('/namespace\s+([^;]+);/i', $contents, $matches)) { - $namespace = $matches[1]; - } - - // Match on starts with interface and has extends in it. - if (preg_match('/interface\s+([^ ]+)\s+extends\s+([^ ]+)/i', $contents, $matches) && isset($namespace)) { - $interface = $matches[1]; - return $namespace . '\\' . $interface; - } - - return NULL; - } - - /** - * Checks if an interface extends another interface. - * - * @param string $interface - * The interface name. - * @param string $baseInterface - * The base interface name. - * - * @return bool - * TRUE if the interface extends the base interface, FALSE otherwise. - */ - protected function doesInterfaceExtend($interface, $baseInterface) { - try { - $reflection = new \ReflectionClass($interface); - - if ($reflection->isInterface() && in_array($baseInterface, $reflection->getInterfaceNames())) { - return TRUE; - } - } - catch (\ReflectionException $e) { - // Ignore. - } - - return FALSE; - } - /** * Gives notice that a provider is disabled. * -- GitLab From 1fa1d85b7bd233b25b18f1b5085ac55848333f03 Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Thu, 30 Jan 2025 13:12:52 +0100 Subject: [PATCH 7/9] Add support for Drupal versions with LegacyHook --- tests/modules/ai_test/ai_test.module | 14 ++++++++++++++ tests/modules/ai_test/ai_test.services.yml | 4 ++++ 2 files changed, 18 insertions(+) create mode 100644 tests/modules/ai_test/ai_test.module create mode 100644 tests/modules/ai_test/ai_test.services.yml diff --git a/tests/modules/ai_test/ai_test.module b/tests/modules/ai_test/ai_test.module new file mode 100644 index 000000000..d9db21e57 --- /dev/null +++ b/tests/modules/ai_test/ai_test.module @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * AI test module file. + */ + +use Drupal\Core\Hook\Attribute\LegacyHook; +use Drupal\ai_test\Hook\OperationTypeHook; + +#[LegacyHook] +function ai_test_ai_operationtype_alter(array &$operation_types) { + \Drupal::service(OperationTypeHook::class)->echoOperationType($operation_types); +} diff --git a/tests/modules/ai_test/ai_test.services.yml b/tests/modules/ai_test/ai_test.services.yml new file mode 100644 index 000000000..d6fa111b4 --- /dev/null +++ b/tests/modules/ai_test/ai_test.services.yml @@ -0,0 +1,4 @@ +services: + Drupal\ai_test\Hook\OperationTypeHook: + class: Drupal\ai_test\Hook\OperationTypeHook + autowire: true -- GitLab From 72f1ae495659adbeb1597b62f35550699b0c6938 Mon Sep 17 00:00:00 2001 From: Fabian Bircher <5370-bircher@users.noreply.drupalcode.org> Date: Thu, 30 Jan 2025 14:27:05 +0100 Subject: [PATCH 8/9] fix phpcs --- tests/modules/ai_test/ai_test.module | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/modules/ai_test/ai_test.module b/tests/modules/ai_test/ai_test.module index d9db21e57..55ab09338 100644 --- a/tests/modules/ai_test/ai_test.module +++ b/tests/modules/ai_test/ai_test.module @@ -8,6 +8,9 @@ use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\ai_test\Hook\OperationTypeHook; +/** + * Implements hook_ai_operationtype_alter(). + */ #[LegacyHook] function ai_test_ai_operationtype_alter(array &$operation_types) { \Drupal::service(OperationTypeHook::class)->echoOperationType($operation_types); -- GitLab From e15ed5c0132f32962bdda528d6e8a3e83426e207 Mon Sep 17 00:00:00 2001 From: Marcus Johansson <me@marcusmailbox.com> Date: Tue, 11 Mar 2025 20:56:19 +0100 Subject: [PATCH 9/9] Updated the echo input --- .../src/OperationType/Echo/EchoInput.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php b/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php index c38cfb8da..26de696dd 100644 --- a/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php +++ b/tests/modules/ai_test/src/OperationType/Echo/EchoInput.php @@ -24,6 +24,27 @@ class EchoInput implements InputInterface { return $this->input; } + /** + * {@inheritdoc} + */ + public function getDebugData(): array { + return []; + } + + /** + * {@inheritdoc} + */ + public function setDebugData(array $debugData): void { + // Do nothing. + } + + /** + * {@inheritdoc} + */ + public function setDebugDataValue(string $key, $value): void { + // Do nothing. + } + /** * Return the input as string. * -- GitLab