Skip to content
Snippets Groups Projects
Commit 8f5643d0 authored by Jan Kellermann's avatar Jan Kellermann
Browse files

Merge branch '3500120-programmatically-add-operationtype' into '1.1.x'

Invoke ai_operationtype_alter.

See merge request !412
parents fdcc6bdb 647a9314
No related branches found
No related tags found
No related merge requests found
Pipeline #445964 failed
......@@ -124,6 +124,7 @@ octect
octocat
oden
oneshot
operationtype
outro
overriden
paranthesis
......
......@@ -275,30 +275,29 @@ final class AiProviderPluginManager extends DefaultPluginManager {
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(),
];
}
// We use a plugin manager only to discover the possible operation types.
$manager = new class(
'OperationType',
$this->namespaces,
$this->moduleHandler,
OperationTypeInterface::class,
OperationType::class
) extends DefaultPluginManager {
/**
* Call protected methods usually called in the constructor.
*/
public function finishSetup(): void {
$this->alterInfo('ai_operationtype');
}
}
// Save to cache.
$this->cacheBackend->set('ai_operation_types', $operation_types);
};
$manager->finishSetup();
// The in situ plugin manager uses the same cache backend.
$manager->setCacheBackend($this->cacheBackend, 'ai_operation_types');
return $operation_types;
return $manager->getDefinitions();
}
/**
......@@ -369,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.
*
......
<?php
/**
* @file
* AI test module file.
*/
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);
}
services:
Drupal\ai_test\Hook\OperationTypeHook:
class: Drupal\ai_test\Hook\OperationTypeHook
autowire: true
<?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['echo']['label'] = 'Echo altered';
}
}
<?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();
}
}
<?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;
}
<?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,
];
}
}
......@@ -38,6 +38,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;
/**
......@@ -54,7 +57,8 @@ class EchoProvider extends AiProviderClientBase implements
SpeechToTextInterface,
TextToSpeechInterface,
ImageClassificationInterface,
TextToImageInterface {
TextToImageInterface,
EchoInterface {
/**
* {@inheritdoc}
......@@ -106,6 +110,7 @@ class EchoProvider extends AiProviderClientBase implements
'text_to_speech',
'moderation',
'image_classification',
'echo',
];
}
......@@ -282,4 +287,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], []);
}
}
<?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');
$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');
$input = $this->randomString();
$output = $provider->echo($input, 'test');
self::assertEquals($input, $output->getNormalized());
self::assertEquals(['echo' => $input], $output->getRawOutput());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment