dolibarr 22.0.5
ai.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2024 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 * or see https://www.gnu.org/
19 */
20
27require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php";
28require_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
29require_once DOL_DOCUMENT_ROOT."/ai/lib/ai.lib.php";
30
31
35class Ai
36{
40 protected $db;
41
45 private $apiService;
46
50 private $apiKey;
51
55 private $apiEndpoint;
56
57 const AI_DEFAULT_PROMPT_FOR_EMAIL = 'You are an email editor. Return all HTML content inside a section tag. Do not add explanation.';
58 const AI_DEFAULT_PROMPT_FOR_WEBPAGE = 'You are a website editor. Return all HTML content inside a section tag. Do not add explanation.';
59 const AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION = 'You are a translator, answer with one and only one translation with no comment and explanation.';
60 const AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE = 'You are a writer, make the answer in the same language than the original text to summarize.';
61 const AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER = 'You are a writer, give only one answer with no comment and explanation and give the answer in the same language than the original text to rephrase.';
62 const AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER = 'Give only one answer with no comment and explanation, I want the text to be ready to copy and paste.';
63
70 public function __construct($db)
71 {
72 $this->db = $db;
73
74 // Get API key according to enabled AI
75 $this->apiService = getDolGlobalString('AI_API_SERVICE', 'chatgpt');
76 $this->apiKey = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_KEY');
77 }
78
88 public function generateContent($instructions, $model = 'auto', $function = 'textgeneration', $format = '')
89 {
90 global $dolibarr_main_data_root;
91
92 $arrayofai = getListOfAIServices();
93
94 // TODO Can store the need for a key into array returned by getListOfAIServices()
95 if (empty($this->apiKey) && in_array($this->apiService, array('chatgpt', 'groq', 'mistral'))) {
96 return array('error' => true, 'message' => 'API key is not defined for the AI enabled service ('.$this->apiService.')');
97 }
98
99 // $this->apiEndpoint is already set here only if it was previously forced.
100
101 if (empty($this->apiEndpoint) && $this->apiService == 'custom' && !getDolGlobalString('AI_API_CUSTOM_URL')) {
102 return array('error' => true, 'message' => 'API URL is not defined for the AI enabled service ('.$this->apiService.')');
103 }
104
105 // In most cases, it is empty and we must get it from $function and $this->apiService
106 if (empty($this->apiEndpoint)) {
107 // Return the endpoint from $this->apiService.
108 if ($function == 'imagegeneration') {
109 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
110 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'images/generations';
111 } elseif ($function == 'audiogeneration') {
112 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
113 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'audio/speech';
114 } elseif ($function == 'transcription') {
115 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
116 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'transcriptions';
117 } else {
118 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
119 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'chat/completions';
120 }
121 }
122
123 // $model may be undefined or 'auto'.
124 // If this is the case, we must get it from $function and $this->apiService
125 if (empty($model) || $model == 'auto') {
126 // Return the model from $this->apiService.
127 if ($function == 'imagegeneration') {
128 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_IMAGE', $arrayofai[$this->apiService][$function]);
129 } elseif ($function == 'audiogeneration') {
130 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_AUDIO', $arrayofai[$this->apiService][$function]);
131 } elseif ($function == 'transcription') {
132 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSCRIPT', $arrayofai[$this->apiService][$function]);
133 } elseif ($function == 'translation') {
134 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSLATE', $arrayofai[$this->apiService][$function]);
135 } else {
136 // else 'textgenerationemail', 'textgenerationwebpage', 'textgeneration', 'texttranslation', 'textsummarize'
137 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TEXT', $arrayofai[$this->apiService]['textgeneration']);
138 }
139 }
140
141 dol_syslog("Call API for apiKey=".substr($this->apiKey, 0, 5).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model);
142
143 try {
144 if (empty($this->apiEndpoint)) {
145 throw new Exception('The AI service '.$this->apiService.' is not yet supported for the type of request '.$function);
146 }
147
148 $configurationsJson = getDolGlobalString('AI_CONFIGURATIONS_PROMPT');
149 $configurations = json_decode($configurationsJson, true);
150
151 $prePrompt = '';
152 $postPrompt = '';
153
154 if (isset($configurations[$function])) {
155 if (isset($configurations[$function]['prePrompt'])) {
156 $prePrompt = $configurations[$function]['prePrompt'];
157 }
158
159 if (isset($configurations[$function]['postPrompt'])) {
160 $postPrompt = $configurations[$function]['postPrompt'];
161 }
162 }
163
164 // Get the default value of prePrompt if not defined
165 if (empty($prePrompt) && $function == 'textgenerationemail') {
166 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EMAIL;
167 }
168 if (empty($prePrompt) && $function == 'textgenerationwebpage') {
169 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_WEBPAGE;
170 }
171 if (empty($prePrompt) && $function == 'textgenerationextrafield') {
172 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER;
173 }
174 if (empty($prePrompt) && $function == 'texttranslation') {
175 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION;
176 }
177 if (empty($prePrompt) && $function == 'textsummarize') {
178 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE;
179 }
180 if (empty($prePrompt) && $function == 'textrephraser') {
181 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER;
182 }
183
184 $fullInstructions = $instructions.($postPrompt ? (preg_match('/[\.\!\?]$/', $instructions) ? '' : '.').' '.$postPrompt : '');
185
186 // Set payload string
187 /*{
188 "messages": [
189 {
190 "content": "You are a helpful assistant.",
191 "role": "system"
192 },
193 {
194 "content": "Hello!",
195 "role": "user"
196 }
197 ],
198 "model": "tinyllama-1.1b",
199 "stream": true,
200 "max_tokens": 2048,
201 "stop": [
202 "hello"
203 ],
204 "frequency_penalty": 0,
205 "presence_penalty": 0,
206 "temperature": 0.7,
207 "top_p": 0.95
208 }*/
209
210 $arrayforpayload = array(
211 'messages' => array(array('role' => 'user', 'content' => $fullInstructions)),
212 'model' => $model,
213 );
214
215 // Add a system message
216 $addDateTimeContext = false;
217 if ($addDateTimeContext) { // @phpstan-ignore-line
218 $prePrompt = ($prePrompt ? $prePrompt.(preg_match('/[\.\!\?]$/', $prePrompt) ? '' : '.').' ' : '').'Today we are '.dol_print_date(dol_now(), 'dayhourtext');
219 }
220 if ($prePrompt) {
221 $arrayforpayload['messages'][] = array('role' => 'system', 'content' => $prePrompt);
222 }
223
224 /*
225 $arrayforpayload['temperature'] = 0.7;
226 $arrayforpayload['max_tokens'] = -1;
227 $arrayforpayload['stream'] = false;
228 */
229
230 $payload = json_encode($arrayforpayload);
231
232 $headers = array(
233 'Authorization: Bearer ' . $this->apiKey,
234 'Content-Type: application/json'
235 );
236
237 if (getDolGlobalString("AI_DEBUG")) {
238 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
239 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
240 $fp = fopen($outputfile, "w"); // overwrite
241
242 if ($fp) {
243 fwrite($fp, var_export($headers, true)."\n");
244 fwrite($fp, var_export($payload, true)."\n");
245
246 fclose($fp);
247 dolChmod($outputfile);
248 }
249 }
250 }
251
252 $localurl = 2; // Accept both local and external endpoints
253 $response = getURLContent($this->apiEndpoint, 'POST', $payload, 1, $headers, array('http', 'https'), $localurl);
254
255 if (empty($response['http_code'])) {
256 throw new Exception('API request failed. No http received');
257 }
258 if (!empty($response['http_code']) && $response['http_code'] != 200) {
259 if (in_array($response['http_code'], array(400, 401, 403, 429)) && !empty($response['content'])) {
260 $tmp = json_decode($response['content'], true);
261 if (!empty($tmp['message'])) {
262 return array(
263 'error' => true,
264 'message' => $tmp['message'],
265 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
266 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
267 'format' => $format,
268 'service' => $this->apiService,
269 'function' => $function
270 );
271 }
272 }
273 throw new Exception('API request on AI endpoint '.$this->apiEndpoint.' failed with status code '.$response['http_code'].(empty($response['content']) ? '' : ' - '.$response['content']));
274 }
275
276 if (getDolGlobalString("AI_DEBUG")) {
277 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
278 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
279 $fp = fopen($outputfile, "a");
280
281 if ($fp) {
282 fwrite($fp, var_export((empty($response['content']) ? 'No content result' : $response['content']), true)."\n");
283
284 fclose($fp);
285 dolChmod($outputfile);
286 }
287 }
288 }
289
290
291 // Decode JSON response
292 $decodedResponse = json_decode($response['content'], true);
293
294 // Extraction content
295 if (!empty($decodedResponse['error'])) {
296 if (is_scalar($decodedResponse['error'])) {
297 $generatedContent = $decodedResponse['error'];
298 } else {
299 $generatedContent = var_export($decodedResponse['error'], true);
300 }
301 } else {
302 $generatedContent = $decodedResponse['choices'][0]['message']['content'];
303 }
304 dol_syslog("ai->generatedContent returned: ".dol_trunc($generatedContent, 50));
305
306 // If content is not HTML, we convert it into HTML
307 if ($format == 'html') {
308 if (!dol_textishtml($generatedContent)) {
309 dol_syslog("Result was detected as not HTML so we convert it into HTML.");
310 $generatedContent = dol_nl2br($generatedContent);
311 } else {
312 dol_syslog("Result was detected as already HTML. Do nothing.");
313 }
314
315 // TODO If content is for website module, we must
316 // - clan html header, keep body only and remove ``` ticks added by AI
317 // - add tags <section contenEditable="true"> </section>
318 }
319
320 return $generatedContent;
321 } catch (Exception $e) {
322 $errormessage = $e->getMessage();
323 if (!empty($response['content'])) {
324 $decodedResponse = json_decode($response['content'], true);
325
326 // With OpenAI, error is into an object error into the content
327 if (!empty($decodedResponse['error']['message'])) {
328 $errormessage .= ' - '.$decodedResponse['error']['message'];
329 }
330 }
331
332 return array(
333 'error' => true,
334 'message' => $errormessage,
335 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
336 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
337 'format' => $format,
338 'service' => $this->apiService,
339 'function' => $function
340 );
341 }
342 }
343}
getListOfAIServices()
Get list of available ai services.
Definition ai.lib.php:64
Class for AI.
Definition ai.class.php:36
generateContent($instructions, $model='auto', $function='textgeneration', $format='')
Generate response of instructions.
Definition ai.class.php:88
__construct($db)
Constructor.
Definition ai.class.php:70
dol_nl2br($stringtoencode, $nl2brmode=0, $forxml=false)
Replace CRLF in string with a HTML BR tag.
dolChmod($filepath, $newmask='')
Change mod of a file.
dol_now($mode='auto')
Return date for now.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false)
Output date in a string format according to outputlangs (or langs if not defined).
dol_textishtml($msg, $option=0)
Return if a text is a html content.
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.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0)
Function to get a content from an URL (use proxy if proxy defined).