AI progress

This commit is contained in:
Adam 2025-10-10 02:26:27 +02:00
parent 66c934e950
commit 155702b653
10 changed files with 322 additions and 62 deletions

View File

@ -1,4 +1,6 @@
using FruitBank.Common.Entities; using DevExpress.Pdf.Native;
using FruitBank.Common;
using FruitBank.Common.Entities;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -11,6 +13,7 @@ using Nop.Web.Areas.Admin.Controllers;
using Nop.Web.Framework; using Nop.Web.Framework;
using Nop.Web.Framework.Mvc.Filters; using Nop.Web.Framework.Mvc.Filters;
using System.Text; using System.Text;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
@ -135,34 +138,144 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
[HttpPost] [HttpPost]
[RequestSizeLimit(10485760)] // 10MB [RequestSizeLimit(10485760)] // 10MB
[RequestFormLimits(MultipartBodyLengthLimit = 10485760)] [RequestFormLimits(MultipartBodyLengthLimit = 10485760)]
public async Task<IActionResult> UploadFile(UploadModel model) public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId, int? partnerId)
{
var files = model.Files;
var shippingDocumentId = model.ShippingDocumentId;
try
{ {
//checks
// - files exist
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Json(new { success = false, errorMessage = "Access denied" }); return Json(new { success = false, errorMessage = "Access denied" });
// - permissions
if (files == null || files.Count == 0) if (files == null || files.Count == 0)
return Json(new { success = false, errorMessage = "No file selected" }); return Json(new { success = false, errorMessage = "No file selected" });
// - do we have partnerId - if so, we already have at least one doecument uploaded earlier
if (partnerId.HasValue)
{
Console.WriteLine($"Associated with Partner ID: {partnerId.Value}");
//let's get the partner
var partner = await _dbContext.Partners.GetByIdAsync(partnerId.Value);
}
var filesList = new List<Files>();
var shippingDocumentToFileList = new List<ShippingDocumentToFiles>();
//iteratation 1: iterate documents to determine their type by AI
foreach (var file in files) foreach (var file in files)
{ {
// Validate file type (PDF only)
var fileName = file.FileName;
var fileSize = file.Length;
var dbFile = new Files();
string pdfText = "";
Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}");
if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) if (!file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
return Json(new { success = false, errorMessage = "Only PDF files are allowed" }); return Json(new { success = false, errorMessage = "Only PDF files are allowed" });
// Validate file size (max 10MB) // Validate file size (max 20MB)
if (file.Length > 10 * 1024 * 1024) if (file.Length > 20 * 1024 * 1024)
return Json(new { success = false, errorMessage = "File size must be less than 10MB" }); return Json(new { success = false, errorMessage = "File size must be less than 10MB" });
// - get text extracted from pdf
// Validate file type (PDF only)
if (file.Length > 0 && file.ContentType == "application/pdf")
{
try
{
// Open the PDF from the IFormFile's stream directly in memory
using (var stream = file.OpenReadStream())
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(stream))
{
// Now you can analyze the PDF content
foreach (var page in pdf.GetPages())
{
// Extract text from each page
pdfText += ContentOrderTextExtractor.GetText(page);
}
// For demonstration, let's just log the extracted text
Console.WriteLine($"Extracted text from {file.FileName}: {pdfText}");
}
}
catch (Exception ex)
{
// Handle potential exceptions during PDF processing
Console.Error.WriteLine($"Error processing PDF file {file.FileName}: {ex.Message}");
return StatusCode(500, $"Error processing PDF file: {ex.Message}");
}
}
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document from the available list, and return them as JSON: documentNumber, documentType. " +
$"Available filetypes: {nameof(DocumentType.Invoice)}, {nameof(DocumentType.ShippingDocument)} , {nameof(DocumentType.OrderConfirmation)}, {nameof(DocumentType.Unknown)}" +
"If you can't find information of any of these, return null value for that field.";
//here I can start preparing the file entity
var metaAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt);
var extractedMetaData = ParseMetaDataAIResponse(metaAnalyzis);
if (extractedMetaData.DocumentNumber != null)
dbFile.RawText = pdfText;
dbFile.FileExtension = "pdf";
dbFile.FileName = extractedMetaData.DocumentNumber;
// - IF WE DON'T HAVE PARTNERID ALREADY: read partner information
// (check if all 3 refers to the same partner)
// save partner information to partners table { Id, Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street }
if (partnerId == null)
{
string partnerAnalysisPrompt = "Extract the partner information from this document, and return them as JSON: name, taxId, certificationNumber, postalCode, country, state, county, city, street. " +
"If you can't find information of any of these, return null value for that field.";
//here I can start preparing the file entity
var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), analysisPrompt);
var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis);
if (extractedPartnerData.Name != null)
{
Console.WriteLine("AI Analysis Partner Result:");
Console.WriteLine(extractedPartnerData.Name);
}
if (extractedPartnerData.TaxId != null)
{
Console.WriteLine(extractedPartnerData.TaxId);
}
if (extractedPartnerData.Country != null) {
Console.WriteLine(extractedPartnerData.Country);
}
if (extractedPartnerData.State != null)
{
Console.WriteLine(extractedPartnerData.State);
}
}
// - save the documents to file system - wwwroot/uploads/orders/order-{orderId}/fileId-documentId.pdf
// where documentId is the number or id IN the document
try
{
// Create upload directory if it doesn't exist // Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "shippingDocuments"); var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId.ToString(), "documents");
Directory.CreateDirectory(uploadsPath); Directory.CreateDirectory(uploadsPath);
// Generate unique filename // Generate unique filename
var fileName = $"{Guid.NewGuid()}_{file.FileName}"; fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine(uploadsPath, fileName); var filePath = Path.Combine(uploadsPath, fileName);
// Save file // Save file
@ -170,23 +283,30 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
{ {
await file.CopyToAsync(stream); await file.CopyToAsync(stream);
} }
// Extract text with PdfPig
var pdfText = new StringBuilder();
using (var pdf = UglyToad.PdfPig.PdfDocument.Open(filePath))
{
foreach (var page in pdf.GetPages())
{
pdfText.AppendLine(page.Text);
} }
catch (Exception ex)
{
Console.WriteLine($"Error saving file: {ex}");
//return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded.");
} }
// Log extracted text for debugging
Console.WriteLine("Extracted PDF text:"); // - create a list of documents to read information and to save the document's information to DB
Console.WriteLine(pdfText.ToString()); // - INSIDE ITERATION:
// - IF SHIPPINGDOCUMENT: read shipping information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Orderdocument: try reading shipping information and financial information
// save document information into DB {Id, PartnerId, ShippingId, DocumentIdNumber, ShippingDate, Country, TotalPallets, IsAllMeasured}
// - IF Invoice: try reading financial information
// save document information into DB {financial db table not created yet}
// - save the documents to Files Table - { name, extension, type, rawtext }
try
{
// Analyze PDF with AI to extract structured data // Analyze PDF with AI to extract structured data
var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText.ToString(), pdfText.ToString(),
"Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field." "Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field."
); );
@ -224,7 +344,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
}; };
}
// var savedDocument = await _documentService.InsertDocumentAsync(document); // var savedDocument = await _documentService.InsertDocumentAsync(document);
@ -237,7 +357,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// document = documentModel // document = documentModel
//}); //});
return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!");
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -245,8 +364,49 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
//return Json(new { success = false, errorMessage = ex.Message }); //return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded."); return BadRequest("No files were uploaded.");
} }
} }
//iteration 2: iterate documents again
// - determine the items listed in the documents by AI
// try to fill all shippingitems information from the iteration
return Ok($"Files for Shipping Document ID {shippingDocumentId} were uploaded successfully!");
}
//[HttpPost]
//public async Task<IActionResult> UploadFile(List<IFormFile> files, int shippingDocumentId)
//{
// if (files == null || files.Count == 0)
// {
// return BadRequest("No files were uploaded.");
// }
// foreach (var file in files)
// {
// // Here, you would implement your logic to save the file.
// // For example, you can save the file to a specific folder on the server
// // or a cloud storage service like Azure Blob Storage.
// // You would use the 'shippingDocumentId' to associate the file with the correct entity in your database.
// // Example: Get file details
// var fileName = file.FileName;
// var fileSize = file.Length;
// // Log or process the file and its associated ID.
// // For demonstration, let's just return a success message.
// Console.WriteLine($"Received file: {fileName} for Document ID: {shippingDocumentId}");
// }
// // Return a success response. The DevExtreme FileUploader expects a 200 OK status.
// return Ok(new { message = "Files uploaded successfully." });
//}
private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse) private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse)
{ {
try try
@ -268,6 +428,79 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
} }
} }
private ExtractedDocumentMetaData ParseMetaDataAIResponse(string aiResponse)
{
try
{
// Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedDocumentMetaData>(
aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return data ?? new ExtractedDocumentMetaData();
}
catch
{
// If JSON parsing fails, return empty data with the raw response in notes
return new ExtractedDocumentMetaData
{
DocumentNumber = $"Unknown",
DocumentType = "Unknown"
};
}
}
private ExtractedPartnerData ParsePartnerDataAIResponse(string aiResponse)
{
try
{
// Try to parse as JSON first
var data = System.Text.Json.JsonSerializer.Deserialize<ExtractedPartnerData>(
aiResponse,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return data ?? new ExtractedPartnerData();
}
catch
{
// If JSON parsing fails, return empty data with the raw response in notes
return new ExtractedPartnerData
{
Name = "Unknown",
CertificationNumber = "Unknown",
TaxId = "Unknown",
PostalCode = "Unknown",
Country = "Unknown",
State = "Unknown",
County = "Unknown",
City = "Unknown",
Street = "Unknown",
};
}
}
private class ExtractedDocumentMetaData
{
public string DocumentNumber { get; set; }
public string DocumentType { get; set; }
}
private class ExtractedPartnerData
{
//Name, TaxId, CertificationNumber, PostalCode, Country, State, County, City, Street,
public string Name { get; set; }
public string TaxId { get; set; }
public string CertificationNumber { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string State { get; set; }
public string County { get; set; }
public string City { get; set; }
public string Street { get; set; }
}
// Helper class for extracted data // Helper class for extracted data
private class ExtractedDocumentData private class ExtractedDocumentData
{ {

View File

@ -329,7 +329,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
Console.WriteLine(pdfText.ToString()); Console.WriteLine(pdfText.ToString());
// Analyze PDF with AI to extract structured data // Analyze PDF with AI to extract structured data
var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalyzisFromText( var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText.ToString(), pdfText.ToString(),
"Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field." "Extract the following information from this shipping document and return as JSON: documentDate, recipientName, senderName, invoiceNumber, totalAmount, itemCount, notes. If a field is not found, return null for that field."
); );

View File

@ -340,7 +340,7 @@
Width = "80" Width = "80"
} }
}; };
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.NeedsMeasurement)) gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
{ {
Title = "Needs Measurement", Title = "Needs Measurement",
Width = "100", Width = "100",

View File

@ -146,6 +146,9 @@
) )
<input type="hidden" name="ShippingDocumentId" value="<%- data.Id %>" /> <input type="hidden" name="ShippingDocumentId" value="<%- data.Id %>" />
<% if (data.PartnerId) { %>
<input type="hidden" name="PartnerId" value="<%- data.PartnerId %>" />
<% } %>
@(Html.DevExtreme().Button() @(Html.DevExtreme().Button()
.Text("Upload Files") .Text("Upload Files")

View File

@ -31,4 +31,8 @@ public class PartnerDbTable : MgDbTableBase<Partner>
public Task<Partner> GetByIdAsync(int id, bool loadRelations) public Task<Partner> GetByIdAsync(int id, bool loadRelations)
=> GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id); => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id);
public Task<Partner> GetByIdNameAsync(string name, bool loadRelations)
=> GetAll(loadRelations).FirstOrDefaultAsync(p => p.Name == name);
} }

