2024-01-16 14:52:52 +00:00
< ? php
2026-03-14 18:48:17 +00:00
/* Copyright ( C ) 2022 Alice Adminson < aadminson @ example . com >
* Copyright ( C ) 2024 - 2025 Frédéric France < frederic . france @ free . fr >
* Copyright ( C ) 2024 MDW < mdeweerd @ users . noreply . github . com >
2026-06-13 23:05:01 +00:00
* Copyright ( C ) 2026 Anthony Damhet < a . damhet @ progiseize . fr >
2026-03-14 18:48:17 +00:00
* Copyright ( C ) 2026 Nick Fragoulis
2024-01-16 14:52:52 +00:00
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
2026-03-14 18:48:17 +00:00
* but WITHOUT ANY WARRANTY , without even the implied warranty of
2024-01-16 14:52:52 +00:00
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*/
/**
2024-04-06 15:38:39 +00:00
* \file htdocs / ai / lib / ai . lib . php
2024-01-16 14:52:52 +00:00
* \ingroup ai
* \brief Library files with common functions for Ai
*/
2025-04-12 01:35:44 +00:00
include_once DOL_DOCUMENT_ROOT . '/ai/class/ai.class.php' ;
2025-02-16 18:56:03 +00:00
/**
* Prepare admin pages header
*
2025-02-17 02:49:11 +00:00
* @ return array < string , array < string , string >>
2025-02-16 18:56:03 +00:00
*/
2025-02-19 13:44:54 +00:00
function getListOfAIFeatures ()
2025-02-16 18:56:03 +00:00
{
global $langs ;
$arrayofaifeatures = array (
2025-04-14 17:37:33 +00:00
'textgenerationemail' => array ( 'label' => $langs -> trans ( 'TextGeneration' ) . ' (' . $langs -> trans ( " EmailContent " ) . ')' , 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_EMAIL ),
'textgenerationwebpage' => array ( 'label' => $langs -> trans ( 'TextGeneration' ) . ' (' . $langs -> trans ( " WebsitePage " ) . ')' , 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_WEBPAGE ),
2025-04-12 01:35:44 +00:00
'textgeneration' => array ( 'label' => $langs -> trans ( 'TextGeneration' ) . ' (' . $langs -> trans ( " Other " ) . ')' , 'picto' => '' , 'status' => 'notused' , 'function' => 'TEXT' ),
2025-04-14 17:37:33 +00:00
'texttranslation' => array ( 'label' => $langs -> trans ( 'TextTranslation' ), 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION ),
'textsummarize' => array ( 'label' => $langs -> trans ( 'TextSummarize' ), 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE ),
2026-04-03 03:11:14 +00:00
'textspellchecker' => array ( 'label' => $langs -> trans ( 'TextSpellChecker' ), 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER ),
2026-04-15 19:47:31 +00:00
'textrephrase' => array ( 'label' => $langs -> trans ( 'TextRephraser' ), 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER ),
2025-04-12 01:35:44 +00:00
2025-04-25 23:59:36 +00:00
'textgenerationextrafield' => array ( 'label' => $langs -> trans ( 'TextGeneration' ) . ' (' . $langs -> trans ( " ExtrafieldFiller " ) . ')' , 'picto' => '' , 'status' => 'dolibarr' , 'function' => 'TEXT' , 'placeholder' => Ai :: AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER ),
2025-04-12 01:35:44 +00:00
'imagegeneration' => array ( 'label' => 'ImageGeneration' , 'picto' => '' , 'status' => 'notused' , 'function' => 'IMAGE' ),
'videogeneration' => array ( 'label' => 'VideoGeneration' , 'picto' => '' , 'status' => 'notused' , 'function' => 'VIDEO' ),
'audiogeneration' => array ( 'label' => 'AudioGeneration' , 'picto' => '' , 'status' => 'notused' , 'function' => 'AUDIO' ),
'transcription' => array ( 'label' => 'AudioTranscription' , 'picto' => '' , 'status' => 'notused' , 'function' => 'TRANSCRIPT' ),
2026-01-18 13:21:09 +00:00
'translation' => array ( 'label' => 'AudioTranslation' , 'picto' => '' , 'status' => 'notused' , 'function' => 'TRANSLATE' ),
2026-03-12 20:13:49 +00:00
'docparsing' => array ( 'label' => 'DocumentParsing' , 'picto' => '' , 'status' => 'experimental' , 'function' => 'DOCPARSING' )
2025-02-16 18:56:03 +00:00
);
return $arrayofaifeatures ;
}
2025-02-19 13:44:54 +00:00
/**
* Get list of available ai services
*
2025-02-19 14:19:52 +00:00
* @ return array < int | string , mixed >
2025-02-19 13:44:54 +00:00
*/
function getListOfAIServices ()
{
global $langs ;
2025-03-26 19:00:56 +00:00
$arrayofai = array (
2025-04-07 18:29:38 +00:00
'-1' => array ( 'label' => $langs -> trans ( 'SelectAService' )),
'chatgpt' => array (
2026-03-14 18:48:17 +00:00
'label' => 'ChatGPT (OpenAI)' ,
'url' => 'https://api.openai.com/v1/' ,
'setup' => 'https://platform.openai.com/account/api-keys' ,
'textgeneration' => array ( 'default' => 'gpt-5.2' ), // Flagship model released late 2025, updated Feb 2026
'imagegeneration' => array ( 'default' => 'gpt-image-1.5' ), // Replaced DALL-E 3; 4x faster and native to GPT-5
'audiogeneration' => array ( 'default' => 'gpt-audio-1.5' ), // New Feb 23, 2026 release for high-fidelity audio out
'videogeneration' => array ( 'default' => 'sora-2' ), // OpenAI's standard API video model
'transcription' => array ( 'default' => 'whisper-large-v3-turbo' ), // The current speed/accuracy benchmark for ASR
'translation' => array ( 'default' => 'whisper-large-v3-turbo' ), // Still the best for multi-language audio translation
'docparsing' => array ( 'default' => 'gpt-5.2' ), // Uses the new Responses API / Vision capabilities
'adapter_type' => 'openai'
2025-04-07 18:29:38 +00:00
),
'groq' => array (
2026-03-14 18:48:17 +00:00
'label' => 'Groq (LPU Inference)' ,
'url' => 'https://api.groq.com/openai/v1/' ,
'setup' => 'https://console.groq.com/keys' ,
'textgeneration' => array ( 'default' => 'llama-4-8b-instant' ), // February 2026 flagship for extreme speed (1,000+ t/s)
2026-03-12 20:13:49 +00:00
'imagegeneration' => array ( 'default' => 'na' ),
'audiogeneration' => array ( 'default' => 'na' ),
'videogeneration' => array ( 'default' => 'na' ),
2026-03-14 18:48:17 +00:00
'transcription' => array ( 'default' => 'whisper-large-v3-turbo' ), // Groq's specialized high-speed Whisper implementation
'translation' => array ( 'default' => 'whisper-large-v3-turbo' ), // High-speed audio translation to English
'docparsing' => array ( 'default' => 'llama-4-70b-versatile' ), // Best for structured data extraction from text
'adapter_type' => 'openai'
2025-04-07 18:29:38 +00:00
),
'mistral' => array (
2026-03-14 18:48:17 +00:00
'label' => 'Mistral AI' ,
2025-04-07 18:29:38 +00:00
'url' => 'https://api.mistral.ai/v1/' ,
2026-03-14 18:48:17 +00:00
'setup' => 'https://console.mistral.ai/api-keys/' ,
2026-03-14 18:51:54 +00:00
'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)
2026-03-14 18:48:17 +00:00
'imagegeneration' => array ( 'default' => 'na' ),
'audiogeneration' => array ( 'default' => 'na' ),
'videogeneration' => array ( 'default' => 'na' ),
'transcription' => array ( 'default' => 'na' ),
'translation' => array ( 'default' => 'na' ),
'docparsing' => array ( 'default' => 'pixtral-12b-latest' ), // Mistral's native vision/doc model
'adapter_type' => 'openai'
),
'deepseek' => array (
'label' => 'DeepSeek' ,
'url' => 'https://api.deepseek.com' ,
'setup' => 'https://platform.deepseek.com/api_keys' ,
'textgeneration' => array ( 'default' => 'deepseek-v4' ), // Released Feb 2026, flagship MoE model
'imagegeneration' => array ( 'default' => 'deepseek-janus-2' ), // DeepSeek's latest multimodal vision/gen model
'audiogeneration' => array ( 'default' => 'na' ),
'videogeneration' => array ( 'default' => 'na' ),
'transcription' => array ( 'default' => 'na' ),
'translation' => array ( 'default' => 'na' ),
'docparsing' => array ( 'default' => 'deepseek-v4' ), // Massive 1M context support for parsing
'adapter_type' => 'openai'
),
'perplexity' => array (
'label' => 'Perplexity (Sonar)' ,
'url' => 'https://api.perplexity.ai' ,
'setup' => 'https://www.perplexity.ai/settings/api' ,
'textgeneration' => array ( 'default' => 'sonar-pro' ), // Flagship search model as of Feb 2026
2026-03-12 20:13:49 +00:00
'imagegeneration' => array ( 'default' => 'na' ),
'audiogeneration' => array ( 'default' => 'na' ),
'videogeneration' => array ( 'default' => 'na' ),
'transcription' => array ( 'default' => 'na' ),
'translation' => array ( 'default' => 'na' ),
2026-03-14 18:48:17 +00:00
'docparsing' => array ( 'default' => 'sonar-reasoning' ), // Best for analyzing search-grounded docs
'adapter_type' => 'openai'
),
'zai' => array (
'label' => 'Zhipu AI (GLM)' ,
'url' => 'https://api.z.ai/api/paas/v4' ,
'setup' => 'https://docs.z.ai/guides/overview/quick-start' ,
'textgeneration' => array ( 'default' => 'glm-5' ), // Flagship released February 11, 2026
'imagegeneration' => array ( 'default' => 'cogview-4' ), // Zhipu's latest SOTA image generator
'audiogeneration' => array ( 'default' => 'cogvlm2-audio' ), // High-fidelity conversational audio
'videogeneration' => array ( 'default' => 'cogvideox-2' ), // Flagship API video model
'transcription' => array ( 'default' => 'na' ),
'translation' => array ( 'default' => 'na' ),
'docparsing' => array ( 'default' => 'glm-5' ), // Top-tier agentic document processing
'adapter_type' => 'openai'
2025-04-07 18:29:38 +00:00
),
'custom' => array (
'label' => 'Custom' ,
2025-04-12 01:35:44 +00:00
'url' => 'https://domainofapi.com/v1/' ,
2025-09-20 12:58:04 +00:00
'setup' => 'Ask your AI provider how to get your API key' ,
2026-03-12 20:13:49 +00:00
'textgeneration' => array ( 'default' => 'tinyllama-1.1b' ),
'imagegeneration' => array ( 'default' => 'mixtral-8x7b-32768' ),
'audiogeneration' => array ( 'default' => 'mixtral-8x7b-32768' ),
'videogeneration' => array ( 'default' => 'na' ),
'transcription' => array ( 'default' => 'mixtral-8x7b-32768' ),
'translation' => array ( 'default' => 'mixtral-8x7b-32768' ),
'docparsing' => array ( 'default' => 'na' ),
2026-03-14 18:48:17 +00:00
'adapter_type' => 'openai'
),
// --- SPECIALIZED ADAPTERS ---
'anthropic' => array (
'label' => 'Anthropic (Claude)' ,
'url' => 'https://api.anthropic.com/v1/' ,
'setup' => 'https://console.anthropic.com/' ,
'textgeneration' => array ( 'default' => 'claude-opus-4-6' ), // Released Feb 2026; features a 1M context window
'imagegeneration' => array ( 'default' => 'na' ), // Anthropic remains focused on text/code logic
'audiogeneration' => array ( 'default' => 'na' ),
'videogeneration' => array ( 'default' => 'na' ),
'transcription' => array ( 'default' => 'na' ),
'translation' => array ( 'default' => 'na' ),
'docparsing' => array ( 'default' => 'claude-opus-4-6' ), // Leading model for "Computer Use" and PDF analysis
'adapter_type' => 'anthropic'
),
'google' => array (
'label' => 'Google Gemini' ,
'url' => 'https://generativelanguage.googleapis.com/v1beta/' ,
'setup' => 'https://aistudio.google.com/' ,
'textgeneration' => array ( 'default' => 'gemini-3.1-pro-preview' ), // Flagship reasoning model released Feb 19, 2026
'imagegeneration' => array ( 'default' => 'nano-banana-pro' ), // Latest SOTA image model (Gemini 3 Pro Image)
'audiogeneration' => array ( 'default' => 'gemini-2.5-pro-tts' ), // High-fidelity native speech synthesis
'videogeneration' => array ( 'default' => 'veo-3.1' ), // Google's flagship cinematic video API
'transcription' => array ( 'default' => 'gemini-3.1-pro-preview' ), // Native multi-modal audio reasoning
'translation' => array ( 'default' => 'gemini-3.1-pro-preview' ), // Native audio-to-text translation
'docparsing' => array ( 'default' => 'gemini-3.1-pro-preview' ), // Massive 2M+ context window for full repo parsing
'adapter_type' => 'google'
2025-04-07 18:29:38 +00:00
)
2025-02-19 13:44:54 +00:00
);
2025-03-26 19:00:56 +00:00
return $arrayofai ;
2025-02-19 13:44:54 +00:00
}
2026-03-14 18:48:17 +00:00
/**
2026-04-15 19:47:31 +00:00
* Tests the connection to an AI service using its API key and URL by sending message " Hello "
2026-03-14 18:48:17 +00:00
*
* This function supports multiple AI providers ( Google Gemini , Anthropic Claude , and OpenAI - compatible APIs like
* Mistral , Groq , and DeepSeek ) . It constructs a minimal , provider - specific request payload and sends it
* to the given endpoint to verify that the API key is valid and the service is reachable .
*
* @ param string $service The identifier of the AI service ( e . g . , 'google' , 'anthropic' , 'openai' , 'mistral' ) .
* @ param string $key The API key for the service .
* @ param string $url The base URL of the AI service ' s API endpoint .
*
* @ return array { success : bool , message : string } An associative array indicating the result of the test .
* - 'success' is true on a successful connection ( HTTP 2 xx ), false otherwise .
* - 'message' provides details , such as " OK (HTTP 200) " or an error description .
*/
function testAIConnection ( string $service , string $key , string $url ) : array
{
if ( empty ( $key )) {
return [ 'success' => false , 'message' => 'API Key is empty' ];
}
// Load Defaults (Ensure this function exists or handle the error)
if ( ! function_exists ( 'getListOfAIServices' )) {
return [ 'success' => false , 'message' => 'Configuration helper function missing.' ];
}
$list = getListOfAIServices ();
$defUrl = $list [ $service ][ 'url' ] ? ? '' ;
// Use model from config, fallback to hardcoded if necessary
$defaultModel = $list [ $service ][ 'model' ] ? ? 'unknown' ;
// Normalize URL
if ( empty ( $url )) {
$url = $defUrl ;
}
$url = rtrim ( $url , '/' );
$data = [];
$headers = [ " Content-Type: application/json " ];
$model = '' ;
if ( empty ( $model )) {
2026-04-15 19:47:31 +00:00
$model = getDolGlobalString ( 'AI_API_' . strtoupper ( $service ) . '_MODEL_TEXT' );
2026-03-14 18:48:17 +00:00
}
// GOOGLE
if ( $service == 'google' || strpos ( $url , 'googleapis' ) !== false ) {
if ( strpos ( $url , ':generateContent' ) === false ) {
if ( strpos ( $url , 'models' ) === false ) {
$url .= " /models/ $model :generateContent " ;
} else {
$url .= " / $model :generateContent " ;
}
}
$url .= " ?key= " . $key ;
$data = [ " contents " => [ [ " parts " => [ [ " text " => " Hello " ] ] ] ], " generationConfig " => [ " maxOutputTokens " => 5 ]];
} elseif ( $service == 'anthropic' || strpos ( $url , 'anthropic' ) !== false ) { // ANTHROPIC
if ( strpos ( $url , 'messages' ) === false ) $url .= '/messages' ;
$headers [] = " x-api-key: $key " ;
$headers [] = " anthropic-version: 2023-06-01 " ;
$data = [
" model " => $model , // Uses Configured Model
" messages " => [[ " role " => " user " , " content " => " Hello " ]],
" max_tokens " => 5
];
} else {
if ( strpos ( $url , '/chat/completions' ) === false ) $url .= '/chat/completions' ;
$headers [] = " Authorization: Bearer $key " ;
$data = [
" model " => $model , // Uses Configured Model (from Priority Chain)
" messages " => [[ " role " => " user " , " content " => " Hello " ]],
" max_tokens " => 5
];
}
// Execute cURL
$ch = curl_init ();
curl_setopt ( $ch , CURLOPT_URL , $url );
curl_setopt ( $ch , CURLOPT_POST , true );
curl_setopt ( $ch , CURLOPT_POSTFIELDS , json_encode ( $data ));
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , $headers );
curl_setopt ( $ch , CURLOPT_TIMEOUT , 10 );
// Optional: Add SSL verification if behind a proxy with self-signed certs
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec ( $ch );
$httpCode = curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
$err = curl_error ( $ch );
curl_close ( $ch );
if ( $err ) {
return [ 'success' => false , 'message' => " Curl Error: $err " ];
}
if ( $httpCode >= 200 && $httpCode < 300 ) {
return [ 'success' => true , 'message' => " OK (HTTP $httpCode ). " ];
} else {
$json = json_decode ( $result , true );
// Attempt to find the error message in various common structures
$msg = $json [ 'error' ][ 'message' ] ? ? $json [ 'message' ] ? ? substr ( $result , 0 , 150 );
return [ 'success' => false , 'message' => " HTTP $httpCode . Error: $msg " ];
}
}
/**
* Log AI Request with Raw Payloads
*
* @ param DoliDB $db Database object
* @ param User $user User object
* @ param string $query The query sent to the AI
* @ param array < string , mixed > $response The full response from the AI
* @ param string $provider The AI provider ( e . g . , 'OpenAI' , 'Anthropic' )
* @ param float $time Execution time in seconds
* @ param float $confidence Confidence score from the AI ( if any )
* @ param string $status Status of the request ( e . g . , 'success' , 'error' )
* @ param string $error Error message , if any
* @ param string $rawReq Raw request payload
* @ param string $rawRes Raw response payload
2026-04-07 23:27:04 +00:00
* @ return int Return 0
2026-03-14 18:48:17 +00:00
*/
2026-04-07 23:27:04 +00:00
function ai_log_request ( $db , $user , $query , array $response , $provider , float $time , float $confidence , $status , $error = '' , $rawReq = '' , $rawRes = '' )
2026-03-14 18:48:17 +00:00
{
global $conf ;
if ( ! getDolGlobalInt ( 'AI_LOG_REQUESTS' )) {
2026-04-07 23:27:04 +00:00
return 0 ;
2026-03-14 18:48:17 +00:00
}
$tool = isset ( $response [ 'tool' ]) ? ( string ) $response [ 'tool' ] : '' ;
if ( dol_strlen ( $rawReq ) > 60000 ) {
$rawReq = dol_substr ( $rawReq , 0 , 60000 ) . '... [Truncated]' ;
}
2025-04-07 18:29:38 +00:00
2026-03-14 18:48:17 +00:00
$rawResStr = ( string ) $rawRes ;
if ( dol_strlen ( $rawResStr ) > 60000 ) {
$rawResStr = dol_substr ( $rawResStr , 0 , 60000 ) . '... [Truncated]' ;
}
2025-04-07 18:29:38 +00:00
2026-03-14 18:48:17 +00:00
$sql = " INSERT INTO " . MAIN_DB_PREFIX . " ai_request_log ( " ;
$sql .= " entity, date_request, fk_user, query_text, tool_name, provider, " ;
$sql .= " execution_time, confidence, status, error_msg, raw_request_payload, raw_response_payload " ;
$sql .= " ) VALUES ( " ;
$sql .= (( int ) $conf -> entity ) . " , " ;
$sql .= " ' " . $db -> idate ( dol_now ()) . " ', " ;
$sql .= (( int ) $user -> id ) . " , " ;
$sql .= " ' " . $db -> escape ( $query ) . " ', " ;
$sql .= " ' " . $db -> escape ( $tool ) . " ', " ;
$sql .= " ' " . $db -> escape ( $provider ) . " ', " ;
$sql .= (( float ) $time ) . " , " ;
$sql .= (( float ) $confidence ) . " , " ;
$sql .= " ' " . $db -> escape ( $status ) . " ', " ;
$sql .= " ' " . $db -> escape ( $error ) . " ', " ;
$sql .= " ' " . $db -> escape ( $rawReq ) . " ', " ;
$sql .= " ' " . $db -> escape ( $rawResStr ) . " ' " ;
$sql .= " ) " ;
$resql = $db -> query ( $sql );
if ( ! $resql ) {
dol_print_error ( $db );
}
2026-04-07 23:27:04 +00:00
return 0 ;
2026-03-14 18:48:17 +00:00
}
2025-04-07 18:29:38 +00:00
2025-04-03 15:34:09 +00:00
/**
* Get list for AI summarize
*
* @ return array < int | string , mixed >
*/
function getListForAISummarize ()
{
$arrayforaisummarize = array (
2025-04-07 11:59:36 +00:00
//'20_w' => 'SummarizeTwentyWords',
2025-04-03 15:34:09 +00:00
'50_w' => 'SummarizeFiftyWords' ,
'100_w' => 'SummarizeHundredWords' ,
'200_w' => 'SummarizeTwoHundredWords' ,
'1_p' => 'SummarizeOneParagraphs' ,
2025-04-04 16:16:29 +00:00
'2_p' => 'SummarizeTwoParagraphs' ,
'25_pc' => 'SummarizeTwentyFivePercent' ,
'50_pc' => 'SummarizeFiftyPercent' ,
'75_pc' => 'SummarizeSeventyFivePercent'
2025-04-03 15:34:09 +00:00
);
return $arrayforaisummarize ;
}
2025-04-09 13:47:09 +00:00
/**
* Get list for AI style of writing
*
* @ return array < int | string , mixed >
*/
function getListForAIRephraseStyle ()
{
$arrayforaierephrasestyle = array (
2026-04-15 19:47:31 +00:00
'spellchecker' => 'RephraseSpellChecker' ,
2025-04-09 13:47:09 +00:00
'professional' => 'RephraseStyleProfessional' ,
2026-04-03 03:11:14 +00:00
'humouristic' => 'RephraseStyleHumouristic' ,
2025-04-09 13:47:09 +00:00
);
return $arrayforaierephrasestyle ;
}
2024-01-16 14:52:52 +00:00
/**
* Prepare admin pages header
*
2024-09-18 01:27:25 +00:00
* @ return array < array { 0 : string , 1 : string , 2 : string } >
2024-01-16 14:52:52 +00:00
*/
function aiAdminPrepareHead ()
{
global $langs , $conf ;
$langs -> load ( " agenda " );
$h = 0 ;
$head = array ();
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/setup.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " Settings " );
$head [ $h ][ 2 ] = 'settings' ;
$h ++ ;
2024-02-07 18:41:50 +00:00
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/custom_prompt.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " CustomPrompt " );
$head [ $h ][ 2 ] = 'custom' ;
$h ++ ;
2026-05-19 23:27:19 +00:00
if ( getDolGlobalString ( " MAIN_FEATURES_LEVEL " ) >= 2 ) {
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/assistant.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " Assistant " );
$head [ $h ][ 2 ] = 'assistant' ;
$h ++ ;
}
2025-09-20 12:58:04 +00:00
if ( getDolGlobalString ( " MAIN_FEATURES_LEVEL " ) >= 2 ) {
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/server_mcp.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " MCPServer " );
$head [ $h ][ 2 ] = 'servermcp' ;
$h ++ ;
}
2026-06-10 09:41:10 +00:00
if ( getDolGlobalString ( " MAIN_FEATURES_LEVEL " ) >= 2 ) {
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/configure_tools.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " ToolAccessControl " );
$head [ $h ][ 2 ] = 'tools' ;
$h ++ ;
}
2024-01-16 14:52:52 +00:00
/*
$head [ $h ][ 0 ] = dol_buildpath ( " /ai/admin/myobject_extrafields.php " , 1 );
$head [ $h ][ 1 ] = $langs -> trans ( " ExtraFields " );
$head [ $h ][ 2 ] = 'myobject_extrafields' ;
$h ++ ;
*/
// Show more tabs from modules
// Entries must be declared in modules descriptor with line
//$this->tabs = array(
// 'entity:+tabname:Title:@ai:/ai/mypage.php?id=__ID__'
//); // to add new tab
//$this->tabs = array(
// 'entity:-tabname:Title:@ai:/ai/mypage.php?id=__ID__'
//); // to remove a tab
complete_head_from_modules ( $conf , $langs , null , $head , $h , 'ai@ai' );
complete_head_from_modules ( $conf , $langs , null , $head , $h , 'ai@ai' , 'remove' );
return $head ;
}
2026-06-13 02:16:48 +00:00
2026-06-13 23:05:01 +00:00
/**
* Resolve the AI provider / service currently configured for the AI Assistant
* ( e . g . " ChatGPT (OpenAI) " , " Google Gemini " , " Anthropic (Claude) " ), so it can be
* displayed in the chat header . The precise model name is intentionally not
* shown here , only which AI is in use .
*
* @ return string The provider label , or '' if no service is configured
*/
function getAiAssistantProviderLabel ()
{
$serviceKey = getDolGlobalString ( 'AI_API_SERVICE' );
if ( empty ( $serviceKey ) || $serviceKey === '-1' ) {
return '' ;
}
$services = getListOfAIServices ();
return isset ( $services [ $serviceKey ][ 'label' ]) ? ( string ) $services [ $serviceKey ][ 'label' ] : ( string ) $serviceKey ;
}
2026-06-13 02:16:48 +00:00
/**
* Build the configuration array consumed by the AI Assistant chat frontend ( ai / js / ai_assistant . js ) .
* It is serialized as JSON into the data - ai - config attribute of the chat container .
*
2026-06-13 23:05:01 +00:00
* @ return array { mode : string , labels : array < string , string > , baseUrl : string , token : string , userInitial : string }
2026-06-13 02:16:48 +00:00
*/
function getAiChatAssistantConfig ()
{
2026-06-13 23:05:01 +00:00
global $langs , $user ;
2026-06-13 02:16:48 +00:00
$keys = array (
// General UI
'NoDataAvailable' ,
'Error' ,
'NoRecordFound' ,
'Download' ,
'Show' ,
'Confirm' ,
'ConfirmAiAction' ,
'ClearChatHistoryTitle' ,
'HistoryCleared' ,
'Send' ,
'TypeYourQuestion' ,
// Placeholders & Status
'TypeOrSpeak' ,
'UploadLocalDoc' ,
'UploadCloudDoc' ,
'DocLoaded' ,
'Listening' ,
'Transcribed' ,
'NoSpeech' ,
'ProcessingAudio' ,
'Timeout' ,
'Cancelled' ,
// Engine Specific
'CloudSpeechReady' ,
'WhisperReady' ,
'DownloadingModel' ,
'ModelLoading' ,
// Document Processing
'ProcessingFile' ,
'ReadingPdf' ,
'PdfError' ,
'UnsupportedFileType' ,
'TryingOCR' ,
'OcrProgress' ,
'SwitchingAIModel' ,
'OcrFailed' ,
'ReadingWord' ,
'ReadingExcel' ,
'ReadingOdf' ,
// Errors
'MicError' ,
'MicTooQuiet' ,
'ConnectionBlocked' ,
'ConnectionBlockedHelp' ,
'WorkerInitFailed' ,
'NetworkError' ,
'AIError' ,
'EmptyAIResponse' ,
'BrowserNotSupported' ,
// Actions & Dialogs
'YesProceed' ,
'Cancel' ,
'Submit' ,
'ActionCancelled' ,
'ExecutingTool' ,
'FetchingData' ,
'GeneratingLink' ,
'Found' ,
'TypeResponse' ,
'OpenVerb' ,
// Voice Confirmation
'VoiceYesNo' ,
'VoiceQuiet' ,
'PleaseRepeat' ,
'HeardText' ,
// Context
'DocContextIntro' ,
'DocContextOutro'
);
$ai_translations = array ();
foreach ( $keys as $key ) {
$ai_translations [ $key ] = $langs -> transnoentitiesnoconv ( $key );
}
$ai_translations [ 'DownloadPdf' ] = $langs -> transnoentitiesnoconv ( " Download " ) . ' PDF' ;
$ai_translations [ 'CloudVoiceRequiresSecureContext' ] = $langs -> trans (
" CloudVoiceRequiresSecureContext " ,
" HTTPS " ,
" localhost " ,
" Whisper "
);
2026-06-13 23:05:01 +00:00
// First letter of the current user name, shown in the "user" message avatar
$userinitial = '' ;
if ( is_object ( $user )) {
$namesource = $user -> firstname ? $user -> firstname : ( $user -> login ? $user -> login : '' );
$userinitial = dol_strtoupper ( dol_substr ( $namesource , 0 , 1 ));
}
2026-06-13 02:16:48 +00:00
return array (
'mode' => getDolGlobalString ( 'AI_DEFAULT_INPUT_MODE' ),
'labels' => $ai_translations ,
// Endpoints are called with absolute URLs so the chat also works when
// injected into another page (topbar popover) and not only when served
// from /ai/assistant/index.php.
'baseUrl' => dol_buildpath ( '/ai/assistant/' , 1 ),
'token' => newToken (),
2026-06-13 23:05:01 +00:00
'userInitial' => $userinitial ,
2026-06-13 02:16:48 +00:00
);
}
/**
* Build the HTML of the AI Assistant chat interface .
* Shared by the standalone page ( ai / assistant / index . php ) and the topbar popover
* fragment ( ai / assistant / popover . php ) so both render the exact same chat .
*
* @ param string $mode 'page' for the standalone full page , 'popover' for the topbar popover fragment
* @ return string HTML content
*/
function getAiChatAssistantHtml ( $mode = 'page' )
{
2026-07-24 04:54:27 +00:00
global $langs , $user , $conf ;
2026-06-13 02:16:48 +00:00
$out = '' ;
// Config travels as a data attribute: <script> tags injected via innerHTML
// are never executed by the browser, so a window.AI_CONFIG inline script
// would not work for the AJAX-loaded popover.
$out .= '<div class="ai-chat-container' . ( $mode === 'popover' ? ' ai-in-popover' : '' ) . '"' ;
$out .= ' data-ai-config="' . dol_escape_htmltag ( json_encode ( getAiChatAssistantConfig ())) . '"' ;
if ( $mode === 'page' ) {
$out .= ' data-ai-autoinit="1"' ;
}
$out .= '>' ;
// Header
$out .= '<div class="chat-header">' ;
if ( $mode === 'popover' ) {
// In the popover the title links to the full standalone page
2026-06-13 23:05:01 +00:00
$title = img_picto ( '' , 'fa-robot' , '' , 0 , 0 , 0 , '' , 'paddingright' ) . $langs -> trans ( " AIAssistant " );
2026-06-13 02:16:48 +00:00
$title = '<a href="' . dol_buildpath ( '/ai/assistant/index.php' , 1 ) . '" class="ai-header-link" title="' . dol_escape_htmltag ( $langs -> trans ( " AIOpenFullPage " )) . '">' . $title . '</a>' ;
2026-06-13 23:05:01 +00:00
$out .= '<h2>' . $title . '</h2>' ;
} else {
// Full page: assistant identity (avatar + title + current LLM model)
$out .= '<div class="chat-header-id">' ;
$out .= '<span class="chat-header-avatar">' . img_picto ( '' , 'fa-robot' ) . '</span>' ;
$out .= '<span class="chat-header-text">' ;
$out .= '<span class="chat-header-title">' . $langs -> trans ( " AIAssistant " ) . '</span>' ;
$aiprovider = getAiAssistantProviderLabel ();
if ( $aiprovider !== '' ) {
$out .= '<span class="chat-header-status" title="' . dol_escape_htmltag ( $langs -> trans ( " AIProviderInUse " )) . '">' . dol_escape_htmltag ( $aiprovider ) . '</span>' ;
}
$out .= '</span>' ;
$out .= '</div>' ;
2026-06-13 02:16:48 +00:00
}
$out .= '<div class="header-controls">' ;
2026-06-13 23:05:01 +00:00
// Engine Switcher (restyled as a pill with a sparkle icon)
2026-06-13 02:16:48 +00:00
$out .= '<select id="engine-select" class="engine-select">' ;
$out .= '<option value="text">' . $langs -> transnoentitiesnoconv ( " OptionTextOnly " ) . '</option>' ;
$out .= '<option value="cloud">' . $langs -> transnoentitiesnoconv ( " OptionCloudFast " ) . '</option>' ;
$out .= '<option value="whisper">' . $langs -> transnoentitiesnoconv ( " OptionWhisperLocal " ) . '</option>' ;
$out .= '<option value="local_docs">' . $langs -> transnoentitiesnoconv ( " OptionLocalParsing " ) . '</option>' ;
$out .= '<option value="cloud_docs">' . $langs -> transnoentitiesnoconv ( " OptionCloudParsing " ) . '</option>' ;
$out .= '</select>' ;
2026-06-13 23:05:01 +00:00
// Clear Button
$out .= '<button type="button" id="clear-btn" class="icon-btn" title="' . dol_escape_htmltag ( $langs -> trans ( " ClearChatHistoryTitle " )) . '">' ;
$out .= img_picto ( '' , 'fa-trash' ) . ' <span class="ai-btn-label">' . $langs -> trans ( " Clear " ) . '</span>' ;
$out .= '</button>' ;
2026-06-13 02:16:48 +00:00
if ( $mode === 'popover' ) {
// Window controls of the popover (handled by the bootstrap JS in main.inc.php)
$out .= '<button type="button" id="ai-expand-btn" class="icon-btn ai-window-btn" title="' . dol_escape_htmltag ( $langs -> trans ( " AIExpandPanel " )) . '" data-title-expand="' . dol_escape_htmltag ( $langs -> trans ( " AIExpandPanel " )) . '" data-title-reduce="' . dol_escape_htmltag ( $langs -> trans ( " AIReducePanel " )) . '"><i class="fa fa-expand-alt"></i></button>' ;
$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>' ;
}
$out .= '</div>' ;
$out .= '</div>' ;
// Chat History
$out .= '<div id="chat-history" class="chat-history">' ;
2026-06-13 23:05:01 +00:00
if ( $mode === 'popover' ) {
// Compact greeting line for the narrow popover
$out .= '<div class="msg system">' . $langs -> trans ( " AIWelcomeMessage " ) . '</div>' ;
} else {
// Full page: rich empty-state welcome screen (hidden by JS as soon as the
// conversation starts, restored on Clear). Quick cards send a localized
// ready-made prompt on click (data-prompt).
$welcomename = $user -> firstname ? $user -> firstname : ( is_object ( $user ) ? $user -> login : '' );
$quickcards = array (
array ( 'icon' => 'fa-file-invoice-dollar' , 'key' => 'Invoices' ),
array ( 'icon' => 'fa-chart-line' , 'key' => 'Revenue' ),
array ( 'icon' => 'fa-coins' , 'key' => 'Finance' ),
array ( 'icon' => 'fa-warehouse' , 'key' => 'Inventory' ),
);
$out .= '<div class="chat-welcome">' ;
$out .= '<div class="chat-welcome-avatar">' . img_picto ( '' , 'fa-robot' ) . '</div>' ;
$out .= '<h2 class="chat-welcome-title">' . dol_escape_htmltag ( $langs -> trans ( " AIGreeting " , $welcomename )) . '</h2>' ;
$out .= '<p class="chat-welcome-subtitle">' . dol_escape_htmltag ( $langs -> trans ( " AIGreetingSubtitle " )) . '</p>' ;
$out .= '<div class="chat-welcome-actions">' ;
foreach ( $quickcards as $card ) {
$out .= '<button type="button" class="ai-quick-card" data-prompt="' . dol_escape_htmltag ( $langs -> transnoentitiesnoconv ( " AIQuick " . $card [ 'key' ] . " Prompt " )) . '">' ;
$out .= '<span class="ai-quick-icon">' . img_picto ( '' , $card [ 'icon' ]) . '</span>' ;
$out .= '<span class="ai-quick-text">' ;
$out .= '<span class="ai-quick-title">' . dol_escape_htmltag ( $langs -> trans ( " AIQuick " . $card [ 'key' ] . " Title " )) . '</span>' ;
$out .= '<span class="ai-quick-desc">' . dol_escape_htmltag ( $langs -> trans ( " AIQuick " . $card [ 'key' ] . " Desc " )) . '</span>' ;
$out .= '</span>' ;
$out .= '</button>' ;
}
$out .= '</div>' ;
$out .= '</div>' ;
}
2026-06-13 02:16:48 +00:00
$out .= '</div>' ;
2026-06-13 23:05:01 +00:00
// Controls: a single rounded "pill" holding the attach/mic buttons, the
// textarea and the send button.
2026-06-13 02:16:48 +00:00
$out .= '<div class="chat-controls">' ;
2026-06-13 23:05:01 +00:00
$out .= '<div class="chat-input-pill">' ;
2026-06-13 02:16:48 +00:00
// Upload Wrapper (Visible only in Doc modes)
2026-07-24 04:54:27 +00:00
$out .= '<div id="upload-wrapper" class="upload-wrapper ai-hidden">' ;
2026-06-13 02:16:48 +00:00
$out .= '<input type="file" id="file-upload" accept=".pdf,.txt,.xml,.png,.jpg,.jpeg,.doc,.docx,.xls,.xlsx,.odt,.ods" style="display: none;">' ;
$out .= '<button type="button" id="upload-btn" class="round-btn" title="' . dol_escape_htmltag ( $langs -> transnoentitiesnoconv ( " AttachFile " )) . '">' . img_picto ( '' , 'fa-paperclip' ) . '</button>' ;
$out .= '</div>' ;
2026-06-13 23:05:01 +00:00
// Microphone Wrapper (Visible only in Voice modes)
2026-07-24 04:54:27 +00:00
$out .= '<div id="mic-wrapper" class="mic-wrapper ai-hidden">' ;
2026-06-13 23:05:01 +00:00
$out .= '<button type="button" id="mic-btn" class="round-btn mic-btn" title="' . dol_escape_htmltag ( $langs -> trans ( " ToggleMicrophone " )) . '">' . img_picto ( '' , 'fa-microphone' ) . '</button>' ;
$out .= '</div>' ;
2026-06-13 02:16:48 +00:00
// Text Input
2026-07-24 04:54:27 +00:00
$out .= '<textarea id="user-input" class="ia-input" rows="1"' ;
if ( empty ( $conf -> dol_optimize_smallscreen )) {
$out .= ' placeholder="' . dol_escape_htmltag ( $langs -> trans ( " TypeYourQuestion " )) . '"' ;
}
$out .= ' autocomplete="off" spellcheck="false"></textarea>' ;
2026-06-13 02:16:48 +00:00
// Send Button
2026-06-13 23:05:01 +00:00
$out .= '<button type="button" id="send-btn" class="chat-send-btn" title="' . dol_escape_htmltag ( $langs -> trans ( " SendPrompt " )) . '">' . img_picto ( '' , 'fa-paper-plane' ) . '</button>' ;
$out .= '</div>' ;
2026-06-13 02:16:48 +00:00
$out .= '</div>' ;
$out .= '<div id="status-bar"></div>' ;
$out .= '</div>' ;
return $out ;
}