Skip to content

Commit

Permalink
Freelygive/ai recipe (#14)
Browse files Browse the repository at this point in the history
* Implemented recipe and install wizard for AI (alt text) in DXPR

* Fixed so it checks if the key is set correctly

* Fixed so the new chat_with_image_vision is set as well

* Better error handling on the validation functionality

* Longer form fields for long organization keys

---------

Co-authored-by: Neslee Canil Pinto <[email protected]>
  • Loading branch information
ivanboring and Neslee authored Aug 29, 2024
1 parent 65fc334 commit 7359c2d
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 4 deletions.
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.
],
'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(),
]);
}
}
}

/**
* 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'],
],
],
];

$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()]));
}
}
}

/**
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));
}

/**
* 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);
}

}

0 comments on commit 7359c2d

Please sign in to comment.