Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freelygive/ai recipe #14

Merged
merged 10 commits into from
Aug 29, 2024
2 changes: 2 additions & 0 deletions dxpr_cms_image_media_type/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"description": "Adds focal point-based cropping to the image media type.",
"version": "dev-main",
"require": {
"drupal/ai": "1.0.x-dev@dev",
"drupal/ai_image_alt_text": "1.0.x-dev@dev",
"drupal/core": ">=10.3",
"drupal/focal_point": "^2"
}
Expand Down
30 changes: 30 additions & 0 deletions dxpr_cms_image_media_type/recipe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ recipes:
- core/recipes/image_media_type
install:
- focal_point
- key
- ai
- provider_openai
- provider_anthropic
- ai_image_alt_text
config:
import:
focal_point: '*'
ai_image_alt_text: '*'
actions:
core.entity_form_display.media.image.media_library:
setComponent:
Expand All @@ -21,3 +27,27 @@ config:
preview_image_style: media_library
preview_link: true
offsets: '50,50'
ai_image_alt_text.settings:
simple_config_update:
autogenerate: true
hide_button: true
user.role.content_administrator:
ensure_exists:
id: content_administrator
label: 'Content Administrator'
grantPermission: 'generate ai alt tags'
user.role.content_editor:
ensure_exists:
id: content_editor
label: 'Content Editor'
grantPermission: 'generate ai alt tags'
user.role.marketer:
ensure_exists:
id: marketer
label: 'Marketer'
grantPermission: 'generate ai alt tags'
user.role.site_bui:
ensure_exists:
id: site_bui
label: 'Site Builder'
grantPermission: 'generate ai alt tags'
29 changes: 29 additions & 0 deletions dxpr_cms_installer/dxpr_cms_installer.profile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ function dxpr_cms_installer_install_tasks(): array {
'type' => 'form',
'function' => ConfigureAPIKeysForm::class,
],
'dxpr_cms_uninstall_unused_ai_modules' => [
// Uninstall the unused AI provider module.
],
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
'dxpr_cms_installer_uninstall_myself' => [
// As a final task, this profile should uninstall itself.
],
Expand Down Expand Up @@ -156,6 +159,32 @@ function dxpr_cms_installer_apply_recipes(array &$install_state): array {
return $batch;
}

/**
* Uninstalls the unused AI provider module.
*/
function dxpr_cms_uninstall_unused_ai_modules(): void {
$providers = ['anthropic', 'openai'];
$provider_plugin = \Drupal::service('ai.provider');
$unusable_providers = [];
foreach ($providers as $provider) {
// Create an instance and check if its usable (setup).
$plugin = $provider_plugin->createInstance($provider);
if (!$plugin->isUsable()) {
$unusable_providers[] = $plugin;
}
}

// If one of the providers worked, uninstall the unused one(s), but if all
// failed, don't do anything.
if (count($unusable_providers) < count($providers)) {
foreach ($unusable_providers as $plugin) {
\Drupal::service(ModuleInstallerInterface::class)->uninstall([
$plugin->getModuleDataName(),
]);
}
}
}
ivanboring marked this conversation as resolved.
Show resolved Hide resolved

/**
* Uninstalls this install profile, as a final step.
*/
Expand Down
148 changes: 144 additions & 4 deletions dxpr_cms_installer/src/Form/ConfigureAPIKeysForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

namespace Drupal\dxpr_cms_installer\Form;

use Drupal\ai\AiProviderPluginManager;
use Drupal\ai\OperationType\Chat\ChatInput;
use Drupal\ai\OperationType\Chat\ChatMessage;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Extension\InfoParserInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\dxpr_builder\Service\DxprBuilderJWTDecoder;
use Drupal\key\Entity\Key;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
Expand Down Expand Up @@ -51,6 +55,13 @@ class ConfigureAPIKeysForm extends FormBase implements ContainerInjectionInterfa
*/
protected $jwtDecoder;

/**
* The AI provider plugin manager.
*
* @var \Drupal\ai\AiProviderPluginManager
*/
protected $aiProviderPluginManager;

