dolibarr 24.0.0-beta
ajaxuploadpage.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2005-2017 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2024-2026 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
5 *
6 * This file is a modified version of datepicker.php from phpBSM to fix some
7 * bugs, to add new features and to dramatically increase speed.
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 */
22
28if (!defined('NOTOKENRENEWAL')) {
29 define('NOTOKENRENEWAL', 1); // Disables token renewal
30}
31if (!defined('NOREQUIREMENU')) {
32 define('NOREQUIREMENU', '1');
33}
34if (!defined('NOREQUIREHTML')) {
35 define('NOREQUIREHTML', '1');
36}
37if (!defined('NOREQUIREAJAX')) {
38 define('NOREQUIREAJAX', '1');
39}
40if (!defined('NOHEADERNOFOOTER')) {
41 define('NOHEADERNOFOOTER', '1');
42}
43
44require_once '../../main.inc.php';
52require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
53require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
54require_once DOL_DOCUMENT_ROOT.'/ai/class/ai.class.php';
55
56
57if (GETPOST('lang', 'aZ09')) {
58 $langs->setDefaultLang(GETPOST('lang', 'aZ09')); // If language was forced on URL by the main.inc.php
59}
60
61$langs->loadLangs(array("main", "other", "exports"));
62
63$action = GETPOST('action', 'aZ09');
64$modulepart = GETPOST('modulepart', 'aZ09');
65
66$upload_dir = $conf->user->dir_temp.'/import';
67
68// Delete the temporary files that are used when uploading files
69//dol_delete_file($upload_dir.'/upload_page-by'.$user->id.'-*');
70
71$file = GETPOST('file');
72
73$reg = array();
74if (preg_match('/^upload_page-([a-z_]+)-uid(\d+)-/', $file, $reg)) {
75 $modulepart = $reg[1];
76
77 if ($reg[2] != $user->id) {
78 accessforbidden('User id found in filename to process does not match current user id');
79 }
80} else {
81 accessforbidden('Bad value for file parameter');
82}
83
84$error = 0;
85$errors = array();
86
87$ai = new Ai($db);
88
89
90/*
91 * Actions
92 */
93
94//
95
96
97
98/*
99 * View
100 */
101
102top_httphead('application/json');
103
104$originalfilename = $file;
105$uid = $thiid = $pid = $erid = $salid = 0;
106if (preg_match('/-uid([\d+])/', $file, $reg)) {
107 $uid = $reg[1];
108 $originalfilename = preg_replace('/-uid\d+/', '', $originalfilename);
109}
110if (preg_match('/-thiid([\d+])/', $file, $reg)) {
111 $thiid = $reg[1];
112 $originalfilename = preg_replace('/-thiid\d+/', '', $originalfilename);
113}
114if (preg_match('/-pid([\d+])/', $file, $reg)) {
115 $pid = $reg[1];
116 $originalfilename = preg_replace('/-pid\d+/', '', $originalfilename);
117}
118if (preg_match('/-erid([\d+])/', $file, $reg)) {
119 $erid = $reg[1];
120 $originalfilename = preg_replace('/-erid\d+/', '', $originalfilename);
121}
122if (preg_match('/-salid([\d+])/', $file, $reg)) {
123 $salid = $reg[1];
124 $originalfilename = preg_replace('/-salid\d+/', '', $originalfilename);
125}
126$originalfilename = preg_replace('/^upload_page-[a-z_]+-/', '', $originalfilename);
127
128
129
130
131//$METHOD = 'thread'; // For ChatGPT.
132$METHOD = 'converttotext'; // For Mistral and most others
133
134
135$docformat = $doctypelabel = $prompt = '';
136$fullpathoffile = $upload_dir.'/'.$file;
137$answer = null;
138$fileContent = '';
139
140// Set the prompting
141if ($modulepart == 'invoice_supplier') {
142 $docformat = 'pdf';
143 $doctypelabel = 'invoice';
144
145 // Call IA to get information on PDF
146 $prompt = 'Analyze the contents of this '.$docformat.' document of an '.$doctypelabel.' and convert all readable text and numerical information into a structured JSON format. Follow these guidelines:
147
148 1. **Hierarchical Structure:** Group related information into logical categories like "document_info", "vendor or issuer", "recipient", "items", "payment_info" and "other" depending on the content.
149 2. **Flexible Field Identification:** Identify and organize common fields that are found such as:
150 - "invoice number", "invoice reference"
151 - "date", "issue date", "due date", "transaction or invoice date"
152 - "vendor name", "vendor vat number", "vendor professional id (siret, siren, ...), "vendor address", "vendor phone", "vendor email",
153 - "items", "products", "services", "product or service label", "product or service ref", "product or service ref"
154 - "totals", "summary", "amounts", "balance"
155 - "notes," "comments," "messages," "terms"
156 3. **Handle Tables and Lists:** If the document contains tables, represent each row as an object in a list with fields like "no", "ref", "description", "quantity", "vat rate", "unit price", "total excluding tax", "total including tax".
157 4. **Normalize Dates:** If dates are present, format them in ISO format (YYYY-MM-DD) whenever possible.
158 5. **Ignore Background Noise:** Exclude background noise, decorative elements, and irrelevant symbols that do not contribute to the data content.
159 6. **Preserve Context:** If the image contains sections, headings, or grouping indicators, use them to create logical hierarchies in the JSON structure.
160 7. **General Usability:** Format the text to be suitable for further processing, analysis, or database import.
161
162 **Example JSON Structure:**
163
164 {
165 "document_info": {
166 "document_ref": "<document ref or number>",
167 "date": "<date>",
168 "title": "<title>"
169 },
170 "vendor": {
171 "name": "<name>",
172 "address": "<address>",
173 "phone": "<phone number>",
174 "email": "<email>",
175 "vatnumber": "<vat number>",
176 "profid": "<professional id>"
177 },
178 "recipient": {
179 "name": "<name>",
180 "address": "<address>",
181 "phone": "<phone number>",
182 "email": "<email>",
183 "vatnumber": "<vat number>",
184 "profid": "<professional id>"
185 },
186 "items": [
187 {
188 "no": "<number>",
189 "ref": "<ref>",
190 "label": "<label>",
191 "description": "<description>",
192 "quantity": "<quantity>",
193 "vatrate": "<vat rate>",
194 "totalinctax": "<total including tax>"
195 "totalexcltax": "<total excluding tax>"
196 }
197 ],
198 "summary": {
199 "subtotal": "<subtotal>",
200 "tax": "<amount of tax>",
201 "total": "<total to pay>"
202 },
203 "payment_methods": [
204 {
205 "method": "<check or cash or card or direct_debit or credit_transfer or other>",
206 "details": "Detail of the payment mode"
207 }
208 ],
209 "payments_done": [
210 {
211 "method": "<check or cash or card or direct_debit or credit_transfer or other>",
212 "amount": "<detail of the payment>",
213 "note":"<other information on payment done>"
214 }
215 ],
216 "notes": "<optional text>"
217 }
218 ';
219}
220
221
222// TODO Move this into an AJAX service and just output the JS code to call the aajax to start
223
224if ($METHOD == 'converttotext') { // @phpstan-ignore-line
225 $result = dolDocToText($fullpathoffile, '', 'fulltext');
226 if (empty($result['error'])) {
227 $fileContent = $result['content'];
228 }
229
230 if ($fileContent) {
231 $prompt = 'This is the content of the document:'."\n\n".substr($fileContent, 0, 12000)."\n\nQuestion: ".$prompt;
232
233 $result = $ai->generateContent($prompt, 'auto', 'docparsing', '');
234 // $result is an array of error messages or a string with answer
235
236 if (is_array($result)) { // If array, there is an error
237 if ($result['error']) {
238 $error++;
239 $errors[] = $result['error'];
240 }
241 if ($result['curl_error_no']) {
242 $error++;
243 $errors[] = $result['curl_error_no'];
244 }
245 } else {
246 $answer = $result;
247 }
248 } else {
249 $errors[] = 'Failed to convert document into TXT';
250 }
251}
252
253
254if ($METHOD == 'thread') { // @phpstan-ignore-line
255 $prompt = '';
256
257 $fileId = 0;
258 $assistantId = 0;
259 $threadId = 0;
260 $runId = 0;
261
262
263 // First part is to send the file
264 $payload = array(
265 "purpose" => "assistants",
266 "file" => new CURLFile($fullpathoffile)
267 );
268
269 $result = $ai->generateContent($payload, 'auto', 'file', '');
270
271 if (is_array($result)) { // If array, there is an error
272 if ($result['error']) {
273 $error++;
274 $errors[] = $result['error'];
275 }
276 if ($result['curl_error_no']) {
277 $error++;
278 $errors[] = $result['curl_error_no'];
279 }
280 } else {
281 $fileId = json_decode($result, true)['id'];
282 }
283
284
285 // Create assistant
286 if (!$error) {
287 $payload = [
288 "name" => "PDF Analyzer",
289 "instructions" => "Analyze PDF and answer precisely",
290 "tools" => [
291 ["type" => "file_search"]
292 ]
293 ];
294
295 $result = $ai->generateContent($payload, 'auto', 'assistant', '', array('OpenAI-Beta', 'assistants=v2'));
296
297 if (is_array($result)) { // If array, there is an error
298 if ($result['error']) {
299 $error++;
300 $errors[] = $result['error'];
301 }
302 if ($result['curl_error_no']) {
303 $error++;
304 $errors[] = $result['curl_error_no'];
305 }
306 } else {
307 $assistantId = json_decode($result, true)['id'];
308 }
309 }
310
311
312 // Create thread
313 if (!$error) {
314 $payload = '{}';
315
316 $result = $ai->generateContent($payload, 'auto', 'thread', '', array('OpenAI-Beta', 'assistants=v2'));
317
318 if (is_array($result)) { // If array, there is an error
319 if ($result['error']) {
320 $error++;
321 $errors[] = $result['error'];
322 }
323 if ($result['curl_error_no']) {
324 $error++;
325 $errors[] = $result['curl_error_no'];
326 }
327 } else {
328 $threadId = json_decode($result, true)['id'];
329 }
330 }
331
332
333 // Add file to thread
334 if (!$error) {
335 $payload = array(
336 "role" => "user",
337 "content" => [
338 [
339 "type" => "input_text",
340 "text" => $prompt
341 ]
342 ],
343 "attachments" => [
344 [
345 "file_id" => $fileId,
346 "tools" => [["type" => "file_search"]]
347 ]
348 ]
349 );
350 $moreendpoint = $threadId.'/messages';
351
352 $result = $ai->generateContent($payload, 'auto', 'thread', '', array('OpenAI-Beta', 'assistants=v2'), $moreendpoint);
353
354 if (is_array($result)) { // If array, there is an error
355 if ($result['error']) {
356 $error++;
357 $errors[] = $result['error'];
358 }
359 if ($result['curl_error_no']) {
360 $error++;
361 $errors[] = $result['curl_error_no'];
362 }
363 }
364 }
365
366
367 // Run the thread
368 if (!$error) {
369 $payload = array(
370 ["assistant_id" => $assistantId]
371 );
372 $moreendpoint = $threadId.'/runs';
373
374 $result = $ai->generateContent($payload, 'auto', 'thread', '', array('OpenAI-Beta', 'assistants=v2'), $moreendpoint);
375
376 if (is_array($result)) { // If array, there is an error
377 if ($result['error']) {
378 $error++;
379 $errors[] = $result['error'];
380 }
381 if ($result['curl_error_no']) {
382 $error++;
383 $errors[] = $result['curl_error_no'];
384 }
385 } else {
386 $runId = json_decode($result, true)['id'];
387 }
388 }
389
390
391 // Poll until end of thread
392 if (!$error) {
393 do {
394 sleep(1);
395
396 $payload = '';
397 $moreendpoint = $threadId.'/runs/'.$runId;
398
399 $result = $ai->generateContent($payload, 'auto', 'thread', '', array('OpenAI-Beta', 'assistants=v2'), $moreendpoint);
400
401 if (is_array($result)) { // If array, there is an error
402 if ($result['error']) {
403 $error++;
404 $errors[] = $result['error'];
405 }
406 if ($result['curl_error_no']) {
407 $error++;
408 $errors[] = $result['curl_error_no'];
409 }
410
411 $status = 'completed';
412 } else {
413 $status = json_decode($result, true)['status'];
414 }
415 } while ($status !== "completed");
416 }
417
418
419 // Get answer
420 if (!$error) {
421 $payload = '';
422 $moreendpoint = $threadId.'/messages';
423
424 $result = $ai->generateContent($prompt, 'auto', 'thread', '', array('OpenAI-Beta', 'assistants=v2'));
425
426 if (is_array($result)) { // If array, there is an error
427 if ($result['error']) {
428 $error++;
429 $errors[] = $result['error'];
430 }
431 if ($result['curl_error_no']) {
432 $error++;
433 $errors[] = $result['curl_error_no'];
434 }
435 } else {
436 $answer = $result;
437 }
438 }
439}
440
441
442// End of page
443
444$db->close();
445
446
447if (!empty($errors)) {
448 http_response_code(500);
449
450 print json_encode(array('errors' => $errors));
451} else {
452 $data = json_decode((string) $answer, true);
453
454 if ($data == null) {
455 $error++;
456 $errors[] = 'Failed to decode answer';
457 print 'Failed to decode answer';
458 } else {
459 print $answer;
460 }
461}
Class for AI feature.
Definition ai.class.php:36
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $db
API class for accounts.
dolDocToText($filetoprocess, $useFullTextIndexation='pdftotext', $options='html')
GETPOST($paramname, $check='alphanohtml', $method=0, $filter=null, $options=null, $noreplace=0)
Return value of a param into GET or POST supervariable.
if(!defined( 'NOREQUIREMENU')) if(!empty(GETPOST('seteventmessages', 'alpha'))) if(!function_exists("llxHeader")) top_httphead($contenttype='text/html', $forcenocache=0)
Show HTTP header.
accessforbidden($message='', $printheader=1, $printfooter=1, $showonlymessage=0, $params=null)
Show a message to say access is forbidden and stop program.