2024-01-17 17:53:35 +01:00
< ? php
2024-03-12 10:01:39 +01:00
/* Copyright ( C ) 2024 Laurent Destailleur < eldy @ users . sourceforge . net >
2024-04-06 17:38:39 +02:00
* Copyright ( C ) 2024 Frédéric France < frederic . france @ free . fr >
2024-09-18 03:27:25 +02:00
* Copyright ( C ) 2024 MDW < mdeweerd @ users . noreply . github . com >
2024-01-17 17:53:35 +01: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 ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* 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 />.
* or see https :// www . gnu . org /
*/
2024-04-06 17:38:39 +02:00
2024-09-18 03:27:25 +02:00
/**
* \file htdocs / ai / class / ai . class . php
* \ingroup ai
* \brief Class files with common methods for Ai
*/
2024-04-06 17:38:39 +02:00
2024-01-29 15:38:13 +01:00
require_once DOL_DOCUMENT_ROOT . " /core/lib/admin.lib.php " ;
2024-03-11 18:00:43 +01:00
require_once DOL_DOCUMENT_ROOT . '/core/lib/geturl.lib.php' ;
2024-01-17 17:53:35 +01:00
/**
* Class for AI
*/
class Ai
{
/**
* @ var DoliDB $db Database object
*/
protected $db ;
2024-03-16 21:33:55 +01:00
2024-01-17 17:53:35 +01:00
/**
2024-04-24 13:07:47 +02:00
* @ var string $apiService
2024-01-17 17:53:35 +01:00
*/
2024-04-24 13:07:47 +02:00
private $apiService ;
2024-01-17 17:53:35 +01:00
/**
* @ var string $apiKey
*/
private $apiKey ;
2024-04-24 13:07:47 +02:00
/**
* @ var string $apiEndpoint
*/
private $apiEndpoint ;
2024-01-17 17:53:35 +01:00
/**
* Constructor
*
2024-01-29 15:38:13 +01:00
* @ param DoliDB $db Database handler
*
2024-01-17 17:53:35 +01:00
*/
2024-01-29 15:38:13 +01:00
public function __construct ( $db )
2024-01-17 17:53:35 +01:00
{
2024-01-29 15:38:13 +01:00
$this -> db = $db ;
2024-03-06 00:46:04 +01:00
2024-04-24 13:07:47 +02:00
// Get API key according to enabled AI
$this -> apiService = getDolGlobalString ( 'AI_API_SERVICE' , 'chatgpt' );
$this -> apiKey = getDolGlobalString ( 'AI_API_' . strtoupper ( $this -> apiService ) . '_KEY' );
2024-01-17 17:53:35 +01:00
}
/**
* Generate response of instructions
2024-03-06 00:46:04 +01:00
*
2024-07-19 15:38:52 +02:00
* @ param string $instructions Instruction to generate content
* @ param string $model Model name ( 'gpt-3.5-turbo' , 'gpt-4-turbo' , 'dall-e-3' , ... )
* @ param string $function Code of the feature we want to use ( 'textgeneration' , 'transcription' , 'audiogeneration' , 'imagegeneration' , 'translation' )
* @ param string $format Format for output ( '' , 'html' , ... )
2024-09-18 03:27:25 +02:00
* @ return string | array { error : bool , message : string , code ? : int , curl_error_no ? : '' | int , format ? : string , service ? : string , function ? : string } $response Text or array if error
2024-01-17 17:53:35 +01:00
*/
2024-03-12 10:01:39 +01:00
public function generateContent ( $instructions , $model = 'auto' , $function = 'textgeneration' , $format = '' )
2024-01-17 17:53:35 +01:00
{
2025-02-16 22:45:14 +01:00
global $dolibarr_main_data_root ;
2024-03-11 20:41:56 +01:00
if ( empty ( $this -> apiKey )) {
2024-07-16 18:08:26 +02:00
return array ( 'error' => true , 'message' => 'API key is not defined for the AI enabled service (' . $this -> apiService . ')' );
2024-03-11 20:41:56 +01:00
}
2024-07-19 15:38:52 +02:00
// $this->apiEndpoint is set here only if forced.
// In most cases, it is empty and we must get it from $function and $this->apiService
2024-03-06 00:46:04 +01:00
if ( empty ( $this -> apiEndpoint )) {
if ( $function == 'imagegeneration' ) {
2024-04-24 13:07:47 +02:00
if ( $this -> apiService == 'chatgpt' ) {
2024-07-16 18:20:23 +02:00
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CHATGPT_URL' , 'https://api.openai.com/v1' ) . '/images/generations' ;
} elseif ( $this -> apiService == 'groq' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_GROK_URL' , 'https://api.groq.com/openai/v1' ) . '/images/generations' ;
} elseif ( $this -> apiService == 'custom' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CUSTOM_URL' , '' ) . '/images/generations' ;
2024-03-06 00:46:04 +01:00
}
2024-07-15 13:40:55 +02:00
} elseif ( $function == 'audiogeneration' ) {
2024-04-24 13:07:47 +02:00
if ( $this -> apiService == 'chatgpt' ) {
2024-07-16 18:20:23 +02:00
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CHATGPT_URL' , 'https://api.openai.com/v1' ) . '/audio/speech' ;
} elseif ( $this -> apiService == 'groq' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_GROK_URL' , 'https://api.groq.com/openai/v1' ) . '/audio/speech' ;
} elseif ( $this -> apiService == 'custom' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CUSTOM_URL' , '' ) . '/audio/speech' ;
2024-03-06 00:46:04 +01:00
}
2024-04-24 13:07:47 +02:00
} elseif ( $function == 'transcription' ) {
if ( $this -> apiService == 'chatgpt' ) {
2024-07-16 18:20:23 +02:00
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CHATGPT_URL' , 'https://api.openai.com/v1' ) . '/transcriptions' ;
} elseif ( $this -> apiService == 'groq' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_GROK_URL' , 'https://api.groq.com/openai/v1' ) . '/transcriptions' ;
} elseif ( $this -> apiService == 'custom' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CUSTOM_URL' , '' ) . '/transcriptions' ;
2024-03-06 00:46:04 +01:00
}
2024-04-24 13:07:47 +02:00
} elseif ( $function == 'translation' ) {
if ( $this -> apiService == 'chatgpt' ) {
2024-07-16 18:20:23 +02:00
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CHATGPT_URL' , 'https://api.openai.com/v1' ) . '/translations' ;
} elseif ( $this -> apiService == 'groq' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_GROK_URL' , 'https://api.groq.com/openai/v1' ) . '/translations' ;
} elseif ( $this -> apiService == 'custom' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CUSTOM_URL' , '' ) . '/translations' ;
2024-04-24 13:07:47 +02:00
}
} else { // else textgeneration...
2024-07-16 18:20:23 +02:00
if ( $this -> apiService == 'chatgpt' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CHATGPT_URL' , 'https://api.openai.com/v1' ) . '/chat/completions' ;
} elseif ( $this -> apiService == 'groq' ) {
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_GROK_URL' , 'https://api.groq.com/openai/v1' ) . '/chat/completions' ;
2024-07-16 18:08:26 +02:00
} elseif ( $this -> apiService == 'custom' ) {
2024-07-16 18:20:23 +02:00
$this -> apiEndpoint = getDolGlobalString ( 'AI_API_CUSTOM_URL' , '' ) . '/chat/completions' ;
2024-03-06 00:46:04 +01:00
}
}
}
2024-07-19 15:38:52 +02:00
// $model may be undefined or 'auto'.
// If this is the case, we must get it from $function and $this->apiService
if ( empty ( $model ) || $model == 'auto' ) {
// Return the endpoint and the model from $this->apiService.
if ( $function == 'imagegeneration' ) {
if ( $this -> apiService == 'chatgpt' ) {
$model = getDolGlobalString ( 'AI_API_CHATGPT_MODEL_IMAGE' , 'dall-e-3' );
} elseif ( $this -> apiService == 'groq' ) {
$model = getDolGlobalString ( 'AI_API_GROK_MODEL_IMAGE' , 'mixtral-8x7b-32768' ); // 'llama3-8b-8192', 'gemma-7b-it'
} elseif ( $this -> apiService == 'custom' ) {
$model = getDolGlobalString ( 'AI_API_CUSTOM_MODEL_IMAGE' , 'dall-e-3' );
}
} elseif ( $function == 'audiogeneration' ) {
if ( $this -> apiService == 'chatgpt' ) {
$model = getDolGlobalString ( 'AI_API_CHATGPT_MODEL_AUDIO' , 'tts-1' );
} elseif ( $this -> apiService == 'groq' ) {
$model = getDolGlobalString ( 'AI_API_GROK_MODEL_AUDIO' , 'mixtral-8x7b-32768' ); // 'llama3-8b-8192', 'gemma-7b-it'
} elseif ( $this -> apiService == 'custom' ) {
$model = getDolGlobalString ( 'AI_API_CUSTOM_MODEL_AUDIO' , 'tts-1' );
}
} elseif ( $function == 'transcription' ) {
if ( $this -> apiService == 'chatgpt' ) {
$model = getDolGlobalString ( 'AI_API_CHATGPT_MODEL_TRANSCRIPT' , 'whisper-1' );
} elseif ( $this -> apiService == 'groq' ) {
$model = getDolGlobalString ( 'AI_API_GROK_MODEL_TRANSCRIPT' , 'mixtral-8x7b-32768' ); // 'llama3-8b-8192', 'gemma-7b-it'
} elseif ( $this -> apiService == 'custom' ) {
2025-02-19 10:59:19 +01:00
$model = getDolGlobalString ( 'AI_API_CUSTOM_MODEL_TRANSCRIPT' , 'whisper-1' );
2024-07-19 15:38:52 +02:00
}
} elseif ( $function == 'translation' ) {
if ( $this -> apiService == 'chatgpt' ) {
$model = getDolGlobalString ( 'AI_API_CHATGPT_MODEL_TRANSLATE' , 'whisper-1' );
} elseif ( $this -> apiService == 'groq' ) {
$model = getDolGlobalString ( 'AI_API_GROK_MODEL_TRANSLATE' , 'mixtral-8x7b-32768' ); // 'llama3-8b-8192', 'gemma-7b-it'
} elseif ( $this -> apiService == 'custom' ) {
2025-02-19 10:59:19 +01:00
$model = getDolGlobalString ( 'AI_API_CUSTOM_MODEL_TRANSLATE' , 'whisper-1' );
2024-07-19 15:38:52 +02:00
}
} else { // else textgeneration...
if ( $this -> apiService == 'chatgpt' ) {
$model = getDolGlobalString ( 'AI_API_CHATGPT_MODEL_TEXT' , 'gpt-3.5-turbo' );
} elseif ( $this -> apiService == 'groq' ) {
$model = getDolGlobalString ( 'AI_API_GROK_MODEL_TEXT' , 'mixtral-8x7b-32768' ); // 'llama3-8b-8192', 'gemma-7b-it'
} elseif ( $this -> apiService == 'custom' ) {
$model = getDolGlobalString ( 'AI_API_CUSTOM_MODEL_TEXT' , 'tinyllama-1.1b' ); // with JAN: 'tinyllama-1.1b', 'mistral-ins-7b-q4'
}
}
}
dol_syslog ( " Call API for apiKey= " . substr ( $this -> apiKey , 0 , 3 ) . '***********, apiEndpoint=' . $this -> apiEndpoint . " , model= " . $model );
2024-03-11 20:41:56 +01:00
2024-01-17 17:53:35 +01:00
try {
2024-04-24 13:07:47 +02:00
if ( empty ( $this -> apiEndpoint )) {
2024-04-24 20:03:46 +02:00
throw new Exception ( 'The AI service ' . $this -> apiService . ' is not yet supported for the type of request ' . $function );
2024-04-24 13:07:47 +02:00
}
2024-03-06 00:46:04 +01:00
$configurationsJson = getDolGlobalString ( 'AI_CONFIGURATIONS_PROMPT' );
2024-02-07 19:41:50 +01:00
$configurations = json_decode ( $configurationsJson , true );
$prePrompt = '' ;
$postPrompt = '' ;
2024-03-06 00:46:04 +01:00
if ( isset ( $configurations [ $function ])) {
if ( isset ( $configurations [ $function ][ 'prePrompt' ])) {
2025-02-16 23:15:51 +01:00
$prePrompt = $configurations [ $function ][ 'prePrompt' ];
2024-02-07 19:41:50 +01:00
}
2024-03-06 00:46:04 +01:00
if ( isset ( $configurations [ $function ][ 'postPrompt' ])) {
$postPrompt = $configurations [ $function ][ 'postPrompt' ];
2024-02-07 19:41:50 +01:00
}
}
2025-02-16 22:45:14 +01:00
$fullInstructions = $instructions . ( $postPrompt ? ( preg_match ( '/[\.\!\?]$/' , $instructions ) ? '' : '.' ) . ' ' . $postPrompt : '' );
2024-03-11 18:03:33 +01:00
2024-07-19 15:38:52 +02:00
// Set payload string
/* {
" messages " : [
{
" content " : " You are a helpful assistant. " ,
" role " : " system "
},
{
" content " : " Hello! " ,
" role " : " user "
}
],
" model " : " tinyllama-1.1b " ,
" stream " : true ,
" max_tokens " : 2048 ,
" stop " : [
" hello "
],
" frequency_penalty " : 0 ,
" presence_penalty " : 0 ,
" temperature " : 0.7 ,
" top_p " : 0.95
} */
2025-02-16 22:45:14 +01:00
$arrayforpayload = array (
'messages' => array ( array ( 'role' => 'user' , 'content' => $fullInstructions )),
2024-07-19 15:38:52 +02:00
'model' => $model ,
2025-02-16 22:45:14 +01:00
);
// Add a system message
2025-02-17 03:49:11 +01:00
$addDateTimeContext = false ;
if ( $addDateTimeContext ) { // @phpstan-ignore-line
2025-02-16 22:45:14 +01:00
$prePrompt = ( $prePrompt ? $prePrompt . ( preg_match ( '/[\.\!\?]$/' , $prePrompt ) ? '' : '.' ) . ' ' : '' ) . 'Today we are ' . dol_print_date ( dol_now (), 'dayhourtext' );
}
2025-02-16 23:15:51 +01:00
if ( $prePrompt ) {
$arrayforpayload [ 'messages' ][] = array ( 'role' => 'system' , 'content' => $prePrompt );
}
2025-02-16 22:45:14 +01:00
/*
$arrayforpayload [ 'temperature' ] = 0.7 ;
$arrayforpayload [ 'max_tokens' ] = - 1 ;
$arrayforpayload [ 'stream' ] = false ;
*/
2024-03-11 18:00:43 +01:00
2025-02-16 22:45:14 +01:00
$payload = json_encode ( $arrayforpayload );
$headers = array (
2024-01-17 17:53:35 +01:00
'Authorization: Bearer ' . $this -> apiKey ,
'Content-Type: application/json'
2025-02-16 22:45:14 +01:00
);
if ( getDolGlobalString ( " AI_DEBUG " )) {
if ( @ is_writable ( $dolibarr_main_data_root )) { // Avoid fatal error on fopen with open_basedir
$outputfile = $dolibarr_main_data_root . " /dolibarr_ai.log " ;
$fp = fopen ( $outputfile , " w " ); // overwrite
if ( $fp ) {
fwrite ( $fp , var_export ( $headers , true ) . " \n " );
fwrite ( $fp , var_export ( $payload , true ) . " \n " );
fclose ( $fp );
dolChmod ( $outputfile );
}
}
}
2024-07-19 15:38:52 +02:00
$localurl = 2 ; // Accept both local and external endpoints
$response = getURLContent ( $this -> apiEndpoint , 'POST' , $payload , 1 , $headers , array ( 'http' , 'https' ), $localurl );
2024-01-17 17:53:35 +01:00
2024-03-16 21:33:55 +01:00
if ( empty ( $response [ 'http_code' ])) {
throw new Exception ( 'API request failed. No http received' );
}
if ( ! empty ( $response [ 'http_code' ]) && $response [ 'http_code' ] != 200 ) {
2024-07-19 15:38:52 +02:00
throw new Exception ( 'API request on AI endpoint ' . $this -> apiEndpoint . ' failed with status code ' . $response [ 'http_code' ] . ( empty ( $response [ 'content' ]) ? '' : ' - ' . $response [ 'content' ]));
}
if ( getDolGlobalString ( " AI_DEBUG " )) {
2025-02-16 22:45:14 +01:00
if ( @ is_writable ( $dolibarr_main_data_root )) { // Avoid fatal error on fopen with open_basedir
$outputfile = $dolibarr_main_data_root . " /dolibarr_ai.log " ;
$fp = fopen ( $outputfile , " a " );
if ( $fp ) {
fwrite ( $fp , var_export (( empty ( $response [ 'content' ]) ? 'No content result' : $response [ 'content' ]), true ) . " \n " );
fclose ( $fp );
dolChmod ( $outputfile );
}
}
2024-01-17 17:53:35 +01:00
}
2024-07-19 15:38:52 +02:00
2024-01-29 15:38:13 +01:00
// Decode JSON response
2024-03-11 18:00:43 +01:00
$decodedResponse = json_decode ( $response [ 'content' ], true );
2024-01-29 15:38:13 +01:00
// Extraction content
2024-04-24 13:07:47 +02:00
$generatedContent = $decodedResponse [ 'choices' ][ 0 ][ 'message' ][ 'content' ];
2024-07-19 15:38:52 +02:00
dol_syslog ( " generatedContent= " . dol_trunc ( $generatedContent , 50 ));
2024-01-17 17:53:35 +01:00
2024-03-12 10:01:39 +01:00
// If content is not HTML, we convert it into HTML
2024-04-24 13:07:47 +02:00
if ( $format == 'html' ) {
if ( ! dol_textishtml ( $generatedContent )) {
dol_syslog ( " Result was detected as not HTML so we convert it into HTML. " );
$generatedContent = dol_nl2br ( $generatedContent );
} else {
dol_syslog ( " Result was detected as already HTML. Do nothing. " );
}
2024-06-06 11:09:43 +02:00
// TODO If content is for website module, we must
// - clan html header, keep body only and remove ``` ticks added by AI
// - add tags <section contenEditable="true"> </section>
2024-03-12 10:01:39 +01:00
}
2024-04-24 13:07:47 +02:00
return $generatedContent ;
2024-01-17 17:53:35 +01:00
} catch ( Exception $e ) {
2024-03-16 21:33:55 +01:00
$errormessage = $e -> getMessage ();
if ( ! empty ( $response [ 'content' ])) {
$decodedResponse = json_decode ( $response [ 'content' ], true );
// With OpenAI, error is into an object error into the content
if ( ! empty ( $decodedResponse [ 'error' ][ 'message' ])) {
$errormessage .= ' - ' . $decodedResponse [ 'error' ][ 'message' ];
}
}
2024-04-24 13:07:47 +02:00
return array (
'error' => true ,
'message' => $errormessage ,
'code' => ( empty ( $response [ 'http_code' ]) ? 0 : $response [ 'http_code' ]),
2024-09-18 03:27:25 +02:00
'curl_error_no' => ( ! empty ( $response [ 'curl_error_no' ]) ? $response [ 'curl_error_no' ] : '' ),
2024-04-24 13:07:47 +02:00
'format' => $format ,
'service' => $this -> apiService ,
2024-09-18 03:27:25 +02:00
'function' => $function
2024-04-24 13:07:47 +02:00
);
2024-01-17 17:53:35 +01:00
}
}
}