This commit is contained in:
Adam 2025-10-25 08:57:44 +02:00
parent 343fb818c7
commit f3ac82acb2
5 changed files with 277 additions and 90 deletions

View File

@ -13,6 +13,7 @@ using Nop.Web.Areas.Admin.Controllers;
using Nop.Web.Framework;
using Nop.Web.Framework.Mvc.Filters;
using System.Text;
using System.Text.Json;
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
@ -231,7 +232,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
}
var filesList = new List<Files>();
//iteratation 1: iterate documents to determine their type by AI
@ -258,7 +259,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
//if (file.Length > 0 && file.ContentType == "application/pdf")
if (file.Length > 0)
{
if (file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)){
if (file.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase))
{
try
{
// Open the PDF from the IFormFile's stream directly in memory
@ -336,7 +338,6 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
_logger.Detail(pdfText);
}
string analysisPrompt = "Extract the document identification number from this document, determine the type of the " +
"document IN ENGLISH 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)}" +
@ -377,65 +378,206 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// - 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)
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(), partnerAnalysisPrompt);
var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis);
//find partner in DB... there is a serious chance that the partner Name and Taxid determines the partner
if (extractedPartnerData.Name != null)
var partners = await _dbContext.Partners.GetAll().ToListAsync();
foreach (var partner in partners)
{
_logger.Detail("AI Analysis Partner Result:");
_logger.Detail(extractedPartnerData.Name);
if (pdfText.Contains(partner.Name) || (partner.TaxId != null && pdfText.Contains(partner.TaxId)))
{
partnerId = partner.Id;
_logger.Detail($"Found existing partner in DB: {partner.Name} (ID: {partner.Id})");
break;
}
}
if (extractedPartnerData.TaxId != null)
if (partnerId == null)
{
_logger.Detail(extractedPartnerData.TaxId);
}
_logger.Detail("No existing partner found in DB, proceeding to extract partner info via AI.");
if (extractedPartnerData.Country != null)
{
string partnerAnalysisPrompt = "You are an agent of Fruitbank, helping to analyze pdf content. Determine which partner of Fruitbank sent this document, extract their data 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.";
_logger.Detail(extractedPartnerData.Country);
}
//here I can start preparing the file entity
var partnerAnalyzis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(pdfText.ToString(), partnerAnalysisPrompt);
var extractedPartnerData = ParsePartnerDataAIResponse(partnerAnalyzis);
if (extractedPartnerData.Name != null)
{
_logger.Detail("AI Analysis Partner Result:");
_logger.Detail(extractedPartnerData.Name);
}
if (extractedPartnerData.TaxId != null)
{
_logger.Detail(extractedPartnerData.TaxId);
}
if (extractedPartnerData.Country != null)
{
_logger.Detail(extractedPartnerData.Country);
}
if (extractedPartnerData.State != null)
{
_logger.Detail(extractedPartnerData.State);
}
if (extractedPartnerData.State != null)
{
_logger.Detail(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
//shortcut
try
{
// Create upload directory if it doesn't exist
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId.ToString(), "documents");
Directory.CreateDirectory(uploadsPath);
// Generate unique filename
fileName = $"{Guid.NewGuid()}_{file.FileName}";
var filePath = Path.Combine(uploadsPath, fileName);
// Save file
using (var stream = new FileStream(filePath, FileMode.Create))
var partnerPrompt = @"Extract the partner/company information from the following text and return ONLY a valid JSON object with these exact fields:
{
await file.CopyToAsync(stream);
""Name"": ""company name"",
""TaxId"": ""tax identification number"",
""CertificationNumber"": ""certification number if exists, otherwise empty string"",
""PostalCode"": ""postal code"",
""Country"": ""country name"",
""State"": ""state if exists, otherwise empty string"",
""County"": ""county if exists, otherwise empty string"",
""City"": ""city name"",
""Street"": ""street address""
}
If a field is not found in the text, use an empty string. Return ONLY the JSON object, no additional text or explanation.
Text to analyze:
" + pdfText;
var partnerResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText,
partnerPrompt
);
var partner = JsonSerializer.Deserialize<Partner>(CleanJsonResponse(partnerResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// Step 2: Extract Shipping Document Information
var shippingDocPrompt = @"Extract the shipping document information from the following text and return ONLY a valid JSON object with these exact fields:
{
""DocumentIdNumber"": ""document ID or reference number if exists, otherwise empty string"",
""ShippingDate"": ""shipping date in ISO format (yyyy-MM-dd)"",
""Country"": ""country of origin for pickup"",
""TotalPallets"": number_of_total_pallets
}
If a field is not found, use empty string for text fields, current date for ShippingDate, and 0 for TotalPallets. Return ONLY the JSON object, no additional text or explanation.
Text to analyze:
" + pdfText;
var shippingDocResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText,
shippingDocPrompt
);
var shippingDocData = JsonSerializer.Deserialize<ShippingDocumentDto>(CleanJsonResponse(shippingDocResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
// Step 3: Extract Shipping Items
var itemsPrompt = @"Extract all shipping items (fruits, vegetables, or products) from the following text and return ONLY a valid JSON array with objects having these exact fields:
[
{
""Name"": ""product name"",
""PalletsOnDocument"": number_of_pallets,
""QuantityOnDocument"": quantity_count,
""NetWeightOnDocument"": net_weight_in_kg,
""GrossWeightOnDocument"": gross_weight_in_kg
}
]
If a numeric field is not found, use 0. Return ONLY the JSON array, no additional text or explanation.
Text to analyze:
" + pdfText;
var itemsResponse = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText,
itemsPrompt
);
var items = JsonSerializer.Deserialize<List<ShippingItem>>(CleanJsonResponse(itemsResponse),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<ShippingItem>();
// Prepare result
var result = new ShippingDocumentAnalysisResult
{
Partner = partner,
ShippingDocument = new ShippingDocument
{
DocumentIdNumber = shippingDocData.DocumentIdNumber,
ShippingDate = shippingDocData.ShippingDate,
Country = shippingDocData.Country,
TotalPallets = shippingDocData.TotalPallets
},
ShippingItems = items
};
return Ok(result);
}
catch (JsonException ex)
{
return BadRequest($"Failed to parse AI response: {ex.Message}");
}
catch (Exception ex)
{
_logger.Error($"Error saving file: {ex.Message}", ex);
//return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded.");
return StatusCode(500, $"An error occurred: {ex.Message}");
}
// - 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
// var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "orders", "order" + shippingDocumentId.ToString(), "documents");
// Directory.CreateDirectory(uploadsPath);
// // Generate unique filename
// fileName = $"{Guid.NewGuid()}_{file.FileName}";
// var filePath = Path.Combine(uploadsPath, fileName);
// // Save file
// using (var stream = new FileStream(filePath, FileMode.Create))
// {
// await file.CopyToAsync(stream);
// }
//}
//catch (Exception ex)
//{
// _logger.Error($"Error saving file: {ex.Message}", ex);
// //return Json(new { success = false, errorMessage = ex.Message });
// return BadRequest("No files were uploaded.");
//}
// - create a list of documents to read information and to save the document's information to DB
// - INSIDE ITERATION:
// - IF SHIPPINGDOCUMENT: read shipping information
@ -446,68 +588,70 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// save document information into DB {financial db table not created yet}
// - save the documents to Files Table - { name, extension, type, rawtext }
try
{
//try
//{
// Analyze PDF with AI to extract structured data
var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
pdfText.ToString(),
"You work for FruitBank. 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."
);
// // Analyze PDF with AI to extract structured data
// var lastPrompt = "You work for FruitBank. 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.";
// Parse AI response (assuming it returns JSON)
var extractedData = ParseShippingDocumentAIResponse(aiAnalysis);
// var aiAnalysis = await _aiCalculationService.GetOpenAIPDFAnalysisFromText(
// pdfText,
// lastPrompt
// );
//TODO: Save document record to database
_logger.Detail("AI Analysis Result:");
_logger.Detail(extractedData.RecipientName);
_logger.Detail(extractedData.SenderName);
_logger.Detail(extractedData.InvoiceNumber);
_logger.Detail(extractedData.TotalAmount.ToString());
_logger.Detail(extractedData.ItemCount.ToString());
_logger.Detail(extractedData.Notes);
// // Parse AI response (assuming it returns JSON)
// var extractedData = ParseShippingDocumentAIResponse(aiAnalysis);
// //TODO: Save document record to database
// _logger.Detail("AI Analysis Result:");
// _logger.Detail(extractedData.RecipientName);
// _logger.Detail(extractedData.SenderName);
// _logger.Detail(extractedData.InvoiceNumber);
// _logger.Detail(extractedData.TotalAmount.ToString());
// _logger.Detail(extractedData.ItemCount.ToString());
// _logger.Detail(extractedData.Notes);
var documentId = 1; // Replace with: savedDocument.Id
// var documentId = 1; // Replace with: savedDocument.Id
// Return structured document model
var documentModel = new ShippingDocumentModel
{
Id = documentId,
ShippingId = shippingDocumentId,
FileName = file.FileName,
FilePath = $"/uploads/shippingDocuments/{fileName}",
FileSize = (int)(file.Length / 1024),
DocumentDate = extractedData.DocumentDate,
RecipientName = extractedData.RecipientName,
SenderName = extractedData.SenderName,
InvoiceNumber = extractedData.InvoiceNumber,
TotalAmount = extractedData.TotalAmount,
ItemCount = extractedData.ItemCount,
Notes = extractedData.Notes,
RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
};
// // Return structured document model
// var documentModel = new ShippingDocumentModel
// {
// Id = documentId,
// ShippingId = shippingDocumentId,
// FileName = file.FileName,
// FilePath = $"/uploads/shippingDocuments/{fileName}",
// FileSize = (int)(file.Length / 1024),
// DocumentDate = extractedData.DocumentDate,
// RecipientName = extractedData.RecipientName,
// SenderName = extractedData.SenderName,
// InvoiceNumber = extractedData.InvoiceNumber,
// TotalAmount = extractedData.TotalAmount,
// ItemCount = extractedData.ItemCount,
// Notes = extractedData.Notes,
// RawAIAnalysis = aiAnalysis // Store the raw AI response for debugging
// };
// var savedDocument = await _documentService.InsertDocumentAsync(document);
// // var savedDocument = await _documentService.InsertDocumentAsync(document);
// Mock saved document ID
// // Mock saved document ID
//return Json(new
//{
// success = true,
// document = documentModel
//});
// //return Json(new
// //{
// // success = true,
// // document = documentModel
// //});
}
catch (Exception ex)
{
_logger.Error($"Error uploading file: {ex.Message}", ex);
//return Json(new { success = false, errorMessage = ex.Message });
return BadRequest("No files were uploaded.");
}
//}
//catch (Exception ex)
//{
// _logger.Error($"Error uploading file: {ex.Message}", ex);
// //return Json(new { success = false, errorMessage = ex.Message });
// return BadRequest("No files were uploaded.");
//}
}
@ -550,6 +694,47 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// return Ok(new { message = "Files uploaded successfully." });
//}
private string CleanJsonResponse(string response)
{
// Remove markdown code blocks if present
response = response.Trim();
if (response.StartsWith("```json"))
{
response = response.Substring(7);
}
else if (response.StartsWith("```"))
{
response = response.Substring(3);
}
if (response.EndsWith("```"))
{
response = response.Substring(0, response.Length - 3);
}
return response.Trim();
}
public class ShippingDocumentAnalysisRequest
{
public string RawText { get; set; }
}
public class ShippingDocumentAnalysisResult
{
public Partner Partner { get; set; }
public ShippingDocument ShippingDocument { get; set; }
public List<ShippingItem> ShippingItems { get; set; }
}
public class ShippingDocumentDto
{
public string DocumentIdNumber { get; set; }
public DateTime ShippingDate { get; set; }
public string Country { get; set; }
public int TotalPallets { get; set; }
}
private ExtractedDocumentData ParseShippingDocumentAIResponse(string aiResponse)
{

View File

@ -23,7 +23,7 @@
var hideNotesBlock = await genericAttributeService.GetAttributeAsync<bool>(customer, hideNotesBlockAttributeName);
}
<form asp-controller="Order" asp-action="Edit" method="post" id="order-form">
<form asp-controller="CustomOrder" asp-action="Edit" method="post" id="order-form">
<div class="content-header clearfix">
<h1 class="float-left">
@T("Admin.Orders.EditOrderDetails") - @Model.CustomOrderNumber

View File

@ -219,6 +219,8 @@
UrlRead = new DataUrl("ProductList", "CustomProduct", null),
SearchButtonId = "search-products",
Length = Model.PageSize,
Ordering = true,
ServerSide = false,
LengthMenu = Model.AvailablePageSizes,
Filters = new List<FilterParameter>
{

View File

@ -1,6 +1,6 @@
@model Nop.Web.Models.Catalog.ProductOverviewModel
@if (Model != null)
@* @if (Model != null)
{
<div class="ai-question-box">
<h5>Kérdezz a termékről!</h5>
@ -16,5 +16,5 @@
});
});
</script>
}
} *@

View File

@ -1,6 +1,6 @@
@model Nop.Web.Models.Catalog.ProductDetailsModel
@if (Model != null)
@* @if (Model != null)
{
<div class="ai-question-box">
<h5>Kérdezz a termékről!</h5>
@ -16,5 +16,5 @@
});
});
</script>
}
} *@