dolibarr 21.0.0-beta
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';
29
30
34class Ai
35{
39 protected $db;
40
44 private $apiService;
45
49 private $apiKey;
50
54 private $apiEndpoint;
55
56
63 public function __construct($db)
64 {
65 $this->db = $db;
66
67 // Get API key according to enabled AI
68 $this->apiService = getDolGlobalString('AI_API_SERVICE', 'chatgpt');
69 $this->apiKey = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_KEY');
70 }
71
81 public function generateContent($instructions, $model = 'auto', $function = 'textgeneration', $format = '')
82 {
83 if (empty($this->apiKey)) {
84 return array('error' => true, 'message' => 'API key is not defined for the AI enabled service ('.$this->apiService.')');
85 }
86
87 // $this->apiEndpoint is set here only if forced.
88 // In most cases, it is empty and we must get it from $function and $this->apiService
89 if (empty($this->apiEndpoint)) {
90 if ($function == 'imagegeneration') {
91 if ($this->apiService == 'chatgpt') {
92 $this->apiEndpoint = getDolGlobalString('AI_API_CHATGPT_URL', 'https://api.openai.com/v1').'/images/generations';
93 } elseif ($this->apiService == 'groq') {
94 $this->apiEndpoint = getDolGlobalString('AI_API_GROK_URL', 'https://api.groq.com/openai/v1').'/images/generations';
95 } elseif ($this->apiService == 'custom') {
96 $this->apiEndpoint = getDolGlobalString('AI_API_CUSTOM_URL', '').'/images/generations';
97 }
98 } elseif ($function == 'audiogeneration') {
99 if ($this->apiService == 'chatgpt') {
100 $this->apiEndpoint = getDolGlobalString('AI_API_CHATGPT_URL', 'https://api.openai.com/v1').'/audio/speech';
101 } elseif ($this->apiService == 'groq') {
102 $this->apiEndpoint = getDolGlobalString('AI_API_GROK_URL', 'https://api.groq.com/openai/v1').'/audio/speech';
103 } elseif ($this->apiService == 'custom') {
104 $this->apiEndpoint = getDolGlobalString('AI_API_CUSTOM_URL', '').'/audio/speech';
105 }
106 } elseif ($function == 'transcription') {
107 if ($this->apiService == 'chatgpt') {
108 $this->apiEndpoint = getDolGlobalString('AI_API_CHATGPT_URL', 'https://api.openai.com/v1').'/transcriptions';
109 } elseif ($this->apiService == 'groq') {
110 $this->apiEndpoint = getDolGlobalString('AI_API_GROK_URL', 'https://api.groq.com/openai/v1').'/transcriptions';
111 } elseif ($this->apiService == 'custom') {
112 $this->apiEndpoint = getDolGlobalString('AI_API_CUSTOM_URL', '').'/transcriptions';
113 }
114 } elseif ($function == 'translation') {
115 if ($this->apiService == 'chatgpt') {
116 $this->apiEndpoint = getDolGlobalString('AI_API_CHATGPT_URL', 'https://api.openai.com/v1').'/translations';
117 } elseif ($this->apiService == 'groq') {
118 $this->apiEndpoint = getDolGlobalString('AI_API_GROK_URL', 'https://api.groq.com/openai/v1').'/translations';
119 } elseif ($this->apiService == 'custom') {
120 $this->apiEndpoint = getDolGlobalString('AI_API_CUSTOM_URL', '').'/translations';
121 }
122 } else { // else textgeneration...
123 if ($this->apiService == 'chatgpt') {
124 $this->apiEndpoint = getDolGlobalString('AI_API_CHATGPT_URL', 'https://api.openai.com/v1').'/chat/completions';
125 } elseif ($this->apiService == 'groq') {
126 $this->apiEndpoint = getDolGlobalString('AI_API_GROK_URL', 'https://api.groq.com/openai/v1').'/chat/completions';
127 } elseif ($this->apiService == 'custom') {
128 $this->apiEndpoint = getDolGlobalString('AI_API_CUSTOM_URL', '').'/chat/completions';
129 }
130 }
131 }
132
133 // $model may be undefined or 'auto'.
134 // If this is the case, we must get it from $function and $this->apiService
135 if (empty($model) || $model == 'auto') {
136 // Return the endpoint and the model from $this->apiService.
137 if ($function == 'imagegeneration') {
138 if ($this->apiService == 'chatgpt') {
139 $model = getDolGlobalString('AI_API_CHATGPT_MODEL_IMAGE', 'dall-e-3');
140 } elseif ($this->apiService == 'groq') {
141 $model = getDolGlobalString('AI_API_GROK_MODEL_IMAGE', 'mixtral-8x7b-32768'); // 'llama3-8b-8192', 'gemma-7b-it'
142 } elseif ($this->apiService == 'custom') {
143 $model = getDolGlobalString('AI_API_CUSTOM_MODEL_IMAGE', 'dall-e-3');
144 }
145 } elseif ($function == 'audiogeneration') {
146 if ($this->apiService == 'chatgpt') {
147 $model = getDolGlobalString('AI_API_CHATGPT_MODEL_AUDIO', 'tts-1');
148 } elseif ($this->apiService == 'groq') {
149 $model = getDolGlobalString('AI_API_GROK_MODEL_AUDIO', 'mixtral-8x7b-32768'); // 'llama3-8b-8192', 'gemma-7b-it'
150 } elseif ($this->apiService == 'custom') {
151 $model = getDolGlobalString('AI_API_CUSTOM_MODEL_AUDIO', 'tts-1');
152 }
153 } elseif ($function == 'transcription') {
154 if ($this->apiService == 'chatgpt') {
155 $model = getDolGlobalString('AI_API_CHATGPT_MODEL_TRANSCRIPT', 'whisper-1');
156 } elseif ($this->apiService == 'groq') {
157 $model = getDolGlobalString('AI_API_GROK_MODEL_TRANSCRIPT', 'mixtral-8x7b-32768'); // 'llama3-8b-8192', 'gemma-7b-it'
158 } elseif ($this->apiService == 'custom') {
159 $model = getDolGlobalString('AI_API_CUSTOM_TRANSCRIPT', 'whisper-1');
160 }
161 } elseif ($function == 'translation') {
162 if ($this->apiService == 'chatgpt') {
163 $model = getDolGlobalString('AI_API_CHATGPT_MODEL_TRANSLATE', 'whisper-1');
164 } elseif ($this->apiService == 'groq') {
165 $model = getDolGlobalString('AI_API_GROK_MODEL_TRANSLATE', 'mixtral-8x7b-32768'); // 'llama3-8b-8192', 'gemma-7b-it'
166 } elseif ($this->apiService == 'custom') {
167 $model = getDolGlobalString('AI_API_CUSTOM_TRANSLATE', 'whisper-1');
168 }
169 } else { // else textgeneration...
170 if ($this->apiService == 'chatgpt') {
171 $model = getDolGlobalString('AI_API_CHATGPT_MODEL_TEXT', 'gpt-3.5-turbo');
172 } elseif ($this->apiService == 'groq') {
173 $model = getDolGlobalString('AI_API_GROK_MODEL_TEXT', 'mixtral-8x7b-32768'); // 'llama3-8b-8192', 'gemma-7b-it'
174 } elseif ($this->apiService == 'custom') {
175 $model = getDolGlobalString('AI_API_CUSTOM_MODEL_TEXT', 'tinyllama-1.1b'); // with JAN: 'tinyllama-1.1b', 'mistral-ins-7b-q4'
176 }
177 }
178 }
179
180 dol_syslog("Call API for apiKey=".substr($this->apiKey, 0, 3).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model);
181
182 try {
183 if (empty($this->apiEndpoint)) {
184 throw new Exception('The AI service '.$this->apiService.' is not yet supported for the type of request '.$function);
185 }
186
187 $configurationsJson = getDolGlobalString('AI_CONFIGURATIONS_PROMPT');
188 $configurations = json_decode($configurationsJson, true);
189
190 $prePrompt = '';
191 $postPrompt = '';
192
193 if (isset($configurations[$function])) {
194 if (isset($configurations[$function]['prePrompt'])) {
195 $prePrompt = $configurations[$function]['prePrompt']; // TODO We can send prePrompt into a separated message with role system.
196 }
197
198 if (isset($configurations[$function]['postPrompt'])) {
199 $postPrompt = $configurations[$function]['postPrompt'];
200 }
201 }
202 $fullInstructions = ($prePrompt ? $prePrompt.' ' : '').$instructions.($postPrompt ? '. '.$postPrompt : '');
203
204 // Set payload string
205 /*{
206 "messages": [
207 {
208 "content": "You are a helpful assistant.",
209 "role": "system"
210 },
211 {
212 "content": "Hello!",
213 "role": "user"
214 }
215 ],
216 "model": "tinyllama-1.1b",
217 "stream": true,
218 "max_tokens": 2048,
219 "stop": [
220 "hello"
221 ],
222 "frequency_penalty": 0,
223 "presence_penalty": 0,
224 "temperature": 0.7,
225 "top_p": 0.95
226 }*/
227 $payload = json_encode([
228 'messages' => [
229 ['role' => 'user', 'content' => $fullInstructions]
230 ],
231 'model' => $model,
232 //'stream' => false
233 ]);
234
235 $headers = ([
236 'Authorization: Bearer ' . $this->apiKey,
237 'Content-Type: application/json'
238 ]);
239
240 $localurl = 2; // Accept both local and external endpoints
241 $response = getURLContent($this->apiEndpoint, 'POST', $payload, 1, $headers, array('http', 'https'), $localurl);
242
243 if (empty($response['http_code'])) {
244 throw new Exception('API request failed. No http received');
245 }
246 if (!empty($response['http_code']) && $response['http_code'] != 200) {
247 throw new Exception('API request on AI endpoint '.$this->apiEndpoint.' failed with status code '.$response['http_code'].(empty($response['content']) ? '' : ' - '.$response['content']));
248 }
249
250 if (getDolGlobalString("AI_DEBUG")) {
251 dol_syslog("response content = ".var_export($response['content'], true));
252 }
253
254 // Decode JSON response
255 $decodedResponse = json_decode($response['content'], true);
256
257 // Extraction content
258 $generatedContent = $decodedResponse['choices'][0]['message']['content'];
259
260 dol_syslog("generatedContent=".dol_trunc($generatedContent, 50));
261
262 // If content is not HTML, we convert it into HTML
263 if ($format == 'html') {
264 if (!dol_textishtml($generatedContent)) {
265 dol_syslog("Result was detected as not HTML so we convert it into HTML.");
266 $generatedContent = dol_nl2br($generatedContent);
267 } else {
268 dol_syslog("Result was detected as already HTML. Do nothing.");
269 }
270
271 // TODO If content is for website module, we must
272 // - clan html header, keep body only and remove ``` ticks added by AI
273 // - add tags <section contenEditable="true"> </section>
274 }
275
276 return $generatedContent;
277 } catch (Exception $e) {
278 $errormessage = $e->getMessage();
279 if (!empty($response['content'])) {
280 $decodedResponse = json_decode($response['content'], true);
281
282 // With OpenAI, error is into an object error into the content
283 if (!empty($decodedResponse['error']['message'])) {
284 $errormessage .= ' - '.$decodedResponse['error']['message'];
285 }
286 }
287
288 return array(
289 'error' => true,
290 'message' => $errormessage,
291 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
292 'curl_error_no' => (!empty($response['curl_error_no']) ? $response['curl_error_no'] : ''),
293 'format' => $format,
294 'service' => $this->apiService,
295 'function' => $function
296 );
297 }
298 }
299}
Class for AI.
Definition ai.class.php:35
generateContent($instructions, $model='auto', $function='textgeneration', $format='')
Generate response of instructions.
Definition ai.class.php:81
__construct($db)
Constructor.
Definition ai.class.php:63
dol_nl2br($stringtoencode, $nl2brmode=0, $forxml=false)
Replace CRLF in string with a HTML BR tag.
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)
Function to get a content from an URL (use proxy if proxy defined).