29include_once DOL_DOCUMENT_ROOT.
'/ai/class/ai.class.php';
41 $arrayofaifeatures = array(
42 'textgenerationemail' => array(
'label' => $langs->trans(
'TextGeneration').
' ('.$langs->trans(
"EmailContent").
')',
'picto' =>
'',
'status' =>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EMAIL),
43 'textgenerationwebpage' => array(
'label' => $langs->trans(
'TextGeneration').
' ('.$langs->trans(
"WebsitePage").
')',
'picto' =>
'',
'status' =>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_WEBPAGE),
44 'textgeneration' => array(
'label' => $langs->trans(
'TextGeneration').
' ('.$langs->trans(
"Other").
')',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'TEXT'),
46 'texttranslation' => array(
'label' => $langs->trans(
'TextTranslation'),
'picto' =>
'',
'status'=>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION),
47 'textsummarize' => array(
'label' => $langs->trans(
'TextSummarize'),
'picto' =>
'',
'status'=>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE),
48 'textspellchecker' => array(
'label' => $langs->trans(
'TextSpellChecker'),
'picto' =>
'',
'status'=>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER),
49 'textrephrase' => array(
'label' => $langs->trans(
'TextRephraser'),
'picto' =>
'',
'status'=>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER),
51 'textgenerationextrafield' => array(
'label' => $langs->trans(
'TextGeneration').
' ('.$langs->trans(
"ExtrafieldFiller").
')',
'picto' =>
'',
'status'=>
'dolibarr',
'function' =>
'TEXT',
'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER),
53 'imagegeneration' => array(
'label' =>
'ImageGeneration',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'IMAGE'),
54 'videogeneration' => array(
'label' =>
'VideoGeneration',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'VIDEO'),
55 'audiogeneration' => array(
'label' =>
'AudioGeneration',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'AUDIO'),
56 'transcription' => array(
'label' =>
'AudioTranscription',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'TRANSCRIPT'),
57 'translation' => array(
'label' =>
'AudioTranslation',
'picto' =>
'',
'status' =>
'notused',
'function' =>
'TRANSLATE'),
58 'docparsing' => array(
'label' =>
'DocumentParsing',
'picto' =>
'',
'status' =>
'experimental',
'function' =>
'DOCPARSING')
61 return $arrayofaifeatures;
74 '-1' => array(
'label' => $langs->trans(
'SelectAService')),
76 'label' =>
'ChatGPT (OpenAI)',
77 'url' =>
'https://api.openai.com/v1/',
78 'setup' =>
'https://platform.openai.com/account/api-keys',
79 'textgeneration' => array(
'default' =>
'gpt-5.6'),
80 'imagegeneration' => array(
'default' =>
'gpt-image-2'),
81 'audiogeneration' => array(
'default' =>
'gpt-audio-1.5'),
82 'videogeneration' => array(
'default' =>
'sora-2'),
83 'transcription' => array(
'default' =>
'gpt-transcribe'),
84 'translation' => array(
'default' =>
'gpt-5.6'),
85 'docparsing' => array(
'default' =>
'gpt-5.6'),
86 'adapter_type' =>
'openai'
89 'label' =>
'Groq (LPU Inference)',
90 'url' =>
'https://api.groq.com/openai/v1/',
91 'setup' =>
'https://console.groq.com/keys',
92 'textgeneration' => array(
'default' =>
'llama-4-8b-instant'),
93 'imagegeneration' => array(
'default' =>
'na'),
94 'audiogeneration' => array(
'default' =>
'na'),
95 'videogeneration' => array(
'default' =>
'na'),
96 'transcription' => array(
'default' =>
'whisper-large-v3-turbo'),
97 'translation' => array(
'default' =>
'whisper-large-v3-turbo'),
98 'docparsing' => array(
'default' =>
'llama-4-70b-versatile'),
99 'adapter_type' =>
'openai'
102 'label' =>
'Mistral AI',
103 'url' =>
'https://api.mistral.ai/v1/',
104 'setup' =>
'https://console.mistral.ai/api-keys/',
105 'textgeneration' => array(
'default' =>
'mistral-small-latest',
'examples' =>
'mistral-tiny-latest, mistral-small-latest, mistral-medium-latest, mistral-large-latest'),
106 'imagegeneration' => array(
'default' =>
'na'),
107 'audiogeneration' => array(
'default' =>
'na'),
108 'videogeneration' => array(
'default' =>
'na'),
109 'transcription' => array(
'default' =>
'na'),
110 'translation' => array(
'default' =>
'na'),
111 'docparsing' => array(
'default' =>
'pixtral-12b-latest'),
112 'adapter_type' =>
'openai'
115 'label' =>
'DeepSeek',
116 'url' =>
'https://api.deepseek.com',
117 'setup' =>
'https://platform.deepseek.com/api_keys',
118 'textgeneration' => array(
'default' =>
'deepseek-v4'),
119 'imagegeneration' => array(
'default' =>
'deepseek-janus-2'),
120 'audiogeneration' => array(
'default' =>
'na'),
121 'videogeneration' => array(
'default' =>
'na'),
122 'transcription' => array(
'default' =>
'na'),
123 'translation' => array(
'default' =>
'na'),
124 'docparsing' => array(
'default' =>
'deepseek-v4'),
125 'adapter_type' =>
'openai'
127 'perplexity' => array(
128 'label' =>
'Perplexity (Sonar)',
129 'url' =>
'https://api.perplexity.ai',
130 'setup' =>
'https://www.perplexity.ai/settings/api',
131 'textgeneration' => array(
'default' =>
'sonar-pro'),
132 'imagegeneration' => array(
'default' =>
'na'),
133 'audiogeneration' => array(
'default' =>
'na'),
134 'videogeneration' => array(
'default' =>
'na'),
135 'transcription' => array(
'default' =>
'na'),
136 'translation' => array(
'default' =>
'na'),
137 'docparsing' => array(
'default' =>
'sonar-reasoning'),
138 'adapter_type' =>
'openai'
141 'label' =>
'Zhipu AI (GLM)',
142 'url' =>
'https://api.z.ai/api/paas/v4',
143 'setup' =>
'https://docs.z.ai/guides/overview/quick-start',
144 'textgeneration' => array(
'default' =>
'glm-5'),
145 'imagegeneration' => array(
'default' =>
'cogview-4'),
146 'audiogeneration' => array(
'default' =>
'cogvlm2-audio'),
147 'videogeneration' => array(
'default' =>
'cogvideox-2'),
148 'transcription' => array(
'default' =>
'na'),
149 'translation' => array(
'default' =>
'na'),
150 'docparsing' => array(
'default' =>
'glm-5'),
151 'adapter_type' =>
'openai'
155 'url' =>
'https://domainofapi.com/v1/',
156 'setup' =>
'Ask your AI provider how to get your API key',
157 'textgeneration' => array(
'default' =>
'tinyllama-1.1b'),
158 'imagegeneration' => array(
'default' =>
'mixtral-8x7b-32768'),
159 'audiogeneration' => array(
'default' =>
'mixtral-8x7b-32768'),
160 'videogeneration' => array(
'default' =>
'na'),
161 'transcription' => array(
'default' =>
'mixtral-8x7b-32768'),
162 'translation' => array(
'default' =>
'mixtral-8x7b-32768'),
163 'docparsing' => array(
'default' =>
'na'),
164 'adapter_type' =>
'openai'
167 'anthropic' => array(
168 'label' =>
'Anthropic (Claude)',
169 'url' =>
'https://api.anthropic.com/v1/',
170 'setup' =>
'https://console.anthropic.com/',
171 'textgeneration' => array(
'default' =>
'claude-opus-5'),
172 'imagegeneration' => array(
'default' =>
'na'),
173 'audiogeneration' => array(
'default' =>
'na'),
174 'videogeneration' => array(
'default' =>
'na'),
175 'transcription' => array(
'default' =>
'na'),
176 'translation' => array(
'default' =>
'na'),
177 'docparsing' => array(
'default' =>
'claude-opus-5'),
178 'adapter_type' =>
'anthropic'
181 'label' =>
'Google Gemini',
182 'url' =>
'https://generativelanguage.googleapis.com/v1beta/',
183 'setup' =>
'https://aistudio.google.com/',
184 'textgeneration' => array(
'default' =>
'gemini-3.1-pro-preview'),
185 'imagegeneration' => array(
'default' =>
'nano-banana-pro'),
186 'audiogeneration' => array(
'default' =>
'gemini-2.5-pro-tts'),
187 'videogeneration' => array(
'default' =>
'veo-3.1'),
188 'transcription' => array(
'default' =>
'gemini-3.1-pro-preview'),
189 'translation' => array(
'default' =>
'gemini-3.1-pro-preview'),
190 'docparsing' => array(
'default' =>
'gemini-3.1-pro-preview'),
191 'adapter_type' =>
'google'
216 return [
'success' =>
false,
'message' =>
'API Key is empty'];
220 if (!function_exists(
'getListOfAIServices')) {
221 return [
'success' =>
false,
'message' =>
'Configuration helper function missing.'];
225 $defUrl = $list[$service][
'url'] ??
'';
227 $defaultModel = $list[$service][
'model'] ??
'unknown';
233 $url = rtrim($url,
'/');
236 $headers = [
"Content-Type: application/json"];
244 if ($service ==
'google' || strpos($url,
'googleapis') !==
false) {
245 if (strpos($url,
':generateContent') ===
false) {
246 if (strpos($url,
'models') ===
false) {
247 $url .=
"/models/$model:generateContent";
249 $url .=
"/$model:generateContent";
252 $url .=
"?key=" . $key;
253 $data = [
"contents" => [ [
"parts" => [ [
"text" =>
"Hello"] ] ] ],
"generationConfig" => [
"maxOutputTokens" => 5]];
254 } elseif ($service ==
'anthropic' || strpos($url,
'anthropic') !==
false) {
255 if (strpos($url,
'messages') ===
false) $url .=
'/messages';
256 $headers[] =
"x-api-key: $key";
257 $headers[] =
"anthropic-version: 2023-06-01";
260 "messages" => [[
"role" =>
"user",
"content" =>
"Hello"]],
264 if (strpos($url,
'/chat/completions') ===
false) $url .=
'/chat/completions';
265 $headers[] =
"Authorization: Bearer $key";
269 "messages" => [[
"role" =>
"user",
"content" =>
"Hello"]],
275 include_once DOL_DOCUMENT_ROOT.
'/core/lib/geturl.lib.php';
279 global $dolibarr_ai_allow_local_endpoints;
280 $localurl = empty($dolibarr_ai_allow_local_endpoints) ? 0 : 2;
282 $result =
getURLContent($url,
'POST', json_encode($data), 1, $headers, array(
'http',
'https'), $localurl, -1, 0, 10);
283 $httpCode = (int) ($result[
'http_code'] ?? 0);
284 $responseContent = (string) ($result[
'content'] ??
'');
286 if (!empty($result[
'curl_error_no'])) {
287 return [
'success' =>
false,
'message' =>
"Curl Error: ".($result[
'curl_error_msg'] ??
'')];
290 if ($httpCode >= 200 && $httpCode < 300) {
291 return [
'success' =>
true,
'message' =>
"OK (HTTP $httpCode)."];
293 $json = json_decode($responseContent,
true);
295 $msg = $json[
'error'][
'message'] ?? $json[
'message'] ?? substr($responseContent, 0, 150);
296 return [
'success' =>
false,
'message' =>
"HTTP $httpCode. Error: $msg"];
322 if (empty($attachments)) {
325 $langs->load(
"other");
328 $error = $langs->trans(
"AIAttachmentBlockedByPrivacy");
336 if ($maxfiles > 0 && count($attachments) > $maxfiles) {
337 $error = $langs->trans(
"AIAttachmentTooMany", (
string) $maxfiles);
342 $allowedmimes = array(
'application/pdf',
'image/png',
'image/jpeg',
'image/gif',
'image/webp');
347 $allowedmimes[] =
'image/heic';
348 $allowedmimes[] =
'image/heif';
352 foreach ($attachments as $att) {
353 if (!in_array($att[
'mime'], $allowedmimes,
true)) {
354 $error = $langs->trans(
"AIAttachmentTypeNotAllowed", $att[
'mime']);
359 $bytes = (int) (strlen($att[
'data']) * 3 / 4);
360 $totalbytes += $bytes;
361 if ($bytes > $maxbytes || $totalbytes > $maxbytes) {
362 $error = $langs->trans(
"AIAttachmentTooLarge", (
string)
getDolGlobalInt(
'AI_ATTACHMENT_MAX_MB', 10));
389 $head = (int) floor($max / 2);
390 $tail = $max - $head;
393 .
"\n... [Truncated ".($len - $max).
" chars] ...\n"
416function ai_log_request($db, $user, $query, array $response, $provider,
float $time,
float $confidence, $status, $error =
'', $rawReq =
'', $rawRes =
'', array
$context = array(), &$logId =
null)
426 $tool = isset($response[
'tool']) ? (string) $response[
'tool'] :
'';
433 $rawResStr = (string) $rawRes;
434 if (empty(
$context[
'preserve_payloads'])) {
439 $sql =
"INSERT INTO " . $db->prefix() .
"ai_request_log (";
440 $sql .=
"entity, date_request, fk_user, query_text, tool_name, provider, ";
441 $sql .=
"execution_time, confidence, status, error_msg, raw_request_payload, raw_response_payload";
447 $sql .=
", fk_actioncomm, input_hash, output_hash, security_hash";
450 $sql .=
", tokens_input, tokens_output, model";
452 $sql .=
") VALUES (";
453 $sql .= ((int)
$conf->entity) .
", ";
454 $sql .=
"'" . $db->idate(
dol_now()) .
"', ";
455 $sql .= ((int) $user->id) .
", ";
456 $sql .=
"'" . $db->escape($query) .
"', ";
457 $sql .=
"'" . $db->escape($tool) .
"', ";
458 $sql .=
"'" . $db->escape($provider) .
"', ";
459 $sql .= ((float) $time) .
", ";
460 $sql .= ((float) $confidence) .
", ";
461 $sql .=
"'" . $db->escape($status) .
"', ";
462 $sql .=
"'" . $db->escape($error) .
"', ";
463 $sql .=
"'" . $db->escape($rawReq) .
"', ";
464 $sql .=
"'" . $db->escape($rawResStr) .
"'";
466 $sql .=
", ".(!empty(
$context[
'fk_actioncomm']) &&
$context[
'fk_actioncomm'] > 0 ? (int)
$context[
'fk_actioncomm'] :
'NULL');
467 $sql .=
", '".$db->escape(
$context[
'input_hash'] ??
'').
"'";
468 $sql .=
", '".$db->escape(
$context[
'output_hash'] ??
'').
"'";
469 $sql .=
", '".$db->escape(
$context[
'security_hash'] ??
'').
"'";
472 $sql .=
", ".(isset(
$context[
'tokens_input']) ? (int)
$context[
'tokens_input'] :
'NULL');
473 $sql .=
", ".(isset(
$context[
'tokens_output']) ? (int)
$context[
'tokens_output'] :
'NULL');
474 $sql .=
", '".$db->escape((
string) (
$context[
'model'] ??
'')).
"'";
478 $resql = $db->query($sql);
480 dol_syslog(__FUNCTION__.
": ".$db->lasterror(), LOG_ERR);
482 $logId = (int) $db->last_insert_id($db->prefix().
"ai_request_log");
495 $arrayforaisummarize = array(
497 '50_w' =>
'SummarizeFiftyWords',
498 '100_w' =>
'SummarizeHundredWords',
499 '200_w' =>
'SummarizeTwoHundredWords',
500 '1_p' =>
'SummarizeOneParagraphs',
501 '2_p' =>
'SummarizeTwoParagraphs',
502 '25_pc' =>
'SummarizeTwentyFivePercent',
503 '50_pc' =>
'SummarizeFiftyPercent',
504 '75_pc' =>
'SummarizeSeventyFivePercent'
507 return $arrayforaisummarize;
517 $arrayforaierephrasestyle = array(
518 'spellchecker' =>
'RephraseSpellChecker',
519 'professional' =>
'RephraseStyleProfessional',
520 'humouristic' =>
'RephraseStyleHumouristic',
523 return $arrayforaierephrasestyle;
533 global $langs,
$conf;
535 $langs->load(
"agenda");
541 $head[$h][1] = $langs->trans(
"Settings");
542 $head[$h][2] =
'settings';
545 $head[$h][0] =
dol_buildpath(
"/ai/admin/custom_prompt.php", 1);
546 $head[$h][1] = $langs->trans(
"CustomPrompt");
547 $head[$h][2] =
'custom';
552 $head[$h][1] = $langs->trans(
"Assistant");
553 $head[$h][2] =
'assistant';
559 $head[$h][1] = $langs->trans(
"MCPServer");
560 $head[$h][2] =
'servermcp';
565 $head[$h][0] =
dol_buildpath(
"/ai/admin/configure_tools.php", 1);
566 $head[$h][1] = $langs->trans(
"ToolAccessControl");
567 $head[$h][2] =
'tools';
604 if (empty($serviceKey) || $serviceKey ===
'-1') {
610 return isset($services[$serviceKey][
'label']) ? (string) $services[$serviceKey][
'label'] : (string) $serviceKey;
629 if (empty($serviceKey) || $serviceKey ==
'-1') {
630 return array(
'service' =>
'',
'models' => array());
633 if (!$forcerefresh) {
636 $cache = json_decode($cacheraw,
true);
637 if (is_array($cache) && !empty($cache[
'service']) && $cache[
'service'] === $serviceKey
638 && !empty($cache[
'ts']) && (
dol_now() - (
int) $cache[
'ts']) < 3600
639 && !empty($cache[
'models']) && is_array($cache[
'models'])) {
640 return array(
'service' => $serviceKey,
'models' => $cache[
'models']);
645 include_once DOL_DOCUMENT_ROOT.
'/core/lib/geturl.lib.php';
646 include_once DOL_DOCUMENT_ROOT.
'/core/lib/admin.lib.php';
649 $adapterType = $servicesList[$serviceKey][
'adapter_type'] ??
'openai';
650 $defUrl = $servicesList[$serviceKey][
'url'] ??
'';
651 $baseUrl = rtrim(
getDolGlobalString(
'AI_API_'.strtoupper($serviceKey).
'_URL') ?: $defUrl,
'/');
654 if (preg_match(
'/^crypt:/', $apiKey)) {
657 if (empty($apiKey) || empty($baseUrl)) {
658 return array(
'service' => $serviceKey,
'models' => array());
662 if ($adapterType ===
'anthropic') {
663 $headers = array(
'x-api-key: '.$apiKey,
'anthropic-version: 2023-06-01');
664 $res =
getURLContent($baseUrl.
'/models?limit=100',
'GET',
'', 1, $headers, array(
'http',
'https'), 2);
665 $json = json_decode($res[
'content'] ??
'',
true);
666 foreach ((array) ($json[
'data'] ?? array()) as $m) {
667 if (!empty($m[
'id'])) {
668 $models[] = (string) $m[
'id'];
671 } elseif ($adapterType ===
'google') {
672 $res =
getURLContent($baseUrl.
'/models?pageSize=200&key='.urlencode($apiKey),
'GET',
'', 1, array(), array(
'http',
'https'), 2);
673 $json = json_decode($res[
'content'] ??
'',
true);
674 foreach ((array) ($json[
'models'] ?? array()) as $m) {
675 if (!empty($m[
'name'])) {
676 $models[] = preg_replace(
'/^models\//',
'', (
string) $m[
'name']);
681 $headers = array(
'Authorization: Bearer '.$apiKey);
682 $res =
getURLContent($baseUrl.
'/models',
'GET',
'', 1, $headers, array(
'http',
'https'), 2);
683 $json = json_decode($res[
'content'] ??
'',
true);
684 foreach ((array) ($json[
'data'] ?? array()) as $m) {
685 if (!empty($m[
'id'])) {
686 $models[] = (string) $m[
'id'];
691 $models = array_values(array_unique($models));
694 if (count($models)) {
695 dolibarr_set_const($db,
'AI_MODELS_LIST_CACHE', json_encode(array(
'service' => $serviceKey,
'ts' =>
dol_now(),
'models' => $models)),
'chaine', 0,
'',
$conf->entity);
698 return array(
'service' => $serviceKey,
'models' => $models);
715 foreach ($models as $cand) {
717 similar_text(strtolower($missing), strtolower($cand), $pct);
719 if (strtok(strtolower($missing),
'-') === strtok(strtolower($cand),
'-')) {
722 if ($score > $bestScore) {
727 return ($bestScore >= 50.0) ? $best :
'';
738 global
$conf, $langs, $user;
740 $langs->loadLangs(array(
'main',
'bills',
'companies',
'products',
'other'));
744 'AIAttachmentBlockedByPrivacy',
'AIAttachmentHeicUnsupported',
'AIAttachmentTooMany',
'MissingInformation',
'CouldYouClarify',
745 'Ref',
'Label',
'ThirdParty',
'Customer',
'Paid',
'Status',
'Type',
'Email',
'Town',
'Date',
746 'DateInvoice',
'DateMaxPayment',
'AmountHT',
'AmountTTC',
'AmountVAT',
'RemainderToPay',
747 'Price',
'PriceTTC',
'VATRate',
'CustomerCode',
'SupplierCode',
'Supplier',
'TotalHT',
'TotalTTC',
755 'ConfirmAiAction',
'ConfirmAiWrite',
756 'ClearChatHistoryTitle',
781 'UnsupportedFileType',
794 'ConnectionBlockedHelp',
799 'BrowserNotSupported',
800 'AISessionExpiredReload',
807 'AIContextAutoTitle',
809 'AIContextClearTitle',
812 'AIContextAttachmentOnly',
846 $ai_translations = array();
847 foreach ($keys as $key) {
848 $ai_translations[$key] = $langs->transnoentitiesnoconv($key);
854 $ai_translations[
'AIContextCounter'] = $langs->transnoentitiesnoconv(
'AIContextCounter',
'%s',
'%s',
'%s');
855 $ai_translations[
'AIContextAuto'] = $langs->transnoentitiesnoconv(
'AIContextAuto',
'%s');
856 $ai_translations[
'AIContextAutoTitle'] = $langs->transnoentitiesnoconv(
'AIContextAutoTitle',
'%s');
857 $ai_translations[
'AIAttachmentTooMany'] = $langs->transnoentitiesnoconv(
'AIAttachmentTooMany',
'%s');
858 $ai_translations[
'DownloadPdf'] = $langs->transnoentitiesnoconv(
"Download").
' PDF';
859 $ai_translations[
'CloudVoiceRequiresSecureContext'] = $langs->trans(
860 "CloudVoiceRequiresSecureContext",
868 if (is_object($user)) {
869 $namesource = $user->firstname ? $user->firstname : ($user->login ? $user->login :
'');
875 'labels' => $ai_translations,
890 'currency' =>
$conf->currency,
891 'locale' => str_replace(
'_',
'-', $langs->getDefaultLang()),
892 'urlRoot' => DOL_URL_ROOT,
898 'userInitial' => $userinitial,
912 global $langs, $user,
$conf;
919 $out .=
'<div class="ai-chat-container'.($mode ===
'popover' ?
' ai-in-popover' :
'').
'"';
921 if ($mode ===
'page') {
922 $out .=
' data-ai-autoinit="1"';
927 $out .=
'<div class="chat-header">';
928 if ($mode ===
'popover') {
930 $title = img_picto(
'',
'fa-robot',
'', 0, 0, 0,
'',
'paddingright').$langs->trans(
"AIAssistant");
931 $title =
'<a href="'.dol_buildpath(
'/ai/assistant/index.php', 1).
'" class="ai-header-link" title="'.
dol_escape_htmltag($langs->trans(
"AIOpenFullPage")).
'">'.$title.
'</a>';
932 $out .=
'<h2>'.$title.
'</h2>';
935 $out .=
'<div class="chat-header-id">';
936 $out .=
'<span class="chat-header-avatar">'.img_picto(
'',
'fa-robot').
'</span>';
937 $out .=
'<span class="chat-header-text">';
938 $out .=
'<span class="chat-header-title">'.$langs->trans(
"AIAssistant").
'</span>';
940 if ($aiprovider !==
'') {
941 $out .=
'<span class="chat-header-status" title="'.dol_escape_htmltag($langs->trans(
"AIProviderInUse")).
'">'.
dol_escape_htmltag($aiprovider).
'</span>';
946 $out .=
'<div class="header-controls">';
949 $out .=
'<select id="model-select" class="engine-select model-select" title="'.dol_escape_htmltag($langs->trans(
"AIModelToUse")).
'">';
950 $out .=
'<option value="">'.$langs->transnoentitiesnoconv(
"AIModelAuto").
'</option>';
953 $out .=
'<select id="engine-select" class="engine-select">';
954 $out .=
'<option value="text">'.$langs->transnoentitiesnoconv(
"OptionTextOnly").
'</option>';
955 $out .=
'<option value="cloud">'.$langs->transnoentitiesnoconv(
"OptionCloudFast").
'</option>';
956 $out .=
'<option value="whisper">'.$langs->transnoentitiesnoconv(
"OptionWhisperLocal").
'</option>';
962 $out .=
'<button type="button" id="clear-btn" class="icon-btn" title="'.dol_escape_htmltag($langs->trans(
"ClearChatHistoryTitle")).
'">';
963 $out .= img_picto(
'',
'fa-trash').
' <span class="ai-btn-label">'.$langs->trans(
"Clear").
'</span>';
965 if ($mode ===
'popover') {
969 $out .=
'<button type="button" id="ai-expand-btn" class="icon-btn ai-window-btn" title="'.dol_escape_htmltag($langs->trans(
"AIOpenFullPage")).
'" data-fullscreen-url="'.
dol_buildpath(
'/ai/assistant/index.php', 1).
'"><i class="fa fa-expand"></i></button>';
970 $out .=
'<button type="button" id="ai-close-btn" class="icon-btn ai-window-btn" title="'.dol_escape_htmltag($langs->trans(
"Close")).
'"><i class="fa fa-times"></i></button>';
976 $out .=
'<div id="chat-history" class="chat-history">';
977 if ($mode ===
'popover') {
979 $out .=
'<div class="msg system">'.$langs->trans(
"AIWelcomeMessage").
'</div>';
984 $welcomename = $user->firstname ? $user->firstname : (is_object($user) ? $user->login :
'');
986 array(
'icon' =>
'fa-file-invoice-dollar',
'key' =>
'Invoices'),
987 array(
'icon' =>
'fa-chart-line',
'key' =>
'Revenue'),
988 array(
'icon' =>
'fa-coins',
'key' =>
'Finance'),
989 array(
'icon' =>
'fa-warehouse',
'key' =>
'Inventory'),
991 $out .=
'<div class="chat-welcome">';
992 $out .=
'<div class="chat-welcome-avatar">'.img_picto(
'',
'fa-robot').
'</div>';
993 $out .=
'<h2 class="chat-welcome-title">'.dol_escape_htmltag($langs->trans(
"AIGreeting", $welcomename)).
'</h2>';
994 $out .=
'<p class="chat-welcome-subtitle">'.dol_escape_htmltag($langs->trans(
"AIGreetingSubtitle")).
'</p>';
995 $out .=
'<div class="chat-welcome-actions">';
996 foreach ($quickcards as $card) {
997 $out .=
'<button type="button" class="ai-quick-card" data-prompt="'.dol_escape_htmltag($langs->transnoentitiesnoconv(
"AIQuick".$card[
'key'].
"Prompt")).
'">';
998 $out .=
'<span class="ai-quick-icon">'.img_picto(
'', $card[
'icon']).
'</span>';
999 $out .=
'<span class="ai-quick-text">';
1000 $out .=
'<span class="ai-quick-title">'.dol_escape_htmltag($langs->trans(
"AIQuick".$card[
'key'].
"Title")).
'</span>';
1001 $out .=
'<span class="ai-quick-desc">'.dol_escape_htmltag($langs->trans(
"AIQuick".$card[
'key'].
"Desc")).
'</span>';
1003 $out .=
'</button>';
1012 $out .=
'<div class="chat-controls">';
1016 $out .=
'<div id="file-chip-area" class="file-chip-area ai-hidden"></div>';
1017 $out .=
'<div class="chat-input-pill">';
1019 $out .=
'<div id="upload-wrapper" class="upload-wrapper">';
1020 $out .=
'<input type="file" id="file-upload" multiple accept=".pdf,.txt,.xml,.png,.jpg,.jpeg,.heic,.heif,.doc,.docx,.xls,.xlsx,.odt,.ods" style="display: none;">';
1021 $out .=
'<button type="button" id="upload-btn" class="round-btn" title="'.dol_escape_htmltag($langs->transnoentitiesnoconv(
"AttachFile")).
'">'.img_picto(
'',
'fa-paperclip').
'</button>';
1024 $out .=
'<div id="mic-wrapper" class="mic-wrapper ai-hidden">';
1025 $out .=
'<button type="button" id="mic-btn" class="round-btn mic-btn" title="'.dol_escape_htmltag($langs->trans(
"ToggleMicrophone")).
'">'.img_picto(
'',
'fa-microphone').
'</button>';
1028 $out .=
'<textarea id="user-input" class="ia-input" rows="1"';
1029 if (empty(
$conf->dol_optimize_smallscreen)) {
1030 $out .=
' placeholder="'.dol_escape_htmltag($langs->trans(
"TypeYourQuestion")).
'"';
1032 $out .=
' autocomplete="off" spellcheck="false"></textarea>';
1034 $out .=
'<button type="button" id="send-btn" class="chat-send-btn" title="'.dol_escape_htmltag($langs->trans(
"SendPrompt")).
'">'.img_picto(
'',
'fa-paper-plane').
'</button>';
1038 $out .=
'<div id="status-bar"></div>';
1068 if (!empty($_SERVER[
'HTTP_X_CSRF_TOKEN'])) {
1069 $token = (string) $_SERVER[
'HTTP_X_CSRF_TOKEN'];
1071 $token =
GETPOST(
'token',
'alpha');
1074 $sessiontokens = array();
1075 if (!empty($_SESSION[
'token'])) {
1076 $sessiontokens[] = (string) $_SESSION[
'token'];
1078 if (!empty($_SESSION[
'newtoken'])) {
1079 $sessiontokens[] = (string) $_SESSION[
'newtoken'];
1083 foreach ($sessiontokens as $sessiontoken) {
1084 if (!empty($token) && hash_equals($sessiontoken, $token)) {
1093 .
' refused by CSRF protection (invalid or missing token).',
1097 http_response_code(403);
1098 header(
'Content-Type: application/json');
1099 echo json_encode(array(
'error' =>
'Invalid CSRF token'));
1124 if (!is_array($payload) || $elementtype ===
'') {
1128 static $cache = array();
1129 if (!isset($cache[$elementtype])) {
1130 require_once DOL_DOCUMENT_ROOT.
'/core/class/extrafields.class.php';
1132 $extrafields->fetch_name_optionals_label($elementtype);
1133 $attrs = $extrafields->attributes[$elementtype] ?? array();
1134 $personal = array();
1135 foreach (($attrs[
'personal_data'] ?? array()) as $code => $flag) {
1136 if (!empty($flag)) {
1137 $personal[] =
'options_'.$code;
1140 $cache[$elementtype] = $personal;
1142 if (empty($cache[$elementtype])) {
1146 foreach ($payload as $key => $value) {
1147 if ($key ===
'array_options' && is_array($value)) {
1148 foreach ($cache[$elementtype] as $personalKey) {
1149 unset($payload[$key][$personalKey]);
1151 } elseif (is_array($value)) {
dolibarr_set_const($db, $name, $value, $type='chaine', $visible=0, $note='', $entity=1)
Insert a parameter (key,value) into database (delete old key then insert it again).
testAIConnection(string $service, string $key, string $url)
Tests the connection to an AI service using its API key and URL by sending message "Hello".
getListOfAIFeatures()
Prepare admin pages header.
aiAdminPrepareHead()
Prepare admin pages header.
getAiProviderModelList($db, $forcerefresh=false)
Return the list of model ids offered by the configured AI provider, with a 1-hour cache in the consta...
getListOfAIServices()
Get list of available ai services.
getAiChatAssistantConfig()
Build the configuration array consumed by the AI Assistant chat frontend (ai/js/ai_assistant....
ai_validate_attachments(array $attachments, &$error)
Validate chat attachments before they reach any LLM provider.
aiTruncateForLog($text, $max=60000)
Trim a payload for the request log, keeping the beginning and the end.
getListForAIRephraseStyle()
Get list for AI style of writing.
ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error='', $rawReq='', $rawRes='', array $context=array(), &$logId=null)
Log AI Request with Raw Payloads.
aiStripPersonalExtrafields($db, $payload, $elementtype)
Remove extrafields flagged as personal data from an API-shaped payload.
getAiAssistantProviderLabel()
Resolve the AI provider/service currently configured for the AI Assistant (e.g.
getListForAISummarize()
Get list for AI summarize.
aiSuggestClosestModel($missing, array $models)
Suggest the closest available model id for a model that disappeared from the provider's list: same fa...
aiCheckCsrfToken($context='')
Check the anti-CSRF token of a request sent to one of the AI Assistant endpoints.
getAiChatAssistantHtml($mode='page')
Build the HTML of the AI Assistant chat interface.
dol_now($mode='gmt')
Return date for now.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
newToken()
Return the value of token currently saved into session with name 'newtoken'.
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0, $nodefault=0)
Return value of a param into GET or POST supervariable.
dol_strtoupper($string, $encoding="UTF-8")
Convert a string to upper.
dol_substr($string, $start, $length=null, $stringencoding='', $trunconbytes=0)
Make a substring.
dol_buildpath($path, $type=0, $returnemptyifnotfound=0)
Return path of url or filesystem.
complete_head_from_modules($conf, $langs, $object, &$head, &$h, $type, $mode='add', $filterorigmodule='')
Complete or removed entries into a head array (used to build tabs).
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).
dol_escape_htmltag($stringtoescape, $keepb=0, $keepn=0, $noescapetags='', $escapeonlyhtmltags=0, $cleanalsojavascript=0)
$context
@method int call_trigger(string $triggerName, ?User $user)
dolDecrypt($chain, $key='', $patterntotest='')
Decode a string with a symmetric encryption.