29if (!defined(
'NOTOKENRENEWAL')) {
30 define(
'NOTOKENRENEWAL', 1);
32if (!defined(
'NOREQUIREMENU')) {
33 define(
'NOREQUIREMENU', 1);
35if (!defined(
'NOREQUIREHTML')) {
36 define(
'NOREQUIREHTML', 1);
38if (!defined(
'NOREQUIREAJAX')) {
39 define(
'NOREQUIREAJAX', 1);
43if (!defined(
'NOCSRFCHECK')) {
44 define(
'NOCSRFCHECK', 1);
47require
'../../main.inc.php';
48require_once DOL_DOCUMENT_ROOT .
'/ai/class/mcp.class.php';
49require_once DOL_DOCUMENT_ROOT .
'/ai/lib/ai.lib.php';
50require_once DOL_DOCUMENT_ROOT .
'/ai/class/llmadapter.class.php';
51require_once DOL_DOCUMENT_ROOT .
'/ai/class/privacy_guard.class.php';
52require_once DOL_DOCUMENT_ROOT .
'/core/lib/security2.lib.php';
56 http_response_code(403);
60global $db, $user,
$conf, $langs;
64if (!$user->hasRight(
'ai',
'assistant',
'use')) {
79define(
'HIGH_CONFIDENCE', 0.8);
80define(
'MEDIUM_CONFIDENCE', 0.5);
81define(
'LOW_CONFIDENCE', 0.3);
84$startTime = microtime(
true);
87$providerUsed =
"offline";
96if (!$assistantEnabled) {
98 "tool" =>
"respond_to_user",
100 "message" =>
"AI assistant service is currently disabled. Please contact your administrator to enable it."
104 echo json_encode($response);
108set_time_limit($timeout + 5);
112 $raw_input = file_get_contents(
'php://input');
113 $data = json_decode($raw_input,
true);
114 $query = isset($data[
'query']) ? trim($data[
'query']) :
'';
123 $aiPageContextLine =
'';
124 $ctxNamesToMask = array();
125 if (!empty($data[
'context']) && is_array($data[
'context'])) {
126 $ctxElement = isset($data[
'context'][
'element']) ? (string) $data[
'context'][
'element'] :
'';
127 $ctxId = isset($data[
'context'][
'id']) ? (int) $data[
'context'][
'id'] : 0;
137 'facture' => array(
'/compta/facture/class/facture.class.php',
'Facture',
'customer invoice',
'facture',
'lire'),
138 'invoice_supplier' => array(
'/fourn/class/fournisseur.facture.class.php',
'FactureFournisseur',
'supplier invoice',
'fournisseur',
'facture',
'lire'),
139 'commande' => array(
'/commande/class/commande.class.php',
'Commande',
'sales order',
'commande',
'lire'),
140 'order_supplier' => array(
'/fourn/class/fournisseur.commande.class.php',
'CommandeFournisseur',
'supplier order',
'fournisseur',
'commande',
'lire'),
141 'propal' => array(
'/comm/propal/class/propal.class.php',
'Propal',
'commercial proposal',
'propal',
'lire'),
142 'supplier_proposal' => array(
'/supplier_proposal/class/supplier_proposal.class.php',
'SupplierProposal',
'supplier proposal',
'supplier_proposal',
'lire'),
143 'societe' => array(
'/societe/class/societe.class.php',
'Societe',
'thirdparty',
'societe',
'lire'),
144 'product' => array(
'/product/class/product.class.php',
'Product',
'product or service',
'produit',
'lire'),
145 'shipping' => array(
'/expedition/class/expedition.class.php',
'Expedition',
'shipment',
'expedition',
'lire'),
146 'reception' => array(
'/reception/class/reception.class.php',
'Reception',
'reception',
'reception',
'lire'),
147 'project' => array(
'/projet/class/project.class.php',
'Project',
'project',
'projet',
'lire'),
148 'project_task' => array(
'/projet/class/task.class.php',
'Task',
'project task',
'projet',
'lire'),
149 'contrat' => array(
'/contrat/class/contrat.class.php',
'Contrat',
'contract',
'contrat',
'lire'),
150 'fichinter' => array(
'/fichinter/class/fichinter.class.php',
'Fichinter',
'intervention',
'ficheinter',
'lire'),
151 'ticket' => array(
'/ticket/class/ticket.class.php',
'Ticket',
'support ticket',
'ticket',
'read'),
152 'member' => array(
'/adherents/class/adherent.class.php',
'Adherent',
'member',
'adherent',
'lire'),
153 'expensereport' => array(
'/expensereport/class/expensereport.class.php',
'ExpenseReport',
'expense report',
'expensereport',
'lire'),
154 'holiday' => array(
'/holiday/class/holiday.class.php',
'Holiday',
'leave request',
'holiday',
'read'),
155 'don' => array(
'/don/class/don.class.php',
'Don',
'donation',
'don',
'lire'),
156 'action' => array(
'/comm/action/class/actioncomm.class.php',
'ActionComm',
'agenda event',
'agenda',
'myactions',
'read'),
157 'bom' => array(
'/bom/class/bom.class.php',
'BOM',
'bill of materials',
'bom',
'read'),
158 'mo' => array(
'/mrp/class/mo.class.php',
'Mo',
'manufacturing order',
'mrp',
'read'),
159 'stock' => array(
'/product/stock/class/entrepot.class.php',
'Entrepot',
'warehouse',
'stock',
'lire'),
160 'contact' => array(
'/contact/class/contact.class.php',
'Contact',
'contact',
'societe',
'contact',
'lire'),
161 'bank_account' => array(
'/compta/bank/class/account.class.php',
'Account',
'bank account',
'banque',
'lire'),
162 'category' => array(
'/categories/class/categorie.class.php',
'Categorie',
'category (tag)',
'categorie',
'lire'),
163 'knowledgerecord' => array(
'/knowledgemanagement/class/knowledgerecord.class.php',
'KnowledgeRecord',
'knowledge article',
'knowledgemanagement',
'knowledgerecord',
'read'),
164 'recruitmentjobposition' => array(
'/recruitment/class/recruitmentjobposition.class.php',
'RecruitmentJobPosition',
'job position',
'recruitment',
'recruitmentjobposition',
'read'),
170 $extraArr = json_decode($ctxExtra,
true);
171 if (is_array($extraArr)) {
172 foreach ($extraArr as $extra) {
173 if (!empty($extra[
'element']) && !empty($extra[
'classfile']) && !empty($extra[
'classname']) && !empty($extra[
'rights'][0]) && !isset($ctxMap[$extra[
'element']])) {
174 $ctxMap[(string) $extra[
'element']] = array(
175 (
string) $extra[
'classfile'],
176 (string) $extra[
'classname'],
177 (
string) ($extra[
'label'] ?? $extra[
'element']),
178 (
string) $extra[
'rights'][0],
179 (string) ($extra[
'rights'][1] ??
'read'),
180 (string) ($extra[
'rights'][2] ??
'')
186 if ($ctxId > 0 && isset($ctxMap[$ctxElement]) && $user->hasRight($ctxMap[$ctxElement][3], $ctxMap[$ctxElement][4], $ctxMap[$ctxElement][5] ??
'')) {
187 require_once DOL_DOCUMENT_ROOT.$ctxMap[$ctxElement][0];
188 $ctxObj =
new $ctxMap[$ctxElement][1]($db);
189 if ($ctxObj->fetch($ctxId) > 0 && (empty($ctxObj->entity) || in_array((
int) $ctxObj->entity, explode(
',',
getEntity($ctxElement))))) {
190 $ctxThirdpartyName =
'';
191 if (empty($doRedact) && !empty($ctxObj->socid)) {
195 require_once DOL_DOCUMENT_ROOT.
'/societe/class/societe.class.php';
197 if ($ctxSoc->fetch((
int) $ctxObj->socid) > 0) {
198 $ctxThirdpartyName =
" (".dol_string_nohtmltag($ctxSoc->name).
")";
201 if (empty($doRedact) && $ctxElement ===
'societe' && !empty($ctxObj->name)) {
202 $ctxThirdpartyName =
" (".dol_string_nohtmltag((
string) $ctxObj->name).
")";
207 $ctxNamesToMask[] = (string) $ctxObj->ref;
208 if (!empty($ctxObj->label)) {
209 $ctxNamesToMask[] = (string) $ctxObj->label;
211 if (!empty($ctxObj->name)) {
212 $ctxNamesToMask[] = (string) $ctxObj->name;
214 $ctxRefPart =
" with ref \"".$ctxObj->ref.
"\"";
215 if (!empty($doRedact) && in_array($ctxElement, array(
'societe',
'contact'),
true)) {
218 $aiPageContextLine =
"The user is currently viewing the ".$ctxMap[$ctxElement][2].$ctxRefPart.
" (id ".(int) $ctxObj->id.(!empty($ctxObj->socid) ?
", thirdparty id ".(int) $ctxObj->socid.$ctxThirdpartyName : $ctxThirdpartyName).
").";
219 $aiPageContextLine .=
" When the user says \"this\"/\"it\" or refers to the current document, use these identifiers as tool arguments";
220 if ($ctxElement ===
'societe') {
221 $aiPageContextLine .=
" - in particular, this thirdparty id is the socid/customer id for any create or search tool";
223 $aiPageContextLine .=
". NEVER ask the user for ids already given here; pass names/refs the user wrote (products, etc.) directly in the matching ref arguments - tools resolve them.";
224 dol_syslog(
"AI Pro: page context accepted: ".$ctxElement.
" #".$ctxId);
226 dol_syslog(
"AI Pro: page context rejected (fetch/entity): ".$ctxElement.
" #".$ctxId, LOG_WARNING);
228 } elseif ($ctxId > 0) {
229 dol_syslog(
"AI Pro: page context rejected (whitelist/rights): ".$ctxElement.
" #".$ctxId, LOG_WARNING);
230 } elseif (!empty($data[
'context'][
'dashboard']) && is_string($data[
'context'][
'dashboard'])) {
232 if (preg_match(
'/^[a-z0-9 _-]+$/i', $dash)) {
233 $aiPageContextLine =
"The user is currently on the ".$dash.
" dashboard page. Questions about \"here\"/\"this page\" concern that module's data.";
234 dol_syslog(
"AI Pro: dashboard context accepted: ".$dash);
236 } elseif (!empty($data[
'context'][
'list']) && $ctxElement !==
'' && (!empty($data[
'context'][
'filters']) || !empty($data[
'context'][
'ids']) || !empty($data[
'context'][
'selected']))) {
243 foreach ((is_array($data[
'context'][
'filters'] ??
null) ? $data[
'context'][
'filters'] : array()) as $fk => $fv) {
244 if (!is_string($fv) || !preg_match(
'/^(search_[a-z0-9_]+|sall|search_all|sortfield|sortorder)$/', (
string) $fk)) {
256 foreach (array(
'ids' => 100,
'selected' => 25) as $idkey => $cap) {
257 if (!empty($data[
'context'][$idkey]) && is_array($data[
'context'][$idkey])) {
259 foreach ($data[
'context'][$idkey] as $v) {
263 if (count($clean) >= $cap) {
267 if (!empty($clean)) {
268 $idsPart .= ($idkey ===
'ids' ?
" Visible row ids: " :
" Checked/selected row ids (act on these when the user says the selected ones): ").implode(
',', $clean).
".";
272 if (!empty($parts) || $idsPart !==
'') {
273 $aiPageContextLine =
"The user is currently viewing the \"".preg_replace(
'/[^a-z0-9_]/',
'', $ctxElement).
"\" list".(!empty($parts) ?
" filtered by: ".implode(
', ', $parts) :
"").
".".$idsPart;
274 $aiPageContextLine .=
" To act on \"this list\"/\"these records\"/\"the selected ones\", use these ids or translate the filters into the matching arguments of the list/report tools.";
275 dol_syslog(
"AI Pro: list context accepted: ".$ctxElement.
" (".count($parts).
" filters".($idsPart !==
'' ?
", ids" :
"").
")");
281 if (empty($query) &&
GETPOST(
'query',
'alphanohtml') ==
'/tools') {
287 echo json_encode([
"status" =>
"ok"]);
297 $attachments = array();
298 if (strpos($query,
'__FILE_ATTACHMENT__') !==
false) {
299 $query = preg_replace_callback(
300 '/__FILE_ATTACHMENT__\[([^\]]*)\]::([A-Za-z0-9+\/=\r\n]+)/',
305 static function (array $m) use (&$attachments) {
306 $attachments[] = array(
307 'mime' => ($m[1] !==
'' ? $m[1] :
'application/octet-stream'),
308 'data' => preg_replace(
'/\s+/',
'', $m[2])
310 return '[attached document]';
314 $query = trim((
string) $query);
315 if ($query ===
'' || $query ===
'[attached document]') {
316 $query =
'Analyze the attached document and describe its content.';
323 $attachmenterror =
'';
326 echo json_encode(array(
327 "tool" =>
"respond_to_user",
328 "arguments" => array(
"message" => $attachmenterror)
334 $langs->loadLangs(array(
"main",
"bills",
"orders",
"propal",
"supplier_invoice",
"supplier_order",
"projects",
"other"));
377 $dynamicStopWords = [];
378 foreach ($blockKeys as $key) {
379 $word = $langs->transnoentities($key);
387 $commonCommands = [
'show',
'find',
'search',
'list',
'get',
'voir',
'chercher',
'affiche',
'lista',
'buscar'];
388 $dynamicStopWords = array_unique(array_merge($dynamicStopWords, $commonCommands));
391 $cleanQuery = preg_replace(
'/[^\p{L}\p{N}\s\-]/u',
'', $query);
392 $words = preg_split(
'/\s+/', $cleanQuery, -1, PREG_SPLIT_NO_EMPTY);
393 $count = count($words);
394 $candidates = array();
398 $isValidPhrase =
function (
string $phrase) use ($dynamicStopWords):
int {
399 $phrase = trim($phrase);
404 if (mb_strlen($phrase) < 3) {
415 $parts = explode(
' ', $phrase);
418 if (in_array($firstWord, $dynamicStopWords)) {
419 return count($parts) > 1 ? 2 : 0;
426 $strictCandidates = array();
427 for ($i = 0; $i < $count; $i++) {
428 $phrases = array($words[$i]);
429 if ($i + 1 < $count) {
430 $phrases[] = $words[$i] .
' ' . $words[$i + 1];
432 if ($i + 2 < $count) {
433 $phrases[] = $words[$i] .
' ' . $words[$i + 1] .
' ' . $words[$i + 2];
435 foreach ($phrases as $phrase) {
436 $valid = $isValidPhrase($phrase);
438 $candidates[] = $phrase;
440 $strictCandidates[$phrase] =
true;
446 usort($candidates,
function (
string $a,
string $b): int {
447 return mb_strlen($b) - mb_strlen($a);
450 dol_syslog(
"parse_intent.php We have candidates into text that may be a thirdparty. List is ".implode(
',', $candidates), LOG_DEBUG);
452 if (!empty($candidates)) {
453 foreach ($candidates as $phrase) {
454 if (isset($strictCandidates[$phrase])) {
458 $sql =
"SELECT rowid, nom FROM " . MAIN_DB_PREFIX .
"societe WHERE nom = '" . $db->escape($phrase) .
"' OR nom LIKE '" . $db->escape($phrase) .
" %' ORDER BY LENGTH(nom) LIMIT 1";
461 $sql =
"SELECT rowid, nom FROM " . MAIN_DB_PREFIX .
"societe WHERE nom LIKE '" . $db->escape($phrase) .
"%' LIMIT 1";
464 $res = $db->query($sql);
466 if ($res && $obj = $db->fetch_object($res)) {
468 $query = preg_replace(
'/\b' . preg_quote($phrase,
'/') .
'\b/iu',
"socid:" . $obj->rowid, $query);
476 $usageContext = array();
480 if ($doRedact && class_exists(
'PrivacyGuard')) {
482 $query = $guard->mask($query);
486 if (strpos($query,
'[[') !==
false) {
487 $query .=
"\n\n(Note: tokens like [[REF_1]] or [[ADDR_2]] above are privacy-masked real values. Use them verbatim as tool argument values - they are replaced with the real data before execution. Do not refuse the task because of them and do not ask the user to re-provide masked details.)";
494 $allToolsSchema = [];
496 if ($serviceKey && $serviceKey !==
'-1') {
497 $providerUsed = $serviceKey;
498 $mcp =
new McpHandler($db, $user, $conf, McpHandler::CTX_ASSISTANT);
506 $allToolsSchema = $mcp->getToolsSchema();
507 $llmToolsBase = $mcp->getToolsSchemaForLLM();
510 if ($query ==
'/tools') {
511 $s =
'----- loadedTools (scan of family tools, not tools)'.
"\n";
512 $s .=
'<pre>' . json_encode($mcp->loadedTools, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) .
'</pre>';
515 $s .=
'----- toolsByName'.
"\n";
516 $s .=
'<pre>' . json_encode($mcp->toolsByName, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) .
'</pre>';
519 $s .=
'----- allToolsSchema (non system + system)'.
"\n";
520 $s .=
'<pre>' . json_encode($allToolsSchema, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) .
'</pre>';
523 "tool" =>
"respond_to_user",
533 echo json_encode($finalResponse);
550 if (!empty($detectedCategories)) {
551 dol_syslog(
"AI Pro: Non-Latin query classified into ".implode(
',', $detectedCategories).
". Filtering schema.");
554 dol_syslog(
"AI Pro: Non-Latin language detected, no category match. Sending full (cleaned) schema.");
555 $toolsSchema = $llmToolsBase;
564 dol_syslog(
"AI Pro: Latin script. Detected: " . json_encode($detectedCategories) .
". Filtered to " . count($toolsSchema) .
" tools.");
577 $isLargeSchema = count($toolsSchema) > 100;
581 $basePrompt =
getDolGlobalString(
'AI_INTENT_PROMPT') ?:
"You are a professional Dolibarr assistant.";
583 $systemRules =
"\n\nRules: Respond ONLY JSON and ensure any json string does not contains special chars and are correctly json encoded. Format: {\"tool\":..., \"arguments\":{...}}. ";
584 $systemRules .=
"ALWAYS write user-facing text (the message/question/answer argument values) in the SAME LANGUAGE as the user's message. English context notes, tool names, or schemas never change the response language. ";
585 $systemRules .=
"When a tool matches the user request, CALL it - never explain limitations instead of acting, and never claim a capability is missing while a matching tool is listed. Only when genuinely NO tool can fulfill the request, use respond_to_user to say the feature is not available. ";
589 $toolsForLLM = array();
592 $systemPrompt = $basePrompt .
"\n\n";
593 $systemPrompt .=
"Tools:\n" . json_encode($toolsForLLM, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
600 $systemRules .=
" Privacy masking is active: values like [[REF_1]], [[ADDR_2]], [[EMAIL_3]], [[PHONE_4]], [[ZIP_5]] are masked real data. Treat them as valid values: when a tool argument needs such a datum, pass the placeholder exactly as written — it is replaced by the real value before execution. Never refuse a task because values look masked, and never invent replacements for them.";
605 $systemPrompt .= $systemRules .
" Current date: " .
date(
'Y-m-d') .
" (" .
date(
'l') .
").";
606 if (!empty($aiPageContextLine)) {
613 if (!empty($doRedact) && !empty($guard)) {
614 $aiPageContextLine = $guard->mask($guard->maskNames($aiPageContextLine, $ctxNamesToMask));
616 $systemPrompt .=
"\n\nPage context: ".$aiPageContextLine;
618 $systemPrompt .=
" Resolve relative periods yourself from the current date — today, yesterday, this week, this month, last month, this quarter, this year — into explicit YYYY-MM-DD values for date parameters (e.g. this month = first day of the current month to the current date). Never ask the user for dates you can compute.";
624 if (preg_match(
'/^crypt:/', $apiKey)) {
628 $defUrl = $servicesList[$serviceKey][
'url'] ??
'';
641 $rawDefault = $servicesList[$serviceKey][
'textgeneration'] ??
null;
642 if (is_array($rawDefault)) {
643 $defModel = $rawDefault[
'default'] ??
'gpt-4o-mini';
645 $defModel = $rawDefault ?:
'gpt-4o-mini';
647 $prefix =
'AI_API_' . strtoupper($serviceKey);
652 if (is_array($model)) {
653 $model = $model[
'default'] ?? $defModel;
655 if (!is_string($model) || $model ===
'') {
656 $model = (string) $defModel;
660 if (!empty($data[
'model']) && is_string($data[
'model'])) {
661 $reqModel = preg_replace(
'/[^a-zA-Z0-9._:\/-]/',
'', $data[
'model']);
662 if ($reqModel !==
'' && strlen($reqModel) <= 100) {
666 $adapterType = $servicesList[$serviceKey][
'adapter_type'] ??
'openai';
672 if (!empty($apiKey)) {
675 dol_syslog(
"parse_intent.php Call AI API", LOG_DEBUG);
681 if (!empty($aiPageContextLine)) {
686 $aiPageContextShort = strtok($aiPageContextLine,
".").
".";
687 $query .=
"\n\n(Context: ".$aiPageContextShort.
")";
696 if (!empty($data[
'history']) && is_array($data[
'history'])) {
698 foreach (array_slice($data[
'history'], 0, 12) as $turn) {
699 if (!is_array($turn) || empty($turn[
'text']) || !is_string($turn[
'text'])) {
702 $htext = preg_replace(
'/__FILE_ATTACHMENT__\[[^\]]*\]::[^\s]+/',
'[attachment removed]', $turn[
'text']);
703 $htext = trim((
string) $htext);
711 $htext = $guard->mask($htext);
714 if ($histBudget < 0) {
717 $history[] = array(
'role' => ((($turn[
'role'] ??
'') ===
'assistant') ?
'assistant' :
'user'),
'text' => $htext);
726 if (!empty($history)) {
727 $systemPrompt .=
"\n\nCONVERSATION CONTEXT: the earlier turns are context only, to resolve references like \"this one\" or \"the second\". The ONLY request to act on is the LAST user message. Never resume, redo or complete an earlier request, even one that looks unanswered or unfinished.";
729 $rawResponse = $adapter->generate($systemPrompt, $query,
'text', $attachments, $history);
732 dol_syslog(
'rawResponse='.$rawResponse, LOG_DEBUG);
737 $rawRequestLog = $adapter->lastRequest;
738 $rawResponseLog = $adapter->lastResponse;
742 if (!empty($adapter->lastUsage)) {
743 $usageContext = array(
744 'tokens_input' => (
int) ($adapter->lastUsage[
'input'] ?? 0),
745 'tokens_output' => (
int) ($adapter->lastUsage[
'output'] ?? 0),
746 'model' => (
string) ($adapter->lastUsage[
'model'] ?? $model),
751 if (is_string($rawResponse) && strpos($rawResponse,
'Error:') === 0) {
752 $errorDetails = $rawResponse;
753 } elseif ($rawResponse) {
755 $clean = preg_replace(
'/```json\s*|\s*```/s',
'', $rawResponse);
756 $clean = trim($clean);
759 if (preg_match(
'/^\{.*\}$/s', $clean, $matches)) {
760 $clean = $matches[0];
761 } elseif (preg_match(
'/\{.*\}/s', $clean, $matches) && strpos($matches[0],
'"tool"') !==
false) {
766 $clean = $matches[0];
771 $clean = $guard->unmaskAiResponse($clean);
775 $clean = preg_replace(
'/[\r\n]/',
' ', $clean);
778 if (strpos($clean,
'{') === 0) {
780 $intentJSON = json_decode($clean,
true);
785 if (is_array($intentJSON) && ($intentJSON[
'tool'] ??
'') ===
'respond_to_user' && empty($intentJSON[
'arguments'][
'message'])) {
789 $intentJSON[
'arguments'][
'message'] =
'I could not produce an answer for this request. Please rephrase or add details.';
791 if (is_array($intentJSON) && ($intentJSON[
'tool'] ??
'') ===
'ask_for_clarification' && empty($intentJSON[
'arguments'][
'question'])) {
792 $a = isset($intentJSON[
'arguments']) && is_array($intentJSON[
'arguments']) ? $intentJSON[
'arguments'] : array();
794 if (!empty($a[
'reason'])) {
795 $qparts[] = (string) $a[
'reason'];
797 if (!empty($a[
'missing_argument']) && stripos(implode(
' ', $qparts), (
string) $a[
'missing_argument']) ===
false) {
798 $qparts[] =
"Missing: ".(string) $a[
'missing_argument'];
800 if (empty($qparts) && !empty($a[
'message'])) {
801 $qparts[] = (string) $a[
'message'];
803 $intentJSON[
'arguments'][
'question'] = !empty($qparts) ? implode(
' ', $qparts) :
'Could you provide the missing information?';
807 "tool" =>
"respond_to_user",
815 if ($guard && isset($intentJSON[
'arguments'])) {
820 if ($intentJSON && isset($intentJSON[
'tool'])) {
821 $validToolNames = array_column($allToolsSchema,
'name');
822 if (!in_array($intentJSON[
'tool'], $validToolNames)) {
824 $reqTokens = explode(
'_',
dol_strtolower((
string) $intentJSON[
'tool']));
825 $candidates = array();
826 foreach ($validToolNames as $realName) {
827 if (strpos($realName, $reqTokens[0].
'_') !== 0) {
830 if (!array_diff($reqTokens, explode(
'_', $realName))) {
831 $candidates[] = $realName;
834 if (count($candidates) === 1) {
835 dol_syslog(
"AI Validation: tool '".$intentJSON[
'tool'].
"' recovered to '".$candidates[0].
"'.", LOG_INFO);
836 $intentJSON[
'tool'] = $candidates[0];
839 if (!in_array($intentJSON[
'tool'], $validToolNames)) {
840 dol_syslog(
"AI Validation: Tool '" . $intentJSON[
'tool'] .
"' not found in filtered schema. Send error message via respond_to_user.", LOG_WARNING);
844 "tool" =>
"respond_to_user",
846 "message" =>
"I apologize, but the requested functionality is not currently available in the system."
854 if ($intentJSON && $confidence === 0.0) {
855 $mappedToolsSchema = array_column($toolsSchema,
null,
'name');
858 dol_syslog(
"parse_intent.php AI Intent: " . json_encode([
'query' => $query,
'intent' => $intentJSON,
'confidence' => $confidence]), LOG_DEBUG);
866 if (!$intentJSON || !isset($intentJSON[
'tool'])) {
867 $message = $langs->transnoentitiesnoconv(
'AICannotUnderstandRequest');
872 if (strpos($errorDetails,
'Error:') === 0) {
873 $reason = trim(preg_replace(
'/^Error:\s*(API|cURL #\d+)?\s*/',
'', $errorDetails));
874 $reason =
dol_trunc(preg_replace(
'/\s+/',
' ', $reason), 200);
875 if (preg_match(
'/high demand|overloaded|rate limit|quota|too many requests|try again|timed? ?out|HTTP (429|502|503|504)/i', $errorDetails)) {
876 $message = $langs->transnoentitiesnoconv(
'AIProviderBusy', $reason);
878 $message = $langs->transnoentitiesnoconv(
'AIProviderError', $reason);
882 "tool" =>
"respond_to_user",
884 "message" => $message
887 if (strpos($errorDetails,
'Error:') === 0) {
890 $finalResponse[
'status'] =
'error';
894 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, microtime(
true) - $startTime, 0.0, $langs->transnoentitiesnoconv(
'Error'), $errorDetails, $rawRequestLog, $rawResponseLog, $usageContext);
897 echo json_encode($finalResponse);
902 $needsConfirmation =
false;
903 $toolName = $intentJSON[
'tool'] ??
'';
909 if (in_array($toolName, array(
'respond_to_user',
'reject_general_question'),
true) && isset($intentJSON[
'arguments']) && is_array($intentJSON[
'arguments'])) {
910 if (empty($intentJSON[
'arguments'][
'message'])) {
911 foreach (array(
'response',
'text',
'answer',
'content',
'reply',
'output') as $altkey) {
912 if (!empty($intentJSON[
'arguments'][$altkey])) {
913 $intentJSON[
'arguments'][
'message'] = $intentJSON[
'arguments'][$altkey];
925 if ($toolName ===
'ask_for_confirmation' && empty($intentJSON[
'arguments'][
'original_intent'])) {
927 foreach (array(
'message',
'action',
'question',
'text') as $altkey) {
928 if (!empty($intentJSON[
'arguments'][$altkey]) && is_string($intentJSON[
'arguments'][$altkey])) {
929 $question = $intentJSON[
'arguments'][$altkey];
933 dol_syslog(
"parse_intent.php model emitted ask_for_confirmation without an action, downgraded to respond_to_user", LOG_WARNING);
934 $toolName =
'respond_to_user';
935 $intentJSON = array(
'tool' =>
'respond_to_user',
'arguments' => array(
'message' => ($question !==
'' ? $question : $langs->transnoentitiesnoconv(
'AICannotUnderstandRequest'))));
955 if (
getDolGlobalInt(
'AI_CHAT_TWO_STEP_WRITE') && is_array($intentJSON) && $toolName !==
'' && isset($adapter, $mcp, $history, $systemPrompt, $toolsSchema) && is_object($adapter) && is_object($mcp) && is_array($history) && is_array($toolsSchema)) {
961 $isWriteTool =
function ($name, array $args) use ($mcp) {
962 if (preg_match(
'/(create|update|delete|add|remove|modify|edit|validate|pay|send)/i', $name)) {
965 $inst = $mcp->toolsByName[$name] ??
null;
966 if (is_object($inst) && method_exists($inst,
'writeConfirmationPreview')) {
967 return ((
string) $inst->writeConfirmationPreview($name, $args)) !==
McpTool::NO_WRITE;
971 $readArgs = (isset($intentJSON[
'arguments']) && is_array($intentJSON[
'arguments'])) ? $intentJSON[
'arguments'] : array();
972 $systemTools = array(
'respond_to_user',
'reject_general_question',
'ask_for_clarification',
'ask_for_confirmation',
'navigate_to_page');
973 if (
aiQueryAsksForWrite($query, $langs) && !in_array($toolName, $systemTools,
true) && !$isWriteTool($toolName, $readArgs)) {
974 $readResult = $mcp->executeTool($toolName, $readArgs);
975 if (is_array($readResult) && !isset($readResult[
'error']) && (($readResult[
'resultType'] ??
'') !==
'input_required')) {
977 ai_log_request($db, $user, $query, $intentJSON, $providerUsed, microtime(
true) - $startTime, $confidence,
'Step1', $errorDetails, $rawRequestLog, $rawResponseLog, $usageContext);
981 $snippet = (string) json_encode($readResult, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
983 $snippet =
dol_substr($snippet, 0, 1500).
' ...';
985 $snippet =
'['.$toolName.
' result] '.$snippet;
987 $snippet = $guard->mask($snippet);
989 $history2 = $history;
990 $history2[] = array(
'role' =>
'assistant',
'text' => $snippet);
991 $systemPrompt2 = $systemPrompt.
"\n\nSTEP 2: the read tool ".$toolName.
" was already executed for you; its result is the last assistant turn. Now perform the WRITE the user asked for in the last user message, with the ids found in that result. If the result does not identify one object with certainty, or the write cannot be done, answer with respond_to_user and say why. Do not call a read tool again.";
993 $rawResponse2 = $adapter->generate($systemPrompt2, $query,
'text', $attachments, $history2);
994 $rawRequestLog = $adapter->lastRequest;
995 $rawResponseLog = $adapter->lastResponse;
996 if (!empty($adapter->lastUsage)) {
997 $usageContext = array(
998 'tokens_input' => (
int) ($adapter->lastUsage[
'input'] ?? 0),
999 'tokens_output' => (
int) ($adapter->lastUsage[
'output'] ?? 0),
1000 'model' => (
string) ($adapter->lastUsage[
'model'] ?? (isset($model) ? $model :
'')),
1005 if (is_string($rawResponse2) && strpos($rawResponse2,
'Error:') !== 0) {
1006 $clean2 = trim((
string) preg_replace(
'/```json\s*|\s*```/s',
'', $rawResponse2));
1008 if (preg_match(
'/\{.*\}/s', $clean2, $m2)) {
1012 $clean2 = $guard->unmaskAiResponse($clean2);
1014 $intent2 = json_decode((
string) preg_replace(
'/[\r\n]/',
' ', $clean2),
true);
1015 if (is_array($intent2) && $guard && isset($intent2[
'arguments'])) {
1019 $args2 = (is_array($intent2) && isset($intent2[
'arguments']) && is_array($intent2[
'arguments'])) ? $intent2[
'arguments'] : array();
1020 if (is_array($intent2) && !empty($intent2[
'tool']) && is_string($intent2[
'tool']) && in_array($intent2[
'tool'], array_column($allToolsSchema,
'name'),
true) && $isWriteTool($intent2[
'tool'], $args2)) {
1021 dol_syslog(
"parse_intent.php two-step: read ".$toolName.
" then write ".$intent2[
'tool'], LOG_INFO);
1022 $intentJSON = $intent2;
1023 $toolName = $intent2[
'tool'];
1024 $confidence =
calculateConfidence($intentJSON, array_column($toolsSchema,
null,
'name'), (
string) $rawResponse2);
1028 dol_syslog(
"parse_intent.php two-step: second answer is not a write, the turn ends on the read ".$toolName, LOG_INFO);
1034 if ($askForConfirmation > 0) {
1035 $isModifyOperation = preg_match(
'/(create|update|delete|add|remove|modify|edit)/i', $toolName);
1037 if ($askForConfirmation == 1 && $isModifyOperation) {
1038 $needsConfirmation =
true;
1039 } elseif ($askForConfirmation == 2) {
1040 $needsConfirmation =
true;
1045 if ($needsConfirmation) {
1046 $allToolsMap = !empty($allToolsSchema)
1047 ? array_column($allToolsSchema,
null,
'name')
1049 $toolDescription = $allToolsMap[$toolName][
'description'] ??
'No description available';
1050 $arguments = $intentJSON[
'arguments'] ?? [];
1058 $toolInstance = $mcp->toolsByName[$toolName] ??
null;
1059 if (is_object($toolInstance) && method_exists($toolInstance,
'writeConfirmationPreview')) {
1060 $preview = (string) $toolInstance->writeConfirmationPreview($toolName, $arguments);
1065 if (!empty($twoStepLabel)) {
1067 $action .=
' - '.$twoStepLabel;
1070 $confirmationResponse = [
1071 "tool" =>
"ask_for_confirmation",
1073 "action" => $action,
1074 "details" => $details,
1075 "original_intent" => $intentJSON
1080 ai_log_request($db, $user, $query, $confirmationResponse, $providerUsed, microtime(
true) - $startTime, $confidence, $langs->transnoentitiesnoconv(
"Confirm"), $errorDetails, $rawRequestLog, $rawResponseLog, $usageContext);
1083 echo json_encode($confirmationResponse);
1088 if ($confidence < LOW_CONFIDENCE) {
1090 "tool" =>
"respond_to_user",
1092 "message" =>
"I'm not confident about understanding your request. Please try rephrasing it with more specific details."
1097 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, microtime(
true) - $startTime, $confidence,
'low_confidence', $errorDetails, $rawRequestLog, $rawResponseLog, $usageContext);
1100 echo json_encode($finalResponse);
1105 if ($confidence < MEDIUM_CONFIDENCE && isset($intentJSON[
'arguments'])) {
1106 $intentJSON[
'arguments'][
'_confidence_note'] =
"I'm moderately confident about this interpretation. Please verify the results.";
1110 $finalResponse = $intentJSON;
1111 $execTime = microtime(
true) - $startTime;
1112 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, $execTime, $confidence, $langs->transnoentitiesnoconv(
"Success"), $errorDetails, $rawRequestLog, $rawResponseLog, $usageContext);
1115 echo json_encode($finalResponse);
1116}
catch (Throwable $e) {
1117 $friendlyResponse = [
1118 "tool" =>
"respond_to_user",
1120 "message" =>
"I'm experiencing technical difficulties. Please try again later or contact your administrator."
1124 $realErrorForLog =
"PHP Exception: " . $e->getMessage() .
" in " . $e->getFile() .
" on line " . $e->getLine();
1126 dol_syslog(
"AI Critical Error: " . $realErrorForLog, LOG_ERR);
1128 if (function_exists(
'ai_log_request') && is_object($db)) {
1132 $query ??
'unknown',
1135 microtime(
true) - $startTime,
1139 $rawRequestLog ??
'',
1140 $rawResponseLog ??
'',
1141 $usageContext ?? array()
1146 echo json_encode($friendlyResponse);
1162 foreach (array(
'Create',
'Add',
'Modify',
'Update',
'Delete',
'Remove',
'Validate',
'Send') as $key) {
1168 $verbs = array_merge($verbs, array(
1169 'create',
'add',
'modify',
'update',
'delete',
'remove',
'validate',
'send',
'change',
'set',
'edit',
'rename',
'close',
'cancel',
1170 'crée',
'cree',
'créer',
'creer',
'ajoute',
'ajouter',
'modifie',
'modifier',
'mets',
'met',
'mettre',
'change',
'changer',
'passe',
'passer',
1171 'supprime',
'supprimer',
'efface',
'effacer',
'retire',
'retirer',
'valide',
'valider',
'envoie',
'envoyer',
'enregistre',
'enregistrer',
'renomme',
'renommer',
'clôture',
'cloture',
'annule',
'annuler'
1174 foreach (array_unique(array_filter($verbs)) as $verb) {
1175 $escaped[] = preg_quote($verb,
'/');
1177 $pattern =
'/(^|[^\p{L}])('.implode(
'|', $escaped).
')([^\p{L}]|$)/iu';
1178 return (
bool) preg_match($pattern,
dol_strtolower((
string) $query));
1192 foreach ($writeArgs as $k => $v) {
1193 if ((is_int($v) || (is_string($v) && ctype_digit($v))) && preg_match(
'/(^id$|_id$|^socid$|^fk_)/', (
string) $k)) {
1200 $rows = $readResult;
1201 if (isset($rows[
'data']) && is_array($rows[
'data'])) {
1202 $rows = $rows[
'data'];
1204 if (isset($rows[
'id']) || isset($rows[
'rowid'])) {
1205 $rows = array($rows);
1207 foreach ($rows as $row) {
1208 if (!is_array($row)) {
1211 $rowid = (int) ($row[
'id'] ?? $row[
'rowid'] ?? 0);
1212 if ($rowid > 0 && in_array($rowid, $ids,
true)) {
1213 foreach (array(
'name',
'nom',
'ref',
'label',
'subject',
'title',
'login') as $key) {
1214 if (!empty($row[$key]) && is_string($row[$key])) {
1243 if ($guard ===
null) {
1247 if (is_array($data)) {
1253 function ($item) use ($guard) {
1260 if (is_string($data)) {
1261 return $guard->unmask($data);
1282 if (preg_match(
'/\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/u', $text)) {
1287 if (preg_match(
'/\p{Cyrillic}/u', $text)) {
1292 if (preg_match(
'/\p{Greek}/u', $text)) {
1297 if (preg_match(
'/\p{Arabic}/u', $text)) {
1302 if (preg_match(
'/\p{Hebrew}/u', $text)) {
1307 if (preg_match(
'/\p{Thai}/u', $text)) {
1331 if (class_exists(
'Normalizer')) {
1332 $decomposed = Normalizer::normalize($w, Normalizer::FORM_D);
1333 if ($decomposed !==
false) {
1334 $w = (string) preg_replace(
'/\p{Mn}+/u',
'', $decomposed);
1341 } elseif ($len == 5) {
1343 } elseif ($len < 3) {
1377 $langs->loadLangs(array(
"main",
"bills",
"orders",
"propal",
"companies",
"products",
"projects",
"dict",
"sendings",
"receptions",
"ticket",
"members",
"agenda",
"interventions"));
1390 'keys' => [
'Bill',
'Invoice',
'Payment',
'Cheque',
'VAT',
'BillStatusUnpaid',
'BillStatusPaid',
'BillStatusDraft',
'Member',
'Subscription'],
1391 'synonyms' => [
'paid',
'unpaid',
'pay',
'money',
'cost',
'amount',
'overdue',
'member',
'membership',
'subscription',
'cotisation',
'adhesion']
1400 'keys' => [
'Order',
'Proposal',
'Quote',
'SupplierOrder',
'OrderStatusDraft',
'Reception',
'Shipment',
'Delivery',
'Intervention'],
1401 'synonyms' => [
'sale',
'buy',
'purchase',
'contract',
'shipping',
'quote',
'reception',
'shipment',
'delivery',
'receive',
'intervention']
1409 'keys' => [
'ThirdParty',
'Customer',
'Supplier',
'Contact',
'Company',
'Ticket',
'Member',
'Subscription',
'Event',
'Agenda'],
1410 'synonyms' => [
'client',
'partner',
'address',
'phone',
'vendor',
'ticket',
'support',
'incident',
'member',
'adherent',
'membership',
'meeting',
'appointment',
'rdv',
'category',
'categorie',
'tag']
1413 'keys' => [
'Product',
'Service',
'Stock',
'Warehouse'],
1414 'synonyms' => [
'item',
'inventory',
'sku',
'location',
'qty',
'warehouse',
'category',
'categorie',
'tag']
1417 'keys' => [
'Project',
'Task',
'Ticket',
'Event',
'Agenda',
'Intervention'],
1418 'synonyms' => [
'task',
'team',
'deadline',
'planning',
'milestone',
'ticket',
'event',
'meeting',
'appointment',
'rdv',
'intervention']
1421 'keys' => [
'Report',
'Statistics',
'Turnover',
'Revenue',
'Income'],
1422 'synonyms' => [
'report',
'statistics',
'analytics',
'chart',
'total',
'turnover']
1427 static $langsEnUs =
null;
1428 if ($langsEnUs ===
null) {
1431 $langsEnUs->setDefaultLang(
'en_US');
1432 $langsEnUs->loadLangs(array(
'main',
'bills',
'companies',
'products',
'projects',
'orders',
'propal',
'stocks',
'other',
'ticket',
'members',
'agenda',
'interventions'));
1435 $detectedCategories = [];
1436 foreach ($intentMap as $category => $data) {
1438 foreach ($data[
'keys'] as $key) {
1443 foreach (array($langs->transnoentities($key), $langsEnUs->transnoentities($key)) as $trans) {
1444 if ($trans ===
'' || ($trans === $key && preg_match(
'/[A-Z]/',
dol_substr($key, 1, 200)))) {
1453 $keywords[] = $trans;
1456 $keywords = array_unique($keywords);
1461 foreach ($data[
'synonyms'] as $syn) {
1464 $keywords = array_unique($keywords);
1467 foreach ($keywords as $word) {
1472 if (preg_match(
'/\b' . preg_quote($word,
'/') .
's?\b/u', $searchQuery)) {
1473 $detectedCategories[] = $category;
1480 $needleStem = $stem;
1481 if ($needleStem !==
'' && mb_strpos($normalizedQuery, $needleStem) !==
false) {
1482 $detectedCategories[] = $category;
1488 return $detectedCategories;
1513 if (empty($activeCategories) || (count($activeCategories) === 1 && $activeCategories[0] ===
'global')) {
1517 $targetCategories = array_merge([
'global'], $activeCategories);
1520 foreach ($allTools as $tool) {
1521 $toolCats = $tool[
'categories'] ?? [
'global'];
1522 if (count(array_intersect($toolCats, $targetCategories)) > 0) {
1523 if (count($activeCategories) > 0 && $toolCats === [
'global']) {
1526 $filtered[] = $tool;
1530 if (count($filtered) < 3) {
1531 dol_syslog(
"AI Filter: Too few tools (" . count($filtered) .
"). Reverting to full schema.", LOG_WARNING);
1550 foreach ($tools as $tool) {
1559 $desc = $tool[
'description'];
1562 $toolParams = $tool[
'parameters'] ?? $tool[
'inputSchema'] ?? [];
1564 if ($isLargeSchema && isset($toolParams[
'properties']) && is_array($toolParams[
'properties'])) {
1565 $requiredList = $toolParams[
'required'] ?? [];
1566 $newProperties = [];
1568 foreach ($toolParams[
'properties'] as $propKey => $propData) {
1569 $isRequired = in_array($propKey, $requiredList);
1579 if (!$isRequired && isset($propData[
'default'])) {
1584 if (isset($propData[
'type']) && $propData[
'type'] ===
'string') {
1585 unset($propData[
'type']);
1591 if (isset($propData[
'description'])) {
1592 unset($propData[
'description']);
1602 if (isset($propData[
'type']) && $propData[
'type'] ===
'object' && isset($propData[
'properties'])) {
1603 unset($propData[
'properties']);
1604 unset($propData[
'required']);
1605 $propData[
'description'] =
"JSON object";
1608 $newProperties[$propKey] = $propData;
1611 $toolParams[
'properties'] = $newProperties;
1614 unset($toolParams[
'type']);
1615 unset($toolParams[
'additionalProperties']);
1619 'name' => $tool[
'name'],
1620 'description' => $desc,
1621 'parameters' => $toolParams
1647 $factors[
'parse_success'] = 0.4;
1651 $hasRequiredFields = !empty($intentJSON[
'tool']) && !empty($intentJSON[
'arguments']);
1652 $factors[
'completeness'] = $hasRequiredFields ? 0.3 : 0.0;
1655 $isValidSchema =
false;
1658 if (isset($intentJSON[
'tool']) && isset($toolsSchema[$intentJSON[
'tool']])) {
1660 $schema = $toolsSchema[$intentJSON[
'tool']][
'parameters']
1661 ?? $toolsSchema[$intentJSON[
'tool']][
'inputSchema']
1665 $providedParams = array_keys($intentJSON[
'arguments'] ?? []);
1668 $properties = $schema[
'properties'] ?? [];
1669 $requiredList = $schema[
'required'] ?? [];
1671 $missingParams = [];
1674 foreach ($properties as $paramKey => $paramDetails) {
1676 if (in_array($paramKey, $requiredList)) {
1678 if (!in_array($paramKey, $providedParams)) {
1679 $missingParams[] = $paramKey;
1685 $isValidSchema = empty($missingParams);
1688 $factors[
'schema_validation'] = $isValidSchema ? 0.2 : 0.0;
1691 $qualityScore = 0.0;
1692 if (is_string($rawResponse)) {
1694 if (!preg_match(
'/error|fail|unable|cannot|sorry/i', $rawResponse)) {
1695 $qualityScore += 0.05;
1699 if (isset($intentJSON[
'tool']) && isset($toolsSchema[$intentJSON[
'tool']])) {
1700 $qualityScore += 0.05;
1703 $factors[
'response_quality'] = $qualityScore;
1706 $confidence = array_sum($factors);
1709 return max(0.0, min(1.0, $confidence));
1720 $formattedArgs = [];
1721 foreach ($arguments as $key => $value) {
1722 if (is_array($value)) {
1723 $formattedArgs[] =
"- {$key}: " . (empty($value) ?
"(empty)" : json_encode($value, JSON_PRETTY_PRINT));
1725 $formattedArgs[] =
"- {$key}: {$value}";
1728 return implode(
"\n", $formattedArgs);
1739 if (preg_match(
'/^(create|update|delete|list|show|find|search|get|view|validate|send)/i', $toolName, $matches)) {
1740 return strtolower($matches[1]);
1742 return 'perform this action';
getListOfAIServices()
Get list of available ai services.
ai_validate_attachments(array $attachments, &$error)
Validate chat attachments before they reach any LLM provider.
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.
aiCheckCsrfToken($context='')
Check the anti-CSRF token of a request sent to one of the AI Assistant endpoints.
Class to handle MCP (Model Context Protocol).
Class to manage privacy data masking and unmasking.
Class to manage third parties objects (customers, suppliers, prospects...)
Class to manage translations.
dol_strtolower($string, $encoding="UTF-8")
Convert a string to lower.
dol_string_nohtmltag($stringtoclean, $removelinefeed=1, $pagecodeto='UTF-8', $strip_tags=0, $removedoublespaces=1)
Clean a string from all HTML tags and entities.
dol_strlen($string, $stringencoding='UTF-8')
Make a strlen call.
getDolGlobalInt($key, $default=0)
Return a Dolibarr global constant int value.
dol_string_unaccent($str)
Clean a string from all accent characters to be used as ref, login or by dol_sanitizeFileName.
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_substr($string, $start, $length=null, $stringencoding='', $trunconbytes=0)
Make a substring.
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '...' if string larger than length.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
isModEnabled($module)
Is Dolibarr module enabled.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
extractActionFromTool($toolName)
Extract action from tool name.
formatArgumentsForDisplay($arguments)
Format arguments for display in confirmation.
isComplexScript(string $text)
Detects if the query uses Non-Latin Scripts.
calculateConfidence($intentJSON, $toolsSchema, $rawResponse)
Calculate confidence score based on multiple factors.
classifyIntentUniversal(string $query, Translate $langs)
Detect intent categories from a user query.
catch(Throwable $e) aiQueryAsksForWrite($query, $langs)
Does the user's message ask for a write (create / update / delete / ...)? Translated keys of the curr...
aiNormalizeForMatch($word, $asStem=false)
Normalize a keyword or query for inflection-tolerant non-Latin matching.
aiLabelOfResolvedObject(array $readResult, array $writeArgs)
Name of the object a write targets, taken from the read result that produced its id (so the confirmat...
filterToolsProfessional(array $allTools, array $activeCategories)
Filter a list of tools based on active intent categories.
recursiveUnmaskValues($data, ?PrivacyGuard $guard)
Recursively unmask values in a dataset.
cleanToolSchemaForLLM(array $tools, bool $isLargeSchema=false)
Compresses tool schema by removing optional parameters with defaults and stripping descriptions,...
print $langs trans('Date')." left Ref Label right Qty right Price right TotalHT right TotalTTC right right right right right right right right right centpercent right TotalHT right n right VAT right n right TotalVAT right n No sujeto a RE IRPF right TotalLT1 right n right TotalLT2 right n right TotalTTC right n takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency takeposcustomercurrency right TotalTTC takeposcustomercurrency right takeposcustomercurrency n right Paid right PaymentTypeShortLIQ right SELECT p pos_change as p datep as date
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.
dolDecrypt($chain, $key='', $patterntotest='')
Decode a string with a symmetric encryption.