28if (!defined(
'NOTOKENRENEWAL')) {
29 define(
'NOTOKENRENEWAL', 1);
31if (!defined(
'NOREQUIREMENU')) {
32 define(
'NOREQUIREMENU', 1);
34if (!defined(
'NOREQUIREHTML')) {
35 define(
'NOREQUIREHTML', 1);
37if (!defined(
'NOREQUIREAJAX')) {
38 define(
'NOREQUIREAJAX', 1);
40if (!defined(
'NOCSRFCHECK')) {
41 define(
'NOCSRFCHECK', 1);
44require
'../../main.inc.php';
45require_once DOL_DOCUMENT_ROOT .
'/ai/class/mcp.class.php';
46require_once DOL_DOCUMENT_ROOT .
'/ai/lib/ai.lib.php';
47require_once DOL_DOCUMENT_ROOT .
'/ai/class/llmadapter.class.php';
48require_once DOL_DOCUMENT_ROOT .
'/core/lib/functions.lib.php';
49require_once DOL_DOCUMENT_ROOT .
'/ai/class/privacy_guard.class.php';
50require_once DOL_DOCUMENT_ROOT .
'/core/lib/security2.lib.php';
54 http_response_code(403);
62if (!$user->hasRight(
'ai',
'assistant',
'use')) {
73define(
'HIGH_CONFIDENCE', 0.8);
74define(
'MEDIUM_CONFIDENCE', 0.5);
75define(
'LOW_CONFIDENCE', 0.3);
78$startTime = microtime(
true);
81$providerUsed =
"offline";
90if (!$assistantEnabled) {
92 "tool" =>
"respond_to_user",
94 "message" =>
"AI assistant service is currently disabled. Please contact your administrator to enable it."
98 echo json_encode($response);
102set_time_limit($timeout + 5);
106 $raw_input = file_get_contents(
'php://input');
107 $data = json_decode($raw_input,
true);
108 $query = isset($data[
'query']) ? trim($data[
'query']) :
'';
112 echo json_encode([
"status" =>
"ok"]);
117 $langs->loadLangs(array(
"main",
"bills",
"orders",
"propal",
"supplier_invoice",
"supplier_order",
"projects",
"other"));
160 $dynamicStopWords = [];
161 foreach ($blockKeys as $key) {
162 $word = $langs->transnoentities($key);
164 $dynamicStopWords[] = mb_strtolower($word);
170 $commonCommands = [
'show',
'find',
'search',
'list',
'get',
'voir',
'chercher',
'affiche',
'lista',
'buscar'];
171 $dynamicStopWords = array_unique(array_merge($dynamicStopWords, $commonCommands));
174 $cleanQuery = preg_replace(
'/[^\p{L}\p{N}\s\-]/u',
'', $query);
175 $words = preg_split(
'/\s+/', $cleanQuery, -1, PREG_SPLIT_NO_EMPTY);
176 $count = count($words);
177 $candidates = array();
180 $isValidPhrase =
function (
string $phrase) use ($dynamicStopWords):
bool {
181 $phrase = trim($phrase);
186 if (mb_strlen($phrase) < 3) {
192 $parts = explode(
' ', $phrase);
193 $firstWord = mb_strtolower($parts[0]);
195 if (in_array($firstWord, $dynamicStopWords)) {
203 for ($i = 0; $i < $count; $i++) {
205 if ($isValidPhrase($words[$i])) {
206 $candidates[] = $words[$i];
209 if ($i + 1 < $count) {
210 $phrase = $words[$i] .
' ' . $words[$i + 1];
211 if ($isValidPhrase($phrase)) {
212 $candidates[] = $phrase;
216 if ($i + 2 < $count) {
217 $phrase = $words[$i] .
' ' . $words[$i + 1] .
' ' . $words[$i + 2];
218 if ($isValidPhrase($phrase)) {
219 $candidates[] = $phrase;
224 usort($candidates,
function (
string $a,
string $b): int {
225 return mb_strlen($b) - mb_strlen($a);
228 dol_syslog(
"parse_intent.php We have candidates into text that may be a thirdparty. List is ".implode(
',', $candidates), LOG_DEBUG);
230 if (!empty($candidates)) {
231 foreach ($candidates as $phrase) {
233 $sql =
"SELECT rowid, nom FROM " . MAIN_DB_PREFIX .
"societe WHERE nom LIKE '" .
$db->escape($phrase) .
"%' LIMIT 1";
235 $res =
$db->query($sql);
237 if ($res && $obj =
$db->fetch_object($res)) {
239 $query = preg_replace(
'/\b' . preg_quote($phrase,
'/') .
'\b/iu',
"socid:" . $obj->rowid, $query);
248 if ($doRedact && class_exists(
'PrivacyGuard')) {
250 $query = $guard->mask($query);
256 $allToolsSchema = [];
258 if ($serviceKey && $serviceKey !==
'-1') {
259 $providerUsed = $serviceKey;
260 $mcp =
new McpHandler($db, $user, $conf, McpHandler::CTX_ASSISTANT);
268 $allToolsSchema = $mcp->getToolsSchema();
269 $llmToolsBase = $mcp->getToolsSchemaForLLM();
278 dol_syslog(
"AI Pro: Non-Latin language detected. Sending full (cleaned) schema.");
279 $toolsSchema = $llmToolsBase;
287 dol_syslog(
"AI Pro: Latin script. Detected: " . json_encode($detectedCategories) .
". Filtered to " . count($toolsSchema) .
" tools.");
300 $isLargeSchema = count($toolsSchema) > 100;
304 $basePrompt =
getDolGlobalString(
'AI_INTENT_PROMPT') ?:
"You are a professional Dolibarr assistant.";
306 $systemRules =
"\n\nRules: Respond ONLY JSON and ensure any json string does not contains special chars and are correctly json encoded. Format: {\"tool\":..., \"arguments\":{...}}. ";
307 $systemRules .=
"IMPORTANT: If the user asks for functionality that is NOT available in the list of Tools above, you MUST use the tool 'respond_to_user' to inform them that the specific feature is not available.";
311 $toolsForLLM = array();
314 $systemPrompt = $basePrompt .
"\n\n";
315 $systemPrompt .=
"Tools:\n" . json_encode($toolsForLLM, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
316 $systemPrompt .= $systemRules .
" Date: " .
date(
'Y-m-d');
322 if (preg_match(
'/^crypt:/', $apiKey)) {
326 $defUrl = $servicesList[$serviceKey][
'url'] ??
'';
339 $rawDefault = $servicesList[$serviceKey][
'textgeneration'] ??
null;
340 if (is_array($rawDefault)) {
341 $defModel = $rawDefault[
'default'] ??
'gpt-4o-mini';
343 $defModel = $rawDefault ?:
'gpt-4o-mini';
345 $prefix =
'AI_API_' . strtoupper($serviceKey);
350 if (is_array($model)) {
351 $model = $model[
'default'] ?? $defModel;
353 if (!is_string($model) || $model ===
'') {
354 $model = (
string) $defModel;
356 $adapterType = $servicesList[$serviceKey][
'adapter_type'] ??
'openai';
362 if (!empty($apiKey)) {
365 dol_syslog(
"parse_intent.php Call AI API", LOG_DEBUG);
367 $rawResponse = $adapter->generate($systemPrompt, $query);
370 dol_syslog(
'rawResponse='.$rawResponse, LOG_DEBUG);
375 $rawRequestLog = $adapter->lastRequest;
376 $rawResponseLog = $adapter->lastResponse;
379 if (is_string($rawResponse) && strpos($rawResponse,
'Error:') === 0) {
380 $errorDetails = $rawResponse;
381 } elseif ($rawResponse) {
383 $clean = preg_replace(
'/```json\s*|\s*```/s',
'', $rawResponse);
384 $clean = trim($clean);
387 if (preg_match(
'/^\{.*\}$/s', $clean, $matches)) {
388 $clean = $matches[0];
393 $clean = $guard->unmaskAiResponse($clean);
397 $clean = preg_replace(
'/[\r\n]/',
' ', $clean);
400 if (strpos($clean,
'{') === 0) {
402 $intentJSON = json_decode($clean,
true);
405 "tool" =>
"respond_to_user",
413 if ($guard && isset($intentJSON[
'arguments'])) {
418 if ($intentJSON && isset($intentJSON[
'tool'])) {
419 $validToolNames = array_column($allToolsSchema,
'name');
420 if (!in_array($intentJSON[
'tool'], $validToolNames)) {
421 dol_syslog(
"AI Validation: Tool '" . $intentJSON[
'tool'] .
"' not found in filtered schema. Send error message via respond_to_user.", LOG_WARNING);
425 "tool" =>
"respond_to_user",
427 "message" =>
"I apologize, but the requested functionality is not currently available in the system."
435 if ($intentJSON && $confidence === 0.0) {
436 $mappedToolsSchema = array_column($toolsSchema,
null,
'name');
439 dol_syslog(
"parse_intent.php AI Intent: " . json_encode([
'query' => $query,
'intent' => $intentJSON,
'confidence' => $confidence]), LOG_DEBUG);
447 if (!$intentJSON || !isset($intentJSON[
'tool'])) {
449 "tool" =>
"respond_to_user",
451 "message" =>
"I'm having trouble understanding your request. Please try rephrasing it differently. If the problem persists, please contact your administrator to check the AI connection status."
456 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, microtime(
true) - $startTime, 0.0, $langs->transnoentitiesnoconv(
'Error'), $errorDetails, $rawRequestLog, $rawResponseLog);
459 echo json_encode($finalResponse);
464 $needsConfirmation =
false;
465 $toolName = $intentJSON[
'tool'] ??
'';
471 if (in_array($toolName, array(
'respond_to_user',
'reject_general_question'),
true) && isset($intentJSON[
'arguments']) && is_array($intentJSON[
'arguments'])) {
472 if (empty($intentJSON[
'arguments'][
'message'])) {
473 foreach (array(
'response',
'text',
'answer',
'content',
'reply',
'output') as $altkey) {
474 if (!empty($intentJSON[
'arguments'][$altkey])) {
475 $intentJSON[
'arguments'][
'message'] = $intentJSON[
'arguments'][$altkey];
482 if ($askForConfirmation > 0) {
483 $isModifyOperation = preg_match(
'/(create|update|delete|add|remove|modify|edit)/i', $toolName);
485 if ($askForConfirmation == 1 && $isModifyOperation) {
486 $needsConfirmation =
true;
487 } elseif ($askForConfirmation == 2) {
488 $needsConfirmation =
true;
493 if ($needsConfirmation) {
494 $allToolsMap = !empty($allToolsSchema)
495 ? array_column($allToolsSchema,
null,
'name')
497 $toolDescription = $allToolsMap[$toolName][
'description'] ??
'No description available';
498 $arguments = $intentJSON[
'arguments'] ?? [];
503 $confirmationResponse = [
504 "tool" =>
"ask_for_confirmation",
507 "details" => $details,
508 "original_intent" => $intentJSON
513 ai_log_request($db, $user, $query, $confirmationResponse, $providerUsed, microtime(
true) - $startTime, $confidence, $langs->transnoentitiesnoconv(
"Confirm"), $errorDetails, $rawRequestLog, $rawResponseLog);
516 echo json_encode($confirmationResponse);
521 if ($confidence < LOW_CONFIDENCE) {
523 "tool" =>
"respond_to_user",
525 "message" =>
"I'm not confident about understanding your request. Please try rephrasing it with more specific details."
530 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, microtime(
true) - $startTime, $confidence,
'low_confidence', $errorDetails, $rawRequestLog, $rawResponseLog);
533 echo json_encode($finalResponse);
538 if ($confidence < MEDIUM_CONFIDENCE && isset($intentJSON[
'arguments'])) {
539 $intentJSON[
'arguments'][
'_confidence_note'] =
"I'm moderately confident about this interpretation. Please verify the results.";
543 $finalResponse = $intentJSON;
544 $execTime = microtime(
true) - $startTime;
545 ai_log_request($db, $user, $query, $finalResponse, $providerUsed, $execTime, $confidence, $langs->transnoentitiesnoconv(
"Success"), $errorDetails, $rawRequestLog, $rawResponseLog);
548 echo json_encode($finalResponse);
549}
catch (Throwable $e) {
550 $friendlyResponse = [
551 "tool" =>
"respond_to_user",
553 "message" =>
"I'm experiencing technical difficulties. Please try again later or contact your administrator."
557 $realErrorForLog =
"PHP Exception: " . $e->getMessage() .
" in " . $e->getFile() .
" on line " . $e->getLine();
559 dol_syslog(
"AI Critical Error: " . $realErrorForLog, LOG_ERR);
561 if (function_exists(
'ai_log_request') && is_object($db)) {
568 microtime(
true) - $startTime,
572 $rawRequestLog ??
'',
573 $rawResponseLog ??
''
578 echo json_encode($friendlyResponse);
602 if ($guard ===
null) {
606 if (is_array($data)) {
612 function ($item) use ($guard) {
619 if (is_string($data)) {
620 return $guard->unmask($data);
641 if (preg_match(
'/\p{Han}|\p{Hiragana}|\p{Katakana}|\p{Hangul}/u', $text)) {
646 if (preg_match(
'/\p{Cyrillic}/u', $text)) {
651 if (preg_match(
'/\p{Greek}/u', $text)) {
656 if (preg_match(
'/\p{Arabic}/u', $text)) {
661 if (preg_match(
'/\p{Hebrew}/u', $text)) {
666 if (preg_match(
'/\p{Thai}/u', $text)) {
700 $langs->loadLangs(array(
"main",
"bills",
"orders",
"propal",
"companies",
"products",
"projects",
"dict"));
704 'keys' => [
'Bill',
'Invoice',
'Payment',
'Cheque',
'VAT',
'BillStatusUnpaid',
'BillStatusPaid',
'BillStatusDraft'],
705 'synonyms' => [
'paid',
'unpaid',
'pay',
'money',
'cost',
'amount']
708 'keys' => [
'Order',
'Proposal',
'Quote',
'SupplierOrder',
'OrderStatusDraft'],
709 'synonyms' => [
'sale',
'buy',
'purchase',
'contract',
'shipping']
712 'keys' => [
'ThirdParty',
'Customer',
'Supplier',
'Contact',
'Company'],
713 'synonyms' => [
'client',
'partner',
'address',
'phone']
716 'keys' => [
'Product',
'Service',
'Stock',
'Warehouse'],
717 'synonyms' => [
'item',
'inventory',
'sku',
'location',
'qty']
720 'keys' => [
'Project',
'Task'],
721 'synonyms' => [
'milestone',
'gantt',
'team']
724 'keys' => [
'Report',
'Statistics',
'Turnover',
'Revenue',
'Income'],
725 'synonyms' => [
'graph',
'chart',
'analytics',
'dashboard',
'kpi']
729 $detectedCategories = [];
730 foreach ($intentMap as $category => $data) {
732 foreach ($data[
'keys'] as $key) {
733 $trans = $langs->trans($key);
737 $keywords[] = $trans;
738 if (!$isLatin && $key !== $trans) {
739 $keywords[] = strtolower($key);
743 foreach ($data[
'synonyms'] as $syn) {
747 foreach ($keywords as $word) {
752 if (preg_match(
'/\b' . preg_quote($word,
'/') .
's?\b/u', $searchQuery)) {
753 $detectedCategories[] = $category;
757 if (mb_stripos($searchQuery, $word) !==
false) {
758 $detectedCategories[] = $category;
764 return $detectedCategories;
789 if (empty($activeCategories) || (count($activeCategories) === 1 && $activeCategories[0] ===
'global')) {
793 $targetCategories = array_merge([
'global'], $activeCategories);
796 foreach ($allTools as $tool) {
797 $toolCats = $tool[
'categories'] ?? [
'global'];
798 if (count(array_intersect($toolCats, $targetCategories)) > 0) {
799 if (count($activeCategories) > 0 && $toolCats === [
'global']) {
806 if (count($filtered) < 3) {
807 dol_syslog(
"AI Filter: Too few tools (" . count($filtered) .
"). Reverting to full schema.", LOG_WARNING);
826 foreach ($tools as $tool) {
835 $desc = $tool[
'description'];
838 $toolParams = $tool[
'parameters'] ?? $tool[
'inputSchema'] ?? [];
840 if ($isLargeSchema && isset($toolParams[
'properties']) && is_array($toolParams[
'properties'])) {
841 $requiredList = $toolParams[
'required'] ?? [];
844 foreach ($toolParams[
'properties'] as $propKey => $propData) {
845 $isRequired = in_array($propKey, $requiredList);
855 if (!$isRequired && isset($propData[
'default'])) {
860 if (isset($propData[
'type']) && $propData[
'type'] ===
'string') {
861 unset($propData[
'type']);
867 if (isset($propData[
'description'])) {
868 unset($propData[
'description']);
878 if (isset($propData[
'type']) && $propData[
'type'] ===
'object' && isset($propData[
'properties'])) {
879 unset($propData[
'properties']);
880 unset($propData[
'required']);
881 $propData[
'description'] =
"JSON object";
884 $newProperties[$propKey] = $propData;
887 $toolParams[
'properties'] = $newProperties;
890 unset($toolParams[
'type']);
891 unset($toolParams[
'additionalProperties']);
895 'name' => $tool[
'name'],
896 'description' => $desc,
897 'parameters' => $toolParams
923 $factors[
'parse_success'] = 0.4;
927 $hasRequiredFields = !empty($intentJSON[
'tool']) && !empty($intentJSON[
'arguments']);
928 $factors[
'completeness'] = $hasRequiredFields ? 0.3 : 0.0;
931 $isValidSchema =
false;
934 if (isset($intentJSON[
'tool']) && isset($toolsSchema[$intentJSON[
'tool']])) {
936 $schema = $toolsSchema[$intentJSON[
'tool']][
'parameters']
937 ?? $toolsSchema[$intentJSON[
'tool']][
'inputSchema']
941 $providedParams = array_keys($intentJSON[
'arguments'] ?? []);
944 $properties = $schema[
'properties'] ?? [];
945 $requiredList = $schema[
'required'] ?? [];
950 foreach ($properties as $paramKey => $paramDetails) {
952 if (in_array($paramKey, $requiredList)) {
954 if (!in_array($paramKey, $providedParams)) {
955 $missingParams[] = $paramKey;
961 $isValidSchema = empty($missingParams);
964 $factors[
'schema_validation'] = $isValidSchema ? 0.2 : 0.0;
968 if (is_string($rawResponse)) {
970 if (!preg_match(
'/error|fail|unable|cannot|sorry/i', $rawResponse)) {
971 $qualityScore += 0.05;
975 if (isset($intentJSON[
'tool']) && isset($toolsSchema[$intentJSON[
'tool']])) {
976 $qualityScore += 0.05;
979 $factors[
'response_quality'] = $qualityScore;
982 $confidence = array_sum($factors);
985 return max(0.0, min(1.0, $confidence));
997 foreach ($arguments as $key => $value) {
998 if (is_array($value)) {
999 $formattedArgs[] =
"- {$key}: " . (empty($value) ?
"(empty)" : json_encode($value, JSON_PRETTY_PRINT));
1001 $formattedArgs[] =
"- {$key}: {$value}";
1004 return implode(
"\n", $formattedArgs);
1015 if (preg_match(
'/^(create|update|delete|list|show|find|search|get|view|validate|send)/i', $toolName, $matches)) {
1016 return strtolower($matches[1]);
1018 return 'perform this action';
getListOfAIServices()
Get list of available ai services.
ai_log_request($db, $user, $query, array $response, $provider, float $time, float $confidence, $status, $error='', $rawReq='', $rawRes='')
Log AI Request with Raw Payloads.
Class to handle MCP (Model Context Protocol).
Class to manage privacy data masking and unmasking.
Class to manage translations.
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.
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.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
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.
catch(Throwable $e) recursiveUnmaskValues($data, ?PrivacyGuard $guard)
Recursively unmask values in a dataset.
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.
filterToolsProfessional(array $allTools, array $activeCategories)
Filter a list of tools based on active intent categories.
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.