/**
* Configure API Keys Form constructor.
*
Expand All @@ -64,16 +75,23 @@ class ConfigureAPIKeysForm extends FormBase implements ContainerInjectionInterfa
* The config factory service.
* @param \Drupal\dxpr_builder\Service\DxprBuilderJWTDecoder $jwtDecoder
* Parsing DXPR JWT token.
* @param \Drupal\ai\AiProviderPluginManager $aiProviderPluginManager
* The AI provider plugin manager.
*/
public function __construct($root, InfoParserInterface $info_parser,
TranslationInterface $translator,
ConfigFactoryInterface $config_factory,
DxprBuilderJWTDecoder $jwtDecoder) {
public function __construct(
$root,
InfoParserInterface $info_parser,
TranslationInterface $translator,
ConfigFactoryInterface $config_factory,
DxprBuilderJWTDecoder $jwtDecoder,
AiProviderPluginManager $aiProviderPluginManager
) {
$this->root = $root;
$this->infoParser = $info_parser;
$this->stringTranslation = $translator;
$this->configFactory = $config_factory;
$this->jwtDecoder = $jwtDecoder;
$this->aiProviderPluginManager = $aiProviderPluginManager;
}

/**
Expand All @@ -86,6 +104,7 @@ public static function create(ContainerInterface $container) {
$container->get('string_translation'),
$container->get('config.factory'),
$container->get('dxpr_builder.jwt_decoder'),
$container->get('ai.provider')
);
}

Expand Down Expand Up @@ -119,6 +138,41 @@ public function buildForm(array $form, FormStateInterface $form_state, array &$i
];
// }

$form['ai_provider'] = [
'#type' => 'select',
'#title' => $this->t('Select AI Provider'),
'#description' => $this->t('If you want to enable AI features like ai powered alt text generation, select the AI provider you want to use for AI features and fill in the API Key.'),
'#empty_option' => $this->t('No AI'),
'#options' => [
'openai' => $this->t('OpenAI'),
'anthropic' => $this->t('Anthropic'),
],
];

$form['openai_key'] = [
'#type' => 'password',
'#title' => $this->t('OpenAI API key'),
'#description' => $this->t('Get a key from <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com/api-keys</a>.'),
'#maxlength' => 255,
'#states' => [
'visible' => [
':input[name="ai_provider"]' => ['value' => 'openai'],
],
],
];

$form['anthropic_key'] = [
'#type' => 'password',
'#title' => $this->t('Anthropic API key'),
'#description' => $this->t('Get a key from <a href="https://console.anthropic.com/settings/keys" target="_blank">console.anthropic.com/settings/keys</a>.'),
'#maxlength' => 255,
'#states' => [
'visible' => [
':input[name="ai_provider"]' => ['value' => 'anthropic'],
],
],
];

Comment on lines +141 to +175

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The form fields for AI provider selection and API key input are well implemented. However, consider using a more secure method to store the API keys rather than plain text password fields. Storing sensitive data like API keys in plain text can pose security risks.

$form['actions'] = [
'continue' => [
'#type' => 'submit',
Expand All @@ -145,6 +199,36 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if (!empty($google_translation_key)) {
$this->configFactory->getEditable('tmgmt.translator.google')->set('settings.api_key', $google_translation_key)->save();
}

// If the AI provider is set, enable the appropriate modules.
if ($ai_provider = $form_state->getValue('ai_provider')) {
try {
// Setup the key for the provider.
$key_id = $ai_provider . '_key';
$key = Key::create([
'id' => $key_id,
'label' => ucfirst($ai_provider) . ' API Key',
'description' => 'API Key for ' . ucfirst($ai_provider),
'key_type' => 'authentication',
'key_provider' => 'config',
]);
$key->setKeyValue($form_state->getValue($key_id));
$key->save();
// Add the key to the config.
$this->configFactory->getEditable('provider_' . $ai_provider . '.settings')->set('api_key', $key_id)->save();
// Set the default chat and chat_with_image_vision provider.
$this->configFactory->getEditable('ai.settings')->set('default_providers.chat', [
'provider_id' => $ai_provider,
'model_id' => $ai_provider == 'openai' ? 'gpt-4o' : $this->getFirstAiModelId($ai_provider),
])->set('default_providers.chat_with_image_vision', [
'provider_id' => $ai_provider,
'model_id' => $ai_provider == 'openai' ? 'gpt-4o' : $this->getFirstAiModelId($ai_provider),
])->save();
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('An error occurred while saving the AI provider key: @error', ['@error' => $e->getMessage()]));
}
}
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -164,7 +248,63 @@ public function validateForm(array &$form, FormStateInterface $form_state): void
]));
}
}

// If a provider is set we test it.
if ($provider = $form_state->getValue('ai_provider')) {
$key = $provider . '_key';
// It has to be set.
if (empty($form_state->getValue($key))) {
$form_state->setErrorByName($key, $this->t('API key is required, if you want to enable this provider.'));
}
else {
// Try to send a message.
try {
$this->validateAiProvider($provider, $form_state->getValue($key));
} catch (\Exception $e) {
$form_state->setErrorByName($key, $this->t('Your API key seems to be invalid with message %message', [
'%message' => $e->getMessage(),
]));
}
}
}
}

/**
* Test the provider. Will throw an exception if it is invalid.
*
* @param string $provider_id
* The provider ID.
* @param string $api_key
* The API key.
*/
protected function validateAiProvider(string $provider_id, string $api_key) {
/** @var \Drupal\ai\AiProviderInterface|\Drupal\ai\OperationType\Chat\ChatInterface */
$provider = $this->aiProviderPluginManager->createInstance($provider_id);
// Try to send a chat message.
$provider->setAuthentication($api_key);
$models = $provider->getConfiguredModels('chat');
$input = new ChatInput([
new ChatMessage('user', 'Hello'),
]);
// We use the first model to test.
$provider->chat($input, key($models));
}
ivanboring marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get the latest model, will also work on future providers.
*
* @param string $provider_id
* The provider ID.
*
* @return string
* The model ID.
*/
protected function getFirstAiModelId(string $provider_id): string {
// We can assume this works, since validation works.
$provider = $this->aiProviderPluginManager->createInstance($provider_id);
// @todo Change to chat_with_image_vision when it is available.
$models = $provider->getConfiguredModels('chat');
return key($models);
}
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
ivanboring marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +251 to +308

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

The validation of the AI provider and API key is well implemented. The validateAiProvider function correctly tests the validity of the provided API key by attempting to send a chat message. If an exception occurs during this process, it's caught and an error message is displayed. This is a good practice as it ensures that only valid API keys are used.


}
Loading