deepseek-1.x-dev/src/Form/SettingsForm.php
src/Form/SettingsForm.php
<?php
namespace Drupal\deepseek\Form;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Url;
use Drupal\deepseek\AiDbVectorsPluginManager;
use Drupal\deepseek\AiProvidersPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Configure Deepseek settings for this site.
*/
class SettingsForm extends ConfigFormBase {
/**
* Constructs a new setting form object.
*/
public function __construct(
protected AiProvidersPluginManager $providers,
protected AiDbVectorsPluginManager $vectors,
protected RouteProviderInterface $routeProvider,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.ai_providers'),
$container->get('plugin.manager.ai_db_vectors'),
$container->get('router.route_provider'),
);
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'deepseek_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return ['deepseek.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$config = $this->config('deepseek.settings');
$values = $form_state->getUserInput();
$form['provider'] = [
'#type' => 'select',
'#title' => $this->t('AI Provider'),
'#value' => $values['provider'] ?? $config->get('provider'),
'#options' => $this->providers(),
'#ajax' => [
'callback' => [$this, 'reloadForm'],
'wrapper' => $this->getFormId(),
'event' => 'change',
],
// Wrap the form in a div for AJAX replacement.
];
$form['#prefix'] = '<div id="' . $this->getFormId() . '">';
$form['#suffix'] = '</div>';
$api_key = $values['api_key'] ?? $config->get('api_key');
$form['api_key'] = [
'#type' => 'textfield',
'#title' => $this->t('API key'),
'#default_value' => $api_key,
];
// Determine base_url.
$provider = $values['provider'] ?? $config->get('provider');
$defs = $this->getDefinitions($provider);
$url = !empty($config->get('base_url')) ? $config->get('base_url') : $defs['url'];
$model = !empty($config->get('model')) ? $config->get('model') : $defs['model'];
$embedding = !empty($config->get('embedding')) ? $config->get('embedding') : $defs['embedding'];
if (!empty($provider) && (empty($url) || $provider != 'self_host' || ($values && $values['provider'] != $config->get('provider')))) {
$url = $defs['url'];
}
$form['base_url'] = [
'#type' => 'url',
'#title' => $this->t('URL'),
'#default_value' => $url,
'#ajax' => [
'callback' => [$this, 'reloadForm'],
'wrapper' => $this->getFormId(),
'event' => 'change',
],
'#description' => implode('<br/>', [
$this->t('You can set/replace the URL server %url', ['%url' => $defs['url'] ?? '']),
]),
];
$form['load_model'] = [
'#type' => 'checkbox',
'#title' => $this->t('Click to load all available models'),
'#description' => $this->t('Make sure you have good url and api key'),
'#ajax' => [
'callback' => [$this, 'reloadForm'],
'wrapper' => $this->getFormId(),
'options' => ['query' => ['ajax_form' => 1]],
'event' => 'click',
],
'#attributes' => ['type' => 'button'],
];
$models = [];
if (!empty($form_state->getValue('load_model'))) {
$models = $this->loadModel($provider, $url, $api_key);
}
if (empty($models)) {
$form['model'] = [
'#type' => 'textfield',
'#title' => $this->t("Set model's API identifier"),
'#default_value' => $model,
'#description' => [
'#markup' => $this->t('Example: %model', ['%model' => $defs['model'] ?? '']),
],
];
}
else {
$form['model'] = [
'#type' => 'select',
'#title' => $this->t("This model's API identifier"),
'#default_value' => $model,
'#options' => $models,
'#empty_option' => $this->t('- Select -'),
];
}
if (empty($models)) {
$form['embedding'] = [
'#type' => 'textfield',
'#title' => $this->t("RAG This model's embedding"),
'#default_value' => $embedding,
'#description' => [
'#markup' => implode('<br/>', [
$this->t('Example: %model', ['%model' => $defs['embedding'] ?? '']),
$this->t('Leave blank if you do not want integration'),
]),
],
];
}
else {
$form['embedding'] = [
'#type' => 'select',
'#title' => $this->t("RAG This model's embedding"),
'#default_value' => $embedding,
'#options' => $models,
'#empty_option' => $this->t('- Select -'),
'#description' => $this->t('Leave blank if you do not want integration'),
];
}
$dbVectors = $this->dbVectors();
$dbVector = $values['db_vector'] ?? $config->get('db_vector');
$description = [$this->t('Leave blank if you do not want integration')];
$routes = $this->routeProvider->getRoutesByNames(['view.ai.embedding']);
if (!empty($routes)) {
$url = Url::fromRoute('view.ai.embedding');
$linkEmbedding = Link::fromTextAndUrl($this->t('content'), $url)->toString();
}
else {
$linkEmbedding = $this->t('by active VBO with action AI embedding');
}
$description[] = $this->t(
'Embedding manual @embedding', [
'@embedding' => $linkEmbedding,
]
);
$routes = $this->routeProvider->getRoutesByNames(['ai_auto_embedding.settings']);
if (!empty($routes)) {
$url = Url::fromRoute('ai_auto_embedding.settings');
$linkEmbedding = Link::fromTextAndUrl($this->t('Ai auto RAG embedding'), $url)->toString();
}
else {
$linkEmbedding = $this->t('by active module auto AI embedding');
}
$description[] = $this->t(
'Active @embedding', [
'@embedding' => $linkEmbedding,
]
);
$form['database_vector'] = [
'#type' => 'details',
'#title' => $this->t('RAG Database Vector Settings'),
'#description' => !empty($dbVector) ? $this->getDbVectorDefinitions($dbVector)['description'] : '',
'db_vector' => [
'#type' => 'select',
'#title' => $this->t("Database for embedding"),
'#default_value' => $dbVector,
'#options' => $dbVectors,
'#empty_option' => $this->t('- Select -'),
'#description' => implode('. ', $description),
'#ajax' => [
'callback' => [$this, 'reloadForm'],
'wrapper' => $this->getFormId(),
'event' => 'change',
],
],
];
$defDbVectors = $this->vectors->getDefinitions();
$url = $defDbVectors[$dbVector]['url'] ?? '';
$port = $defDbVectors[$dbVector]['port'] ?? '';
$visible = [
[':input[name="db_vector"]' => ['!value' => '']],
];
$invisible = [
[':input[name="db_vector"]' => ['value' => 'mysql']],
[':input[name="db_vector"]' => ['value' => 'mariadb']],
[':input[name="db_vector"]' => ['value' => 'pgsql']],
];
$states = [
'visible' => $visible,
'invisible' => $invisible,
];
$form['database_vector']['url'] = [
'#type' => 'url',
'#title' => $this->t('URL'),
'#default_value' => $config->get('url'),
'#description' => $url,
'#states' => $states,
];
$form['database_vector']['username'] = [
'#type' => 'textfield',
'#title' => $this->t('Username'),
'#default_value' => $config->get('username'),
'#states' => $states,
];
$form['database_vector']['password'] = [
'#type' => 'password',
'#title' => $this->t('Password'),
'#default_value' => $config->get('password'),
'#states' => $states,
];
$form['database_vector']['token'] = [
'#type' => 'textfield',
'#title' => $this->t('Token/API Key'),
'#default_value' => $config->get('token'),
'#states' => $states,
];
$form['database_vector']['db_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Database name'),
'#default_value' => $config->get('db_name'),
'#states' => $states,
];
$form['database_vector']['table_name'] = [
'#type' => 'textfield',
'#title' => $this->t('Table / Collection name'),
'#description' => $this->t('Lock to name @name', ['@name' => 'ai_embedding']),
'#default_value' => $config->get('table_name') ?? 'ai_embedding',
'#states' => $states,
'#attributes' => [
'readonly' => TRUE,
],
];
$form['database_vector']['port'] = [
'#type' => 'number',
'#title' => $this->t('Port'),
'#description' => $port,
'#default_value' => $config->get('port'),
'#states' => $states,
];
$form['database_vector']['dimension'] = [
'#type' => 'number',
'#title' => $this->t('Vector dimensionality'),
'#default_value' => $config->get('dimension'),
'#description' => $this->t('Default dimension is 768. Some models output higher dimensions (e.g., 1024 or 3072 for Gemini). Make sure your database supports the selected size.'),
];
$form['database_vector']['threshold'] = [
'#type' => 'number',
'#title' => $this->t('Threshold'),
'#default_value' => $config->get('threshold'),
'#description' => $this->t('Threshold index to get result.'),
];
$form['system'] = [
'#type' => 'details',
'#title' => $this->t('Role system set prompt'),
'system_prompt' => [
'#type' => 'textarea',
'#title' => $this->t("System prompt"),
'#default_value' => $config->get('system_prompt'),
'#description' => $this->t('It must return markdown format.'),
],
];
$form['mcp'] = [
'#type' => 'details',
'#title' => $this->t('Model context protocol Settings'),
'#description' => implode(' ', [
$this->t('Add url to config, Your mcp server:'),
Url::fromRoute('deepseek.mcp_server', [], ['absolute' => TRUE])->toString(),
]),
'mcp_key' => [
'#type' => 'textfield',
'#title' => $this->t("MCP API key"),
'#default_value' => !empty($config->get('mcp_key')) ? $config->get('mcp_key') : Crypt::randomBytesBase64(20),
'#description' => implode(' ', [
$this->t('Leave blank if you open your mcp server publicly.'),
$this->t('Add to header "X-API-Key":'),
'"' . $config->get('mcp_key') . '"',
]),
],
];
$prompts = $config->get('prompts') ?? '';
$form['prompts'] = [
'#type' => 'textarea',
'#title' => $this->t('Prompts ckeditor example'),
'#description' => $this->t('Yaml format. Provides prompt terms. Array must have id, title, prompt, role'),
'#default_value' => is_array($prompts) ? Yaml::encode($prompts) : $prompts,
'#prefix' => '<div id="' . $this->getFormId() . '-list">',
'#suffix' => '</div>',
'#attributes' => [
'class' => ['yaml-editor'],
],
];
$form['voice'] = [
'#type' => 'select',
'#title' => $this->t('Language for voice chat'),
'#default_value' => $config->get('voice'),
'#options' => $this->languages(),
];
$form['bootstrap'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use bootstrap 5'),
'#default_value' => $config->get('bootstrap'),
'#description' => $this->t("Do not select if you are using bootstrap 5 theme"),
];
$form['#attached']['library'][] = 'deepseek/yaml_editor';
return parent::buildForm($form, $form_state);
}
/**
* AJAX callback to reload the form.
*/
public function reloadForm(array &$form, FormStateInterface $form_state) {
return $form;
}
/**
* Loads model options from the API.
*/
private function loadModel($provider, $url = '', $apiKey = '') {
$options = [];
try {
$plugin = $this->providers->createInstance($provider);
$models = $plugin->models($url, $apiKey);
foreach ($models as $result) {
if (is_object($result) && !empty($result->id)) {
$options[$result->id] = $result->name ?? $result->id;
}
elseif (is_array($result)) {
$options[$result['id']] = $result['name'] ?? $result['id'];
}
}
}
catch (PluginNotFoundException $e) {
}
return $options;
}
/**
* {@inheritdoc}
*/
protected function providers() {
$definitions = $this->providers->getDefinitions();
$provides = [];
foreach ($definitions as $definition) {
$provides[$definition['id']] = $definition['label'];
}
return $provides;
}
/**
* {@inheritdoc}
*/
protected function getDefinitions($provider) {
$definitions = $this->providers->getDefinitions();
return $definitions[$provider] ?? [];
}
/**
* {@inheritdoc}
*/
protected function dbVectors() {
$definitions = $this->vectors->getDefinitions();
$vectors = [];
foreach ($definitions as $definition) {
$vectors[$definition['id']] = $definition['label'];
}
return $vectors;
}
/**
* {@inheritdoc}
*/
protected function getDbVectorDefinitions($dbVector) {
$definitions = $this->vectors->getDefinitions();
return $definitions[$dbVector] ?? [];
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$db_vector = $form_state->getValue('db_vector');
$this->config('deepseek.settings')
->set('base_url', $form_state->getValue('base_url'))
->set('api_key', $form_state->getValue('api_key'))
->set('prompts', $form_state->getValue('prompts'))
->set('provider', $form_state->getValue('provider'))
->set('embedding', $form_state->getValue('embedding'))
->set('bootstrap', $form_state->getValue('bootstrap'))
->set('voice', $form_state->getValue('voice'))
->set('model', $form_state->getValue('model'))
->set('url', $form_state->getValue('url'))
->set('port', $form_state->getValue('port'))
->set('token', $form_state->getValue('token'))
->set('dimension', $form_state->getValue('dimension'))
->set('threshold', $form_state->getValue('threshold'))
->set('username', $form_state->getValue('username'))
->set('password', $form_state->getValue('password'))
->set('db_name', $form_state->getValue('db_name'))
->set('table_name', $form_state->getValue('table_name'))
->set('system_prompt', $form_state->getValue('system_prompt'))
->set('mcp_key', $form_state->getValue('mcp_key'))
->set('db_vector', $db_vector)
->save();
parent::submitForm($form, $form_state);
if (!empty($db_vector)) {
$dbVector = $this->vectors->createInstance($db_vector);
if ($dbVector && !empty($form_state->getValue('table_name'))) {
$dbVector->createTable($form_state->getValue('table_name'), $form_state->getValue('dimension'));
}
}
}
/**
* {@inheritdoc}
*/
private function languages() {
return [
'en-US' => 'English',
'fr-FR' => 'Français',
'it-IT' => 'Italiano',
'es-ES' => 'Español',
'ca-ES' => 'Català',
'gl-ES' => 'Galego',
'pt-PT' => 'Português',
'pt-BR' => 'Brasil',
'vi-VN' => 'Tiếng Việt',
'af-ZA' => 'Afrikaans',
'bs-BA' => 'Bosanski',
'id-ID' => 'Bahasa',
'jv-ID' => 'Basa Java',
'cy-GB' => 'Cymraeg',
'da-DK' => 'Dansk',
'de-DE' => 'Deutsch',
'et-EE' => 'Eesti',
'eu-ES' => 'Euskera',
'fa-IR' => 'Farsi',
'fil-PH' => 'Filipino',
'ga-IE' => 'Gaeilge',
'hr-HR' => 'Hrvatski',
'sw-KE' => 'Kiswahili',
'kk-KZ' => 'Қазақ',
'lt-LT' => 'Lietuvių',
'lv-LV' => 'Latviešu',
'mt-MT' => 'Malti',
'hu-HU' => 'Magyar',
'nl-NL' => 'Nederlands',
'uz-UZ' => 'Oʻzbekcha',
'pl-PL' => 'Polski',
'sq-AL' => 'Shqip',
'sv-SE' => 'Svenska',
'fi-FI' => 'Suomi',
'cs-CZ' => 'Čeština',
'is-IS' => 'Íslenska',
'ro-RO' => 'Română',
'tr-TR' => 'Türkçe',
'sk-SK' => 'Slovenčina',
'el-GR' => 'Ελληνικά',
'az-AZ' => 'Azərbaycanca',
'bg-BG' => 'Български',
'sr-RS' => 'Српски',
'mk-MK' => 'Македонски',
'mn-MN' => 'монгол',
'ru-RU' => 'Русский',
'uk-UA' => 'Українська',
'ja-JP' => '日本語',
'ko-KR' => '한국어',
'th-TH' => 'ไทย',
'km-KH' => 'ភាសាខ្មែរ',
'lo-LA' => 'ພາສາລາວ',
'ar-SA' => 'العربية',
'ms-MY' => 'بهاس ملايو',
'ps-AF' => 'پښتو',
'am-ET' => 'አማርኛ',
'bn-IN' => 'বাংলা',
'he-IL' => 'עברית',
'hy-AM' => 'Հայերեն',
'ka-GE' => 'ქართული ენა',
'my-MM' => 'ဗမာစကား',
'si-LK' => 'සිංහල',
'gu-IN' => 'ગુજરાતી',
'hi-IN' => 'हिन्दी',
'kn-IN' => 'ಕನ್ನಡ',
'ml-IN' => 'മലയാളം',
'mr-IN' => 'मराठी',
'or-IN' => 'ଓଡିଆ',
'pa-IN' => 'ਪੰਜਾਬੀ',
'ta-IN' => 'தமிழ்',
'te-IN' => 'తెలుగు',
'ur-IN' => 'اردو',
'ne-NP' => 'नेपाली',
'zh-CN' => '中文',
'zh-HK' => '中文 (香港)',
'zh-TW' => '繁體中文',
];
}
}