View File

@ -176,7 +176,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
{ {
var extendedOrder = new OrderModelExtended(); var extendedOrder = new OrderModelExtended();
CopyModelHelper.CopyPublicProperties(order, extendedOrder); CopyModelHelper.CopyPublicProperties(order, extendedOrder);
extendedOrder.NeedsMeasurement = await ShouldMarkAsNeedsMeasurementAsync(order); extendedOrder.IsMeasurable = await ShouldMarkAsNeedsMeasurementAsync(order);
Console.WriteLine(extendedOrder.Id); Console.WriteLine(extendedOrder.Id);
extendedRows.Add(extendedOrder); extendedRows.Add(extendedOrder);
} }

View File

@ -4,7 +4,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
{ {
public partial record OrderModelExtended : OrderModel public partial record OrderModelExtended : OrderModel
{ {
public bool NeedsMeasurement { get; set; } public bool IsMeasurable { get; set; }
} }
} }

View File

@ -37,9 +37,19 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return response; return response;
} }
public async Task<string> GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion) //public async Task<string> GetOpenAIPDFAnalyzisFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
public async Task<string> GetOpenAIPDFAnalysisFromText(string pdfText, string userQuestion)
{ {
string systemMessage = $"You are a helpful assistant of a webshop, you work in the administration area, with the ADMIN user. The ADMIN user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}"; string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion); var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
var fixedResponse = TextHelper.FixJsonWithoutAI(response); var fixedResponse = TextHelper.FixJsonWithoutAI(response);
@ -47,7 +57,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return fixedResponse; return fixedResponse;
} }
//public async Task<string> GetOpenAIPartnerInfoFromText(string pdfText, string userQuestion)
//{
// string systemMessage = $"You are a pdf analyzis assistant, the user is asking you questions about a PDF document, that you have access to. The content of the PDF document is the following: {pdfText}";
// var response = await _openAIApiService.GetSimpleResponseAsync(systemMessage, userQuestion);
// var fixedResponse = TextHelper.FixJsonWithoutAI(response);
// return fixedResponse;
//}
} }
} }

