dolibarr 25.0.0-alpha
ai.lib.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2022 Alice Adminson <aadminson@example.com>
3 * Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 * Copyright (C) 2026 Anthony Damhet <a.damhet@progiseize.fr>
6 * Copyright (C) 2026 Nick Fragoulis
7 * Copyright (C) 2026 Jose Martinez <jose.martinez@pichinov.com>
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY, without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
29include_once DOL_DOCUMENT_ROOT.'/ai/class/ai.class.php';
30
31
38{
39 global $langs;
40
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'),
45
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),
50
51 'textgenerationextrafield' => array('label' => $langs->trans('TextGeneration').' ('.$langs->trans("ExtrafieldFiller").')', 'picto' => '', 'status'=>'dolibarr', 'function' => 'TEXT', 'placeholder' => Ai::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER),
52
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')
59 );
60
61 return $arrayofaifeatures;
62}
63
70{
71 global $langs;
72
73 $arrayofai = array(
74 '-1' => array('label' => $langs->trans('SelectAService')),
75 'chatgpt' => array(
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'), // updated Oct 2026
80 'imagegeneration' => array('default' => 'gpt-image-2'), // Replaced DALL-E 3; 4x faster and native to GPT-5
81 'audiogeneration' => array('default' => 'gpt-audio-1.5'), // New Feb 23, 2026 release for high-fidelity audio out
82 'videogeneration' => array('default' => 'sora-2'), // OpenAI's standard API video model
83 'transcription' => array('default' => 'gpt-transcribe'), // Dedicated model
84 'translation' => array('default' => 'gpt-5.6'), // Still the best for multi-language audio translation
85 'docparsing' => array('default' => 'gpt-5.6'), // Uses the new Responses API / Vision capabilities
86 'adapter_type' => 'openai'
87 ),
88 'groq' => array(
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'), // February 2026 flagship for extreme speed (1,000+ t/s)
93 'imagegeneration' => array('default' => 'na'),
94 'audiogeneration' => array('default' => 'na'),
95 'videogeneration' => array('default' => 'na'),
96 'transcription' => array('default' => 'whisper-large-v3-turbo'), // Groq's specialized high-speed Whisper implementation
97 'translation' => array('default' => 'whisper-large-v3-turbo'), // High-speed audio translation to English
98 'docparsing' => array('default' => 'llama-4-70b-versatile'), // Best for structured data extraction from text
99 'adapter_type' => 'openai'
100 ),
101 'mistral' => array(
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'), // Points to Mistral Small 3 (updated Feb 2026)
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'), // Mistral's native vision/doc model
112 'adapter_type' => 'openai'
113 ),
114 'deepseek' => array(
115 'label' => 'DeepSeek',
116 'url' => 'https://api.deepseek.com',
117 'setup' => 'https://platform.deepseek.com/api_keys',
118 'textgeneration' => array('default' => 'deepseek-v4'), // Released Feb 2026, flagship MoE model
119 'imagegeneration' => array('default' => 'deepseek-janus-2'), // DeepSeek's latest multimodal vision/gen model
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'), // Massive 1M context support for parsing
125 'adapter_type' => 'openai'
126 ),
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'), // Flagship search model as of Feb 2026
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'), // Best for analyzing search-grounded docs
138 'adapter_type' => 'openai'
139 ),
140 'zai' => array(
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'), // Flagship released February 11, 2026
145 'imagegeneration' => array('default' => 'cogview-4'), // Zhipu's latest SOTA image generator
146 'audiogeneration' => array('default' => 'cogvlm2-audio'), // High-fidelity conversational audio
147 'videogeneration' => array('default' => 'cogvideox-2'), // Flagship API video model
148 'transcription' => array('default' => 'na'),
149 'translation' => array('default' => 'na'),
150 'docparsing' => array('default' => 'glm-5'), // Top-tier agentic document processing
151 'adapter_type' => 'openai'
152 ),
153 'custom' => array(
154 'label' => 'Custom',
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'
165 ),
166 // --- SPECIALIZED ADAPTERS ---
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'), // Current Anthropic flagship; 1M context window
172 'imagegeneration' => array('default' => 'na'), // Anthropic remains focused on text/code logic
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'), // Leading model for "Computer Use" and PDF analysis
178 'adapter_type' => 'anthropic'
179 ),
180 'google' => array(
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'), // Flagship reasoning model released Feb 19, 2026
185 'imagegeneration' => array('default' => 'nano-banana-pro'), // Latest SOTA image model (Gemini 3 Pro Image)
186 'audiogeneration' => array('default' => 'gemini-2.5-pro-tts'), // High-fidelity native speech synthesis
187 'videogeneration' => array('default' => 'veo-3.1'), // Google's flagship cinematic video API
188 'transcription' => array('default' => 'gemini-3.1-pro-preview'), // Native multi-modal audio reasoning
189 'translation' => array('default' => 'gemini-3.1-pro-preview'), // Native audio-to-text translation
190 'docparsing' => array('default' => 'gemini-3.1-pro-preview'), // Massive 2M+ context window for full repo parsing
191 'adapter_type' => 'google'
192 )
193 );
194
195 return $arrayofai;
196}
197
213function testAIConnection(string $service, string $key, string $url): array
214{
215 if (empty($key)) {
216 return ['success' => false, 'message' => 'API Key is empty'];
217 }
218
219 // Load Defaults (Ensure this function exists or handle the error)
220 if (!function_exists('getListOfAIServices')) {
221 return ['success' => false, 'message' => 'Configuration helper function missing.'];
222 }
223
224 $list = getListOfAIServices();
225 $defUrl = $list[$service]['url'] ?? '';
226 // Use model from config, fallback to hardcoded if necessary
227 $defaultModel = $list[$service]['model'] ?? 'unknown';
228
229 // Normalize URL
230 if (empty($url)) {
231 $url = $defUrl;
232 }
233 $url = rtrim($url, '/');
234
235 $data = [];
236 $headers = ["Content-Type: application/json"];
237
238 $model = '';
239 if (empty($model)) {
240 $model = getDolGlobalString('AI_API_' . strtoupper($service) . '_MODEL_TEXT');
241 }
242
243 // GOOGLE
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";
248 } else {
249 $url .= "/$model:generateContent";
250 }
251 }
252 $url .= "?key=" . $key;
253 $data = ["contents" => [ ["parts" => [ ["text" => "Hello"] ] ] ], "generationConfig" => ["maxOutputTokens" => 5]];
254 } elseif ($service == 'anthropic' || strpos($url, 'anthropic') !== false) { // ANTHROPIC
255 if (strpos($url, 'messages') === false) $url .= '/messages';
256 $headers[] = "x-api-key: $key";
257 $headers[] = "anthropic-version: 2023-06-01";
258 $data = [
259 "model" => $model, // Uses Configured Model
260 "messages" => [["role" => "user", "content" => "Hello"]],
261 "max_tokens" => 5
262 ];
263 } else {
264 if (strpos($url, '/chat/completions') === false) $url .= '/chat/completions';
265 $headers[] = "Authorization: Bearer $key";
266
267 $data = [
268 "model" => $model, // Uses Configured Model (from Priority Chain)
269 "messages" => [["role" => "user", "content" => "Hello"]],
270 "max_tokens" => 5
271 ];
272 }
273
274 // Execute request with the Dolibarr HTTP wrapper (handles proxy, SSL and logging)
275 include_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
276
277 // By default, we accept only external endpoints ($dolibarr_ai_allow_local_endpoints is not set).
278 // To allow local endpoints, we must set $dolibarr_ai_allow_local_endpoints to 1 or 2 in conf.php.
279 global $dolibarr_ai_allow_local_endpoints;
280 $localurl = empty($dolibarr_ai_allow_local_endpoints) ? 0 : 2;
281
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'] ?? '');
285
286 if (!empty($result['curl_error_no'])) {
287 return ['success' => false, 'message' => "Curl Error: ".($result['curl_error_msg'] ?? '')];
288 }
289
290 if ($httpCode >= 200 && $httpCode < 300) {
291 return ['success' => true, 'message' => "OK (HTTP $httpCode)."];
292 } else {
293 $json = json_decode($responseContent, true);
294 // Attempt to find the error message in various common structures
295 $msg = $json['error']['message'] ?? $json['message'] ?? substr($responseContent, 0, 150);
296 return ['success' => false, 'message' => "HTTP $httpCode. Error: $msg"];
297 }
298}
299
300
317function ai_validate_attachments(array $attachments, &$error)
318{
319 global $langs;
320
321 $error = '';
322 if (empty($attachments)) {
323 return true;
324 }
325 $langs->load("other"); // owns the AIAttachment* keys; callers load it later or not at all
326
327 if (getDolGlobalInt('AI_PRIVACY_REDACTION', 0)) {
328 $error = $langs->trans("AIAttachmentBlockedByPrivacy");
329
330 return false;
331 }
332
333 // Cap the number of attachments server-side too: the chat enforces it
334 // client-side only, and other callers may not. 0 means unlimited.
335 $maxfiles = getDolGlobalInt('AI_ATTACHMENT_MAX_FILES', 5);
336 if ($maxfiles > 0 && count($attachments) > $maxfiles) {
337 $error = $langs->trans("AIAttachmentTooMany", (string) $maxfiles);
338
339 return false;
340 }
341
342 $allowedmimes = array('application/pdf', 'image/png', 'image/jpeg', 'image/gif', 'image/webp');
343 // HEIC/HEIF reach this point only through the native-send fallback of the
344 // chat (browser unable to transcode): acceptable solely when the active
345 // provider consumes them (Gemini); other providers 400 on the MIME.
346 if ((getListOfAIServices()[getDolGlobalString('AI_API_SERVICE')]['adapter_type'] ?? '') === 'google') {
347 $allowedmimes[] = 'image/heic';
348 $allowedmimes[] = 'image/heif';
349 }
350 $maxbytes = getDolGlobalInt('AI_ATTACHMENT_MAX_MB', 10) * 1024 * 1024;
351 $totalbytes = 0;
352 foreach ($attachments as $att) {
353 if (!in_array($att['mime'], $allowedmimes, true)) {
354 $error = $langs->trans("AIAttachmentTypeNotAllowed", $att['mime']);
355
356 return false;
357 }
358 // 3/4 ratio: decoded size of a base64 payload without decoding it.
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));
363
364 return false;
365 }
366 }
367
368 return true;
369}
370
383function aiTruncateForLog($text, $max = 60000)
384{
385 $len = dol_strlen($text);
386 if ($len <= $max) {
387 return $text;
388 }
389 $head = (int) floor($max / 2);
390 $tail = $max - $head;
391
392 return dol_substr($text, 0, $head)
393 ."\n... [Truncated ".($len - $max)." chars] ...\n"
394 .dol_substr($text, $len - $tail, $tail);
395}
396
416function ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error = '', $rawReq = '', $rawRes = '', array $context = array(), &$logId = null)
417{
418 global $conf;
419
420 $logId = 0;
421
422 if (!getDolGlobalInt('AI_LOG_REQUESTS')) {
423 return 0;
424 }
425
426 $tool = isset($response['tool']) ? (string) $response['tool'] : '';
427
428 // Keep both ends when trimming: a request payload starts with the tool
429 // schemas (tens of kB, identical on every call) and ends with the system
430 // rules, the user query and the page context - the part anyone reads a
431 // log for. Cutting only the tail threw exactly that away.
432 // Structured tool output must remain valid JSON for subsequent reads.
433 $rawResStr = (string) $rawRes;
434 if (empty($context['preserve_payloads'])) {
435 $rawReq = aiTruncateForLog($rawReq, 60000);
436 $rawResStr = aiTruncateForLog($rawResStr, 60000);
437 }
438
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";
442 // Each optional group keys on ITS OWN entries, so a caller passing only
443 // token usage does not drag empty audit hashes along, and vice versa.
444 $hasAudit = isset($context['fk_actioncomm']) || isset($context['input_hash']) || isset($context['output_hash']) || isset($context['security_hash']);
445 $hasUsage = isset($context['tokens_input']) || isset($context['tokens_output']) || isset($context['model']);
446 if ($hasAudit) {
447 $sql .= ", fk_actioncomm, input_hash, output_hash, security_hash";
448 }
449 if ($hasUsage) {
450 $sql .= ", tokens_input, tokens_output, model";
451 }
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) . "'";
465 if ($hasAudit) {
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'] ?? '')."'";
470 }
471 if ($hasUsage) {
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'] ?? ''))."'";
475 }
476 $sql .= ")";
477
478 $resql = $db->query($sql);
479 if (!$resql) {
480 dol_syslog(__FUNCTION__.": ".$db->lasterror(), LOG_ERR);
481 } else {
482 $logId = (int) $db->last_insert_id($db->prefix()."ai_request_log");
483 }
484
485 return 0;
486}
487
494{
495 $arrayforaisummarize = array(
496 //'20_w' => 'SummarizeTwentyWords',
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'
505 );
506
507 return $arrayforaisummarize;
508}
509
516{
517 $arrayforaierephrasestyle = array(
518 'spellchecker' => 'RephraseSpellChecker',
519 'professional' => 'RephraseStyleProfessional',
520 'humouristic' => 'RephraseStyleHumouristic',
521 );
522
523 return $arrayforaierephrasestyle;
524}
525
532{
533 global $langs, $conf;
534
535 $langs->load("agenda");
536
537 $h = 0;
538 $head = array();
539
540 $head[$h][0] = dol_buildpath("/ai/admin/setup.php", 1);
541 $head[$h][1] = $langs->trans("Settings");
542 $head[$h][2] = 'settings';
543 $h++;
544
545 $head[$h][0] = dol_buildpath("/ai/admin/custom_prompt.php", 1);
546 $head[$h][1] = $langs->trans("CustomPrompt");
547 $head[$h][2] = 'custom';
548 $h++;
549
550 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 1) {
551 $head[$h][0] = dol_buildpath("/ai/admin/assistant.php", 1);
552 $head[$h][1] = $langs->trans("Assistant");
553 $head[$h][2] = 'assistant';
554 $h++;
555 }
556
557 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 1) {
558 $head[$h][0] = dol_buildpath("/ai/admin/server_mcp.php", 1);
559 $head[$h][1] = $langs->trans("MCPServer");
560 $head[$h][2] = 'servermcp';
561 $h++;
562 }
563
564 if (getDolGlobalString("MAIN_FEATURES_LEVEL") >= 1) {
565 $head[$h][0] = dol_buildpath("/ai/admin/configure_tools.php", 1);
566 $head[$h][1] = $langs->trans("ToolAccessControl");
567 $head[$h][2] = 'tools';
568 $h++;
569 }
570
571 /*
572 $head[$h][0] = dol_buildpath("/ai/admin/myobject_extrafields.php", 1);
573 $head[$h][1] = $langs->trans("ExtraFields");
574 $head[$h][2] = 'myobject_extrafields';
575 $h++;
576 */
577
578 // Show more tabs from modules
579 // Entries must be declared in modules descriptor with line
580 //$this->tabs = array(
581 // 'entity:+tabname:Title:@ai:/ai/mypage.php?id=__ID__'
582 //); // to add new tab
583 //$this->tabs = array(
584 // 'entity:-tabname:Title:@ai:/ai/mypage.php?id=__ID__'
585 //); // to remove a tab
586 complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai');
587
588 complete_head_from_modules($conf, $langs, null, $head, $h, 'ai@ai', 'remove');
589
590 return $head;
591}
592
602{
603 $serviceKey = getDolGlobalString('AI_API_SERVICE');
604 if (empty($serviceKey) || $serviceKey === '-1') {
605 return '';
606 }
607
608 $services = getListOfAIServices();
609
610 return isset($services[$serviceKey]['label']) ? (string) $services[$serviceKey]['label'] : (string) $serviceKey;
611}
612
624function getAiProviderModelList($db, $forcerefresh = false)
625{
626 global $conf;
627
628 $serviceKey = getDolGlobalString('AI_API_SERVICE');
629 if (empty($serviceKey) || $serviceKey == '-1') {
630 return array('service' => '', 'models' => array());
631 }
632
633 if (!$forcerefresh) {
634 $cacheraw = getDolGlobalString('AI_MODELS_LIST_CACHE');
635 if ($cacheraw) {
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']);
641 }
642 }
643 }
644
645 include_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
646 include_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
647
648 $servicesList = getListOfAIServices();
649 $adapterType = $servicesList[$serviceKey]['adapter_type'] ?? 'openai';
650 $defUrl = $servicesList[$serviceKey]['url'] ?? '';
651 $baseUrl = rtrim(getDolGlobalString('AI_API_'.strtoupper($serviceKey).'_URL') ?: $defUrl, '/');
652
653 $apiKey = getDolGlobalString('AI_API_'.strtoupper($serviceKey).'_KEY');
654 if (preg_match('/^crypt:/', $apiKey)) {
655 $apiKey = dolDecrypt($apiKey, $conf->file->instance_unique_id);
656 }
657 if (empty($apiKey) || empty($baseUrl)) {
658 return array('service' => $serviceKey, 'models' => array());
659 }
660
661 $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'];
669 }
670 }
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']);
677 }
678 }
679 } else {
680 // OpenAI-compatible providers (OpenAI, Mistral, Groq, DeepSeek, custom...)
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'];
687 }
688 }
689 }
690
691 $models = array_values(array_unique($models));
692 sort($models);
693
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);
696 }
697
698 return array('service' => $serviceKey, 'models' => $models);
699}
700
711function aiSuggestClosestModel($missing, array $models)
712{
713 $best = '';
714 $bestScore = -1.0;
715 foreach ($models as $cand) {
716 $pct = 0.0;
717 similar_text(strtolower($missing), strtolower($cand), $pct);
718 $score = $pct;
719 if (strtok(strtolower($missing), '-') === strtok(strtolower($cand), '-')) {
720 $score += 15.0; // same family beats a slightly closer string of another family
721 }
722 if ($score > $bestScore) {
723 $bestScore = $score;
724 $best = $cand;
725 }
726 }
727 return ($bestScore >= 50.0) ? $best : '';
728}
729
737{
738 global $conf, $langs, $user;
739
740 $langs->loadLangs(array('main', 'bills', 'companies', 'products', 'other'));
741
742 $keys = array(
743 // Table header labels for common API fields (see FIELD_LABELS in ai_assistant.js)
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',
748 // General UI
749 'NoDataAvailable',
750 'Error',
751 'NoRecordFound',
752 'Download',
753 'Show',
754 'Confirm',
755 'ConfirmAiAction', 'ConfirmAiWrite',
756 'ClearChatHistoryTitle',
757 'HistoryCleared',
758 'Send',
759 'TypeYourQuestion',
760
761 // Placeholders & Status
762 'TypeOrSpeak',
763 'DocLoaded',
764 'Listening',
765 'Transcribed',
766 'NoSpeech',
767 'ProcessingAudio',
768 'Timeout',
769 'Cancelled',
770
771 // Engine Specific
772 'CloudSpeechReady',
773 'WhisperReady',
774 'DownloadingModel',
775 'ModelLoading',
776
777 // Document Processing
778 'ProcessingFile',
779 'ReadingPdf',
780 'PdfError',
781 'UnsupportedFileType',
782 'TryingOCR',
783 'OcrProgress',
784 'SwitchingAIModel',
785 'OcrFailed',
786 'ReadingWord',
787 'ReadingExcel',
788 'ReadingOdf',
789
790 // Errors
791 'MicError',
792 'MicTooQuiet',
793 'ConnectionBlocked',
794 'ConnectionBlockedHelp',
795 'WorkerInitFailed',
796 'NetworkError',
797 'AIError',
798 'EmptyAIResponse',
799 'BrowserNotSupported',
800 'AISessionExpiredReload',
801
802 // Context pins
803 'AIContextPinOn',
804 'AIContextPinOff',
805 'AIContextCounter',
806 'AIContextAuto',
807 'AIContextAutoTitle',
808 'AIContextClear',
809 'AIContextClearTitle',
810 'AIContextAll',
811 'AIContextAllTitle',
812 'AIContextAttachmentOnly',
813
814 // Actions & Dialogs
815 'YesProceed',
816 'Cancel',
817 'Submit',
818 'ActionCancelled',
819 'ExecutingTool',
820 'FetchingData',
821 'GeneratingLink',
822 'Found',
823 'File',
824 'Preview',
825 'TypeResponse',
826 'OpenVerb',
827
828 // Voice Confirmation
829 'VoiceYesNo',
830 'VoiceQuiet',
831 'PleaseRepeat',
832 'HeardText',
833
834 // Context
835 'DocContextIntro',
836 'DocContextOutro',
837
838 // Model picker
839 'AIModelAuto',
840 'AIModelFast',
841 'AIModelBalanced',
842 'AIModelDeep',
843 'AIModelSavedGone'
844 );
845
846 $ai_translations = array();
847 foreach ($keys as $key) {
848 $ai_translations[$key] = $langs->transnoentitiesnoconv($key);
849 }
850 // Keys whose %s placeholders are consumed CLIENT-side: trans() always
851 // sprintf()s the string (empty defaults eat the %s - same trap as the
852 // TakePOS split-amount labels), so re-feed literal '%s' as parameters to
853 // keep the placeholders intact for the JS .replace() calls.
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",
861 "HTTPS",
862 "localhost",
863 "Whisper"
864 );
865
866 // First letter of the current user name, shown in the "user" message avatar
867 $userinitial = '';
868 if (is_object($user)) {
869 $namesource = $user->firstname ? $user->firstname : ($user->login ? $user->login : '');
870 $userinitial = dol_strtoupper(dol_substr($namesource, 0, 1));
871 }
872
873 return array(
874 'mode' => getDolGlobalString('AI_DEFAULT_INPUT_MODE'),
875 'labels' => $ai_translations,
876 // Presentation context for tool results: money, date and label
877 // formatting happen client-side on raw API data.
878 'privacyRedaction' => getDolGlobalInt('AI_PRIVACY_REDACTION', 0),
879 // Attachment count cap, so the client mirrors the server-side guard
880 // of ai_validate_attachments() instead of hardcoding its own.
881 'maxAttachments' => getDolGlobalInt('AI_ATTACHMENT_MAX_FILES', 5),
882 // Recent exchanges that follow the model by default (sliding window).
883 // Off (0) until an administrator decides otherwise: past answers carry
884 // business content the privacy masking does not cover, so sending them
885 // back on every request is not a default the module takes by itself.
886 'autoContext' => getDolGlobalInt('AI_CHAT_CONTEXT_AUTO_EXCHANGES', 0),
887 // Gemini is the only wired provider taking HEIC natively; the chat JS
888 // falls back to it when the browser cannot transcode HEIC to JPEG.
889 'providerAcceptsHeic' => ((getListOfAIServices()[getDolGlobalString('AI_API_SERVICE')]['adapter_type'] ?? '') === 'google' ? 1 : 0),
890 'currency' => $conf->currency,
891 'locale' => str_replace('_', '-', $langs->getDefaultLang()),
892 'urlRoot' => DOL_URL_ROOT,
893 // Endpoints are called with absolute URLs so the chat also works when
894 // injected into another page (topbar popover) and not only when served
895 // from /ai/assistant/index.php.
896 'baseUrl' => dol_buildpath('/ai/assistant/', 1),
897 'token' => newToken(),
898 'userInitial' => $userinitial,
899 );
900}
901
910function getAiChatAssistantHtml($mode = 'page')
911{
912 global $langs, $user, $conf;
913
914 $out = '';
915
916 // Config travels as a data attribute: <script> tags injected via innerHTML
917 // are never executed by the browser, so a window.AI_CONFIG inline script
918 // would not work for the AJAX-loaded popover.
919 $out .= '<div class="ai-chat-container'.($mode === 'popover' ? ' ai-in-popover' : '').'"';
920 $out .= ' data-ai-config="'.dol_escape_htmltag(json_encode(getAiChatAssistantConfig())).'"';
921 if ($mode === 'page') {
922 $out .= ' data-ai-autoinit="1"';
923 }
924 $out .= '>';
925
926 // Header
927 $out .= '<div class="chat-header">';
928 if ($mode === 'popover') {
929 // In the popover the title links to the full standalone page
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>';
933 } else {
934 // Full page: assistant identity (avatar + title + current LLM model)
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>';
939 $aiprovider = getAiAssistantProviderLabel();
940 if ($aiprovider !== '') {
941 $out .= '<span class="chat-header-status" title="'.dol_escape_htmltag($langs->trans("AIProviderInUse")).'">'.dol_escape_htmltag($aiprovider).'</span>';
942 }
943 $out .= '</span>';
944 $out .= '</div>';
945 }
946 $out .= '<div class="header-controls">';
947 // Model picker pill: 'Auto' (provider default) + presets + the dynamic model
948 // list fetched from ajax/list_models.php by the JS. Choice kept in localStorage.
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>';
951 $out .= '</select>';
952 // Engine Switcher (restyled as a pill with a sparkle icon)
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>';
957 // Note: the legacy 'local_docs'/'cloud_docs' selector modes are gone — documents
958 // are now attached with the always-visible paperclip button and routed
959 // automatically (local extraction first, cloud parsing as fallback).
960 $out .= '</select>';
961 // Clear Button
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>';
964 $out .= '</button>';
965 if ($mode === 'popover') {
966 // Window controls of the popover (handled by the bootstrap JS in main.inc.php).
967 // The expand button opens the standalone full page (/ai/assistant/index.php)
968 // in the current tab; the popover always stays in its large ("expanded") state.
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>';
971 }
972 $out .= '</div>';
973 $out .= '</div>';
974
975 // Chat History
976 $out .= '<div id="chat-history" class="chat-history">';
977 if ($mode === 'popover') {
978 // Compact greeting line for the narrow popover
979 $out .= '<div class="msg system">'.$langs->trans("AIWelcomeMessage").'</div>';
980 } else {
981 // Full page: rich empty-state welcome screen (hidden by JS as soon as the
982 // conversation starts, restored on Clear). Quick cards send a localized
983 // ready-made prompt on click (data-prompt).
984 $welcomename = $user->firstname ? $user->firstname : (is_object($user) ? $user->login : '');
985 $quickcards = array(
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'),
990 );
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>';
1002 $out .= '</span>';
1003 $out .= '</button>';
1004 }
1005 $out .= '</div>';
1006 $out .= '</div>';
1007 }
1008 $out .= '</div>';
1009
1010 // Controls: a single rounded "pill" holding the attach/mic buttons, the
1011 // textarea and the send button.
1012 $out .= '<div class="chat-controls">';
1013 // Attached-file chip (icon + name + remove cross), shown above the input pill
1014 // once a document has been attached with the paperclip. The document content
1015 // itself NEVER appears in the input nor in the conversation.
1016 $out .= '<div id="file-chip-area" class="file-chip-area ai-hidden"></div>';
1017 $out .= '<div class="chat-input-pill">';
1018 // Upload Wrapper (always visible: documents can be attached in any mode)
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>';
1022 $out .= '</div>';
1023 // Microphone Wrapper (Visible only in Voice modes)
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>';
1026 $out .= '</div>';
1027 // Text Input
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")).'"';
1031 }
1032 $out .= ' autocomplete="off" spellcheck="false"></textarea>';
1033 // Send Button
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>';
1035 $out .= '</div>';
1036 $out .= '</div>';
1037
1038 $out .= '<div id="status-bar"></div>';
1039
1040 $out .= '</div>';
1041
1042 return $out;
1043}
1044
1066{
1067 $token = '';
1068 if (!empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
1069 $token = (string) $_SERVER['HTTP_X_CSRF_TOKEN'];
1070 } else {
1071 $token = GETPOST('token', 'alpha');
1072 }
1073
1074 $sessiontokens = array();
1075 if (!empty($_SESSION['token'])) {
1076 $sessiontokens[] = (string) $_SESSION['token'];
1077 }
1078 if (!empty($_SESSION['newtoken'])) {
1079 $sessiontokens[] = (string) $_SESSION['newtoken'];
1080 }
1081
1082 $valid = false;
1083 foreach ($sessiontokens as $sessiontoken) {
1084 if (!empty($token) && hash_equals($sessiontoken, $token)) {
1085 $valid = true;
1086 break;
1087 }
1088 }
1089
1090 if (!$valid) {
1091 dol_syslog(
1092 '[AI] Request to '.($context !== '' ? $context : $_SERVER['PHP_SELF'])
1093 .' refused by CSRF protection (invalid or missing token).',
1094 LOG_WARNING
1095 );
1096
1097 http_response_code(403);
1098 header('Content-Type: application/json');
1099 echo json_encode(array('error' => 'Invalid CSRF token'));
1100 exit;
1101 }
1102}
1103
1122function aiStripPersonalExtrafields($db, $payload, $elementtype)
1123{
1124 if (!is_array($payload) || $elementtype === '') {
1125 return $payload;
1126 }
1127
1128 static $cache = array();
1129 if (!isset($cache[$elementtype])) {
1130 require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
1131 $extrafields = new ExtraFields($db);
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;
1138 }
1139 }
1140 $cache[$elementtype] = $personal;
1141 }
1142 if (empty($cache[$elementtype])) {
1143 return $payload;
1144 }
1145
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]);
1150 }
1151 } elseif (is_array($value)) {
1152 // List responses: each row carries its own array_options.
1153 $payload[$key] = aiStripPersonalExtrafields($db, $value, $elementtype);
1154 }
1155 }
1156
1157 return $payload;
1158}
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".
Definition ai.lib.php:213
getListOfAIFeatures()
Prepare admin pages header.
Definition ai.lib.php:37
aiAdminPrepareHead()
Prepare admin pages header.
Definition ai.lib.php:531
getAiProviderModelList($db, $forcerefresh=false)
Return the list of model ids offered by the configured AI provider, with a 1-hour cache in the consta...
Definition ai.lib.php:624
getListOfAIServices()
Get list of available ai services.
Definition ai.lib.php:69
getAiChatAssistantConfig()
Build the configuration array consumed by the AI Assistant chat frontend (ai/js/ai_assistant....
Definition ai.lib.php:736
ai_validate_attachments(array $attachments, &$error)
Validate chat attachments before they reach any LLM provider.
Definition ai.lib.php:317
aiTruncateForLog($text, $max=60000)
Trim a payload for the request log, keeping the beginning and the end.
Definition ai.lib.php:383
getListForAIRephraseStyle()
Get list for AI style of writing.
Definition ai.lib.php:515
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.
Definition ai.lib.php:416
aiStripPersonalExtrafields($db, $payload, $elementtype)
Remove extrafields flagged as personal data from an API-shaped payload.
Definition ai.lib.php:1122
getAiAssistantProviderLabel()
Resolve the AI provider/service currently configured for the AI Assistant (e.g.
Definition ai.lib.php:601
getListForAISummarize()
Get list for AI summarize.
Definition ai.lib.php:493
aiSuggestClosestModel($missing, array $models)
Suggest the closest available model id for a model that disappeared from the provider's list: same fa...
Definition ai.lib.php:711
aiCheckCsrfToken($context='')
Check the anti-CSRF token of a request sent to one of the AI Assistant endpoints.
Definition ai.lib.php:1065
getAiChatAssistantHtml($mode='page')
Build the HTML of the AI Assistant chat interface.
Definition ai.lib.php:910
Class to manage standard extra fields.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
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)
Definition html.lib.php:181
$context
@method int call_trigger(string $triggerName, ?User $user)
Definition logout.php:42
dolDecrypt($chain, $key='', $patterntotest='')
Decode a string with a symmetric encryption.