View File

@ -3,6 +3,7 @@ using FruitBank.Common.Interfaces;
using Nop.Core; using Nop.Core;
using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Orders; using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Catalog; using Nop.Services.Catalog;
using Nop.Services.Common; using Nop.Services.Common;
using Nop.Services.Events; using Nop.Services.Events;
@ -113,7 +114,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
var store = await _storeContext.GetCurrentStoreAsync(); var store = await _storeContext.GetCurrentStoreAsync();
// itt adjuk hozzá a GenericAttribute flag-et az orderhez // itt adjuk hozzá a GenericAttribute flag-et az orderhez
await _genericAttributeService.SaveAttributeAsync(order, await _genericAttributeService.SaveAttributeAsync(order,
"PendingMeasurement", true, store.Id); nameof(OrderModelExtended.IsMeasurable), true, store.Id);
// status pending // status pending
// paymentstatus pending // paymentstatus pending
} }

View File

@ -1,4 +1,5 @@
using Nop.Core.Domain.Orders; using Nop.Core.Domain.Orders;
using Nop.Plugin.Misc.FruitBankPlugin.Models;
using Nop.Services.Common; using Nop.Services.Common;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services namespace Nop.Plugin.Misc.FruitBankPlugin.Services
@ -23,7 +24,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
return false; return false;
return await _genericAttributeService.GetAttributeAsync<bool>( return await _genericAttributeService.GetAttributeAsync<bool>(
order, "PendingMeasurement", order.StoreId); order, nameof(OrderModelExtended.IsMeasurable), order.StoreId);
} }
} }
} }